diff --git a/opentelemetry-sdk/src/api/metrics/measurement.zig b/opentelemetry-sdk/src/api/metrics/measurement.zig index 35a9626..d50ce18 100644 --- a/opentelemetry-sdk/src/api/metrics/measurement.zig +++ b/opentelemetry-sdk/src/api/metrics/measurement.zig @@ -263,6 +263,8 @@ pub const Measurements = struct { data: MeasurementsData, + resource: ?[]const Attribute = null, + pub fn deinit(self: *Measurements, allocator: std.mem.Allocator) void { switch (self.data) { inline else => |list| { diff --git a/opentelemetry-sdk/src/api/trace/span.zig b/opentelemetry-sdk/src/api/trace/span.zig index 55d87d4..2774c9c 100644 --- a/opentelemetry-sdk/src/api/trace/span.zig +++ b/opentelemetry-sdk/src/api/trace/span.zig @@ -179,6 +179,7 @@ pub const Span = struct { is_recording: bool, allocator: std.mem.Allocator, scope: InstrumentationScope, + resource: ?[]const attribute.Attribute = null, const Self = @This(); diff --git a/opentelemetry-sdk/src/sdk/metrics/exporters/otlp.zig b/opentelemetry-sdk/src/sdk/metrics/exporters/otlp.zig index ec54473..dd702bf 100644 --- a/opentelemetry-sdk/src/sdk/metrics/exporters/otlp.zig +++ b/opentelemetry-sdk/src/sdk/metrics/exporters/otlp.zig @@ -24,6 +24,7 @@ const view = @import("../view.zig"); const protobuf = @import("protobuf"); const pbcommon = @import("opentelemetry-proto").common_v1; +const pbresource = @import("opentelemetry-proto").resource_v1; const pbmetrics = @import("opentelemetry-proto").metrics_v1; const pbcollector_metrics = @import("opentelemetry-proto").collector_metrics_v1; @@ -107,8 +108,17 @@ pub const OTLPExporter = struct { .metrics = metrics, }; } + const resource_attributes = try resourceToProtobuf( + self.allocator, + if (data.len > 0) data[0].resource else null, + ); + resource_metrics[0] = pbmetrics.ResourceMetrics{ - .resource = null, //FIXME support resource attributes + .resource = pbresource.Resource{ + .attributes = resource_attributes, + .dropped_attributes_count = 0, + .entity_refs = std.ArrayList(pbcommon.EntityRef).empty, + }, .scope_metrics = std.ArrayList(pbmetrics.ScopeMetrics).fromOwnedSlice(scope_metrics), .schema_url = "", }; @@ -204,6 +214,17 @@ fn attributeToProtobuf(allocator: std.mem.Allocator, attribute: Attribute) !pbco }; } +fn resourceToProtobuf(allocator: std.mem.Allocator, resource: ?[]const Attribute) !std.ArrayList(pbcommon.KeyValue) { + var kvs = std.ArrayList(pbcommon.KeyValue).empty; + if (resource) |attrs| { + try kvs.ensureTotalCapacityPrecise(allocator, attrs.len); + for (attrs) |attr| { + kvs.appendAssumeCapacity(try attributeToProtobuf(allocator, attr)); + } + } + return kvs; +} + pub fn attributesToProtobufKeyValueList(allocator: std.mem.Allocator, attributes: ?[]Attribute) !pbcommon.KeyValueList { if (attributes) |attrs| { var kvs: pbcommon.KeyValueList = .{ .values = try .initCapacity(allocator, attrs.len) }; @@ -484,3 +505,33 @@ test "exporters/otlp init/deinit" { var exporter = try OTLPExporter.init(allocator, io, view.DefaultTemporality, config); defer exporter.deinit(); } + +test "exporters/otlp resource conversion" { + const allocator = std.testing.allocator; + + const resource = [_]Attribute{ + .{ .key = "service.name", .value = .{ .string = "test-service" } }, + .{ .key = "service.version", .value = .{ .string = "1.2.3" } }, + }; + + var kvs = try resourceToProtobuf(allocator, &resource); + defer { + for (kvs.items) |*kv| kv.deinit(allocator); + kvs.deinit(allocator); + } + + try std.testing.expectEqual(@as(usize, 2), kvs.items.len); + try std.testing.expectEqualStrings("service.name", kvs.items[0].key); + try std.testing.expectEqualStrings("test-service", kvs.items[0].value.?.value.?.string_value); + try std.testing.expectEqualStrings("service.version", kvs.items[1].key); + try std.testing.expectEqualStrings("1.2.3", kvs.items[1].value.?.value.?.string_value); +} + +test "exporters/otlp resource conversion without a resource" { + const allocator = std.testing.allocator; + + var kvs = try resourceToProtobuf(allocator, null); + defer kvs.deinit(allocator); + + try std.testing.expectEqual(@as(usize, 0), kvs.items.len); +} diff --git a/opentelemetry-sdk/src/sdk/metrics/reader.zig b/opentelemetry-sdk/src/sdk/metrics/reader.zig index 9a9c35e..ec0f720 100644 --- a/opentelemetry-sdk/src/sdk/metrics/reader.zig +++ b/opentelemetry-sdk/src/sdk/metrics/reader.zig @@ -110,6 +110,7 @@ pub const MetricReader = struct { for (measurements) |*m| { try self.temporal_aggregation.process(m, self.temporality); + m.resource = mp.resource; } // The exporter takes ownership of the data points, which are deinitialized diff --git a/opentelemetry-sdk/src/sdk/trace/exporters/otlp.zig b/opentelemetry-sdk/src/sdk/trace/exporters/otlp.zig index 9e39aae..d970792 100644 --- a/opentelemetry-sdk/src/sdk/trace/exporters/otlp.zig +++ b/opentelemetry-sdk/src/sdk/trace/exporters/otlp.zig @@ -145,9 +145,19 @@ pub const OTLPExporter = struct { try scope_spans_list.append(self.allocator, scope_span); } + var resource_attributes = std.ArrayList(pbcommon.KeyValue).empty; + if (spans.len > 0) { + if (spans[0].resource) |attrs| { + try resource_attributes.ensureTotalCapacityPrecise(self.allocator, attrs.len); + for (attrs) |attr| { + resource_attributes.appendAssumeCapacity(try attributeToOTLP(attr.key, attr.value)); + } + } + } + const resource_span = pbtrace.ResourceSpans{ .resource = pbresource.Resource{ - .attributes = std.ArrayList(pbcommon.KeyValue).empty, + .attributes = resource_attributes, .dropped_attributes_count = 0, .entity_refs = std.ArrayList(pbcommon.EntityRef).empty, }, @@ -462,3 +472,79 @@ test "OTLPExporter basic functionality" { // We expect a connection error since there's no OTLP server running try std.testing.expectError(error.ConnectionRefused, result); } + +test "span resource is exported" { + const allocator = std.testing.allocator; + const io = std.testing.io; + + var env_map = std.process.Environ.Map.init(allocator); + defer env_map.deinit(); + + var config = try otlp.ConfigOptions.init(allocator, &env_map); + defer config.deinit(); + + var exporter = try OTLPExporter.init(allocator, io, config); + defer exporter.deinit(); + + const resource = [_]attribute.Attribute{ + .{ .key = "service.name", .value = .{ .string = "test-service" } }, + .{ .key = "service.version", .value = .{ .string = "1.2.3" } }, + }; + + const trace_id = trace.TraceID.init([16]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }); + const span_id = trace.SpanID.init([8]u8{ 1, 2, 3, 4, 5, 6, 7, 8 }); + var trace_state = trace.TraceState.init(allocator); + defer trace_state.deinit(); + + const span_context = trace.SpanContext.init(trace_id, span_id, trace.TraceFlags.default(), trace_state, false); + const scope = InstrumentationScope{ .name = "test-lib", .version = "1.0.0" }; + var test_span = trace.Span.init(allocator, span_context, "test-span", .Internal, scope); + test_span.resource = &resource; + defer test_span.deinit(); + + var spans = [_]trace.Span{test_span}; + + var request = try exporter.spansToOTLPRequest(spans[0..]); + defer exporter.cleanupRequest(&request); + + try std.testing.expectEqual(@as(usize, 1), request.resource_spans.items.len); + + const exported = request.resource_spans.items[0].resource.?; + try std.testing.expectEqual(@as(usize, 2), exported.attributes.items.len); + try std.testing.expectEqualStrings("service.name", exported.attributes.items[0].key); + try std.testing.expectEqualStrings("test-service", exported.attributes.items[0].value.?.value.?.string_value); + try std.testing.expectEqualStrings("service.version", exported.attributes.items[1].key); + try std.testing.expectEqualStrings("1.2.3", exported.attributes.items[1].value.?.value.?.string_value); +} + +test "spans without a resource export no resource attributes" { + const allocator = std.testing.allocator; + const io = std.testing.io; + + var env_map = std.process.Environ.Map.init(allocator); + defer env_map.deinit(); + + var config = try otlp.ConfigOptions.init(allocator, &env_map); + defer config.deinit(); + + var exporter = try OTLPExporter.init(allocator, io, config); + defer exporter.deinit(); + + const trace_id = trace.TraceID.init([16]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }); + const span_id = trace.SpanID.init([8]u8{ 1, 2, 3, 4, 5, 6, 7, 8 }); + var trace_state = trace.TraceState.init(allocator); + defer trace_state.deinit(); + + const span_context = trace.SpanContext.init(trace_id, span_id, trace.TraceFlags.default(), trace_state, false); + const scope = InstrumentationScope{ .name = "test-lib", .version = "1.0.0" }; + var test_span = trace.Span.init(allocator, span_context, "test-span", .Internal, scope); + defer test_span.deinit(); + + var spans = [_]trace.Span{test_span}; + + var request = try exporter.spansToOTLPRequest(spans[0..]); + defer exporter.cleanupRequest(&request); + + const exported = request.resource_spans.items[0].resource.?; + try std.testing.expectEqual(@as(usize, 0), exported.attributes.items.len); +} diff --git a/opentelemetry-sdk/src/sdk/trace/provider.zig b/opentelemetry-sdk/src/sdk/trace/provider.zig index f7b5052..4f67785 100644 --- a/opentelemetry-sdk/src/sdk/trace/provider.zig +++ b/opentelemetry-sdk/src/sdk/trace/provider.zig @@ -321,6 +321,7 @@ pub const Tracer = struct { // Create the span with instrumentation scope var span = trace_api.Span.init(allocator, span_context, span_name, options.kind, self.scope); span.is_recording = true; // SDK spans are recording by default + span.resource = self.provider.resource; // Set attributes if provided if (options.attributes) |attrs| {