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/tracer ↔ metric 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/config — three 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.
Describe the feature you'd like
Remove
google.golang.org/grpcfrom 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 ingithub.com/grpc-ecosystem/grpc-gateway/v2andgoogle.golang.org/genproto/googleapis/api.CONTRIBUTING.mdalready singles it out by name: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/opentelemetryto regain control over what the root module pulls in — and the samev3gate. 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, andopenfeature(transitively, viametric).Worth noting for #4190's scoping:
ddtrace/opentelemetryitself — the trace bridge — is already gRPC-free and imports neitherlognormetric. 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:
go.opentelemetry.io/proto/otlp/collector/{logs,metrics}/v1generates 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 theExport*ServiceRequestmessages. Importing it for the message type alone linksgrpc,grpc/codes,grpc/status,grpc/metadata,grpc/grpclogandgrpc-gateway/v2/runtime.Measured: removing only
otlploggrpc+otlpmetricgrpcdrops 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/otlpv1.11.0 (latest): still shipslogs_service_grpc.pb.goandlogs_service.pb.gw.go;go.modstill requiresgrpcandgrpc-gateway/v2.otlploghttpv0.20.0 andotlpmetrichttpv1.44.0 (latest): still import non-slimcollector/logs/v1, still require gRPC indirectly.go.opentelemetry.io/proto/slim/otlpexists with gRPC-freecollector/*packages, but it declares the same protobuf package (opentelemetry.proto.logs.v1) as non-slimgo.opentelemetry.io/proto/otlp, whichddtrace/traceralready uses — linking both panics at init throughprotoregistry. As a library we cannot adopt it: any user who also links non-slim otlp (viaotlptracegrpc, 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.go393 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 ~10OTEL_EXPORTER_OTLP_*variables thatoconf.NewHTTPConfigcurrently 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,WithGRPCExporterandWithExporter, 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 isv3-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*Exporteroptions.What makes this cheap is that the coupling is already almost entirely broken:
ddtrace/opentelemetry/loghas zero in-repo consumers.ddtrace/tracer↔metricis already decoupled throughinternal/otelmetricsinstall; the tracer never imports the metric package.openfeatureneeds bridging, and the hook pattern to do it already exists.The prerequisite work is that a published nested module (
github.com/DataDog/dd-trace-go/ddtrace/opentelemetry/log/v2) cannot importgithub.com/DataDog/dd-trace-go/v2/internal/..., so both packages must first move onto the publicinstrumentationbridge thatcontrib/modules already use. Enumerated, that is smaller than it sounds —env,log,versionand most oftelemetryalready have public equivalents. The gaps:internal/config— three booleans only:LogsOTelEnabled(),RuntimeMetricsOtelEnabled(),OTLPExportMetricsMode().internal/telemetry.Configuration+RegisterAppConfigs— nocontrib/module does config telemetry, so this is the one genuinely missing piece of the bridge.internal.ForEachStringTag/ParseTagString/OtelTagsDelimeter— small tag-parsing helpers.internal/otelmetricsinstall, since the relocated module cannot importinternal/.Additional context
Why
v3: the import path must change fromgithub.com/DataDog/dd-trace-go/v2/ddtrace/opentelemetry/logtogithub.com/DataDog/dd-trace-go/ddtrace/opentelemetry/log/v2(the major-version suffix has to be last forgo getto resolve a nested module, which is why everycontrib/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/v2fixalready 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:
openfeatureflag-evaluation metrics would become opt-in via a blank import. Default behaviour is unaffected — that provider is already a no-op unlessDD_METRICS_OTEL_ENABLED=true— so it only affects users who explicitly enabled OTel metrics and useopenfeature. 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 thego.modrequirements (viago mod edit -json, notgrep— everycontrib/go.modhas areplaceline mentioningcontrib/google.golang.org/grpc/v2) and the package graph (go list -deps -test).GOWORK=offis mandatory: with the workspace activego list -m allreports 1224 modules versus 198 without it, so any assertion made in workspace mode is measuring the workspace rather than whatgo getgives a user.Payoff: a customer not using OTel logs/metrics drops 161 packages from their build and stops inheriting gRPC's CVE stream entirely.