Skip to content

Remove google.golang.org/grpc from the root module [breaking change] #5084

Description

@darccio

Describe the feature you'd like

Remove google.golang.org/grpc from the root module's dependency graph.

It is currently an indirect dependency (v1.82.1) that no root-module code imports directly, yet it accounts for 161 of the root module's 751 build-graph packages (21%) and drags in github.com/grpc-ecosystem/grpc-gateway/v2 and google.golang.org/genproto/googleapis/api.

CONTRIBUTING.md already singles it out by name:

When adding a new dependency, especially for contrib/ packages, prefer the minimum secure versions of any modules rather than the latest versions. This is to avoid forcing upgrades on downstream users for modules such as google.golang.org/grpc which often introduce breaking changes within minor versions.

It is also a recurring source of CVE-bump churn that we pass on to every customer — most recently #5075 (GO-2026-6061).

This is a sibling of #4190 (discussion #4189): same motivation — isolating ddtrace/opentelemetry to regain control over what the root module pulls in — and the same v3 gate. This issue narrows it to the specific mechanism that makes gRPC unremovable today, because that mechanism is not what you would expect.

Is your feature request related to a problem?

Only three root packages pull gRPC in

ddtrace/opentelemetry/log, ddtrace/opentelemetry/metric, and openfeature (transitively, via metric).

Worth noting for #4190's scoping: ddtrace/opentelemetry itself — the trace bridge — is already gRPC-free and imports neither log nor metric. The gRPC problem is narrower than #4190 and can be separated from it.

Dropping the OTLP/gRPC exporters is not sufficient

This is the non-obvious part. The OTLP HTTP exporters pull gRPC in as well:

otlploghttp → go.opentelemetry.io/proto/otlp/collector/logs/v1 → google.golang.org/grpc

go.opentelemetry.io/proto/otlp/collector/{logs,metrics}/v1 generates the gRPC service stubs (logs_service_grpc.pb.go) and the grpc-gateway shim (logs_service.pb.gw.go) into the same Go package as the Export*ServiceRequest messages. Importing it for the message type alone links grpc, grpc/codes, grpc/status, grpc/metadata, grpc/grpclog and grpc-gateway/v2/runtime.

Measured: removing only otlploggrpc + otlpmetricgrpc drops 17 packages and leaves gRPC in place. Any use of the upstream OTLP exporters — HTTP or gRPC — requires gRPC.

There is no upstream fix to wait for

  • go.opentelemetry.io/proto/otlp v1.11.0 (latest): still ships logs_service_grpc.pb.go and logs_service.pb.gw.go; go.mod still requires grpc and grpc-gateway/v2.
  • otlploghttp v0.20.0 and otlpmetrichttp v1.44.0 (latest): still import non-slim collector/logs/v1, still require gRPC indirectly.

go.opentelemetry.io/proto/slim/otlp exists with gRPC-free collector/* packages, but it declares the same protobuf package (opentelemetry.proto.logs.v1) as non-slim go.opentelemetry.io/proto/otlp, which ddtrace/tracer already uses — linking both panics at init through protoregistry. As a library we cannot adopt it: any user who also links non-slim otlp (via otlptracegrpc, otelcol packages, …) would crash in their own binary. Verified that slim is not in our build graph today, so there is no latent conflict — but this is also why upstream cannot simply switch.

Describe alternatives you've considered

1. Implement our own OTLP/HTTP exporter in the root module. Rejected.

The SDK→proto transforms live in upstream internal/ and cannot be imported, only vendored: ~1000 LOC (otlploghttp/internal/transform/log.go 393 LOC, otlpmetrichttp/internal/transform/* 604 LOC) permanently pinned to two upstream versions, plus ~430 LOC of gzip / retry / partial-success handling. It would also mean taking over the ~10 OTEL_EXPORTER_OTLP_* variables that oconf.NewHTTPConfig currently handles for us at zero cost — missing any one of them is a silent customer-facing config regression.

It additionally forces breaking changes on metric.WithHTTPExporter, WithGRPCExporter and WithExporter, whose parameter types are the packages being removed, so their signatures cannot be preserved under this option. (These have been stable public API since v2.5.0.)

Not worth it: a little copying is not better than a little dependency here.

2. Relocate ddtrace/opentelemetry/{log,metric} into nested modules. Preferred, and the reason this is v3-gated.

Keeps the upstream exporters untouched: no vendored code, no reimplemented transports, no env-var surface to take over, OTLP-over-gRPC support unchanged, and every exported signature preserved — including the three With*Exporter options.

What makes this cheap is that the coupling is already almost entirely broken:

  • ddtrace/opentelemetry/log has zero in-repo consumers.
  • ddtrace/tracermetric is already decoupled through internal/otelmetricsinstall; the tracer never imports the metric package.
  • Only openfeature needs bridging, and the hook pattern to do it already exists.
  • Total: ~6100 LOC relocated without rewriting.

The prerequisite work is that a published nested module (github.com/DataDog/dd-trace-go/ddtrace/opentelemetry/log/v2) cannot import github.com/DataDog/dd-trace-go/v2/internal/..., so both packages must first move onto the public instrumentation bridge that contrib/ modules already use. Enumerated, that is smaller than it sounds — env, log, version and most of telemetry already have public equivalents. The gaps:

  • internal/configthree booleans only: LogsOTelEnabled(), RuntimeMetricsOtelEnabled(), OTLPExportMetricsMode().
  • internal/telemetry.Configuration + RegisterAppConfigs — no contrib/ module does config telemetry, so this is the one genuinely missing piece of the bridge.
  • internal.ForEachStringTag / ParseTagString / OtelTagsDelimeter — small tag-parsing helpers.
  • A public replacement for internal/otelmetricsinstall, since the relocated module cannot import internal/.

Additional context

Why v3: the import path must change from github.com/DataDog/dd-trace-go/v2/ddtrace/opentelemetry/log to github.com/DataDog/dd-trace-go/ddtrace/opentelemetry/log/v2 (the major-version suffix has to be last for go get to resolve a nested module, which is why every contrib/ module already looks like this). A root-module forwarding shim is impossible — it would re-import the moved package and bring gRPC straight back — so this cannot be softened into a non-breaking change.

User migration is otherwise mechanical: one go get, one import line per package, and no call-site changes, since all 16 exported symbols across the two packages keep identical signatures. tools/v2fix already has an import-rewrite checker (V1ImportURL) that this could extend, making it a single command.

The one behavioural change to call out in release notes: openfeature flag-evaluation metrics would become opt-in via a blank import. Default behaviour is unaffected — that provider is already a no-op unless DD_METRICS_OTEL_ENABLED=true — so it only affects users who explicitly enabled OTel metrics and use openfeature. The hook path should warn rather than silently no-op in that case.

Regression guardrail (worth landing independently of v3, asserting the current state rather than zero): a CI check that fails if gRPC re-enters the root module. It needs to assert on both the go.mod requirements (via go mod edit -json, not grep — every contrib/go.mod has a replace line mentioning contrib/google.golang.org/grpc/v2) and the package graph (go list -deps -test). GOWORK=off is mandatory: with the workspace active go list -m all reports 1224 modules versus 198 without it, so any assertion made in workspace mode is measuring the workspace rather than what go get gives a user.

Payoff: a customer not using OTel logs/metrics drops 161 packages from their build and stops inheriting gRPC's CVE stream entirely.

Metadata

Metadata

Assignees

No one assigned

    Labels

    nextFor future versionsproposalmore in depth change that requires full team approval

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions