135 lines
7.7 KiB
Go
135 lines
7.7 KiB
Go
package normalization
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
func TestNormalizeTraceRedactsValuesAndCreatesStableSignatures(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"resourceSpans": []interface{}{map[string]interface{}{
|
|
"resource": map[string]interface{}{"attributes": []interface{}{
|
|
map[string]interface{}{"key": "service.name", "value": map[string]string{"stringValue": "checkout"}},
|
|
}},
|
|
"scopeSpans": []interface{}{map[string]interface{}{"spans": []interface{}{map[string]interface{}{
|
|
"traceId": "abc123", "name": "POST /orders/123",
|
|
"startTimeUnixNano": "1000000000", "endTimeUnixNano": "1250000000",
|
|
"attributes": []interface{}{
|
|
map[string]interface{}{"key": "http.route", "value": map[string]string{"stringValue": "/orders/123"}},
|
|
map[string]interface{}{"key": "http.response.status_code", "value": map[string]string{"stringValue": "504"}},
|
|
},
|
|
"status": map[string]string{"code": "2", "message": "database timeout for order 123"},
|
|
}}}},
|
|
}},
|
|
}
|
|
data, _ := json.Marshal(payload)
|
|
first, err := NormalizeEntry("traces", 99, data)
|
|
if err != nil || len(first) != 1 {
|
|
t.Fatalf("normalize failed: %v %+v", err, first)
|
|
}
|
|
second, err := NormalizeEntry("traces", 99, data)
|
|
if err != nil || first[0].ExactSignature != second[0].ExactSignature || first[0].PatternSignature != second[0].PatternSignature {
|
|
t.Fatalf("signatures are not deterministic: %+v %+v", first, second)
|
|
}
|
|
event := first[0]
|
|
if event.Service != "checkout" || event.Route != "/orders/{id}" || event.StatusClass != "5xx" || event.DurationMS != 250 {
|
|
t.Fatalf("unexpected normalized fields: %+v", event)
|
|
}
|
|
if event.MessageNormalized != "database timeout for order <num>" {
|
|
t.Fatalf("unexpected normalized message: %q", event.MessageNormalized)
|
|
}
|
|
if event.TraceID == "" || event.ExactSignature == "" || event.PatternSignature == "" {
|
|
t.Fatalf("missing correlation fields: %+v", event)
|
|
}
|
|
if event.IssueFingerprint == "" || !event.IsTimeout {
|
|
t.Fatalf("missing canonical issue or timeout features: %+v", event)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeLogUsesEventTimestampAndFrontendSource(t *testing.T) {
|
|
data := []byte(`{"resourceLogs":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"web"}}]},"scopeLogs":[{"logRecords":[{"timeUnixNano":"1230000000","severityNumber":17,"body":{"stringValue":"User 123 failed"},"attributes":[{"key":"source","value":{"stringValue":"frontend"}}]}]}]}]}`)
|
|
events, err := NormalizeEntry("logs", 99, data)
|
|
if err != nil || len(events) != 1 {
|
|
t.Fatalf("normalize failed: %v %+v", err, events)
|
|
}
|
|
if events[0].Timestamp != 1230000000 || events[0].Source != "frontend" || !events[0].IsError {
|
|
t.Fatalf("unexpected log event: %+v", events[0])
|
|
}
|
|
if events[0].MessageNormalized != "user <num> failed" {
|
|
t.Fatalf("unexpected normalized log: %+v", events[0])
|
|
}
|
|
}
|
|
|
|
func TestNormalizeLogTreatsHTTPAndGRPCFailuresAsErrors(t *testing.T) {
|
|
data := []byte(`{"resourceLogs":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"api"}}]},"scopeLogs":[{"logRecords":[{"timeUnixNano":"1230000000","body":{"stringValue":"request failed"},"attributes":[{"key":"http.response.status_code","value":{"intValue":"503"}}]},{"timeUnixNano":"1240000000","body":{"stringValue":"rpc failed"},"attributes":[{"key":"rpc.grpc.status_code","value":{"intValue":"14"}}]}]}]}]}`)
|
|
events, err := NormalizeEntry("logs", 99, data)
|
|
if err != nil || len(events) != 2 {
|
|
t.Fatalf("normalize failed: %v %+v", err, events)
|
|
}
|
|
if !events[0].IsError || events[0].StatusClass != "5xx" || !events[1].IsError {
|
|
t.Fatalf("HTTP/gRPC failures must be errors: %+v", events)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeOperationFlagsComeFromStructuredAttributes(t *testing.T) {
|
|
data := []byte(`{"resourceLogs":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"api"}}]},"scopeLogs":[{"logRecords":[{"timeUnixNano":"1230000000","severityNumber":17,"body":{"stringValue":"opaque message"},"attributes":[{"key":"retry.count","value":{"intValue":"1"}},{"key":"request.cancelled","value":{"boolValue":true}}]}]}]}]}`)
|
|
events, err := NormalizeEntry("logs", 99, data)
|
|
if err != nil || len(events) != 1 {
|
|
t.Fatalf("normalize failed: %v %+v", err, events)
|
|
}
|
|
if !events[0].IsRetry || !events[0].IsCancellation || events[0].IsTimeout {
|
|
t.Fatalf("unexpected structured flags: %+v", events[0])
|
|
}
|
|
}
|
|
|
|
func TestNormalizeMetricDatapointsAndHistogram(t *testing.T) {
|
|
data := []byte(`{"resourceMetrics":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"worker"}}]},"scopeMetrics":[{"metrics":[{"name":"queue.depth","unit":"items","gauge":{"dataPoints":[{"timeUnixNano":"1230000000","asInt":"7","attributes":[{"key":"queue","value":{"stringValue":"payments"}}]}]}},{"name":"request.duration","unit":"ms","histogram":{"dataPoints":[{"timeUnixNano":"1240000000","count":"3","sum":12.5,"min":2,"max":6}]}}]}]}]}`)
|
|
events, err := NormalizeEntry("metrics", 99, data)
|
|
if err != nil || len(events) != 2 {
|
|
t.Fatalf("normalize failed: %v %+v", err, events)
|
|
}
|
|
if events[0].MetricName != "queue.depth" || events[0].MetricType != "gauge" || events[0].MetricValue != 7 || events[0].Timestamp != 1230000000 {
|
|
t.Fatalf("unexpected gauge: %+v", events[0])
|
|
}
|
|
if events[1].MetricType != "histogram" || events[1].MetricCount != 3 || events[1].MetricSum != 12.5 || events[1].MetricMin != 2 || events[1].MetricMax != 6 {
|
|
t.Fatalf("unexpected histogram: %+v", events[1])
|
|
}
|
|
}
|
|
|
|
func TestNormalizeHTTPRoutePrefersSpecificPathOverRootMount(t *testing.T) {
|
|
data := []byte(`{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"web"}}]},"scopeSpans":[{"spans":[{"name":"GET /","attributes":[{"key":"http.route","value":{"stringValue":"/"}},{"key":"url.path","value":{"stringValue":"/api/controller/instances"}}]}]}]}]}`)
|
|
events, err := NormalizeEntry("traces", 99, data)
|
|
if err != nil || len(events) != 1 {
|
|
t.Fatalf("normalize failed: %v %+v", err, events)
|
|
}
|
|
if events[0].Route != "/api/controller/instances" {
|
|
t.Fatalf("expected specific URL path, got %q", events[0].Route)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeHTTPRequestMetadataIsSafeAndNormalized(t *testing.T) {
|
|
data := []byte(`{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"preview"}}]},"scopeSpans":[{"spans":[{"traceId":"trace-1","name":"GET /","attributes":[{"key":"http.method","value":{"stringValue":"GET"}},{"key":"http.host","value":{"stringValue":"preview.example.test:443"}},{"key":"http.target","value":{"stringValue":"/"}},{"key":"http.user_agent","value":{"stringValue":"Chrome"}},{"key":"http.request.header.origin","value":{"stringValue":"https://app.example.test"}},{"key":"http.request.header.referer","value":{"stringValue":"https://app.example.test/dashboard?secret=1"}},{"key":"http.request.header.sec_fetch_site","value":{"stringValue":"same-origin"}},{"key":"http.request.header.sec_fetch_mode","value":{"stringValue":"navigate"}}]}]}]}]}`)
|
|
events, err := NormalizeEntry("traces", 99, data)
|
|
if err != nil || len(events) != 1 {
|
|
t.Fatalf("normalize failed: %v %+v", err, events)
|
|
}
|
|
event := events[0]
|
|
if event.Method != "GET" || event.Host != "preview.example.test" || event.UserAgent != "Chrome" {
|
|
t.Fatalf("unexpected request metadata: %+v", event)
|
|
}
|
|
if event.OriginHost != "app.example.test" || event.RefererHost != "app.example.test" {
|
|
t.Fatalf("request metadata must retain hosts without paths: %+v", event)
|
|
}
|
|
if event.FetchSite != "same-origin" || event.FetchMode != "navigate" {
|
|
t.Fatalf("missing fetch metadata: %+v", event)
|
|
}
|
|
}
|
|
|
|
func TestIsErrorStatusIgnoresMissingResources(t *testing.T) {
|
|
for code, want := range map[int]bool{200: false, 404: false, 410: false, 401: true, 500: true} {
|
|
if got := isErrorStatus(code); got != want {
|
|
t.Errorf("isErrorStatus(%d) = %v, want %v", code, got, want)
|
|
}
|
|
}
|
|
}
|