diff --git a/cmd/root/otel_test.go b/cmd/root/otel_test.go index 341e1ae924..d66ab9ce17 100644 --- a/cmd/root/otel_test.go +++ b/cmd/root/otel_test.go @@ -1,19 +1,104 @@ package root import ( + "context" + "io" + "net/http" + "net/http/httptest" + "os" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - semconv "go.opentelemetry.io/otel/semconv/v1.43.0" + coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1" + "google.golang.org/protobuf/proto" ) +// A literal, not semconv.SchemaURL, so a semconv bump is an explicit, +// reviewed change. NoError also guards against ErrSchemaURLConflict if +// resource.Default()'s semconv version diverges. func TestNewOTelResourceUsesCurrentSchemaURL(t *testing.T) { t.Parallel() res, err := newOTelResource() require.NoError(t, err) - assert.Equal(t, semconv.SchemaURL, res.SchemaURL()) + assert.Equal(t, "https://opentelemetry.io/schemas/1.43.0", res.SchemaURL()) +} + +// TestTraceExportEmitsResourceSchemaURL verifies the schema URL on the +// wire: the OTLP payload's resource schema_url must be the documented +// literal, while the scope schema_url stays empty because docker-agent +// sets none on its own tracers. +func TestTraceExportEmitsResourceSchemaURL(t *testing.T) { + // Not parallel: t.Setenv. Clear env that would alter sampling or + // body encoding; endpoint env vars are already overridden by the + // explicit WithEndpointURL. + for _, key := range []string{ + "OTEL_TRACES_SAMPLER", + "OTEL_TRACES_SAMPLER_ARG", + "OTEL_EXPORTER_OTLP_COMPRESSION", + "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION", + } { + t.Setenv(key, "") + os.Unsetenv(key) + } + + // Empty ExportTraceServiceResponse: the full-success OTLP reply. + respBody, err := proto.Marshal(&coltracepb.ExportTraceServiceResponse{}) + require.NoError(t, err) + + type export struct { + path string + body []byte + readErr error + } + exports := make(chan export, 4) + + // No assertions here — the handler runs on a server goroutine. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, readErr := io.ReadAll(r.Body) + select { + case exports <- export{path: r.URL.Path, body: body, readErr: readErr}: + default: + } + w.Header().Set("Content-Type", "application/x-protobuf") + _, _ = w.Write(respBody) + })) + t.Cleanup(srv.Close) + + res, err := newOTelResource() + require.NoError(t, err) + tp, err := newTracerProvider(t.Context(), res, srv.URL) + require.NoError(t, err) + t.Cleanup(func() { + // t.Context() is already canceled in cleanup; WithoutCancel + // detaches it so shutdown still gets a bounded 5s window. + ctx, cancel := context.WithTimeout(context.WithoutCancel(t.Context()), 5*time.Second) + defer cancel() + _ = tp.Shutdown(ctx) + }) + + _, span := tp.Tracer("test").Start(t.Context(), "schema-url-probe") + span.End() + require.NoError(t, tp.ForceFlush(t.Context())) + + var got export + select { + case got = <-exports: + case <-time.After(10 * time.Second): + t.Fatal("no OTLP trace export received") + } + require.NoError(t, got.readErr) + assert.Equal(t, "/v1/traces", got.path) + + var req coltracepb.ExportTraceServiceRequest + require.NoError(t, proto.Unmarshal(got.body, &req)) + require.Len(t, req.ResourceSpans, 1) + rs := req.ResourceSpans[0] + assert.Equal(t, "https://opentelemetry.io/schemas/1.43.0", rs.SchemaUrl) + require.Len(t, rs.ScopeSpans, 1) + assert.Empty(t, rs.ScopeSpans[0].SchemaUrl) } // TestProvidersWithoutEndpoint verifies all three providers build cleanly diff --git a/docs/community/opentelemetry/index.md b/docs/community/opentelemetry/index.md index ae6b3c9a13..7f273601d6 100644 --- a/docs/community/opentelemetry/index.md +++ b/docs/community/opentelemetry/index.md @@ -90,6 +90,23 @@ docker agent run agent.yaml --otel > > Both backends accept the traces signal only. Docker Agent also wires metric and log exporters at the same endpoint, so their periodic exports return `404` against trace-only backends. This is harmless to traces but appears in the debug log. Point a full OTLP collector at the endpoint if you also want metrics and logs. +## Schema URL + +Every OTLP export — traces, metrics, and logs — carries a resource `schema_url` that declares which version of the [semantic conventions](https://opentelemetry.io/docs/specs/semconv/) the resource attributes follow. Docker Agent emits: + +```text +https://opentelemetry.io/schemas/1.43.0 +``` + +The URL resolves to the published [1.43.0 schema](https://opentelemetry.io/schemas/1.43.0) and identifies the conventions the data was produced under. Docker Agent sets no schema URL on its own tracer, meter, or logger scopes, so their instrumentation-scope `schema_url` fields are empty. Bundled instrumentation can set one — the ADK tracer (`gcp.vertex.agent`, active when serving A2A) declares `1.36.0` on its scope. A non-empty scope `schema_url` applies only to that scope's data and is independent of the resource `schema_url`. + +For most consumers this is pass-through metadata: collectors and backends that ingest or forward OTLP need no changes when the version advances. Schema-aware consumers can use the URL to translate telemetry between convention versions. + +> [!NOTE] +> **Pipelines that validate or translate schemas** +> +> If your pipeline checks schema URLs against an allowlist or translates telemetry from bundled schema definitions (for example the Collector's schema processor), add `1.43.0` to its supported versions when upgrading Docker Agent — before the OTel SDK 1.45 migration, Docker Agent emitted `https://opentelemetry.io/schemas/1.41.0`. No published schema transformation applies to Docker Agent telemetry in the move from `1.41.0` to `1.43.0`: the `1.42.0` schema's only transform renames a `v8js` metric Docker Agent does not emit, and `1.43.0` lists no transforms. + ## Inspecting traces locally Use `--debug` to print telemetry activity to the debug log (`~/.cagent/cagent.debug.log` by default) without standing up a backend: diff --git a/go.mod b/go.mod index 1fa72dc116..ef74c20490 100644 --- a/go.mod +++ b/go.mod @@ -80,6 +80,7 @@ require ( go.opentelemetry.io/otel/sdk/log v0.21.0 go.opentelemetry.io/otel/sdk/metric v1.45.0 go.opentelemetry.io/otel/trace v1.45.0 + go.opentelemetry.io/proto/otlp v1.11.0 go.yaml.in/yaml/v4 v4.0.0-rc.6 golang.org/x/image v0.45.0 golang.org/x/net v0.58.0 @@ -89,6 +90,7 @@ require ( golang.org/x/term v0.45.0 google.golang.org/adk/v2 v2.2.1-0.20260818092052-16e33673bc9e google.golang.org/genai v1.69.0 + google.golang.org/protobuf v1.36.11 gopkg.in/dnaeon/go-vcr.v4 v4.0.7 gotest.tools/v3 v3.5.2 modernc.org/sqlite v1.57.0 @@ -213,7 +215,6 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect - go.opentelemetry.io/proto/otlp v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/crypto v0.55.0 // indirect golang.org/x/mod v0.38.0 // indirect @@ -224,7 +225,6 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect google.golang.org/grpc v1.83.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.74.4 // indirect