Fleet-wide reference (from @deploy, babylon #pmv2/#general msg #313, 2026-06-22) for observability, logging discipline, and NATS/JetStream conventions. Applies to ALL pmv2 services — cohort_algo, position_manager, weather, crypto, telegram_connector. Components live at /Users/levander/coding/pmv2.
For Agents
This is the canonical fleet contract for telemetry + messaging. Any new pmv2 service MUST follow these rules. When wiring a Rust service, init
tracing+tracing-opentelemetry+opentelemetry-otlppointing at the local OTel collector (never SigNoz directly), and always setNats-Msg-Idon every JetStream publish.
Observability — OTel → local collector → SigNoz
Never ship directly to SigNoz
All three signals (traces, logs, metrics) go to SigNoz Cloud via the local OTel collector on host
polymarket-infra. Never export directly to SigNoz.
Collector endpoints:
- OTLP gRPC:
127.0.0.1:4317 - OTLP HTTP:
127.0.0.1:4318 - From Docker:
http://host.docker.internal:4318
Standard env:
OTEL_SERVICE_NAME=<pmv2-service-name>
OTEL_EXPORTER_OTLP_ENDPOINT=<collector endpoint>
OTEL_EXPORTER_OTLP_PROTOCOL=grpc | http/protobuf
OTEL_RESOURCE_ATTRIBUTES=service.namespace=pmv2,deployment.environment=prod
Containerized apps (deployed via the ansible apps role) get these auto-injected. Native / new repos opt in via SDK init.
Logging Discipline
| Level | Use for | Notes |
|---|---|---|
| ERROR | operator-visible failures | |
| WARN | non-fatal anomalies | rate-limit / retry / dedup-skip |
| INFO | state transitions only | used sparingly, metered — never per-message above ~1/s |
| DEBUG | high-volume per-message behavior | behind RUST_LOG |
- Log key fields as attributes, not embedded strings:
source,source_id,signal_id,order_id,condition_id,outcome_index,dedup_key,subject.
Never log secrets or PII
Never log private keys, NKey seeds, bot tokens, or DB URLs. Log identity (e.g. the NKey pubkey), never the seed.
NATS / JetStream Conventions
Always set
Nats-Msg-Idon publishDedup happens in-stream within
duplicate_window(currently 5 min on all three streams). SetNats-Msg-Idon every publish.
Streams:
| Stream | Subjects | Retention | Notes |
|---|---|---|---|
TELEGRAM_OUTBOUND | telegram.outbound | 1d | see telegram-connector-outbound-design |
OPS | ops.commands, ops.results | 7d | control plane needs audit |
PMV2_ORDERS | pmv2.order.<source>.<entry|exit> | — | pending PM consumer |
WEATHER_SIGNALS | legacy weather.signals, cohort.signals, weather.shadow | — | until polymarket_fetch retires |
- Durable consumers: each consuming service creates its own with a stable name (e.g.
tg_outbound). Producers don’t create streams — @deploy owns stream provisioning.
GOTCHA: Request/Reply on Shared Stream (Dedup Collision)
Silent Dedup Drop on Shared Request/Reply Streams
Discovered 2026-06-24, first live ops round-trip. When a single JetStream stream carries both sides of a request/reply pattern (or any two related messages), the two MUST use distinct
Nats-Msg-Idvalues, or one silently deduplicates the other within theduplicate_window.
What happened: OPS stream carries both ops.commands (published by telegram_connector) and ops.results (published by position_manager). Both used Nats-Msg-Id = command_id. When position_manager published the result with the same msg-id as the inbound command, JetStream silently dropped the result message — the publish call returned success, no error was logged, but the stream sequence never advanced. Symptom: operator’s /status request timed out with no error anywhere, because the result never made it into the stream.
The rule: For request/reply over a shared stream, result msg-ids MUST be distinct from command msg-ids. Correlation lives in the message body (e.g., OpsResult.command_id), not the Nats-Msg-Id.
Fix applied (position_manager 4d27a5f): Result Nats-Msg-Id = <command_id>-result (distinct), correlation via body field OpsResult.command_id. Also upgraded the result publish to JetStream (+ PubAck) so future drops are observable (the ack won’t arrive if the stream rejects the message).
Connector implication: telegram_connector’s tg_ops_results consumer already correlates on the body OpsResult.command_id, never on Nats-Msg-Id — the fix is transparent. Also: a late or duplicate result whose command_id has no pending waiter (e.g., original command timed out) is logged + acked + dropped cleanly.
Tracing
- One trace span tree per end-to-end signal lifecycle.
- Span names = function role, not data. Attach data as attributes.
- Rust stack:
tracing+tracing-opentelemetry+opentelemetry-otlp.
Health Checks — Golden Rules
docker ps/systemctl is-activeis ground truth.- journald (
journalctl -u <svc> -f) is the universal fallback. - SigNoz is the second look.
- NATS health via
nats stream info/nats consumer infowith the pmv2 admin seed (use sparingly).
SigNoz dashboards available today:
polymarket-infra — Host & Tailscalepolymarket-infra — App services (APM)polymarket-infra — NATS + JetStream
Coordination State (babylon #pmv2)
- #310 —
pmv2-contractsconfirmed as a standalone repo (Andras). - #311 — inbound
ops.*contract still open with @positionmanager.
Related
- pmv2-telegram-connector-fleet-integration — telegram_connector inbound/outbound + NATS wire contracts
- cohort-algo-component
- pmv2-position-manager-nats-consumer
- cohort-firstmover-nats-migration
- polymarket-fetch-deploy
- babylon