cpflow 5.1.0 → 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.
- checksums.yaml +4 -4
- data/.agents/agent-workflow.yml +15 -0
- data/.agents/bin/README.md +19 -0
- data/.agents/bin/docs +5 -0
- data/.agents/bin/lint +5 -0
- data/.agents/bin/setup +5 -0
- data/.agents/bin/test +5 -0
- data/.agents/bin/validate +5 -0
- data/.agents/trusted-github-actors.yml +32 -0
- data/.agents/workflows/ai-rollout-e2e-test.md +166 -0
- data/.github/actions/cpflow-wait-for-health/action.yml +11 -4
- data/.github/workflows/claude-code-review.yml +2 -0
- data/.github/workflows/claude.yml +2 -0
- data/.github/workflows/cpflow-deploy-review-app.yml +16 -1
- data/.github/workflows/cpflow-promote-staging-to-production.yml +224 -37
- data/.github/workflows/rspec-shared.yml +15 -1
- data/.github/workflows/rspec-specific.yml +1 -0
- data/.github/workflows/rspec.yml +58 -1
- data/AGENTS.md +57 -0
- data/CHANGELOG.md +36 -1
- data/CLAUDE.md +3 -0
- data/CONTRIBUTING.md +6 -2
- data/Gemfile.lock +1 -1
- data/README.md +25 -7
- data/docs/ai-github-flow-prompt.md +18 -16
- data/docs/assets/logo/favicon.ico +0 -0
- data/docs/assets/logo/icon-1024.png +0 -0
- data/docs/assets/logo/icon-128.png +0 -0
- data/docs/assets/logo/icon-16.png +0 -0
- data/docs/assets/logo/icon-192.png +0 -0
- data/docs/assets/logo/icon-24.png +0 -0
- data/docs/assets/logo/icon-32.png +0 -0
- data/docs/assets/logo/icon-48.png +0 -0
- data/docs/assets/logo/icon-512.png +0 -0
- data/docs/assets/logo/icon-64.png +0 -0
- data/docs/assets/logo/icon-tile.svg +17 -0
- data/docs/assets/logo/mark-transparent.svg +16 -0
- data/docs/ci-automation.md +203 -15
- data/docs/commands.md +16 -1
- data/docs/grafana-opentelemetry.md +699 -0
- data/docs/secrets-and-env-values.md +29 -2
- data/docs/sidebars.ts +70 -0
- data/docs/telemetry/application-instrumentation.md +161 -0
- data/docs/telemetry/collector.md +297 -0
- data/docs/telemetry/index.md +152 -0
- data/docs/telemetry/pipelines.md +98 -0
- data/docs/telemetry/review-apps.md +55 -0
- data/docs/telemetry/troubleshooting.md +92 -0
- data/docs/terraform/example/.controlplane/controlplane.yml +0 -1
- data/docs/terraform/overview.md +11 -0
- data/docs/tips.md +458 -29
- data/examples/controlplane.yml +2 -0
- data/lib/command/ai_github_flow_prompt.rb +2 -2
- data/lib/command/base.rb +17 -2
- data/lib/command/deploy_image.rb +77 -5
- data/lib/command/maintenance_off.rb +1 -0
- data/lib/command/maintenance_on.rb +1 -0
- data/lib/command/promote_app_from_upstream.rb +1 -0
- data/lib/command/ps_wait.rb +2 -10
- data/lib/command/run.rb +25 -5
- data/lib/core/config.rb +94 -0
- data/lib/core/doctor_service.rb +44 -3
- data/lib/core/maintenance_mode.rb +93 -6
- data/lib/core/template_parser.rb +43 -9
- data/lib/cpflow/version.rb +1 -1
- data/lib/generator_templates/controlplane.yml +1 -2
- data/lib/github_flow_templates/.github/cpflow-help.md +23 -1
- data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +224 -39
- metadata +33 -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.
|