diff --git a/observability-tracing-aws-xray/internal/api/gen/models.gen.go b/observability-tracing-aws-xray/internal/api/gen/models.gen.go index 64e535d3..e68e1107 100644 --- a/observability-tracing-aws-xray/internal/api/gen/models.gen.go +++ b/observability-tracing-aws-xray/internal/api/gen/models.gen.go @@ -60,13 +60,8 @@ type ErrorResponseTitle string // TraceSpanDetailsResponse defines model for TraceSpanDetailsResponse. type TraceSpanDetailsResponse struct { - Attributes *[]struct { - // Key The key of the attribute - Key *string `json:"key,omitempty"` - - // Value The value of the attribute - Value *string `json:"value,omitempty"` - } `json:"attributes,omitempty"` + // Attributes The span attributes as a key/value map + Attributes *map[string]interface{} `json:"attributes,omitempty"` // DurationNs The duration of the span in nanoseconds DurationNs *int64 `json:"durationNs,omitempty"` @@ -75,14 +70,10 @@ type TraceSpanDetailsResponse struct { EndTime *time.Time `json:"endTime,omitempty"` // ParentSpanId The parent span ID - ParentSpanId *string `json:"parentSpanId,omitempty"` - ResourceAttributes *[]struct { - // Key The key of the attribute - Key *string `json:"key,omitempty"` + ParentSpanId *string `json:"parentSpanId,omitempty"` - // Value The value of the attribute - Value *string `json:"value,omitempty"` - } `json:"resourceAttributes,omitempty"` + // ResourceAttributes The resource attributes as a key/value map + ResourceAttributes *map[string]interface{} `json:"resourceAttributes,omitempty"` // SpanId The span ID SpanId *string `json:"spanId,omitempty"` @@ -107,7 +98,7 @@ type TraceSpanDetailsResponseStatus string type TraceSpansListResponse struct { // Spans The list of spans Spans *[]struct { - // Attributes The span attributes + // Attributes The span attributes as a key/value map Attributes *map[string]interface{} `json:"attributes,omitempty"` // DurationNs The duration of the span in nanoseconds @@ -119,13 +110,13 @@ type TraceSpansListResponse struct { // ParentSpanId The parent span ID ParentSpanId *string `json:"parentSpanId,omitempty"` - // ResourceAttributes The resource attributes + // ResourceAttributes The resource attributes as a key/value map ResourceAttributes *map[string]interface{} `json:"resourceAttributes,omitempty"` // SpanId The span ID SpanId *string `json:"spanId,omitempty"` - // SpanKind The name of the span + // SpanKind The kind of the span SpanKind *string `json:"spanKind,omitempty"` // SpanName The name of the span diff --git a/observability-tracing-aws-xray/internal/api/gen/server.gen.go b/observability-tracing-aws-xray/internal/api/gen/server.gen.go index d9ff2bc3..5635f3a7 100644 --- a/observability-tracing-aws-xray/internal/api/gen/server.gen.go +++ b/observability-tracing-aws-xray/internal/api/gen/server.gen.go @@ -616,35 +616,34 @@ func (sh *strictHandler) Health(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/observability-tracing-aws-xray/internal/handlers.go b/observability-tracing-aws-xray/internal/handlers.go index eeb8b774..8d3cd01a 100644 --- a/observability-tracing-aws-xray/internal/handlers.go +++ b/observability-tracing-aws-xray/internal/handlers.go @@ -280,44 +280,23 @@ func toSpanDetailsResponse(span *xray.SpanDetail) gen.TraceSpanDetailsResponse { startTime := span.StartTime endTime := span.EndTime - attrs := make([]struct { - Key *string `json:"key,omitempty"` - Value *string `json:"value,omitempty"` - }, 0, len(span.Attributes)) - for _, a := range span.Attributes { - key := a.Key - value := a.Value - attrs = append(attrs, struct { - Key *string `json:"key,omitempty"` - Value *string `json:"value,omitempty"` - }{Key: &key, Value: &value}) + resp := gen.TraceSpanDetailsResponse{ + SpanId: &span.SpanID, + SpanName: &span.SpanName, + SpanKind: &span.SpanKind, + StartTime: &startTime, + EndTime: &endTime, + DurationNs: &dur, + ParentSpanId: &span.ParentSpanID, + Status: ptr(gen.TraceSpanDetailsResponseStatus(span.Status)), } - - resAttrs := make([]struct { - Key *string `json:"key,omitempty"` - Value *string `json:"value,omitempty"` - }, 0, len(span.ResourceAttributes)) - for _, a := range span.ResourceAttributes { - key := a.Key - value := a.Value - resAttrs = append(resAttrs, struct { - Key *string `json:"key,omitempty"` - Value *string `json:"value,omitempty"` - }{Key: &key, Value: &value}) + if span.Attributes != nil { + resp.Attributes = &span.Attributes } - - return gen.TraceSpanDetailsResponse{ - SpanId: &span.SpanID, - SpanName: &span.SpanName, - SpanKind: &span.SpanKind, - StartTime: &startTime, - EndTime: &endTime, - DurationNs: &dur, - ParentSpanId: &span.ParentSpanID, - Status: ptr(gen.TraceSpanDetailsResponseStatus(span.Status)), - Attributes: &attrs, - ResourceAttributes: &resAttrs, + if span.ResourceAttributes != nil { + resp.ResourceAttributes = &span.ResourceAttributes } + return resp } func ptr[T any](v T) *T { diff --git a/observability-tracing-aws-xray/internal/xray/client.go b/observability-tracing-aws-xray/internal/xray/client.go index df5a21e7..f2bf33f3 100644 --- a/observability-tracing-aws-xray/internal/xray/client.go +++ b/observability-tracing-aws-xray/internal/xray/client.go @@ -79,11 +79,6 @@ type SpansResult struct { TookMs int } -type SpanAttribute struct { - Key string - Value string -} - type SpanDetail struct { SpanID string SpanName string @@ -93,8 +88,8 @@ type SpanDetail struct { DurationNs int64 ParentSpanID string Status string - Attributes []SpanAttribute - ResourceAttributes []SpanAttribute + Attributes map[string]interface{} + ResourceAttributes map[string]interface{} } type SpanDetailResult struct { diff --git a/observability-tracing-aws-xray/internal/xray/queries.go b/observability-tracing-aws-xray/internal/xray/queries.go index 10655083..356816c0 100644 --- a/observability-tracing-aws-xray/internal/xray/queries.go +++ b/observability-tracing-aws-xray/internal/xray/queries.go @@ -675,37 +675,37 @@ func segmentToSpanDetail(seg *xraySegment, parentID string) *SpanDetail { Status: segmentStatus(seg), } - attrs := make([]SpanAttribute, 0) + attrs := make(map[string]interface{}) for k, v := range seg.Annotations { - attrs = append(attrs, SpanAttribute{Key: "annotation." + k, Value: fmt.Sprintf("%v", v)}) + attrs["annotation."+k] = v } for k, v := range flattenMap("http", seg.HTTP) { - attrs = append(attrs, SpanAttribute{Key: k, Value: fmt.Sprintf("%v", v)}) + attrs[k] = v } for k, v := range flattenMap("sql", seg.SQL) { - attrs = append(attrs, SpanAttribute{Key: k, Value: fmt.Sprintf("%v", v)}) + attrs[k] = v } if seg.Namespace != "" { - attrs = append(attrs, SpanAttribute{Key: "xray.namespace", Value: seg.Namespace}) + attrs["xray.namespace"] = seg.Namespace } if seg.Origin != "" { - attrs = append(attrs, SpanAttribute{Key: "xray.origin", Value: seg.Origin}) + attrs["xray.origin"] = seg.Origin } detail.Attributes = attrs - resAttrs := make([]SpanAttribute, 0) + resAttrs := make(map[string]interface{}) for k, v := range flattenMap("aws", seg.AWS) { - resAttrs = append(resAttrs, SpanAttribute{Key: k, Value: fmt.Sprintf("%v", v)}) + resAttrs[k] = v } if seg.Origin != "" { - resAttrs = append(resAttrs, SpanAttribute{Key: "cloud.platform", Value: seg.Origin}) + resAttrs["cloud.platform"] = seg.Origin } if seg.Metadata != nil { for ns, v := range seg.Metadata { if nsMap, ok := v.(map[string]interface{}); ok { for k, mv := range nsMap { - resAttrs = append(resAttrs, SpanAttribute{Key: fmt.Sprintf("metadata.%s.%s", ns, k), Value: fmt.Sprintf("%v", mv)}) + resAttrs[fmt.Sprintf("metadata.%s.%s", ns, k)] = mv } } } diff --git a/observability-tracing-aws-xray/internal/xray/queries_test.go b/observability-tracing-aws-xray/internal/xray/queries_test.go index 5006af56..bbe0f704 100644 --- a/observability-tracing-aws-xray/internal/xray/queries_test.go +++ b/observability-tracing-aws-xray/internal/xray/queries_test.go @@ -203,6 +203,42 @@ func TestFlattenMap(t *testing.T) { } } +// Regression for #3886: segment annotations keep their native types in the +// span detail instead of being stringified. +func TestSegmentToSpanDetail_PreservesNativeTypes(t *testing.T) { + seg := &xraySegment{ + ID: "1a2b3c", + Name: "checkout", + StartTime: json.Number("1.0"), + EndTime: json.Number("2.0"), + Annotations: map[string]interface{}{ + "retry_count": 3, + "ratio": 0.5, + "cache_hit": true, + "region": "us-east-1", + }, + } + + detail := segmentToSpanDetail(seg, "") + + if v, ok := detail.Attributes["annotation.retry_count"].(int); !ok || v != 3 { + t.Errorf("retry_count: expected int(3), got %T(%v)", + detail.Attributes["annotation.retry_count"], detail.Attributes["annotation.retry_count"]) + } + if v, ok := detail.Attributes["annotation.ratio"].(float64); !ok || v != 0.5 { + t.Errorf("ratio: expected float64(0.5), got %T(%v)", + detail.Attributes["annotation.ratio"], detail.Attributes["annotation.ratio"]) + } + if v, ok := detail.Attributes["annotation.cache_hit"].(bool); !ok || !v { + t.Errorf("cache_hit: expected bool(true), got %T(%v)", + detail.Attributes["annotation.cache_hit"], detail.Attributes["annotation.cache_hit"]) + } + if v, ok := detail.Attributes["annotation.region"].(string); !ok || v != "us-east-1" { + t.Errorf("region: expected string(us-east-1), got %T(%v)", + detail.Attributes["annotation.region"], detail.Attributes["annotation.region"]) + } +} + func contains(s, substr string) bool { return len(s) >= len(substr) && searchSubstring(s, substr) }