ibd1279/otel-zig
Zig implementation of the OTel API and SDK.
master
This is a zig implementation of the OTel API and SDK. It was build for zig 0.15.1.
Providers are configured using the setupGlobalProvider
pattern with pipeline configuration. The setup is consistent across all three signals (logs, metrics, traces) and involves configuring an exporter, processor, resource, and provider implementation.
The logging system supports integration with the existing std.log
, in additional to otel API calls. This example shows both, using the OTLP exporter.
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Clean up global providers at program exit
defer otel_api.provider_registry.unsetAllProviders();
// Setup global OTel logging provider with OTLP exporter
const provider = try otel_sdk.logs.setupGlobalProvider(
allocator,
.{otel_sdk.logs.SimpleLogRecordProcessor.PipelineStep.init({})
.flowTo(otel_exporters.otlp.OtlpLogExporter.PipelineStep.init(.{}))},
);
defer {
provider.deinit();
provider.destroy();
}
// Only need this to Initialize the std.log bridge.
try otel_sdk.std_log_bridge.init(.{
.enabled = true,
.include_scope_attribute = true,
.instrumentation_scope_name = "dns.query.std_log.example",
.instrumentation_scope_version = "1.0.0",
});
defer otel_sdk.std_log_bridge.deinit();
// Now all std.log calls will automatically emit OpenTelemetry log records after the above.
std.log.info("This is really an OTel log.", .{});
// normal otel calls still work too.
const logger_scope = otel_api.InstrumentationScope{ .name = "multiply", .version = "1.0.0" };
var logger = try logger_provider.getLoggerWithScope(logger_scope);
logger.emitLog(
&.{}, // context
.info, // log level
"HTTP server thread started", // log message
&[_]otel_api.common.AttributeKeyValue{ // attributes.
.{ .key = "address", .value = .{ .string = shared_state.server_address } },
.{ .key = "port", .value = .{ .int = @intCast(shared_state.server_port) } },
},
null, // event_name
);
Metrics setup is very similar, although it doesn't currently support any other integrations.
// Metrics setup
const concrete_provider = try otel_sdk.metrics.setupGlobalProvider(
allocator,
.{otel_sdk.metrics.ManualReader.PipelineStep.init({})
.flowTo(otel_exporters.otlp.OtlpMetricExporter.PipelineStep.init(.{}))},
);
defer {
concrete_provider.deinit();
concrete_provider.destroy();
}
// Get a meter
const scope = otel_api.InstrumentationScope{ .name = "example.metric.otlp", .version = "1.0.0" };
var meter = try otel_api.getGlobalMeterProvider().getMeterWithScope(scope);
Traces is similar to metrics. This example uses the stream exporter to output to stderr.
// Set up trace provider using the new setupGlobalProvider pattern
var stderr_buffer = [_]u8{0} ** 1024;
var stderr = otel_exporters.console.initStream(true, &stderr_buffer);
const concrete_provider = try otel_sdk.trace.setupGlobalProvider(
allocator,
.{otel_sdk.trace.BasicSpanProcessor.PipelineStep.init({})
.flowTo(otel_exporters.stream.SpanDataSink.PipelineStep.init(.{
.writer = &stderr.interface,
.flush_after_each = true,
}))},
);
defer {
concrete_provider.deinit();
concrete_provider.destroy();
}
// Get the global tracer provider interface
var tp = otel_api.getGlobalTracerProvider();
// Get a tracer
const scope = otel_api.InstrumentationScope{ .name = "example-component", .version = "1.0.0" };
var tracer = try tp.getTracerWithScope(scope);
// Create a root context
const ctx = &[_]otel_api.ContextKeyValue{};
// Start a parent span
const parent_result = try tracer.startSpan("parent-operation", .{
.kind = .server,
.attributes = &[_]otel_api.common.AttributeKeyValue{
.{
.key = "http.method",
.value = otel_api.common.AttributeValue{ .string = "GET" },
},
.{
.key = "http.url",
.value = otel_api.common.AttributeValue{ .string = "/api/example" },
},
},
}, ctx);
defer {
parent_result.end(null);
parent_result.deinit();
}
// do stuff before the span is ended.
These examples show the setup of the SDK, but most usages should focus on the APIs exposed from otel_api.getGlobalTracerProvider()
and similar methods.
The API part provides methods for getting and setting Global Providers and the necessary interfaces for using them.
The SDK is structed with subdirectories for logs
, metrics
, and traces
, but they all follow the same general architecture pattern:
┌─────────────────┐
│ api.Provider │ (interface)
└─────────────────┘
△
│ implements
│
┌─────────────────┐
│sdk.StandardProvider│
└─────────────────┘
│
│ uses
│
▼
┌─────────────────┐
│ sdk.Processor │ (interface)
└─────────────────┘
△
│ implements
│
┌─────────────────┐
│sdk.SimpleProcessor│
└─────────────────┘
│
│ uses
│
▼
┌─────────────────┐
│ sdk.Exporter │ (interface)
└─────────────────┘
△
│ implements
│
┌─────────────────┐
│ exporters │ (module)
│ - Console │
│ - OTLP │
│ - etc. │
└─────────────────┘
The flow is: Provider → Processor/Reader → Exporter, with each component being responsible for a specific part of the telemetry pipeline.
trace_state
isn't really handled -- while the type has room for it, the memory implications of using the trace state have not been worked out; it will leak.