cpflow 5.1.1 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/agent-workflow.yml +15 -0
  3. data/.agents/bin/README.md +19 -0
  4. data/.agents/bin/docs +5 -0
  5. data/.agents/bin/lint +5 -0
  6. data/.agents/bin/setup +5 -0
  7. data/.agents/bin/test +5 -0
  8. data/.agents/bin/validate +5 -0
  9. data/.agents/trusted-github-actors.yml +32 -0
  10. data/.agents/workflows/ai-rollout-e2e-test.md +166 -0
  11. data/.github/workflows/claude-code-review.yml +2 -0
  12. data/.github/workflows/claude.yml +2 -0
  13. data/.github/workflows/cpflow-deploy-review-app.yml +16 -1
  14. data/.github/workflows/rspec-shared.yml +10 -3
  15. data/.github/workflows/rspec-specific.yml +1 -0
  16. data/.github/workflows/rspec.yml +58 -1
  17. data/AGENTS.md +57 -0
  18. data/CHANGELOG.md +22 -1
  19. data/CLAUDE.md +3 -0
  20. data/CONTRIBUTING.md +6 -2
  21. data/Gemfile.lock +1 -1
  22. data/README.md +21 -7
  23. data/docs/ai-github-flow-prompt.md +18 -16
  24. data/docs/ci-automation.md +160 -13
  25. data/docs/commands.md +12 -1
  26. data/docs/grafana-opentelemetry.md +699 -0
  27. data/docs/secrets-and-env-values.md +29 -2
  28. data/docs/sidebars.ts +70 -0
  29. data/docs/telemetry/application-instrumentation.md +161 -0
  30. data/docs/telemetry/collector.md +297 -0
  31. data/docs/telemetry/index.md +152 -0
  32. data/docs/telemetry/pipelines.md +98 -0
  33. data/docs/telemetry/review-apps.md +55 -0
  34. data/docs/telemetry/troubleshooting.md +92 -0
  35. data/docs/terraform/example/.controlplane/controlplane.yml +0 -1
  36. data/docs/terraform/overview.md +11 -0
  37. data/docs/tips.md +458 -29
  38. data/examples/controlplane.yml +2 -0
  39. data/lib/command/ai_github_flow_prompt.rb +2 -2
  40. data/lib/command/base.rb +17 -2
  41. data/lib/command/deploy_image.rb +77 -5
  42. data/lib/command/promote_app_from_upstream.rb +1 -0
  43. data/lib/command/ps_wait.rb +2 -10
  44. data/lib/core/config.rb +94 -0
  45. data/lib/core/doctor_service.rb +44 -3
  46. data/lib/core/template_parser.rb +43 -9
  47. data/lib/cpflow/version.rb +1 -1
  48. data/lib/generator_templates/controlplane.yml +1 -2
  49. data/lib/github_flow_templates/.github/cpflow-help.md +10 -0
  50. metadata +21 -2
@@ -0,0 +1,699 @@
1
+ # Grafana and OpenTelemetry on Control Plane
2
+
3
+ Control Plane already exposes useful platform metrics in Grafana: workload CPU,
4
+ memory, restarts, request rate, and similar infrastructure signals. For Rails
5
+ applications, that is a strong starting point, but it does not explain enough
6
+ about application behavior.
7
+
8
+ OpenTelemetry fills the app-level gaps:
9
+
10
+ - request and job latency
11
+ - request and job errors
12
+ - database and Redis span latency
13
+ - external HTTP API latency
14
+ - trace-to-log correlation
15
+ - custom metrics generated from common span or log patterns
16
+
17
+ The fastest dashboards usually come from generated metrics, not raw trace or log
18
+ queries. A collector can receive traces/logs, normalize them, generate focused
19
+ Prometheus metrics, and expose those metrics for Control Plane Grafana to scrape.
20
+
21
+ For the generic Control Plane telemetry template shape, start with
22
+ [Telemetry](/docs/telemetry/) and
23
+ [Collector Workload](/docs/telemetry/collector.md). This guide is the Rails and
24
+ Grafana companion: it focuses on application instrumentation, spanmetrics,
25
+ dashboards, and alerting.
26
+
27
+ ## High-Level Architecture
28
+
29
+ ```text
30
+ Rails workloads
31
+ -> OTLP HTTP traces/logs
32
+ -> internal OpenTelemetry collector workload
33
+ -> span/log processors
34
+ -> generated Prometheus metrics
35
+ -> Control Plane metrics scrape
36
+ -> Grafana dashboards and alerts
37
+ ```
38
+
39
+ The collector should be internal-only. Application workloads send telemetry to
40
+ the collector over the GVC internal network. Grafana reads the generated metrics
41
+ through Control Plane's metrics integration.
42
+
43
+ ## Signals
44
+
45
+ ### Metrics
46
+
47
+ Metrics are aggregated numbers over time. They are the best default for
48
+ dashboards and alerts because they are fast to query and cheap to evaluate.
49
+
50
+ Good examples:
51
+
52
+ - request rate
53
+ - p95 request latency
54
+ - error count
55
+ - Sidekiq job count
56
+ - Redis operation count
57
+ - database operation latency
58
+ - container restart count
59
+
60
+ ### Traces
61
+
62
+ Traces show a request or job broken into spans. They are best for investigating
63
+ why a request was slow or what dependency caused an error.
64
+
65
+ Good examples:
66
+
67
+ - Which DB query made this request slow?
68
+ - Which external API call timed out?
69
+ - Which Sidekiq job failed and what did it call?
70
+
71
+ ### Logs
72
+
73
+ Logs are application events and messages. They are useful for details, but raw
74
+ log queries are often too expensive for broad dashboards. Prefer generating
75
+ targeted metrics from recurring log patterns. See [Tips: Logs](/docs/tips.md#logs)
76
+ for querying Control Plane logs directly.
77
+
78
+ Good examples:
79
+
80
+ - count a known error message
81
+ - correlate an app log with a trace id
82
+ - inspect the exact message after an alert fires
83
+
84
+ ## Rails Application Setup
85
+
86
+ Add OpenTelemetry gems for the instrumentation libraries your app uses:
87
+
88
+ ```ruby
89
+ # Adjust this group to match where the app emits telemetry.
90
+ group :production do
91
+ # OpenTelemetry SDK and exporter
92
+ gem "opentelemetry-sdk", require: false
93
+ gem "opentelemetry-exporter-otlp", require: false
94
+
95
+ # Rails instrumentation registers the Rails framework pieces once.
96
+ gem "opentelemetry-instrumentation-rails", require: false
97
+
98
+ # Add only what your app actually uses.
99
+ gem "opentelemetry-instrumentation-pg", require: false
100
+ gem "opentelemetry-instrumentation-redis", require: false
101
+ gem "opentelemetry-instrumentation-sidekiq", require: false
102
+ gem "opentelemetry-instrumentation-faraday", require: false
103
+ gem "opentelemetry-instrumentation-http", require: false
104
+ end
105
+ ```
106
+
107
+ Use the Bundler group that matches where the app will emit telemetry —
108
+ `:production` above is only an example, and many Control Plane deployments run
109
+ with production gems even for QA or staging. If you need to test OpenTelemetry
110
+ locally or in CI, include the same gems in those Bundler groups too;
111
+ `require: false` keeps them unloaded until the initializer guard enables
112
+ telemetry.
113
+
114
+ Keep OpenTelemetry disabled by default until the collector is deployed and
115
+ reviewed. The `ENABLE_OPEN_TELEMETRY` flag below is a custom application guard,
116
+ not a standard OpenTelemetry environment variable; use it only when this
117
+ initializer reads that flag. Otherwise, rely on your app's own rollout guard plus
118
+ standard SDK variables such as `OTEL_SERVICE_NAME` and
119
+ `OTEL_EXPORTER_OTLP_ENDPOINT`, as described in
120
+ [Application Instrumentation](/docs/telemetry/application-instrumentation.md).
121
+ Place this in a Rails initializer such as
122
+ `config/initializers/opentelemetry.rb` so the Rails instrumentation hooks into
123
+ the framework boot sequence:
124
+
125
+ ```ruby
126
+ if ENV["ENABLE_OPEN_TELEMETRY"] == "true"
127
+ # Require only the instrumentation gems you added to the Gemfile above. A
128
+ # require without a matching gem raises LoadError at boot.
129
+ require "opentelemetry/sdk"
130
+ require "opentelemetry/exporter/otlp"
131
+ require "opentelemetry/instrumentation/rails"
132
+ require "opentelemetry/instrumentation/pg"
133
+ require "opentelemetry/instrumentation/redis"
134
+ require "opentelemetry/instrumentation/sidekiq"
135
+ require "opentelemetry/instrumentation/faraday"
136
+ require "opentelemetry/instrumentation/http"
137
+
138
+ OpenTelemetry::SDK.configure do |config|
139
+ # Set "service.name" directly in the resource so the complete resource is
140
+ # defined in one place and does not depend on SDK merge order between
141
+ # config.service_name= and config.resource=.
142
+ resource_attributes = {
143
+ "service.name" => ENV.fetch("OTEL_SERVICE_NAME") { ENV.fetch("CPLN_WORKLOAD", "rails-app") },
144
+ "original_cpln_org" => ENV["CPLN_ORG"],
145
+ "original_cpln_gvc" => ENV["CPLN_GVC"],
146
+ "original_cpln_workload" => ENV["CPLN_WORKLOAD"],
147
+ "original_cpln_replica" => ENV["CPLN_REPLICA"],
148
+ "original_cpln_image" => ENV["CPLN_IMAGE"],
149
+ "original_commit_hash" => ENV["APP_COMMIT_SHA"]
150
+ }.compact
151
+
152
+ config.resource = OpenTelemetry::SDK::Resources::Resource.create(resource_attributes)
153
+
154
+ # The exporter reads OTEL_EXPORTER_OTLP_ENDPOINT and
155
+ # OTEL_EXPORTER_OTLP_PROTOCOL from the environment — see the notes below.
156
+ exporter = OpenTelemetry::Exporter::OTLP::Exporter.new
157
+
158
+ config.add_span_processor(
159
+ OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
160
+ )
161
+
162
+ config.use "OpenTelemetry::Instrumentation::Rails"
163
+ config.use "OpenTelemetry::Instrumentation::PG", { db_statement: :obfuscate }
164
+ config.use "OpenTelemetry::Instrumentation::Redis", { db_statement: :obfuscate }
165
+ config.use "OpenTelemetry::Instrumentation::Sidekiq"
166
+ config.use "OpenTelemetry::Instrumentation::Faraday"
167
+ config.use "OpenTelemetry::Instrumentation::HTTP"
168
+ end
169
+ end
170
+ ```
171
+
172
+ `APP_COMMIT_SHA` is a custom application variable, not a Control Plane injected
173
+ variable. Set it at image build time or replace it with a helper that derives the
174
+ commit from the image tag.
175
+
176
+ Recommended app workload env:
177
+
178
+ ```yaml
179
+ env:
180
+ # ENABLE_OPEN_TELEMETRY is a custom application guard used by the initializer
181
+ # above. Set it only after the collector is deployed, or replace it with the
182
+ # rollout guard your application already uses.
183
+ # - name: ENABLE_OPEN_TELEMETRY
184
+ # value: "true"
185
+ - name: OTEL_SERVICE_NAME
186
+ value: "<workload-name>"
187
+ - name: OTEL_EXPORTER_OTLP_ENDPOINT
188
+ value: "http://open-telemetry-collector.{{APP_NAME}}.cpln.local:4318"
189
+ - name: OTEL_EXPORTER_OTLP_PROTOCOL
190
+ value: "http/protobuf"
191
+ ```
192
+
193
+ `OTEL_SERVICE_NAME` is the standard OpenTelemetry service name env var; set it
194
+ per workload when possible. `OTEL_EXPORTER_OTLP_ENDPOINT` must be a collector
195
+ base URL using the internal collector workload hostname in the same GVC — not
196
+ `localhost` and not a signal-specific path such as `/v1/traces`. The generic
197
+ telemetry docs use `open-telemetry-collector.{{APP_NAME}}.cpln.local`;
198
+ `cpflow apply-template` replaces `{{APP_NAME}}` with the deployed app name. If
199
+ you apply the YAML directly, replace it manually or keep an equivalent internal
200
+ hostname if your collector workload is named differently. Without these env vars
201
+ the exporter defaults to
202
+ `http://localhost:4318`, which usually does not exist on a Control Plane
203
+ workload, so spans are dropped silently. (Port 4317 is the gRPC default and
204
+ applies to the separate `opentelemetry-exporter-otlp-grpc` gem, which this
205
+ guide does not use.)
206
+
207
+ The `opentelemetry-instrumentation-http` gem instruments the `http.rb` client.
208
+ Apps that use `Net::HTTP`, HTTParty, Excon, Typhoeus, or another HTTP client
209
+ should choose the matching OpenTelemetry instrumentation gem instead.
210
+
211
+ ## Control Plane Resource Attributes
212
+
213
+ Control Plane workloads expose useful environment variables that can be copied
214
+ into OpenTelemetry resource attributes. These make dashboards filterable by org,
215
+ GVC, workload, replica, image, and commit.
216
+
217
+ Use a consistent prefix such as `original_` to distinguish the resource
218
+ attribute names from raw Control Plane env vars like `CPLN_ORG`, `CPLN_GVC`,
219
+ `CPLN_WORKLOAD`, `CPLN_REPLICA`, and `CPLN_IMAGE`. Whatever prefix you choose,
220
+ keep one naming convention per app so dashboards and trace queries stay
221
+ predictable.
222
+
223
+ The initializer above sets the recommended attributes. Use one helper module to
224
+ derive them, then reuse it for traces, logs, and metrics. Keep `original_cpln_replica`, `original_cpln_image`, and
225
+ `original_commit_hash` out of generated Prometheus metric dimensions; they are
226
+ useful on traces but too high-cardinality for ordinary dashboard labels.
227
+
228
+ ## Collector Workload
229
+
230
+ Create an internal collector workload in the same GVC as the app workloads.
231
+ Use [Collector Workload](/docs/telemetry/collector.md) as the canonical template
232
+ for workload shape, identity, secret policy, config delivery, and port/config
233
+ alignment. The values below are a Rails/Grafana starter profile layered on top of
234
+ that template.
235
+
236
+ Recommended ports:
237
+
238
+ - `4318`: OTLP HTTP receiver
239
+ - `9292`: Prometheus metrics endpoint
240
+ - `55679`: zpages/debug endpoint, internal only
241
+
242
+ Use the [Collector Workload firewall example](/docs/telemetry/collector.md#control-plane-workload-template)
243
+ as the canonical YAML shape. For this Rails/Grafana profile, keep internal
244
+ inbound limited to the same GVC, external ingress closed, and outbound egress
245
+ limited to the telemetry backend hostnames or CIDRs the collector needs.
246
+
247
+ Recommended starter container resources:
248
+
249
+ ```yaml
250
+ containers:
251
+ - name: open-telemetry-collector
252
+ cpu: 250m
253
+ memory: 512Mi
254
+ ```
255
+
256
+ Tune CPU and memory from staging observations before production rollout.
257
+
258
+ Recommended env:
259
+
260
+ ```yaml
261
+ env:
262
+ # Custom backend variables, not standard OpenTelemetry env vars. The collector
263
+ # reads them through ${env:VAR} substitution in the config YAML, so each must be
264
+ # referenced as ${env:TELEMETRY_...} in that config to take effect.
265
+ - name: TELEMETRY_BACKEND_TOKEN
266
+ value: "cpln://secret/{{APP_NAME}}-telemetry-backend.TELEMETRY_BACKEND_TOKEN"
267
+ ```
268
+
269
+ The `cpln://secret/<dictionary-name>.<KEY>` reference syntax is documented in
270
+ [Secrets and ENV Values](/docs/secrets-and-env-values.md). If you mount config
271
+ from a dictionary secret instead of baking it into the image, include the
272
+ dictionary key suffix as well, for example
273
+ `cpln://secret/<collector-config-secret>.CONFIG_YAML`. When the collector config
274
+ changes, update the mounted secret or image and run
275
+ `cpflow ps:restart -a $APP_NAME -w open-telemetry-collector` so the workload
276
+ loads the new config.
277
+
278
+ Expose the collector's metrics endpoint to Control Plane:
279
+
280
+ ```yaml
281
+ metrics:
282
+ path: "/metrics"
283
+ port: 9292
284
+ ```
285
+
286
+ ## Collector Config
287
+
288
+ Prefer small source files that build into one generated collector config. This
289
+ is much easier to review than one large YAML file.
290
+
291
+ Suggested structure:
292
+
293
+ ```text
294
+ .controlplane/open_telemetry/
295
+ build_main_collector_config
296
+ check_main_collector_config
297
+ validate_main_collector_config
298
+ main_collector_config.yml
299
+ main_collector_config/
300
+ receivers/
301
+ processors/
302
+ connectors/
303
+ exporters/
304
+ service/
305
+ pipelines/
306
+ ```
307
+
308
+ Minimum collector config components:
309
+
310
+ - OTLP HTTP receiver
311
+ - memory limiter processor
312
+ - transform processor for normalized span attributes
313
+ - spanmetrics connector for generated metrics
314
+ - Prometheus exporter
315
+ - batch processor
316
+ - zpages extension
317
+
318
+ Pin the collector image, and validate the generated config against that exact
319
+ image before deployment — OTTL and spanmetrics feature support varies by
320
+ collector-contrib version.
321
+
322
+ Normalize span attributes before generating metrics:
323
+
324
+ ```yaml
325
+ processors:
326
+ transform/normalize:
327
+ trace_statements:
328
+ - context: span
329
+ statements:
330
+ - set(attributes["instrumentation.name"], instrumentation_scope.name)
331
+ - set(attributes["root_span"], true) where IsRootSpan()
332
+ - set(attributes["root_span"], false) where not IsRootSpan()
333
+ ```
334
+
335
+ `IsRootSpan()` requires collector-contrib v0.105.0 or later (added in
336
+ [contrib #32918](https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/32918)).
337
+ On an older pinned image, replace the two `IsRootSpan()` lines with an explicit
338
+ parent-span-id check — a root span has a zero parent span ID — and validate it
339
+ against that exact image:
340
+
341
+ ```yaml
342
+ - set(attributes["root_span"], true) where parent_span_id == SpanID(0x0000000000000000)
343
+ - set(attributes["root_span"], false) where parent_span_id != SpanID(0x0000000000000000)
344
+ ```
345
+
346
+ Generate a request latency metric from selected root spans. One safe pattern is
347
+ to run a filter processor before the spanmetrics connector that drops spans where
348
+ the normalized `root_span` attribute is not true:
349
+
350
+ ```yaml
351
+ processors:
352
+ filter/non_root_spans:
353
+ error_mode: propagate
354
+ traces:
355
+ span:
356
+ - 'attributes["root_span"] != true'
357
+ ```
358
+
359
+ The filter processor drops spans that match the condition. `error_mode: ignore`
360
+ logs expression errors and continues, while `silent` continues without logging.
361
+ For this filter, either mode can turn a broken condition into a no-op that passes
362
+ every span — including child spans — into the spanmetrics connector, inflating
363
+ metric cardinality. Keep `error_mode: propagate` in development and staging, and
364
+ include a known trace with one root span and one child span in collector
365
+ validation. Keep `propagate` as the default production setting too: OTTL
366
+ evaluation errors and dropped payloads are easier to alert on than a no-op filter
367
+ that floods spanmetrics with child spans. If a team deliberately chooses
368
+ `ignore`, do it only after the OTTL-error and dropped-span alerts in
369
+ [Alert Starting Point](#alert-starting-point) are active and routed to an owner.
370
+
371
+ Processor order is load-bearing here. The condition `attributes["root_span"] != true`
372
+ matches spans where the attribute is **absent** as well as where it is `false`, so
373
+ `transform/normalize` (which sets `root_span`) must run before `filter/non_root_spans`
374
+ in the traces pipeline. Reversed — or with a misconfigured transform — the filter
375
+ drops every span with no error or warning. The `service.pipelines` example below
376
+ shows the required order.
377
+
378
+ Duration-string buckets and `exclude_dimensions` require collector-contrib
379
+ images that support those spanmetrics schemas. If an app pins an older collector,
380
+ use float-second buckets such as `0.005`, `0.010`, and `1.0`, and remove
381
+ unsupported fields.
382
+
383
+ Then feed the filtered trace stream into the connector:
384
+
385
+ ```yaml
386
+ connectors:
387
+ spanmetrics/http_root_span_latency:
388
+ namespace: http_root_span_latency
389
+ dimensions:
390
+ - name: service.name
391
+ - name: original_cpln_workload
392
+ - name: http.route
393
+ exclude_dimensions:
394
+ - span.name
395
+ - span.kind
396
+ histogram:
397
+ explicit:
398
+ buckets:
399
+ - 5ms
400
+ - 10ms
401
+ - 50ms
402
+ - 100ms
403
+ - 250ms
404
+ - 500ms
405
+ - 1s
406
+ - 2s
407
+ - 5s
408
+ - 10s
409
+ ```
410
+
411
+ `span.name` and `span.kind` are excluded because raw Rails span names can carry
412
+ too much cardinality and the root-span filter makes span kind redundant for this
413
+ dashboard. The spanmetrics connector emits status code as a default dimension
414
+ (`status.code`, or `otel.status_code` when that feature gate is enabled), so do
415
+ not add it again under `dimensions`. Compute error rate from the connector's
416
+ request-count series split by that status-code label (errored calls ÷ total
417
+ calls), and confirm the exact metric and label names against your collector image
418
+ since they vary by spanmetrics version. OpenTelemetry sets server span status to
419
+ `Error` for 5xx but not 4xx — add `http.response.status_code` as a dimension if
420
+ you need 4xx granularity. Before enabling the connector, inspect `http.route`
421
+ values in a real staging trace and confirm they are route patterns such as
422
+ `/users/:id`, not raw request paths such as `/users/12345` — raw paths are one of
423
+ the most common causes of a spanmetrics setup overwhelming Prometheus storage.
424
+
425
+ Wire the receiver, processors, connector, and exporters together in one generated
426
+ collector config, with the processor order described above. The minimal example
427
+ below includes the top-level component stubs referenced by `service.pipelines`;
428
+ the spanmetrics connector terminates the traces pipeline and feeds the metrics
429
+ pipeline. The focused snippets above are repeated here intentionally so this
430
+ block can be copied as one complete starting point; keep the focused snippets and
431
+ this consolidated example in sync when changing processor names, filters, or
432
+ histogram buckets:
433
+
434
+ ```yaml
435
+ receivers:
436
+ otlp:
437
+ protocols:
438
+ http:
439
+ endpoint: "0.0.0.0:4318"
440
+
441
+ processors:
442
+ memory_limiter:
443
+ check_interval: 1s
444
+ limit_percentage: 80
445
+ spike_limit_percentage: 20
446
+
447
+ transform/normalize:
448
+ trace_statements:
449
+ - context: span
450
+ statements:
451
+ - set(attributes["instrumentation.name"], instrumentation_scope.name)
452
+ - set(attributes["root_span"], true) where IsRootSpan()
453
+ - set(attributes["root_span"], false) where not IsRootSpan()
454
+
455
+ filter/non_root_spans:
456
+ error_mode: propagate
457
+ traces:
458
+ span:
459
+ - 'attributes["root_span"] != true'
460
+
461
+ batch: {}
462
+
463
+ connectors:
464
+ spanmetrics/http_root_span_latency:
465
+ namespace: http_root_span_latency
466
+ dimensions:
467
+ - name: service.name
468
+ - name: original_cpln_workload
469
+ - name: http.route
470
+ exclude_dimensions:
471
+ - span.name
472
+ - span.kind
473
+ histogram:
474
+ explicit:
475
+ buckets:
476
+ - 5ms
477
+ - 10ms
478
+ - 50ms
479
+ - 100ms
480
+ - 250ms
481
+ - 500ms
482
+ - 1s
483
+ - 2s
484
+ - 5s
485
+ - 10s
486
+
487
+ exporters:
488
+ prometheus:
489
+ endpoint: "0.0.0.0:9292"
490
+
491
+ extensions:
492
+ zpages:
493
+ endpoint: "0.0.0.0:55679"
494
+
495
+ service:
496
+ extensions:
497
+ - zpages
498
+ pipelines:
499
+ traces:
500
+ receivers:
501
+ - otlp
502
+ processors:
503
+ - memory_limiter
504
+ - transform/normalize
505
+ - filter/non_root_spans
506
+ - batch
507
+ exporters:
508
+ - spanmetrics/http_root_span_latency
509
+ metrics:
510
+ receivers:
511
+ - spanmetrics/http_root_span_latency
512
+ processors:
513
+ - memory_limiter
514
+ - batch
515
+ exporters:
516
+ - prometheus
517
+ ```
518
+
519
+ The spanmetrics connector is listed as an exporter on the traces pipeline and as
520
+ a receiver on the metrics pipeline; that shared reference is what links the two.
521
+ Every extension named under `service.extensions` also needs a matching top-level
522
+ `extensions:` block — shown here for `zpages` — or the collector fails to start.
523
+
524
+ In this minimal pipeline the spanmetrics connector is the only span consumer:
525
+ the filter discards every child span (database, Redis, Sidekiq, HTTP clients)
526
+ after the app has paid to generate and export it, and no raw traces are stored.
527
+ This config is metrics-only — the trace-drilldown use cases in
528
+ [Signals: Traces](#traces) (which DB query was slow, which external call timed
529
+ out) are not available with it alone. Enable only the instrumentation whose
530
+ spans a pipeline consumes, and to keep traces for debugging, add a trace
531
+ exporter (for example OTLP to Grafana Tempo) on the traces pipeline alongside
532
+ the spanmetrics connector.
533
+
534
+ At production request rates, generating and exporting 100% of spans before
535
+ filtering can overload the collector and inflate egress cost. For high-traffic
536
+ workloads, consider head-based sampling in the app
537
+ (`OTEL_TRACES_SAMPLER`, e.g. `parentbased_traceidratio`) or a tail-sampling
538
+ processor in the collector.
539
+
540
+ ## Template Guidance
541
+
542
+ Start with an application-owned collector config. Promote pieces into reusable
543
+ Control Plane Flow templates only after at least two applications need the same
544
+ shape.
545
+
546
+ Good candidates for shared templates:
547
+
548
+ - internal collector workload definition
549
+ - standard OTLP receiver ports
550
+ - standard Prometheus metrics endpoint
551
+ - resource attribute normalization
552
+ - validation script names and CI checks
553
+
554
+ Keep application-specific choices in the application repository:
555
+
556
+ - metric names and histogram buckets
557
+ - span filters
558
+ - log patterns converted into metrics
559
+ - dashboard panels and alert thresholds
560
+ - receiver/exporter settings that depend on traffic shape or compliance needs
561
+
562
+ ## Rollout Order
563
+
564
+ Use a non-production GVC for the first rollout.
565
+
566
+ 1. Add application OpenTelemetry gems and initializer. If you use the custom
567
+ `ENABLE_OPEN_TELEMETRY` guard above, leave it unset so the initializer guard
568
+ keeps telemetry disabled.
569
+ 2. Add collector config source files, generated config, and local validation
570
+ scripts.
571
+ 3. Add the internal collector workload with external ingress closed.
572
+ 4. Deploy the collector while app telemetry is still disabled.
573
+ 5. Confirm the collector starts cleanly and exposes `/metrics`.
574
+ 6. Choose and record the non-production sampling setting before enabling app
575
+ spans. Start low for high-traffic workloads, then adjust after collector CPU,
576
+ memory, and egress are visible.
577
+ 7. Enable OpenTelemetry for one non-production app workload.
578
+ 8. Confirm generated metrics appear at the collector `/metrics` endpoint.
579
+ 9. Confirm Control Plane Grafana can query those metrics.
580
+ 10. Draft the dashboard from queries or exported JSON and get human review before
581
+ saving it in a shared Grafana folder.
582
+ 11. Add alerts only after the dashboard queries are stable.
583
+
584
+ For production, repeat the same sequence with explicit approval, a written
585
+ rollback, and a short observation window after each change.
586
+
587
+ ## Dashboard Starting Point
588
+
589
+ Start with a small dashboard. Do not begin by copying a large JSON dashboard
590
+ model.
591
+
592
+ Suggested rows:
593
+
594
+ 1. Request overview
595
+ - requests per second
596
+ - p50/p95/p99 latency
597
+ - error count and error rate
598
+
599
+ 2. Workload health
600
+ - CPU
601
+ - memory
602
+ - restarts
603
+ - replica count
604
+
605
+ 3. Dependencies
606
+ - Postgres latency and operation count
607
+ - Redis latency and operation count
608
+ - external HTTP latency
609
+
610
+ 4. Background jobs
611
+ - job count
612
+ - job latency
613
+ - job errors
614
+
615
+ 5. Logs and traces links
616
+ - links or notes for drilldown, trace search, and log search
617
+
618
+ Dashboard review checklist:
619
+
620
+ - panel queries are scoped by service and workload
621
+ - variables use low-cardinality labels
622
+ - p95/p99 panels use histogram queries, not client-side percentile transforms
623
+ - dashboard JSON contains no secrets or customer identifiers
624
+ - dashboard folder and permissions are intentional
625
+ - changes are exported or recorded before saving over an existing dashboard
626
+
627
+ ## Alert Starting Point
628
+
629
+ Start with low-noise alerts:
630
+
631
+ - container restarts
632
+ - sustained high memory usage
633
+ - sustained high request error rate
634
+ - request latency above a reviewed threshold
635
+ - Rack timeout count
636
+ - collector unhealthy or no metrics arriving
637
+ - collector span throughput spiking above baseline, or the filter's dropped-span
638
+ count falling to zero — an early signal that a transform/filter regression is
639
+ passing child spans into spanmetrics (metric names vary by collector version)
640
+
641
+ The [RAM](/docs/tips.md#ram) and [CPU](/docs/tips.md#cpu) sections in Tips walk
642
+ through creating the memory, restart, and CPU alerts in Grafana.
643
+
644
+ Avoid broad anomaly alerts until the baseline is understood. Week-over-week
645
+ comparison can be useful, but it can also be noisy when traffic shifts by a few
646
+ minutes or has bot contamination.
647
+
648
+ Alert review checklist:
649
+
650
+ - every alert has a named owner
651
+ - every threshold has a short reason
652
+ - every page has a tested runbook or rollback note
653
+ - new alerts route to a test or non-paging contact point first
654
+ - production paging changes are approved separately from dashboard changes
655
+
656
+ ## Validation
657
+
658
+ Before deploying:
659
+
660
+ ```sh
661
+ # Replace with your app's actual OpenTelemetry spec path.
662
+ bundle exec rspec spec/open_telemetry/
663
+
664
+ # Replace with your app's actual collector validation script names.
665
+ ./.controlplane/open_telemetry/check_main_collector_config
666
+ ./.controlplane/open_telemetry/validate_main_collector_config
667
+ ```
668
+
669
+ Before enabling in staging:
670
+
671
+ - app boots with OpenTelemetry disabled
672
+ - app boots with OpenTelemetry enabled
673
+ - collector starts and passes readiness checks
674
+ - collector `/metrics` endpoint has generated samples
675
+ - `http.route` in a staging trace shows route patterns (`/users/:id`), not raw
676
+ paths, before enabling the spanmetrics connector
677
+ - Grafana can query the metrics
678
+ - disabling OpenTelemetry is enough to roll back app-side impact
679
+
680
+ Before enabling in production:
681
+
682
+ - staging has run long enough to observe collector CPU and memory
683
+ - sampling is explicitly configured and reviewed for expected traffic, egress,
684
+ and metric accuracy
685
+ - dashboards are reviewed by a human
686
+ - alerts are routed to a non-paging or test contact point first
687
+ - rollback steps are written down
688
+ - production approval is explicit
689
+
690
+ ## Safety Notes
691
+
692
+ - Treat Grafana dashboards and alert rules as production-impacting settings.
693
+ - Export or draft dashboard JSON before saving live dashboards.
694
+ - Use non-production apps for first rollout.
695
+ - Keep collector external ingress closed unless a specific receiver requires it.
696
+ - Do not put secrets in dashboard JSON, templates, or screenshots.
697
+ - Keep high-cardinality attributes — user IDs, URLs with IDs, raw SQL, request
698
+ IDs, trace IDs — out of metric dimensions; prefer labels such as workload,
699
+ service, and version.