UNPKG

skills-ws

Version:

87 agent skills for AI coding assistants — marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.

124 lines (100 loc) • 4.68 kB
## Contents - Distributed Tracing: Practical Patterns - Span Naming Conventions - Sampling Strategies - Context Propagation Across Services ## Distributed Tracing: Practical Patterns ### Span Naming Conventions ``` # Good — consistent, searchable, useful for aggregation http.request GET /api/users/:id db.query SELECT users cache.get user:profile:123 queue.publish order.created payment.stripe.charge email.send welcome # Bad — too specific (high cardinality) or too vague GET /api/users/12345 ← every user ID creates a unique span processRequest ← useless for filtering doStuff ← really? ``` ### Sampling Strategies **Head vs. tail — know which one you can actually use.** A *head* sampler decides at span **start**, before the request has run. At that moment the status code, latency, and most attributes don't exist yet — so a head sampler **cannot** "always keep errors." The common ask ("keep 10% of traffic but 100% of errors and slow requests") is a *tail* decision: it must run after the trace finishes, in the **OTel Collector's `tail_sampling` processor**, never in the SDK. | | Head sampling (SDK) | Tail sampling (Collector) | |---|---|---| | Decides | at trace start | after trace completes | | Can key on errors/latency? | No (not known yet) | Yes | | Cost | cheap, no buffering | buffers all spans in memory until decision | | Where | app process | collector (needs all spans of a trace at one collector) | **Head sampling — the one thing it's good for (cheap, uniform rate):** ```typescript import { TraceIdRatioBasedSampler, ParentBasedSampler } from '@opentelemetry/sdk-trace-base'; // Keep 10% of root traces; ALWAYS honor an upstream service's decision so a // trace is either fully kept or fully dropped across services. Set on NodeSDK // via `sampler:` (or env: OTEL_TRACES_SAMPLER=parentbased_traceidratio, // OTEL_TRACES_SAMPLER_ARG=0.1). const sampler = new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(0.1), }); ``` **Tail sampling — keep all errors + slow traces, downsample the boring ones.** This lives in the Collector (the `otel-collector` service above; the `-contrib` image has this processor). Apps export 100% to the collector; the collector decides what to keep: ```yaml # otel/collector.yaml receivers: otlp: protocols: grpc: { endpoint: 0.0.0.0:4317 } http: { endpoint: 0.0.0.0:4318 } processors: # Buffer spans per trace, then apply policies once the trace is complete. # Size memory: num_traces ≈ expected_new_traces_per_sec × decision_wait × ~2. tail_sampling: decision_wait: 10s num_traces: 100000 expected_new_traces_per_sec: 1000 policies: # 1) Keep every errored trace (status now known — this is the whole point of tail). - name: errors type: status_code status_code: { status_codes: [ERROR] } # 2) Keep every slow trace (> 1s end-to-end). - name: slow type: latency latency: { threshold_ms: 1000 } # 3) Otherwise keep a 10% probabilistic sample. - name: sample-the-rest type: probabilistic probabilistic: { sampling_percentage: 10 } exporters: otlp/tempo: endpoint: tempo:4317 tls: { insecure: true } # in-cluster plaintext; use TLS across trust boundaries prometheusremotewrite: endpoint: http://prometheus:9090/api/v1/write # Prometheus 3.x remote-write receiver service: pipelines: traces: receivers: [otlp] processors: [tail_sampling] exporters: [otlp/tempo] metrics: receivers: [otlp] exporters: [prometheusremotewrite] ``` > **Scaling caveat:** tail sampling requires *all spans of a trace to reach the same collector instance*. With more than one collector you need a two-tier setup — a routing/load-balancing layer that hashes on `trace_id` (the `loadbalancing` exporter) feeding a pool of tail-sampling collectors. A single replica is fine until you outgrow its memory. ### Context Propagation Across Services ```typescript // Service A — outgoing HTTP request import { context, propagation } from '@opentelemetry/api'; async function callServiceB() { const headers: Record<string, string> = {}; // Inject trace context into outgoing headers propagation.inject(context.active(), headers); const response = await fetch('http://service-b/api/data', { headers }); return response.json(); } // Service B — incoming request (auto-instrumented by OTel HTTP instrumentation) // The trace context is automatically extracted from incoming headers // No manual code needed — just ensure both services use OTel ``` ---