Skip to content

feat(metrics): Add runtime-switchable DogStatsD UDS support, migrate Rust to metrics-exporter-dogstatsd, and propagate runtime global tags#7796

Merged
phacops merged 45 commits into
masterfrom
feat/dogstatsd-uds-support
Jul 15, 2026
Merged

feat(metrics): Add runtime-switchable DogStatsD UDS support, migrate Rust to metrics-exporter-dogstatsd, and propagate runtime global tags#7796
phacops merged 45 commits into
masterfrom
feat/dogstatsd-uds-support

Conversation

@phacops

@phacops phacops commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replace the Rust metrics pipeline with metrics-exporter-dogstatsd (native DogStatsD protocol, client-side aggregation, UDP + Unix domain socket transports)
  • Add Unix domain socket (UDS) support for DogStatsD on both the Python and Rust sides, selectable at runtime via the use_dogstatsd_uds sentry-option
  • Add runtime global tag propagation so tags set via set_global_tag() (e.g. assigned_partitions, min_partition during Kafka rebalancing) are included in every DogStatsD metric payload
  • Remove duplicate storage/consumer_group tag setting in the Rust consumers, since those are already passed as static labels to the DogStatsD builder

Transport selection

The use_dogstatsd_uds sentry-option (in the snuba namespace, read by both Python via snuba.state.sentry_options.get_option and Rust via sentry_options::options("snuba"), same key) selects the preferred transport. A deployment configured with only one transport uses that one regardless of the flag; the flag is authoritative only when both are configured (a configured socket never overrides an available UDP target while the flag is off), so host/port stay configured as the UDP rollback target.

Configuration flag false (default) flag true
host/port and socket UDP UDS
host/port only UDP UDP
socket only (no host/port) UDS UDS
neither no metrics no metrics

DOGSTATSD_HOST/DOGSTATSD_PORT come from SNUBA_STATSD_ADDR (or DOGSTATSD_HOST + DOGSTATSD_PORT). The socket address comes from SNUBA_DOGSTATSD_SOCKET_PATH and is a full address including the transport scheme (e.g. unixgram:///run/dogstatsd.sock for datagram, unix:///run/dogstatsd.sock for stream). It is passed verbatim to both runtimes — the Rust exporter parses the scheme, and the Python datadog client strips it and selects the socket kind — so no scheme is hardcoded on either side.

Switching between UDP and UDS (rollout / rollback)

  1. Prepare (one deploy): configure both the UDP target and SNUBA_DOGSTATSD_SOCKET_PATH, and leave the use_dogstatsd_uds sentry-option at false. Everything runs on UDP.
  2. Switch to UDS: set the use_dogstatsd_uds sentry-option to true, then restart the consumers (rolling restart — no redeploy).
  3. Roll back to UDP: set it back to false and restart. Instant rollback, no redeploy.
  4. Make UDS permanent: keep the flag on and leave host/port configured as the dormant fallback.

Notes

  • With both host/port and a socket configured, the flag toggles between them and host/port is the UDP rollback target. A deployment with only a socket configured sends over UDS regardless of the flag (there is no UDP target to prefer or fall back to); one with only host/port sends over UDP.
  • The flag takes effect on the next restart, not live: both runtimes build the backend once (metrics-exporter-dogstatsd's .install() and arroyo's metrics::init() are once-per-process; Python's metrics is a module-level singleton whose client factory resolves the transport once per process on first metric emission). A rolling restart with no code change is the intended mechanism.
  • This toggles UDP↔UDS within the new metrics-exporter-dogstatsd backend; there is no runtime fallback to the previous statsdproxy pipeline (that needs a revert).

Runtime config source

The use_dogstatsd_uds flag lives in the sentry-options snuba namespace (schema: sentry-options/schemas/snuba/schema.json), the same store the rest of snuba's runtime config now uses. Both runtimes read the same key: Python through snuba.state.sentry_options.get_option("use_dogstatsd_uds", False) and Rust through sentry_options::options("snuba"). Reads are defensive — an uninitialized options client, a missing key, or a wrong type all default to false (prefer UDP).

Implementation

Python (create_metrics()): returns DummyMetricsBackend only when neither a UDP (host/port) nor a UDS (socket) transport is configured — a static decision, because create_metrics() runs at snuba.environment import time and importing snuba.state.sentry_options there would be a circular import. When a transport is configured it builds the DogStatsd client lazily; the factory resolves the transport once per process on first metric emission (by which point init_options() has run) — UDS when a socket is set and the flag selects it (or there's no UDP target), otherwise UDP. Resolving once per process keeps every thread on the same transport (DatadogMetricsBackend builds a client per thread). The socket address is passed to the datadog client verbatim (the client strips the scheme).

Rust: the new DogStatsDBackend adapts arroyo's Recorder trait to the metrics crate facade installed by the exporter. Histograms are emitted as DogStatsD histograms (h), not distributions, so the agent-side .avg/.count/.median/.95percentile/.max sub-metrics are unchanged — timers move from the ms wire type to h, which Datadog treats identically. A shared create_dogstatsd_backend() with a pure select_transport() applies the same gating as Python; both consumer.rs and accepted_outcomes_consumer.rs go through it. The socket address is passed to the exporter's with_remote_address verbatim (it parses the scheme). The use_dogstatsd_uds_enabled() reader uses sentry_options::options("snuba") (native, no pyo3), and init_with_schemas runs at consumer startup before the metrics backend is built.

Runtime global tags: a thread-safe global tag map (a sorted BTreeMap behind a LazyLock/RwLock) stores tags set at runtime. set_global_tag() writes to both this map and the Sentry scope; record_metric() appends every entry to each metric, so tags like assigned_partitions and min_partition appear in DogStatsD payloads.

Follows the same pattern as getsentry/relay#5675.

Test plan

  • cargo test --workspace — passes (the only failures require ClickHouse/Redis/Python infra and fail identically on master in a bare environment)
  • cargo clippy --workspace --all-targets -- -D warnings — no warnings
  • cargo fmt --check — clean
  • Unit tests for the global tag store: set/get, overwrite dedup, BTreeMap sort order (isolated per-test key namespaces)
  • Unit tests for the Rust select_transport gating matrix (incl. socket-only → UDS)
  • Python tests for create_metrics UDP/UDS/socket-only selection, process-wide transport resolution, verbatim socket address, and the no-transport dummy path
  • Verify metrics flow over UDS in staging (set the use_dogstatsd_uds sentry-option to true + restart)
  • Verify global tags propagate in DogStatsD payloads in staging

🤖 Generated with Claude Code

phacops added 5 commits March 4, 2026 13:52
Add UDS as an alternative transport for reporting metrics to DogStatsd,
alongside the existing UDP path. In Python, this is controlled by the
`use_dogstatsd_uds` runtime config flag. In Rust, the socket path is
passed through the consumer config pipeline and preferred over UDP when
present.

The default socket path is `/var/run/datadog/dsd.socket`, configurable
via the `SNUBA_DOGSTATSD_SOCKET_PATH` environment variable.

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/df5Kw0WzWjehG9JtCoOzCyP26xppU-Mei2HymuQJatM
Extract the common global tag setup into a single block, choosing the
backend (UDS vs UDP) first and then applying shared configuration.

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/PXyfvNv6PB8v5UP2payHqvUh6lSTy_8lE7QH6H8tU9o
…ixUpstream

Replace the custom UnixUpstream middleware with cadence's built-in
BufferedUnixMetricSink, which already provides the same 512-byte
buffered UDS transport. This removes a file and leverages a well-tested
library instead.

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/mpfDfVQ7ALBfclRYP4cw_R3ypl2IhChN8cxM7pcaUyY
Pass storage and consumer_group tags to the UDS backend via
StatsdRecorder::with_tag instead of the statsdproxy AddGlobalTags
middleware. set_global_tag is still called for Sentry scope tags.

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/V26YRfVXmvlDYDieSQ0UFx5soTUFz1D5coqoNKUGApk
…ogstatsd

Use metrics-exporter-dogstatsd for both UDP and UDS transports,
removing the cadence and statsdproxy dependencies. The new
DogStatsDBackend provides native DogStatsD protocol support with
client-side aggregation built in.

This simplifies the metrics pipeline from
statsdproxy (Upstream/AggregateMetrics/AddGlobalTags) + cadence to a
single exporter with built-in aggregation and global labels.

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/wuD5d2s6g_PVGjhylOUjPEALVpYSHWc_Uqq4OQ8RD6Q
@phacops phacops changed the title feat(metrics): Add Unix domain socket support for DogStatsd feat(metrics): Add UDS support and migrate to metrics-exporter-dogstatsd Mar 5, 2026
@phacops
phacops marked this pull request as ready for review March 5, 2026 17:10
@phacops
phacops requested a review from a team as a code owner March 5, 2026 17:10
Comment thread rust_snuba/src/metrics/statsd.rs Outdated
Comment thread snuba/settings/__init__.py Outdated
Comment thread rust_snuba/src/metrics/statsd.rs Outdated
Comment thread rust_snuba/src/consumer.rs Outdated
phacops added 2 commits March 5, 2026 09:27
The DogStatsDBuilder's set_global_prefix() already prepends the prefix
to all metric keys. The manual prefix in record_metric() was applying
it a second time, resulting in names like snuba.consumer.snuba.consumer.key.

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/yNYhYQs5p2MewtRCxK7gIDSQmeP3GQUcNzmewFdG1n0
Without this, the socket path always has a value, causing the Rust
consumer to unconditionally prefer UDS over UDP and crash in
environments where the socket doesn't exist.

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/gKz1Hb8a0jqABMB6pXUrvW8vduz1-NWXBsA1ck7OB20
Comment thread rust_snuba/src/metrics/statsd.rs
Comment thread snuba/utils/metrics/util.py Outdated
Comment thread rust_snuba/src/metrics/statsd.rs Outdated
The metrics-exporter-dogstatsd crate defaults to sending histograms as
distributions (d). Disable this to keep using DogStatsD histograms (h),
matching the existing behavior.

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/8WIrttAgOHZtq804L6DAZsBUhsP7cvdCPxGosPgN3mg
Comment thread rust_snuba/Cargo.lock Outdated
Re-resolve dependencies so sentry_arroyo 2.38.3 uses sentry-core 0.41.0
instead of pulling in a second sentry-core 0.46.2.

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/rjZ2lzFmeZvcAjnOk0zEK8nc-zhGDPIFUu4itpW1eAQ
Comment thread rust_snuba/src/consumer.rs
Comment thread rust_snuba/src/consumer.rs Outdated
Update rust-toolchain.toml from pinned 1.85.0 to stable channel so
newer dependency versions (e.g. time 0.3.47) can be used without
manually tracking MSRV.

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/wbVtV_UrXNIcixIo7NhOCJn5XUpV-Zx38aXqCPjQvpE
Comment thread snuba/utils/metrics/util.py
phacops and others added 2 commits March 5, 2026 11:14
rdkafka-sys 4.10.0 (pulled in by sentry_arroyo 2.38.3) requires
libcurl headers for building librdkafka from source.

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/gKXtckTzheTqwwfUjVL-pvJdVs2zUGu5PMvDmGHK0Kk
sentry_protos 0.7.1 added TraceItemType::ProcessingError. Map it to
"processing_errors" for COGS tracking.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread rust_snuba/src/metrics/statsd.rs
- Allow clippy::result_large_err on accumulator closures in factory_v2
- Allow clippy::large_enum_variant on errors::Message enum
- Use .is_multiple_of() instead of manual % check in generic_metrics

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread snuba/utils/metrics/util.py Outdated
Upgrade rdkafka-sys to 4.10.0 (matching relay) and install
libcurl4-openssl-dev in CI/Docker where needed for librdkafka 2.12.1.
Fix uuid type ambiguity in replays tests exposed by Rust 1.94, update
bench to use renamed DogStatsDBackend, suppress result_large_err lint
in python_processor_infinite, and use static metadata string in
DogStatsD adapter instead of misleading module_path!().

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/syhPGGyt06o4xJ3ijv1AtTtCMCh5v_jYJ-BqcSsYyUA
@phacops
phacops requested a review from a team as a code owner March 5, 2026 19:54
Comment thread snuba/utils/metrics/util.py Outdated
Update insta snapshots to account for new fields (fingerprint, etc.)
from updated sentry-kafka-schemas and sentry_protos versions.

Co-Authored-By: Claude <noreply@anthropic.com>

Agent transcript: https://claudescope.sentry.dev/share/Ky2hBIs_19wk0Z4QAJIvvGZlLkSg8i82ONi2XjxTzc4
claude added 2 commits June 28, 2026 15:38
create_metrics() returned DummyMetricsBackend whenever DOGSTATSD_HOST and
DOGSTATSD_PORT were both unset, and that guard ran before the UDS check. A
UDS-only deployment (use_dogstatsd_uds + DOGSTATSD_SOCKET_PATH, no UDP host/port)
would therefore silently fall back to the no-op backend and drop all metrics --
exactly the failure mode when switching fully over to UDS.

Evaluate the UDS transport before the host/port guards so UDS works as an
independent transport, matching the Rust create_dogstatsd_backend() behavior.
Adds a test for the UDS-only (no host/port) case.

Reported by Seer code review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3
The previous UDS-only fix nested the host/port guards inside 'if not use_uds:',
which broke mypy's narrowing on the UDP path (host: str | None / port: int | None
no longer matched DogStatsd's str/int params), failing the pre-commit mypy hook.

Restructure so the host/port ValueError guard runs at function scope after the
UDS early-return: UDS still works without host/port, and mypy narrows host/port
to non-None for the UDP branch. Behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3
Comment thread rust_snuba/src/metrics/statsd.rs
…metrics

create_metrics() runs at snuba.environment import time (module-level
'metrics = create_metrics(...)'). snuba.state imports snuba.environment and reads
environment.metrics at its own import time, so importing snuba.state from inside
create_metrics() before environment finishes initializing is a circular import.

The previous UDS change read the use_dogstatsd_uds flag (importing snuba.state)
unconditionally, which broke the no-metrics path -- the Docker image's
'snuba --help' smoke test (no DOGSTATSD_* env set) crashed with
'partially initialized module snuba.environment has no attribute metrics'.

Only import snuba.state / read the flag when DOGSTATSD_SOCKET_PATH is configured.
The no-metrics path now returns DummyMetricsBackend without touching snuba.state
(as before), UDS-only still works, and host/port type narrowing is preserved.
Adds a regression test asserting the dummy path never calls state.get_config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3
Comment thread snuba/utils/metrics/util.py Outdated
Comment thread rust_snuba/src/metrics/global_tags.rs
create_metrics() runs while snuba.environment is being imported (module-level
metrics = create_metrics(...)). snuba.state binds MetricsWrapper(environment.metrics)
at its own import time, so importing snuba.state from create_metrics() before
environment finishes is a circular import. The prior socket-path gate still hit
this whenever DOGSTATSD_SOCKET_PATH was set -- including the documented prepare
step (socket set, flag off) -- which would crash process startup with
AttributeError (flagged by Cursor Bugbot). Note master's create_metrics never
imported snuba.state; the dependency was introduced with the UDS flag.

Move the use_dogstatsd_uds lookup into the DogStatsd client factory, which
DatadogMetricsBackend invokes lazily on first metric emission (per thread), long
after imports complete. The no-metrics path returns DummyMetricsBackend and the
transport choice (UDP vs UDS) is decided at runtime. Static config still picks
Dummy vs real backend at create time, so no snuba.state import happens during
environment import for any configuration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 033eaf7. Configure here.

Comment thread snuba/utils/metrics/util.py
Comment thread snuba/utils/metrics/util.py
The global_tags tests share the process-wide GLOBAL_TAGS static and run in
parallel, so test_set_and_get_global_tags (set env=production) could race
test_overwrite_global_tag (set env=staging) on the same key and flake. Give each
test its own key prefix so they can't collide. Reported by Seer code review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3
Comment thread snuba/utils/metrics/util.py
claude added 2 commits June 28, 2026 16:27
…ne transport

Three reviewers flagged that Python and Rust disagreed for a socket-only deploy
(DOGSTATSD_SOCKET_PATH set, no host/port) with the flag off: Python sent over the
socket while Rust disabled metrics.

Make both consistent and the flag authoritative: host/port (UDP) is the baseline
transport, and UDS is used only when use_dogstatsd_uds is on AND a socket path is
configured. With the flag off it never falls back to the socket, so host/port stay
configured as the UDP/rollback target -- matching the documented 'set both' rollout.
A deployment with only a socket and no host/port now has no transport and returns
the dummy/disabled backend in both languages (previously Python used the socket).

Python: host/port unset -> DummyMetricsBackend (static decision, no snuba.state
import), so the circular-import fix is preserved; the flag is read lazily in the
client factory only when host/port are configured. Rust select_transport: no
host/port -> Disabled regardless of the flag. Tests updated for both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3
metrics-exporter-dogstatsd enables client telemetry by default, which emits a new
family of datadog.dogstatsd.client.* metrics (metrics, packets_sent, bytes_sent,
aggregated_context, ...) that the previous statsdproxy pipeline never sent. Disable
it so migrating to the new backend does not change the set of metrics emitted -- the
consumer sends only its own metrics, over UDP or UDS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3

@onewland onewland left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems fine to me, I'm wondering if you were able to test locally at all; not with unit tests, but by starting a snuba consumer <storage> and seeing some data flow across the wire with the new metrics recorder, maybe with a UDP capture utility or something.

I've just seen that sort of test catch problems before merge before.

phacops commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Good call. Short of a full snuba consumer <storage> run (that's the last unchecked box — I'll do it in staging), I verified the new recorder actually puts the right bytes on the wire: I pointed DogStatsDBackend at a local socket and captured the raw DogStatsD payloads emitted by record_metric for a counter, gauge, and timer (with a runtime min_partition global tag set), over both transports.

UDP:

snuba.consumer.messages_processed:3|c|#topic:events,min_partition:4,storage:spans,consumer_group:spans-consumers
snuba.consumer.assigned_partitions:7.0|g|#instance:0,min_partition:4,storage:spans,consumer_group:spans-consumers
snuba.consumer.process_latency_ms:30.0|h|#step:decode,min_partition:4,storage:spans,consumer_group:spans-consumers

UDS (unixgram://…): byte-for-byte identical.

Confirms:

  • counter → c, gauge → g, timer → h (Datadog treats h and the old ms identically — same .avg/.count/.median/.95percentile/.max sub-metrics);
  • prefix applied exactly once (no double-prefix);
  • per-metric tags + static storage/consumer_group + the runtime min_partition tag on every line;
  • no datadog.dogstatsd.client.* telemetry (disabled so the migration doesn't add metrics);
  • UDS emits exactly the same payloads as UDP.

Still to do: the end-to-end capture from a running consumer in staging.


Generated by Claude Code

claude added 4 commits July 8, 2026 20:24
Resolve conflicts with master:

- CI workflows (ci.yml, ddl-changes.yml): keep the libcurl system-deps steps and
  take master's setup-uv bump to v8.3.0.
- Cargo.toml: keep the higher dependency versions and the statsdproxy removal from
  this branch, and adopt master's serde_json raw_value feature (now required by the
  generic_metrics parser that dropped serde(flatten)).
- metrics/statsd.rs: master migrated the Rust runtime-config reader to sentry-options
  and removed runtime_config::get_str_config. Read the use_dogstatsd_uds rollout flag
  directly from the legacy Redis runtime config via snuba.state.get_str_config so it
  stays live-flippable from snuba-admin (no deploy) and reads the same store as the
  Python create_metrics(); failures default to the UDP path.

cargo check/clippy/fmt clean; cargo test --workspace has only the pre-existing
ClickHouse/Python infra-dependent failures (same on master).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3
…support

# Conflicts:
#	.github/workflows/ci.yml
#	.github/workflows/ddl-changes.yml
Move the use_dogstatsd_uds rollout flag off the legacy Redis runtime config
onto the sentry-options "snuba" namespace, so both the Python and Rust
DogStatsD backends read it from the same store the rest of snuba's runtime
config now uses (migrated in the sentry-options move on master).

- Add a boolean `use_dogstatsd_uds` key to sentry-options/schemas/snuba/schema.json.
- Rust: use_dogstatsd_uds_enabled() now reads sentry_options::options("snuba")
  instead of calling snuba.state.get_str_config over pyo3. init_with_schemas
  runs at consumer startup before the metrics backend is built, and any read
  failure defaults to false (the stable UDP path).
- Python: create_metrics() reads snuba.state.sentry_options.get_option
  ("use_dogstatsd_uds", False) lazily in the client factory. The lazy read still
  avoids the snuba.state circular import at snuba.environment import time; by
  first-emit time init_options() has run, and get_option defaults to False if not.
- Update the create_metrics tests to patch get_option (boolean) instead of
  state.get_config.

The flag stays authoritative and restart-scoped: flipping the option and
restarting switches transports with no redeploy, and host/port remain the UDP
rollback target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3
…pshots

The test_rust job failed on `processors::llm_proxy_cost::tests::schema`. Root
cause was accidental dependency drift in rust_snuba/Cargo.toml, not the metrics
change: this branch pinned `schemars = "0.8.22"` (master pins "0.8.16") and
`json-schema-diff = "0.1.8"` (master "0.1.7"), plus a wholesale bump of ~25
other crates that had over-resolved the lockfile (e.g. a stray schemars 0.9/1.2
pulled in via serde_with 3.21).

The schema tests diff each processor's schemars-generated schema against the
canonical sentry-kafka-schemas schema and snapshot the breaking changes.
schemars 0.8.22 emits numeric `format` annotations (uint64/uint32/double) that
0.8.16 does not, which added FormatAdd diffs. A prior session regenerated most
snapshots against 0.8.22, but the llm-proxy-cost processor was added to master
later with a 0.8.16-style snapshot, so it failed under this branch's 0.8.22.

Fix by realigning with master instead of touching snapshots:
- Reset Cargo.toml/Cargo.lock to master and re-apply only the metrics deps
  (add metrics + metrics-exporter-dogstatsd, drop cadence + statsdproxy). The
  Cargo.toml diff vs master is now metrics-only; the lock diff is just the
  metrics subtree swap.
- Revert the 0.8.22-regenerated schema snapshots to master's (events,
  profiles-call-tree, snuba-profile-chunks) and delete the two snapshots that
  only exist under 0.8.22 (processed-profiles, snuba-queries) -- under 0.8.16
  those processors have an empty schema diff and write no snapshot.

All Rust schema tests pass; the remaining local test failures are the
Python/ClickHouse-infra tests that also fail on master in a bare environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3
Comment thread snuba/utils/metrics/util.py Outdated
claude added 3 commits July 15, 2026 03:04
Seer flagged that create_metrics()'s client factory read the use_dogstatsd_uds
option on each thread's first metric emission. DatadogMetricsBackend builds a
DogStatsd client per thread, so flipping the option at runtime without a restart
could leave already-warmed threads on the old transport while new threads picked
up the new one -- UDP and UDS metrics from the same process.

Resolve the UDS-vs-UDP decision once per process and cache it (double-checked
locking), so every thread's client uses the same transport. The value is fixed
until restart, matching the documented "flip the option, then restart" contract;
a mid-process flip is now a clean no-op instead of splitting threads across
transports. Reading at create_metrics() time (the literal "read at startup"
suggestion) isn't possible -- it runs during snuba.environment import and reading
sentry-options there is a circular import -- so the read stays lazy but is now
memoized process-wide rather than per-thread.

Add a test that emits on the main thread with the flag off, flips it on, then
emits from a fresh thread and asserts that thread still builds a UDP client.

The Rust backend already reads the option once at consumer startup before
installing the global recorder, so it is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3
Previously a deployment with only DOGSTATSD_SOCKET_PATH set (no host/port)
emitted no metrics -- host/port was treated as the required baseline transport,
so a socket alone resolved to disabled/Dummy even with use_dogstatsd_uds on.

Change the gating so use_dogstatsd_uds selects the *preferred* transport and a
single configured transport is used regardless of the flag:

  - socket + host/port : flag picks UDS (on) or UDP (off)
  - host/port only     : UDP
  - socket only        : UDS   <-- was: no metrics
  - neither            : no metrics

The flag stays authoritative when both transports are configured (a socket never
overrides an available UDP target while off), so host/port remain the UDP
rollback target; it just no longer gates the socket-only case, where UDS is the
only transport available.

- Rust select_transport(): UDS when a socket is set and (flag || no UDP target),
  else UDP when host/port set, else disabled.
- Python create_metrics(): return Dummy only when neither host/port nor socket is
  set; validate host/port are both-or-neither (partial UDP config still raises);
  the lazily-resolved decision uses UDS when a socket is set and (flag || no
  host/port).
- Update the sentry-options schema description and both docstrings; add Rust
  uds_when_socket_only + rename disabled_when_nothing_configured; add Python
  socket-only-uses-UDS and partial-host/port-raises tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3
…heme)

Stop hardcoding the `unixgram://` scheme in the Rust UDS path so the full socket
address can be supplied via SNUBA_DOGSTATSD_SOCKET_PATH. DogStatsDBackend::new_uds
now passes socket_path straight to the exporter's with_remote_address, which
parses the transport from the scheme (`unixgram://` datagram, `unix://` stream).

The same env var already reaches the Python datadog client verbatim, and that
client strips a `unixgram://`/`unixstream://`/`unix://` scheme itself and selects
the socket kind, so no scheme handling is needed on the Python side either -- one
full address (e.g. `unixgram:///run/dogstatsd.sock`) now works for both runtimes.
Using `unixgram://` preserves the previous datagram behavior.

- Rust: new_uds forwards socket_path unchanged; doc updated; select_transport
  tests use a scheme'd address.
- Python: document the verbatim/scheme contract in create_metrics; tests use a
  scheme'd address and assert it is passed through unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3
@phacops
phacops enabled auto-merge (squash) July 15, 2026 17:06
This PR touches rust_snuba/src/processors/replays.rs. Its CODEOWNERS entry
listed only @getsentry/replay-backend, and since CODEOWNERS is last-match-wins
(owners do not merge), that overrode the /rust_snuba @getsentry/owners-snuba
rule and left owners-snuba off the file. Add @getsentry/owners-snuba alongside
replay-backend, matching every other team-scoped entry in the file (e.g.
eap_spans.rs and the dataset configs), so both teams own the file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FrDg6sMR1aG1xJf6VBod3
@phacops
phacops merged commit c3866bb into master Jul 15, 2026
69 checks passed
@phacops
phacops deleted the feat/dogstatsd-uds-support branch July 15, 2026 17:31
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.

6 participants