Skip to content

feat: add OpenTelemetry tracing and Pyroscope profiling to the router - #417

Open
anantadwi13 wants to merge 8 commits into
mainfrom
improve-profiling-tracing-implementation
Open

feat: add OpenTelemetry tracing and Pyroscope profiling to the router#417
anantadwi13 wants to merge 8 commits into
mainfrom
improve-profiling-tracing-implementation

Conversation

@anantadwi13

@anantadwi13 anantadwi13 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Migrates engines/router's tracer from OpenTracing/Jaeger-client to OpenTelemetry (OTLP HTTP exporter, W3C + B3 propagation), and adds pyroscope-go continuous profiling to both engines/router and api.
    api also gains its own OTel HTTP-layer tracing alongside New Relic, with OTel/Pyroscope config templated into deployed router pods via RouterDefaults.
  • Restores the classic Jaeger/Thrift tracer in engines/router as a deprecated, independently-toggleable backend that can run simultaneously alongside OTel (a MultiTracer fans out span creation to both,
    keeping each backend's parent-span chain independent), since a downstream system still requires Jaeger's native Thrift ingestion. Every Jaeger* identifier/env var/OpenAPI field is marked deprecated in favour of
    the Otel equivalent, with no removal date yet.
  • Tags router Pyroscope profiles with pod_name/pod_namespace (via the Kubernetes downward API), so individual replicas of a multi-replica router deployment can be told apart in Pyroscope, alongside the
    existing router_name tag. Includes a RouterDefaults.PyroscopeIncludePodTags flag (default true) to opt out deployment-wide.
  • Unbreaks CI after the above: bumps Go 1.22 → 1.25 and golangci-lint 1.56 → 2.12 across api/engines/router/engines/experiment (the new OTel exporter's dependency graph requires it), migrates
    .golangci.yml to the v2 config schema, and fixes the vet/lint findings that only surfaced under the new toolchain.
  • Fixes router deploys failing outright on clusters where Knative's kubernetes.podspec-fieldref feature gate is disabled (the default) — Knative's admission webhook rejects the POD_NAME/POD_NAMESPACE
    downward-API env vars used for pod-identity tagging above. DeployKnativeService now dry-runs the deploy first and silently drops those env vars if the cluster rejects them, using only the create/update
    permission Turing's deploy credential already needs — no new RBAC required on any target cluster.
  • Exposes the router's OTel trace sampling ratio as RouterDefaults.OtelSamplingRatio (env var APP_OTEL_SAMPLING_RATIO), which previously had no way to be configured from the API even though the router engine
    already supported it. Also lowers the default sampling ratio from 1 (sample everything) to 0.01 for the API's own tracing, the new router default, and the router engine's own fallback.

Testing

  • api/engines/router unit tests pass (servicebuilder, config, instrumentation/profiling, instrumentation/tracing).
  • cluster package coverage for the dry-run fallback (TestDeployKnativeServiceFieldRefRejectedFallsBackWithoutIt) verifies the fieldRef env vars are stripped and the deploy retried when the target cluster
    rejects them.
  • CI's e2e (test-e2e) exercises the fallback path implicitly, since its Knative Serving version predates the kubernetes.podspec-fieldref feature flag entirely — there's currently no e2e coverage of a cluster
    where the flag is enabled.

@anantadwi13
anantadwi13 force-pushed the improve-profiling-tracing-implementation branch from bddd64c to 8e79a92 Compare August 18, 2026 16:23
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.38%. Comparing base (b8399c8) to head (6ba301d).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #417   +/-   ##
=======================================
  Coverage   96.38%   96.38%           
=======================================
  Files          26       26           
  Lines        2101     2101           
=======================================
  Hits         2025     2025           
  Misses         76       76           
Flag Coverage Δ
sdk-test-3.10 96.38% <ø> (ø)
sdk-test-3.11 96.38% <ø> (ø)
sdk-test-3.12 96.38% <ø> (ø)
sdk-test-3.9 96.38% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@anantadwi13
anantadwi13 force-pushed the improve-profiling-tracing-implementation branch 2 times, most recently from 8e79a92 to 0baf466 Compare August 19, 2026 04:35
anantadwi13 and others added 4 commits August 19, 2026 11:36
Migrate engines/router's tracer from OpenTracing/Jaeger-client to
OpenTelemetry (OTLP HTTP exporter, W3C + B3 trace-context propagation),
and add pyroscope-go continuous profiling to both engines/router and
api. api also gains its own OTel HTTP-layer tracing alongside New
Relic, and templates Pyroscope config (including generic HTTP auth
headers) into deployed router pods via RouterDefaults.

- engines/router: new OTel-based tracing.Tracer interface, B3
  propagation, pyroscope-go profiling package, dev-compose wiring.
- api: OTel tracing + pyroscope-go profiler in server.Run(), Otel/
  Pyroscope config sections, PyroscopeEnabled persisted on
  RouterVersion.LogConfig, env vars injected into router pods.
- Config validation: required_if=Enabled checks and empty-address
  guards for OTel/Pyroscope endpoints.
- OpenAPI spec, example.yaml, and generated SDK client updated to
  match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… config to api

Restores the classic Jaeger/Thrift tracer in engines/router as a deprecated,
independently-toggleable backend that runs simultaneously alongside the
existing OTel/OTLP backend, since a downstream system still requires
Jaeger's native Thrift ingestion. A MultiTracer fans out span creation to
both backends when both are enabled, tracking each backend's own parent
span chain independently so nested spans stay correctly parented per
backend.

engines/router:
- Restore JaegerConfig (Enabled/CollectorEndpoint/ReporterAgentHost/
  ReporterAgentPort, Thrift transport), add a separate OtelConfig
  (Enabled/CollectorEndpoint/SamplingRatio, OTLP HTTP).
- New JaegerTracer adapts the classic jaeger-client-go (OpenTracing API)
  onto the trace.Tracer/trace.Span API directly via a hand-rolled facade
  (go.opentelemetry.io/otel/bridge/opentracing bridges the opposite
  direction and can't be used for this).
- New MultiTracer fans out to every enabled backend via a dedicated
  context key (multiParentsKey) so each backend's nested spans parent
  from its own prior span, not a shared identity.
- Tracer interface drops InitGlobalTracer in favour of typed
  newOtelTracer/newJaegerTracer constructors, since the two backends take
  different config types.
- Local dev compose/env wired for both backends.

api (purely additive — nothing persisted, public, or SDK-facing renamed):
- RouterDefaults, models.LogConfig, and the OpenAPI contract gain
  OtelEnabled/OtelCollectorEndpoint next to the existing (now
  Deprecated:-commented) Jaeger fields.
- servicebuilder injects APP_OTEL_ENABLED/APP_OTEL_COLLECTOR_ENDPOINT
  alongside the existing Jaeger env vars on every deployed router.
- Go and Python generated clients regenerated from the updated spec.

Every Jaeger* identifier, config field, env var, and the OpenAPI
jaeger_enabled property is marked deprecated in favour of the Otel
equivalent, with no removal date yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Injects POD_NAME/POD_NAMESPACE into the router container via the
Kubernetes downward API and reports them as pod_name/pod_namespace
Pyroscope tags (alongside the existing router_name tag), so profiles
from individual replicas of a router deployment can be told apart.

A new RouterDefaults.PyroscopeIncludePodTags flag (default true) lets
an operator opt out of the pod tags deployment-wide, e.g. if per-pod
label cardinality is undesirable in Pyroscope. It's templated into
routers as APP_PYROSCOPE_INCLUDE_POD_TAGS, mirroring how
PyroscopeServerAddress/PyroscopeHTTPHeaders are already passed through.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Bump Go 1.22 -> 1.25 across api/router/experiment/hardcoded-plugin
  go.mod files (CI's GO_VERSION, and the golang:1.22-alpine base image
  in each component's own build Dockerfile): the new OTel OTLP HTTP
  exporter pulls grpc-gateway v2.19.0 -> kr/pretty v0.3.1, whose
  fmtsort import only exists in go-internal v1.16.0+, which requires
  go>=1.25. This only surfaced in engines/experiment (and its example
  plugin) because both locally `replace` engines/router, so `go mod
  tidy`/`vendor` must resolve the combined dependency graph.
- Bump golangci-lint 1.56.2 -> 2.12.2 (and golangci-lint-action v2/v3
  -> v7, migrating the shared .golangci.yml to the v2 config schema):
  no v1.x golangci-lint release supports Go 1.25's export data format.
  v2 also merges the `gosimple` linter into `staticcheck`, which by
  default pulls in the `stylecheck`/quickfix check families this repo
  never enabled -- restricted staticcheck.checks to keep the same
  check scope as before.
- Add //nolint:staticcheck at every intentional usage of the
  newly-deprecated Jaeger config fields/types, since the PR's own
  backward-compat code paths trigger SA1019 on their own deprecated
  symbols.
- Fix vet/lint findings that only surfaced once Go 1.25's govet and
  golangci-lint v2.12.2 ran over the affected packages: non-constant
  format strings passed to fmt.Errorf, reflect.Ptr -> reflect.Pointer,
  a named return shadowing the builtin `error` identifier, and two
  redundant nil-checks before len() on slices.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…C needed

Knative's Revision admission webhook rejects env[].valueFrom.fieldRef
unless the alpha kubernetes.podspec-fieldref feature gate is set to
Enabled in the target cluster (disabled by default), so router deploys
were failing outright with:

  admission webhook "validation.webhook.serving.knative.dev" denied
  the request: validation failed: must not set the field(s):
  ...valueFrom.fieldRef

Rather than reading Knative's own config-features ConfigMap to detect
this (which needs a new RBAC grant per target cluster, separate from
whatever Turing already needs there), probe for the capability with a
dry-run of the exact deploy request itself, using only the create/update
permission on Knative Services that Turing's deploy credential already
requires for every deploy.

DeployKnativeService now: if the built service has any
env[].valueFrom.fieldRef (the router's POD_NAME/POD_NAMESPACE), it
dry-runs the Create/Update first. If the cluster rejects it with that
specific admission error, the fields are stripped in place and a
warning is logged; otherwise the error propagates as a normal deploy
failure. The real Create/Update then proceeds as before, with or
without those fields depending on the probe.

buildRouterEnvs/NewRouterService go back to unconditionally including
the fieldRef env vars -- all the capability handling lives in the
cluster/controller layer instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…o 0.01

The router engine already supported configuring its OTel trace sampling
ratio via APP_OTEL_SAMPLING_RATIO (engines/router/missionctl/config,
default 1 -- sample everything), but Turing's API had no way to set it:
RouterDefaults had no corresponding field, and buildRouterEnvs never
set the env var, so every deployed router silently sampled 100% of
traces regardless of the API's own Otel.SamplingRatio (which only
affects the API server's own self-tracing, not routers it deploys).

Add RouterDefaults.OtelSamplingRatio, threaded into buildRouterEnvs as
APP_OTEL_SAMPLING_RATIO, so operators can configure this per Turing
deployment like the other RouterDefaults fields.

Also lower the default sampling ratio from 1 to 0.01 in three places:
the API's own Otel.SamplingRatio, the new RouterDefaults.OtelSamplingRatio
default, and the router engine's own default when the env var is unset
-- sampling everything is rarely necessary and gets expensive at scale.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Trailing newline only, no content changes. Touches sdk/** so this PR's
sdk workflow computes a fresh dev version and re-runs the publish job,
after the previous run collided with an already-published PyPI version
(0.16.2.post10.dev0 -- the dev version scheme derives its "postN" purely
from commit count since the last tag, which is shared across branches,
so an unrelated commit elsewhere had already claimed that exact version).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds RouterDefaults.PyroscopeCustomTags (map[string]string), serialized
into a single APP_PYROSCOPE_CUSTOM_TAGS env var the same way
PyroscopeHTTPHeaders already is. The router merges these into its
Pyroscope tags alongside the built-in router_name/pod_name/
pod_namespace tags, which always win on key collision.

Also adds the same CustomTags support to the Turing API's own
Pyroscope config (config.PyroscopeConfig, api/turing/server/
instrumentation.go), which profiles the API server itself and
previously had no way to attach custom tags. It further adds
Pyroscope.IncludePodTags (default true), tagging API profiles with
pod_name/pod_namespace so individual replicas can be told apart -- the
POD_NAME/POD_NAMESPACE env vars are left for the deployer to populate
via the Kubernetes downward API (e.g. turing.extraEnvs in the Helm
chart), since the API's own deployment isn't templated by this
codebase the way routers are.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant