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
@@ -9,10 +9,33 @@ For your "review apps," it is convenient to have simple ENVs stored in plain tex
9
9
  keep some ENVs, like the Rails' `SECRET_KEY_BASE`, out of your source code. For staging and production apps, you will
10
10
  set these values directly at the GVC or workload levels, so none of these ENV values are committed to the source code.
11
11
 
12
+ ## Review app secrets
13
+
14
+ Review apps run application code from pull requests. For repositories with external contributors, do not put sensitive
15
+ production or long-lived staging secrets in review apps. A secret reference such as `cpln://secret/...` protects the
16
+ value while it is stored in Control Plane configuration, but the value becomes readable by application code after it is
17
+ mounted into the review-app workload. Use generated dummy values, disposable databases, review-only renderer credentials,
18
+ and revocable service tokens for review apps.
19
+
12
20
  For storing ENVs in the source code, we can use a level of indirection so that you can store an ENV value in your source
13
21
  code like `cpln://secret/my-app-review-env-secrets.SECRET_KEY_BASE` and then have the secret value stored at the org
14
22
  level, which applies to your GVCs mapped to that org.
15
23
 
24
+ Avoid pointing review-app templates at shared production secret dictionaries. If staging and review apps live in the same
25
+ Control Plane org, keep review-app secret dictionaries separate from persistent staging secrets, and restrict the Control
26
+ Plane identity bound to review workloads so it can reveal only the values those workloads need.
27
+
28
+ Review-app identity and policy templates in `.controlplane/templates/`, plus any `shared_secret_grants` in
29
+ `controlplane.yml`, are read from the PR branch at deploy time for same-repository PRs. `cpflow setup-app` applies
30
+ templates at first review-app creation, and `cpflow deploy-image` can bind configured shared-secret policies on later
31
+ redeploys. A PR can modify either path to bind the review workload's identity to any secret dictionary, including staging
32
+ ones. Each generated trigger path has its own fork guard; see
33
+ [Review app security for repositories with external contributors](./ci-automation.md#review-app-security-for-repositories-with-external-contributors)
34
+ for details. Removing any guard leaves one or more trigger paths unprotected.
35
+
36
+ For repositories with external contributors, treat the entire review-app identity and policy scope as untrusted and ensure
37
+ the staging token cannot reach sensitive resources even if the PR changes the template.
38
+
16
39
  For setting up secrets, you'll need:
17
40
 
18
41
  - **Org-level Secret:** This is where the values will be stored.
@@ -75,8 +98,12 @@ updated, which helps existing review apps recover after the config is added. `cp
75
98
  remove those shared policy bindings when a review app is deleted.
76
99
 
77
100
  For shared databases, keep runtime data isolated by using a per-review-app database name, schema, or tenant key. A common
78
- pattern is to keep the host, user, and password in the shared secret, then have `hooks.post_creation` create the PR-specific
79
- database/schema and `hooks.pre_deletion` drop it.
101
+ pattern is to keep the host, user, and password in the shared secret, then have `hooks.post_creation` create the
102
+ PR-specific database/schema. Avoid a generic `hooks.pre_deletion` that drops the database: `cpflow delete` runs the
103
+ pre-deletion hook before it removes the app workloads, so live connections can make PostgreSQL reject the drop. Stop the
104
+ review app workloads first, or run cleanup from trusted admin automation against the shared Postgres workload. See
105
+ [Share One Control Plane Postgres for Staging and Review Apps](tips.md#share-one-control-plane-postgres-for-staging-and-review-apps)
106
+ for the full pattern.
80
107
 
81
108
  Here are the manual steps for reference. We recommend that you follow the steps above:
82
109
 
data/docs/sidebars.ts ADDED
@@ -0,0 +1,70 @@
1
+ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
2
+
3
+ const sidebars: SidebarsConfig = {
4
+ docsSidebar: [
5
+ {type: 'doc', id: 'index', label: 'Documentation Guide'},
6
+ {type: 'doc', id: 'README', label: 'Overview'},
7
+ {
8
+ type: 'category',
9
+ label: 'Start Here',
10
+ collapsed: false,
11
+ items: [
12
+ {type: 'doc', id: 'migrating-heroku-to-control-plane', label: 'Migrate from Heroku'},
13
+ {type: 'doc', id: 'ci-automation', label: 'Automate GitHub Flow'},
14
+ {type: 'doc', id: 'ai-github-flow-prompt', label: 'AI Rollout Prompt'},
15
+ {type: 'doc', id: 'commands', label: 'Command Reference'},
16
+ ],
17
+ },
18
+ {
19
+ type: 'category',
20
+ label: 'Core Operations',
21
+ items: [
22
+ 'secrets-and-env-values',
23
+ 'dns',
24
+ 'thruster',
25
+ 'troubleshooting',
26
+ 'tips',
27
+ ],
28
+ },
29
+ {
30
+ type: 'category',
31
+ label: 'Telemetry',
32
+ link: {type: 'doc', id: 'telemetry/index'},
33
+ items: [
34
+ 'telemetry/collector',
35
+ 'telemetry/application-instrumentation',
36
+ 'telemetry/pipelines',
37
+ 'telemetry/review-apps',
38
+ 'grafana-opentelemetry',
39
+ 'telemetry/troubleshooting',
40
+ ],
41
+ },
42
+ {
43
+ type: 'category',
44
+ label: 'Data Services',
45
+ items: [
46
+ 'postgres',
47
+ 'rds-private-networking',
48
+ 'redis',
49
+ ],
50
+ },
51
+ {
52
+ type: 'category',
53
+ label: 'Terraform',
54
+ items: [
55
+ 'terraform/overview',
56
+ 'terraform/details',
57
+ ],
58
+ },
59
+ {
60
+ type: 'category',
61
+ label: 'Project',
62
+ items: [
63
+ {type: 'doc', id: 'releasing', label: 'Releasing the Gem'},
64
+ {type: 'doc', id: 'changelog', label: 'Changelog'},
65
+ ],
66
+ },
67
+ ],
68
+ };
69
+
70
+ export default sidebars;
@@ -0,0 +1,161 @@
1
+ # Application Instrumentation
2
+
3
+ Application instrumentation is the code and configuration that creates telemetry
4
+ signals. `cpflow` deploys the infrastructure around that instrumentation, but it
5
+ does not add tracing or metrics libraries to your application.
6
+
7
+ ## Standard Environment Variables
8
+
9
+ Set these at the GVC level when every app workload should inherit them. Set them
10
+ on one workload container when only that workload should emit telemetry.
11
+
12
+ The snippets below are fragments, not complete templates. In a `cpflow`
13
+ template, put the `env` list under `spec.env` for GVC-level values or under the
14
+ target workload container's `spec.containers[].env` list for workload-only
15
+ values. When setting values directly in the Control Plane console or with
16
+ `cpln`, replace `{{APP_NAME}}` with the actual app name before applying it.
17
+
18
+ ```yaml
19
+ env:
20
+ - name: OTEL_EXPORTER_OTLP_ENDPOINT
21
+ value: "http://open-telemetry-collector.{{APP_NAME}}.cpln.local:4318"
22
+ - name: OTEL_EXPORTER_OTLP_PROTOCOL
23
+ value: "http/protobuf"
24
+ - name: OTEL_RESOURCE_ATTRIBUTES
25
+ value: "deployment.environment=staging,service.namespace=example"
26
+ # This GVC-level snippet intentionally omits OTEL_SERVICE_NAME.
27
+ # Set OTEL_SERVICE_NAME per workload container, such as example-web or
28
+ # example-worker, and set inheritEnv: true on each container that should
29
+ # receive these shared values.
30
+ ```
31
+
32
+ Change `deployment.environment=staging` to match the real environment. When this
33
+ value is set at the GVC level, each target workload container must set
34
+ `inheritEnv: true` to receive it. Use workload container env instead when only
35
+ one workload should receive the telemetry settings. Keep `OTEL_SERVICE_NAME`
36
+ workload-specific at the container level.
37
+
38
+ The `http://` collector endpoint is intended for a collector in the same GVC with
39
+ `same-gvc` inbound firewall isolation. Use `https://` only when a shared
40
+ telemetry endpoint terminates TLS; otherwise use the internal collector hostname
41
+ and protocol configured for that collector.
42
+
43
+ When using HTTP transport (`http/protobuf` or `http/json`), modern stable
44
+ OpenTelemetry SDKs treat `OTEL_EXPORTER_OTLP_ENDPOINT` as a base URL and append
45
+ `/v1/traces`, `/v1/metrics`, or `/v1/logs` automatically. For OTLP over gRPC,
46
+ most stable SDKs expect `http://host:port` for insecure connections and
47
+ `https://host:port` for TLS. Some older or pre-stable SDKs accepted a bare
48
+ `host:port`; check your SDK's documentation. For older or pre-stable SDKs, use
49
+ signal-specific variables such as `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` with the
50
+ full signal path when needed.
51
+
52
+ `ENABLE_OPEN_TELEMETRY` is not a standard OpenTelemetry environment variable.
53
+ Use it only if your application code explicitly reads that flag. Otherwise, use
54
+ standard SDK configuration such as `OTEL_SERVICE_NAME` and
55
+ `OTEL_EXPORTER_OTLP_ENDPOINT`, plus your framework's instrumentation setup.
56
+
57
+ ## Optional StatsD TCP Variables
58
+
59
+ Use these only when your application client can send StatsD over TCP and your
60
+ collector has a matching `statsd/tcp` receiver. These environment variable names
61
+ are examples for application code that reads them; many StatsD clients require
62
+ TCP transport to be configured explicitly in code.
63
+
64
+ ```yaml
65
+ env:
66
+ - name: STATSD_HOST
67
+ value: "open-telemetry-collector.{{APP_NAME}}.cpln.local"
68
+ - name: STATSD_PORT
69
+ value: "9127"
70
+ - name: STATSD_PROTOCOL
71
+ value: "tcp"
72
+ ```
73
+
74
+ `9127` is project-specific. The StatsD protocol default is `8125/UDP`; use the
75
+ port your collector's `statsd/tcp` receiver is configured to bind.
76
+
77
+ When applying with `cpln` directly, replace `{{APP_NAME}}` with the actual app
78
+ name.
79
+
80
+ ## Generic Ruby Example
81
+
82
+ This example uses deliberately generic metric names. Keep labels low-cardinality.
83
+ Initialize your client with TCP transport before emitting metrics. The exact
84
+ constructor name varies by library, but do not rely on the client's default
85
+ transport when the collector listens on `statsd/tcp`.
86
+
87
+ ```ruby
88
+ statsd = MyStatsDClient.new(
89
+ host: ENV.fetch("STATSD_HOST"),
90
+ port: Integer(ENV.fetch("STATSD_PORT", "9127")),
91
+ protocol: ENV.fetch("STATSD_PROTOCOL", "tcp")
92
+ )
93
+
94
+ statsd.increment(
95
+ "example.tasks.completed",
96
+ tags: ["task_type:background", "status:success"]
97
+ )
98
+
99
+ statsd.timing(
100
+ "example.jobs.duration_ms",
101
+ elapsed_ms,
102
+ tags: ["job_type:scheduled"]
103
+ )
104
+ ```
105
+
106
+ Avoid tags such as `user_id`, `request_id`, raw URLs, or free-form error
107
+ messages. They create high-cardinality metrics and can leak sensitive data.
108
+
109
+ ## Generic Node.js Example
110
+
111
+ ```javascript
112
+ const statsdHost = process.env.STATSD_HOST;
113
+ if (!statsdHost) {
114
+ throw new Error("STATSD_HOST is not set");
115
+ }
116
+
117
+ const statsd = createStatsDClient({
118
+ host: statsdHost,
119
+ port: (() => {
120
+ const port = parseInt(process.env.STATSD_PORT || "9127", 10);
121
+ if (!Number.isFinite(port)) {
122
+ throw new Error(`Invalid STATSD_PORT: ${process.env.STATSD_PORT}`);
123
+ }
124
+ return port;
125
+ })(),
126
+ protocol: process.env.STATSD_PROTOCOL || "tcp",
127
+ });
128
+
129
+ statsd.increment("example.requests.completed", 1, {
130
+ route: "health_check",
131
+ status: "success",
132
+ });
133
+
134
+ statsd.histogram("example.worker.duration_ms", elapsedMs, {
135
+ worker: "default",
136
+ });
137
+ ```
138
+
139
+ ## Service Names
140
+
141
+ Use names that identify the workload, not a real organization, account, or user.
142
+
143
+ Good:
144
+
145
+ - `example-web`
146
+ - `example-worker`
147
+ - `example-scheduler`
148
+
149
+ Avoid:
150
+
151
+ - real organization names
152
+ - account names
153
+ - user identifiers
154
+ - business-specific nouns copied from one product into reusable docs
155
+
156
+ ## Logs
157
+
158
+ Keep logs structured, redacted, and useful without a collector. If your
159
+ application sends OTLP logs, use the same collector endpoint as traces and
160
+ metrics. If it logs to stdout/stderr, use `cpflow logs` for live tailing and the
161
+ platform log backend for historical queries.
@@ -0,0 +1,297 @@
1
+ # Collector Workload
2
+
3
+ The OpenTelemetry Collector is a telemetry router. It receives data from
4
+ application workloads, optionally processes or samples it, and exports it to one
5
+ or more backends.
6
+
7
+ The two files that must agree are:
8
+
9
+ 1. The Control Plane workload template, which exposes ports.
10
+ 2. The collector `config.yaml`, which binds receivers and exporters to those
11
+ same ports.
12
+
13
+ If these files disagree, telemetry can fail silently.
14
+
15
+ ## Control Plane Workload Template
16
+
17
+ Add a template such as
18
+ `.controlplane/templates/open-telemetry-collector.yml`.
19
+
20
+ ```yaml
21
+ kind: workload
22
+ name: open-telemetry-collector
23
+ spec:
24
+ type: standard
25
+ containers:
26
+ - name: open-telemetry-collector
27
+ image: "registry.example.com/example/open-telemetry-collector:0.155.0"
28
+ args:
29
+ - "--config=/etc/otelcol-contrib/config.yaml"
30
+ # Uncomment when config.yaml references ${env:TELEMETRY_BACKEND_TOKEN}.
31
+ # env:
32
+ # - name: TELEMETRY_BACKEND_TOKEN
33
+ # value: cpln://secret/{{APP_NAME}}-telemetry-backend.TELEMETRY_BACKEND_TOKEN
34
+ cpu: 100m
35
+ memory: 256Mi
36
+ ports:
37
+ # OTLP over HTTP from application SDKs.
38
+ - number: 4318
39
+ protocol: http
40
+ # StatsD over TCP, not UDP. Delete this port unless your collector
41
+ # config enables statsd/tcp and app clients set TCP transport.
42
+ # 9127 is project-specific; StatsD defaults to 8125/UDP.
43
+ # When deleting it, also remove receivers.statsd/tcp and statsd/tcp
44
+ # from service.pipelines.metrics.receivers in config.yaml.
45
+ - number: 9127
46
+ protocol: tcp
47
+ # Prometheus-formatted metrics exposed by the collector.
48
+ - number: 9292
49
+ protocol: http
50
+ defaultOptions:
51
+ autoscaling:
52
+ metric: disabled
53
+ minScale: 1
54
+ maxScale: 1
55
+ capacityAI: false
56
+ firewallConfig:
57
+ internal:
58
+ inboundAllowType: same-gvc
59
+ external:
60
+ # Prefer a narrow allowlist for production. Use the hostnames or CIDRs
61
+ # required by your telemetry backend.
62
+ outboundAllowHostname:
63
+ - telemetry-backend.example.com
64
+ # Use a collector-specific identity when the collector reads telemetry
65
+ # backend secrets that app workloads should not reveal.
66
+ identityLink: "//identity/{{APP_NAME}}-otel-collector-identity"
67
+ ```
68
+
69
+ Use `outboundAllowCIDR` instead of `outboundAllowHostname` when your backend
70
+ requires IP ranges. Avoid leaving collector egress open to `0.0.0.0/0` in
71
+ production unless your security model explicitly accepts that risk.
72
+
73
+ Create the collector identity and a secret policy in a separate
74
+ `open-telemetry-collector-secrets` template before applying the workload
75
+ template if the collector reads backend tokens from Control Plane secrets:
76
+
77
+ ```yaml
78
+ kind: identity
79
+ name: "{{APP_NAME}}-otel-collector-identity"
80
+ description: "{{APP_NAME}}-otel-collector-identity"
81
+
82
+ ---
83
+ kind: policy
84
+ name: "{{APP_NAME}}-otel-collector-secrets"
85
+ description: "{{APP_NAME}}-otel-collector-secrets"
86
+ bindings:
87
+ - permissions:
88
+ - reveal
89
+ principalLinks:
90
+ - "//gvc/{{APP_NAME}}/identity/{{APP_NAME}}-otel-collector-identity"
91
+ targetKind: secret
92
+ targetLinks:
93
+ - "//secret/{{APP_NAME}}-telemetry-backend"
94
+ ```
95
+
96
+ Create the `{{APP_NAME}}-telemetry-backend` dictionary secret with a
97
+ `TELEMETRY_BACKEND_TOKEN` key, or replace that name consistently before
98
+ applying the templates. Keep this policy scoped to only the backend secret the
99
+ collector needs.
100
+
101
+ `cpflow apply-template` replaces `{{APP_NAME}}` with the actual app name. When
102
+ applying the YAML directly with `cpln`, replace `{{APP_NAME}}` manually before
103
+ running `cpln apply`.
104
+
105
+ Keep the collector image pinned to a tested release and update it as part of
106
+ normal dependency maintenance. Do not rely on a floating `latest` tag for
107
+ production workloads.
108
+
109
+ ## Delivering Collector Config
110
+
111
+ The official collector image does not include your application-specific
112
+ `config.yaml`. A simple Control Plane pattern is to build a small collector image
113
+ that starts from the pinned upstream image and copies the config into the path
114
+ used by the workload command.
115
+
116
+ ```dockerfile
117
+ FROM otel/opentelemetry-collector-contrib:0.155.0
118
+
119
+ COPY config.yaml /etc/otelcol-contrib/config.yaml
120
+ ```
121
+
122
+ Build this image, publish it to your registry, and set the workload template
123
+ `image:` field to that published image. The upstream image is only the base image
124
+ for this Dockerfile because it does not contain your `config.yaml`.
125
+
126
+ Baking the config into the image keeps collector startup deterministic. If your
127
+ team already manages runtime files through Control Plane, you can instead mount a
128
+ secret-backed file with a `cpln://secret/...` URI and `path:` entry, following
129
+ the same pattern used by other file-delivery templates.
130
+
131
+ ## Matching Collector Config
132
+
133
+ Mount or bake the collector config at the path passed to `--config`. The contrib
134
+ image convention is `/etc/otelcol-contrib/config.yaml`; use a different path only
135
+ when your image or command is built for it.
136
+
137
+ The collector config must bind every exposed port. This minimal config receives
138
+ OTLP over HTTP, optionally receives StatsD over TCP, and exposes metrics for
139
+ Prometheus scraping.
140
+
141
+ ```yaml
142
+ receivers:
143
+ otlp:
144
+ protocols:
145
+ http:
146
+ endpoint: 0.0.0.0:4318
147
+
148
+ statsd/tcp:
149
+ endpoint: 0.0.0.0:9127
150
+ transport: tcp
151
+ aggregation_interval: 60s # Use 5s during initial validation.
152
+
153
+ processors:
154
+ memory_limiter:
155
+ check_interval: 1s
156
+ limit_percentage: 80
157
+ spike_limit_percentage: 20
158
+
159
+ batch: {}
160
+
161
+ exporters:
162
+ prometheus:
163
+ endpoint: 0.0.0.0:9292
164
+
165
+ # Replace this placeholder with your real trace/log backend.
166
+ otlphttp/backend:
167
+ endpoint: "https://telemetry-backend.example.com"
168
+ # This is a base URL. The exporter appends /v1/traces, /v1/metrics,
169
+ # and /v1/logs automatically. Do not include the signal path here.
170
+ # headers:
171
+ # Authorization: "Bearer ${env:TELEMETRY_BACKEND_TOKEN}"
172
+
173
+ # Optional validation-only exporter. Do not leave debug in production
174
+ # pipelines because it writes telemetry payloads to collector logs.
175
+ # debug:
176
+ # verbosity: basic
177
+
178
+ service:
179
+ pipelines:
180
+ traces:
181
+ receivers: [otlp]
182
+ processors: [memory_limiter, batch]
183
+ exporters: [otlphttp/backend]
184
+
185
+ metrics:
186
+ receivers: [otlp, statsd/tcp]
187
+ processors: [memory_limiter, batch]
188
+ exporters: [prometheus, otlphttp/backend]
189
+
190
+ logs:
191
+ receivers: [otlp]
192
+ processors: [memory_limiter, batch]
193
+ exporters: [otlphttp/backend]
194
+ ```
195
+
196
+ Before applying the config, replace `telemetry-backend.example.com` with your
197
+ real backend endpoint and headers. Keep `memory_limiter` before `batch`, and
198
+ keep the Prometheus exporter only when your platform or backend scrapes collector
199
+ metrics from port `9292`. For push-only metrics backends, remove `prometheus`
200
+ from the metrics pipeline and keep `otlphttp/backend`. If your application only
201
+ sends OTLP metrics and does not use StatsD, also remove the `statsd/tcp`
202
+ receiver block and `statsd/tcp` from the `metrics.receivers` list. See
203
+ [StatsD and UDP](#statsd-and-udp) for the full three-piece removal.
204
+
205
+ If you have no OTLP push backend at all, remove `otlphttp/backend` from the
206
+ traces, metrics, and logs pipelines and delete the `otlphttp/backend` exporter
207
+ block entirely.
208
+
209
+ Store backend tokens such as `TELEMETRY_BACKEND_TOKEN` in Control Plane secrets
210
+ and bind them only to the collector workload identity shown in the template
211
+ above. Create the collector identity and a policy that grants `reveal` on only
212
+ the telemetry backend secret before applying the collector workload. When the
213
+ collector config references `${env:TELEMETRY_BACKEND_TOKEN}`, also add the
214
+ matching `env` entry to the collector workload so Control Plane injects the
215
+ secret value at startup. Use the app identity placeholder only when the collector
216
+ does not need secrets that are isolated from app workloads. See
217
+ [Secrets and ENV Values](../secrets-and-env-values.md) for the recommended
218
+ pattern.
219
+
220
+ The Prometheus exporter exposes an unauthenticated scrape endpoint. With
221
+ `same-gvc` firewall isolation, any workload in the same GVC that can reach port
222
+ `9292` can read exported metrics. Widen collector inbound access only when that
223
+ is acceptable.
224
+
225
+ Port `9292` is an example scrape port. Configure your scraper to match the port
226
+ you expose in the workload template and bind in the collector config.
227
+
228
+ ## StatsD and UDP
229
+
230
+ The upstream OpenTelemetry StatsD receiver defaults to UDP on port `8125`.
231
+ Control Plane workload templates in this repository currently use `http` and
232
+ `tcp` ports, not `udp`. Do not document or deploy UDP StatsD on Control Plane
233
+ unless you have verified that the platform supports the required UDP forwarding
234
+ path for your workload.
235
+
236
+ Safer defaults:
237
+
238
+ 1. Prefer OTLP metrics over HTTP on `4318`.
239
+ 2. Use StatsD over TCP only when your app client and collector build support it.
240
+ 3. Treat UDP StatsD as an environment-specific advanced option.
241
+
242
+ If you remove StatsD, remove all three pieces together: the workload port, the
243
+ `statsd/tcp` receiver block, and `statsd/tcp` from the metrics pipeline
244
+ receivers.
245
+
246
+ ## `controlplane.yml`
247
+
248
+ Include the collector in setup and informational commands:
249
+
250
+ ```yaml
251
+ aliases:
252
+ common: &common
253
+ setup_app_templates:
254
+ - app
255
+ - worker
256
+ - open-telemetry-collector-secrets
257
+ - open-telemetry-collector
258
+
259
+ app_workloads:
260
+ - app
261
+ - worker
262
+
263
+ additional_workloads:
264
+ - open-telemetry-collector
265
+
266
+ apps:
267
+ example:
268
+ <<: *common
269
+ ```
270
+
271
+ Use `open-telemetry-collector-secrets` for the identity and policy YAML shown
272
+ above. List it before `open-telemetry-collector` so `cpflow setup-app` creates
273
+ the collector identity and secret reveal policy before applying the workload
274
+ that references `identityLink`.
275
+
276
+ Keep `app_workloads` limited to real application workloads such as `app` and
277
+ `worker`. Put the collector under `additional_workloads` so informational
278
+ commands show it without treating it as an app process.
279
+
280
+ For an existing app, apply the identity and policy template before applying the
281
+ collector workload:
282
+
283
+ ```sh
284
+ cpflow apply-template open-telemetry-collector-secrets -a $APP_NAME
285
+ cpflow apply-template open-telemetry-collector -a $APP_NAME
286
+ ```
287
+
288
+ For a brand-new app, `cpflow setup-app` applies both collector templates with
289
+ the other entries listed in `setup_app_templates`, in the order shown above.
290
+
291
+ ## Port Agreement Checklist
292
+
293
+ The Control Plane workload template and collector config must agree on every
294
+ exposed port. See
295
+ [Verify Port Agreement](troubleshooting.md#4-verify-port-agreement) for the
296
+ canonical port checklist. Remove any workload port that is not enabled in the
297
+ collector config.