JacobLinCool / JacobLinCool/mars
feat(observability): optional OpenTelemetry for usage, reliability, and realtime performance
- Dominant language
- Rust
- Stars
- 0
- Forks
- 1
- Avg merge
- 10m
- Merged PRs (30d)
- 3
Description
## Summary
Add an **optional OpenTelemetry (OTel)** observability feature for MARS (`mars`, `marsd`, `mars-plugin-host`) to answer two core questions with real data:
1. **Usage**: are users successfully creating/validating/planning/applying profiles, and where do they fail?
2. **Performance/reliability**: is the realtime data plane healthy under real workloads (deadline misses/xruns/plugin instability), and what regressions appear first?
Current state is local logs + `status`/`doctor` snapshots + benchmark gates, which is useful for point-in-time debugging but not enough for longitudinal fleet insight.
## Goals
- Add OTel as an **opt-in** feature with deterministic behavior.
- Provide actionable KPIs for product usage, control-plane reliability, and realtime performance.
- Keep realtime safety: **no exporter/network work in render callback threads**.
- Keep telemetry low-cardinality and privacy-safe by default.
## Non-goals
- No audio payload/content collection.
- No per-sample/per-frame tracing.
- No best-effort legacy telemetry fallback paths.
## Design principles
- **Off by default**.
- **Standard OTel env vars** (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME`, etc.) plus one project gate:
- `MARS_OTEL_MODE=off|required`
- `off` (default): no OTel initialization.
- `required`: initialization must succeed; startup fails fast if provider/exporter cannot be built.
- Realtime thread only updates atomics/ring stats already in memory; a non-RT reporter thread exports snapshots.
- No high-cardinality identifiers in tags (no raw profile path, device UID, bundle id, sink path).
## Telemetry model
### Resource attributes (all signals)
- `service.name` in `{mars-cli, marsd, mars-plugin-host}`
- `service.version` (crate/package version)
- `service.instance.id` (generated per process start)
- `host.arch`, `os.type`, `os.version`
- `mars.component` (for finer grouping: `cli`, `daemon`, `engine`, `ipc`, `capture`, `sink`, `plugin_host`)
### Traces
#### Root spans
- `mars.cli.command`
- attrs: `command`, `json_output`, `exit_code`, `success`
- `mars.ipc.client.request`
- attrs: `command`, `request_id`, `timeout_ms`, `success`
- `mars.daemon.request`
- attrs: `command`, `request_id`, `success`, `exit_code`
#### Apply transaction spans (`marsd::apply_internal`)
Root: `mars.apply.transaction`
- attrs: `dry_run`, `no_delete`, `timeout_ms`, `success`, `rollback`, `error_stage`
Child spans (exact stages aligned to code):
- `mars.apply.stage.profile_validate`
- `mars.apply.stage.plan`
- `mars.apply.stage.external_resolve`
- `mars.apply.stage.driver_compatibility`
- `mars.apply.stage.driver_stage`
- `mars.apply.stage.graph_activate`
- `mars.apply.stage.capture_sync`
- `mars.apply.stage.render_sync`
- `mars.apply.stage.runtime_ready`
- `mars.apply.stage.rollback` (only on failure)
### Metrics
| Name | Type | Unit | Key attrs | Primary insight |
|---|---|---:|---|---|
| `mars.cli.command.count` | Counter | `{command}` | `command`, `success`, `exit_code` | Which commands are used, where users fail |
| `mars.cli.command.duration` | Histogram | `ms` | `command`, `success` | Slow CLI workflows |
| `mars.ipc.request.duration` | Histogram | `ms` | `command`, `success` | IPC latency hotspots |
| `mars.daemon.request.count` | Counter | `{request}` | `command`, `success`, `exit_code` | Daemon API reliability |
| `mars.daemon.request.duration` | Histogram | `ms` | `command`, `success` | Slow daemon endpoints |
| `mars.apply.count` | Counter | `{apply}` | `dry_run`, `no_delete`, `success` | Apply success rate |
| `mars.apply.duration` | Histogram | `ms` | `dry_run`, `no_delete`, `success` | End-to-end apply latency |
| `mars.apply.stage.duration` | Histogram | `ms` | `stage`, `success` | Which apply stage dominates/fails |
| `mars.apply.rollback.count` | Counter | `{rollback}` | `stage` | Rollback frequency by failure stage |
| `mars.profile.nodes` | Histogram | `{nodes}` | `profile_kind` (bucketed) | Complexity distribution |
| `mars.profile.routes` | Histogram | `{routes}` | `profile_kind` (bucketed) | Routing complexity trends |
| `mars.profile.processors` | Histogram | `{processors}` | `profile_kind` (bucketed) | DSP/plugin usage shape |
| `mars.render.cycle.duration` | Histogram | `ns` | `sample_rate`, `buffer_frames` | RT cycle cost distribution |
| `mars.render.cycle.budget_utilization` | Histogram | `ratio` | `sample_rate`, `buffer_frames` | Headroom to callback deadline |
| `mars.render.deadline_miss.count` | Counter | `{miss}` | `sample_rate`, `buffer_frames` | RT deadline violations |
| `mars.render.xrun.count` | Counter | `{xrun}` | `source` (`engine`/`external`/`capture`) | Audio glitch incidence |
| `mars.external.underrun.count` | Counter | `{underrun}` | `input_count`, `output_count` | External I/O instability |
| `mars.external.overrun.count` | Counter | `{overrun}` | `input_count`, `output_count` | External I/O instability |
| `mars.sink.queue.fill_ratio` | Observable Gauge | `ratio` | `sink_kind` (`file`/`stream`) | Sink backpressure early warning |
| `mars.sink.drop.count` | Counter | `{drop}` | `sink_kind` | Data loss in sinks |
| `mars.sink.write_error.count` | Counter | `{error}` | `sink_kind` | Sink writer reliability |
| `mars.capture.tap.active` | Observable Gauge | `{tap}` | `kind` (`process`/`system`) | Tap runtime coverage |
| `mars.capture.tap.failed` | Counter | `{failed_tap}` | `kind` | Tap instability |
| `mars.plugin.process.duration` | Histogram | `ms` | `api`, `health` | Plugin host processing latency |
| `mars.plugin.timeout.count` | Counter | `{timeout}` | `api` | Plugin timeout rate |
| `mars.plugin.error.count` | Counter | `{error}` | `api` | Plugin error rate |
| `mars.plugin.restart.count` | Counter | `{restart}` | `api` | Plugin crash/restart churn |
## Data quality, privacy, and cardinality policy
- Do **not** export: profile path/name, sink file path, raw device UID/name, process bundle IDs, plugin binary path.
- Prefer bounded enums/buckets in attrs:
- `sample_rate` from known set
- `buffer_frames` from known set
- complexity buckets (`nodes`: `1-4`, `5-16`, `17-64`, `65+`)
- Success-path traces: probabilistic sampling.
- Error-path traces (apply failure, rollback, timeout, restart): full sampling.
## Insight KPIs to ship with this feature
- `apply_success_rate = successful applies / total applies`
- `time_to_first_successful_apply`
- `apply_p95_duration_ms`
- `top_failing_apply_stage`
- `render_p99_budget_utilization`
- `xruns_per_minute`
- `plugin_restart_rate`
- `sink_drop_rate`
## Implementation plan
### Phase 1: Shared telemetry foundation
- [ ] Add workspace feature flag `otel` and new crate `mars-telemetry`.
- [ ] `mars-telemetry` initializes tracer + meter providers and exposes typed helpers.
- [ ] Implement `MARS_OTEL_MODE=off|required` semantics.
- [ ] Wire providers into existing `tracing_subscriber` setup in daemon startup.
### Phase 2: CLI + IPC instrumentation
- [ ] Instrument CLI command lifecycle in `crates/mars-cli`.
- [ ] Instrument IPC client request round-trip in `crates/mars-ipc`.
- [ ] Add trace correlation via existing `request_id` field.
### Phase 3: Daemon control-plane instrumentation
- [ ] Instrument `RequestHandler::handle` and all command paths.
- [ ] Instrument `apply_internal` with stage child spans and rollback tagging.
- [ ] Emit apply/plan/validate counters and duration histograms.
### Phase 4: Realtime-safe runtime metrics export
- [ ] Add non-RT periodic reporter (e.g., 1s tick) reading atomics/snapshots.
- [ ] Export render/external/capture/sink/plugin metrics without RT allocations.
- [ ] Ensure no network/export work runs on render thread.
### Phase 5: Plugin/sink/capture deep metrics
- [ ] Instrument plugin host worker (`au_host`) process/timeout/error/restart metrics.
- [ ] Instrument sink queue pressure + dropped batches + write errors.
- [ ] Instrument capture tap active/failed/error counters.
### Phase 6: Docs + dashboards + alerts
- [ ] Add `docs/telemetry.md` with metric dictionary and examples.
- [ ] Provide dashboard JSON/examples for KPI panels.
- [ ] Add alert suggestions (deadline misses, apply failure spike, plugin restart spike).
## Verification and acceptance criteria
- [ ] With default config (`MARS_OTEL_MODE=off`), behavior is unchanged.
- [ ] With `MARS_OTEL_MODE=required`, startup fails fast on invalid OTel config.
- [ ] `apply` traces include all stage spans and rollback span on failure.
- [ ] KPI metrics listed above are queryable from OTLP backend.
- [ ] Realtime performance remains within existing gate limits (`rt_cycle_p99_ratio` policy unchanged).
- [ ] Add tests for metric/trace emission and attr cardinality constraints.
## Risks and mitigations
- **Risk**: high-cardinality explosion from identifiers.
- Mitigation: strict attribute allowlist + bucketed dimensions.
- **Risk**: RT regression from instrumentation.
- Mitigation: atomics-only in RT path; export in non-RT background.
- **Risk**: silent telemetry loss.
- Mitigation: `required` mode fail-fast at startup + exporter error counters.
---
If accepted, this issue should be implemented as a sequence of small PRs matching the phases above.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by reading the phased implementation plan, then inspect crates/mars-cli, crates/mars-ipc, RequestHandler::handle, and apply_internal to understand the proposed boundaries. Review daemon tracing_subscriber setup and the au_host worker before splitting the work into smaller changes. Done means the selected phase meets its stated acceptance criteria without changing default behavior or realtime safety.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- observability
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100