|
| 1 | +# Temporal Google Cloud OpenTelemetry worker |
| 2 | + |
| 3 | +This sample runs a continuously polling Temporal worker in a **Cloud Run worker pool** and |
| 4 | +exports Temporal SDK metrics and traces through a |
| 5 | +[Google-Built OpenTelemetry Collector](https://cloud.google.com/stackdriver/docs/instrumentation/opentelemetry-collector-cloud-run) |
| 6 | +sidecar. |
| 7 | + |
| 8 | +It is intentionally not a Cloud Run function or request-driven Cloud Run service. Worker pools |
| 9 | +keep CPU allocated while the Temporal worker performs continuous background polling. |
| 10 | + |
| 11 | +```text |
| 12 | +Temporal worker |
| 13 | + │ OTLP/gRPC, localhost:4317 |
| 14 | + ▼ |
| 15 | +Google-Built OpenTelemetry Collector sidecar |
| 16 | + ├── metrics ──► Google Managed Service for Prometheus |
| 17 | + └── traces ──► Telemetry (OTLP) API ──► Cloud Trace storage |
| 18 | +``` |
| 19 | + |
| 20 | +The Java process uses `GcpOpenTelemetryPlugin`, which configures the Temporal SDK metrics scope, |
| 21 | +tracing interceptors, OTLP exporters, and shutdown flushing. The plugin defaults to |
| 22 | +`http://localhost:4317` and derives `service.name` from the Cloud Run-provided |
| 23 | +`CLOUD_RUN_WORKER_POOL` environment variable. It reports and exports metrics every 60 seconds by |
| 24 | +default, matching the upstream OpenTelemetry SDK default and the coordinated Temporal GCP plugin |
| 25 | +default across Java, Go, and Python. This sample deliberately does not override that interval. |
| 26 | + |
| 27 | +## Unreleased SDK dependency |
| 28 | + |
| 29 | +At the time this sample was added, `io.temporal:temporal-gcp` had not been released. The Gradle |
| 30 | +build and Dockerfile therefore use `1.37.0-SNAPSHOT` as an explicit placeholder. A normal build |
| 31 | +from Maven Central and the production Docker build remain blocked until a version containing the |
| 32 | +module is published. Do not replace the plugin with hand-written OpenTelemetry configuration; that |
| 33 | +would stop this sample from exercising the supported SDK API. |
| 34 | + |
| 35 | +You can compile and test against an unmodified local `sdk-java` checkout with Gradle composite |
| 36 | +substitution. The sample-level plugin-default test requires the coordinated 60-second SDK change, |
| 37 | +so an older checkout fails instead of silently testing a different cadence: |
| 38 | + |
| 39 | +```bash |
| 40 | +./gradlew \ |
| 41 | + -PtemporalSdkPath=/path/to/sdk-java \ |
| 42 | + :gcp-opentelemetry:test \ |
| 43 | + :gcp-opentelemetry:installDist |
| 44 | +``` |
| 45 | + |
| 46 | +Alternatively, after publishing all required `1.37.0-SNAPSHOT` SDK modules to Maven Local, add |
| 47 | +`-PuseMavenLocal=true`. Once `temporal-gcp` is released, update the default |
| 48 | +`temporalGcpVersion` in `build.gradle` and `TEMPORAL_GCP_VERSION` in `Dockerfile` to that released |
| 49 | +version. |
| 50 | + |
| 51 | +## Files |
| 52 | + |
| 53 | +- `src/main/java/io/temporal/samples/gcp/GcpOpenTelemetryWorker.java` creates the plugin, client, |
| 54 | + and long-lived worker and performs a bounded shutdown on `SIGTERM`. |
| 55 | +- `collector-config.yaml` adapts Google's Cloud Run collector configuration for cumulative |
| 56 | + Prometheus metrics and batched traces. |
| 57 | +- `worker-pool.yaml` deploys the worker and collector as two containers sharing localhost and |
| 58 | + injects the collector configuration from Secret Manager. |
| 59 | +- `Dockerfile` packages the Gradle application as the worker container. |
| 60 | + |
| 61 | +## Metric cadence and collector batching |
| 62 | + |
| 63 | +`GcpOpenTelemetryPlugin` defaults to a 60-second metric reporting and export interval. Applications |
| 64 | +can still override it with `Builder.setMetricsReportInterval(...)`; custom intervals must remain |
| 65 | +above Google Cloud's five-second minimum. When an application supplies its own `OpenTelemetry` |
| 66 | +instance, it must configure that instance's metric-reader cadence separately. |
| 67 | + |
| 68 | +Metric cadence and collector batching solve different problems. This collector does **not** put its |
| 69 | +cumulative OTLP metrics through a batch processor: a forced shutdown flush can arrive immediately |
| 70 | +after a periodic export, and a metric batch could combine both points for the same Prometheus time |
| 71 | +series even when the periodic interval is much longer than the batch timeout. Managed Service for |
| 72 | +Prometheus rejects that request as `Duplicate TimeSeries`. |
| 73 | + |
| 74 | +The five-second ingestion minimum and metric batching are separate constraints. Meeting the |
| 75 | +minimum does not make batching cumulative metrics safe during shutdown. |
| 76 | + |
| 77 | +The dedicated `batch/traces` processor retains a five-second timeout because trace batching is |
| 78 | +independently useful and does not have the cumulative-series collision behavior. Do not add |
| 79 | +`batch/traces` to `metrics/otlp` or introduce another metric batch processor as a substitute for |
| 80 | +choosing an application metric cadence. |
| 81 | + |
| 82 | +## Required Google Cloud APIs |
| 83 | + |
| 84 | +Enable these APIs in the project that hosts the worker pool: |
| 85 | + |
| 86 | +```bash |
| 87 | +gcloud services enable \ |
| 88 | + run.googleapis.com \ |
| 89 | + artifactregistry.googleapis.com \ |
| 90 | + secretmanager.googleapis.com \ |
| 91 | + iam.googleapis.com \ |
| 92 | + cloudresourcemanager.googleapis.com \ |
| 93 | + monitoring.googleapis.com \ |
| 94 | + telemetry.googleapis.com \ |
| 95 | + cloudtrace.googleapis.com \ |
| 96 | + --project="$PROJECT_ID" |
| 97 | +``` |
| 98 | + |
| 99 | +`monitoring.googleapis.com` is required for Google Managed Service for Prometheus ingestion. |
| 100 | +Traces are sent using authenticated OTLP to `telemetry.googleapis.com`; the Cloud Trace API must |
| 101 | +also be enabled or Google Cloud discards trace data received by the Telemetry API. |
| 102 | +The IAM API is used to create the runtime service account. The declarative worker-pool replacement |
| 103 | +workflow can require the Cloud Resource Manager API to resolve the target project. |
| 104 | + |
| 105 | +The account enabling APIs needs `roles/serviceusage.serviceUsageAdmin` (or equivalent |
| 106 | +permissions). |
| 107 | + |
| 108 | +## IAM |
| 109 | + |
| 110 | +Use a user-managed service account as the Cloud Run worker pool service identity. The collector |
| 111 | +uses that identity through Application Default Credentials; do not set |
| 112 | +`GOOGLE_APPLICATION_CREDENTIALS` in Cloud Run. |
| 113 | + |
| 114 | +Grant the worker-pool service account: |
| 115 | + |
| 116 | +- `roles/monitoring.metricWriter` on the telemetry project, for the |
| 117 | + `googlemanagedprometheus` exporter. |
| 118 | +- `roles/telemetry.tracesWriter` on the telemetry project, for OTLP traces sent to the Telemetry |
| 119 | + API. `roles/cloudtrace.agent` also contains the write permission, but the narrower Telemetry role |
| 120 | + is preferred here. |
| 121 | +- `roles/serviceusage.serviceUsageConsumer` on the quota project (the same project in this |
| 122 | + example). |
| 123 | +- `roles/secretmanager.secretAccessor` on the collector-config and Temporal API-key secrets. |
| 124 | + |
| 125 | +For example: |
| 126 | + |
| 127 | +```bash |
| 128 | +gcloud iam service-accounts create temporal-gcp-worker --project="$PROJECT_ID" |
| 129 | + |
| 130 | +SERVICE_ACCOUNT="temporal-gcp-worker@${PROJECT_ID}.iam.gserviceaccount.com" |
| 131 | +for ROLE in \ |
| 132 | + roles/monitoring.metricWriter \ |
| 133 | + roles/telemetry.tracesWriter \ |
| 134 | + roles/serviceusage.serviceUsageConsumer \ |
| 135 | + roles/secretmanager.secretAccessor |
| 136 | +do |
| 137 | + gcloud projects add-iam-policy-binding "$PROJECT_ID" \ |
| 138 | + --member="serviceAccount:${SERVICE_ACCOUNT}" \ |
| 139 | + --role="$ROLE" |
| 140 | +done |
| 141 | +``` |
| 142 | + |
| 143 | +The deployer needs `roles/run.admin` (or the documented worker-pool deployment permissions) and |
| 144 | +`roles/iam.serviceAccountUser` on this service account. Creating the service account, Artifact |
| 145 | +Registry repository, secrets, and their IAM bindings also requires the corresponding administrative |
| 146 | +permissions. The runtime service account does not need those administrative roles. |
| 147 | + |
| 148 | +The collector configuration does not export OTLP logs, so it does not require |
| 149 | +`roles/logging.logWriter`. Cloud Run still captures the worker and collector containers' stdout and |
| 150 | +stderr through its platform logging. |
| 151 | + |
| 152 | +## Build and deploy |
| 153 | + |
| 154 | +The commands below assume an existing Temporal Cloud namespace and API key. |
| 155 | + |
| 156 | +1. Create the secrets. Pin the API key to a numbered version in `worker-pool.yaml`; the collector |
| 157 | + configuration is also pinned to a numbered version and injected as `OTELCOL_CONFIG`. |
| 158 | + |
| 159 | + ```bash |
| 160 | + printf '%s' "$TEMPORAL_API_KEY" | \ |
| 161 | + gcloud secrets create temporal-api-key --data-file=- --project="$PROJECT_ID" |
| 162 | + |
| 163 | + gcloud secrets create temporal-gcp-otel-config \ |
| 164 | + --data-file=gcp-opentelemetry/collector-config.yaml \ |
| 165 | + --project="$PROJECT_ID" |
| 166 | + ``` |
| 167 | + |
| 168 | + If either secret already exists, add a version with `gcloud secrets versions add` instead. |
| 169 | + The manifest loads the collector YAML with `--config=env:OTELCOL_CONFIG`. This is intentional: |
| 170 | + secret-backed file volumes have been rejected by some Cloud Run worker-pool rollouts even where |
| 171 | + current documentation advertises support. |
| 172 | + |
| 173 | +2. After `temporal-gcp` is released, create an Artifact Registry repository and build and push the |
| 174 | + worker image from the repository root: |
| 175 | + |
| 176 | + ```bash |
| 177 | + REGION=us-central1 |
| 178 | + RELEASED_TEMPORAL_GCP_VERSION=REPLACE_AFTER_RELEASE |
| 179 | + IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/temporal-samples/gcp-opentelemetry:latest" |
| 180 | + |
| 181 | + gcloud artifacts repositories create temporal-samples \ |
| 182 | + --repository-format=docker \ |
| 183 | + --location="$REGION" \ |
| 184 | + --project="$PROJECT_ID" |
| 185 | + gcloud auth configure-docker "${REGION}-docker.pkg.dev" |
| 186 | + |
| 187 | + docker build \ |
| 188 | + -f gcp-opentelemetry/Dockerfile \ |
| 189 | + --build-arg "TEMPORAL_GCP_VERSION=${RELEASED_TEMPORAL_GCP_VERSION}" \ |
| 190 | + -t "$IMAGE" \ |
| 191 | + . |
| 192 | + docker push "$IMAGE" |
| 193 | + ``` |
| 194 | + |
| 195 | +3. Edit the placeholders in `worker-pool.yaml`: |
| 196 | + |
| 197 | + - `PROJECT_ID` and `REGION`. |
| 198 | + - `NAMESPACE_ID.ACCOUNT_ID` and the matching Temporal Cloud address. |
| 199 | + - The Temporal API-key and collector-config secret versions if either is not version `1`. |
| 200 | + - Image tag, task queue, instance count, and container resources as appropriate. |
| 201 | + |
| 202 | +4. Deploy the worker pool: |
| 203 | + |
| 204 | + ```bash |
| 205 | + gcloud run worker-pools replace gcp-opentelemetry/worker-pool.yaml \ |
| 206 | + --dry-run \ |
| 207 | + --project="$PROJECT_ID" |
| 208 | + |
| 209 | + gcloud run worker-pools replace gcp-opentelemetry/worker-pool.yaml \ |
| 210 | + --project="$PROJECT_ID" |
| 211 | + ``` |
| 212 | + |
| 213 | +Worker pools use manual instance counts. This manifest starts one continuously allocated instance; |
| 214 | +setting the count to zero disables the worker pool. |
| 215 | + |
| 216 | +## Collector startup and health requirements |
| 217 | + |
| 218 | +The collector is a required dependency, not an optional observability add-on: |
| 219 | + |
| 220 | +- `run.googleapis.com/container-dependencies` declares that `worker` depends on `collector`. |
| 221 | +- The collector enables `health_check` on `0.0.0.0:13133` and has a startup probe on `/`. |
| 222 | + Cloud Run worker pools do not supply a default startup probe. Without this probe Cloud Run can |
| 223 | + start the worker even when the collector failed to load its configuration. |
| 224 | +- The collector configuration is a Secret Manager-backed environment variable loaded through the |
| 225 | + collector's `env` configuration provider. Keep the YAML below the Cloud Run secret-environment |
| 226 | + size limit; this sample configuration is intentionally small. |
| 227 | +- The worker starts only after the collector startup probe succeeds. A liveness probe restarts the |
| 228 | + collector if it later becomes unhealthy. |
| 229 | +- The OTLP receiver listens on `localhost:4317`, which is reachable by both containers because |
| 230 | + containers in a worker-pool instance share a network namespace. |
| 231 | +- A successful health probe confirms that the collector is running and accepted its configuration; |
| 232 | + it does not prove that Google Cloud ingestion and IAM are working. Check collector logs for |
| 233 | + exporter errors and verify both signals after deployment. |
| 234 | + |
| 235 | +If the collector is unavailable after startup, the Temporal worker continues processing work but |
| 236 | +telemetry delivery can be delayed or lost. Treat collector liveness and exporter failures as |
| 237 | +operational alerts. |
| 238 | + |
| 239 | +## Generate and view telemetry |
| 240 | + |
| 241 | +Start `GreetingWorkflow` on task queue `gcp-opentelemetry` with a single string argument. For |
| 242 | +example, with an already configured Temporal CLI: |
| 243 | + |
| 244 | +```bash |
| 245 | +temporal workflow start \ |
| 246 | + --workflow-id gcp-otel-greeting \ |
| 247 | + --type GreetingWorkflow \ |
| 248 | + --task-queue gcp-opentelemetry \ |
| 249 | + --input '"Google Cloud"' |
| 250 | +``` |
| 251 | + |
| 252 | +Temporal SDK metrics appear as Prometheus metrics in Cloud Monitoring. Traces appear in Trace |
| 253 | +Explorer after passing through the Telemetry API. The OpenTelemetry service name defaults to the |
| 254 | +value of `CLOUD_RUN_WORKER_POOL`; set `OTEL_SERVICE_NAME` on the worker container only if you need |
| 255 | +an explicit override. |
| 256 | + |
| 257 | +For end-to-end metric verification, observe at least one normal 60-second periodic export, then |
| 258 | +terminate or replace a worker-pool revision to exercise the forced shutdown flush. Confirm that |
| 259 | +both exports reach Managed Service for Prometheus and that the collector logs contain no |
| 260 | +`Duplicate TimeSeries` rejection. A short smoke test that exercises only one of these paths is not |
| 261 | +sufficient evidence for the cumulative-metric pipeline. |
| 262 | + |
| 263 | +## Shutdown |
| 264 | + |
| 265 | +Cloud Run sends `SIGTERM` and allows 10 seconds before `SIGKILL`. The shutdown hook reserves six |
| 266 | +seconds for graceful worker shutdown, one second for forced shutdown if necessary, and two seconds |
| 267 | +for the plugin's Temporal-metrics and OpenTelemetry flush before closing the service stubs. The |
| 268 | +flush runs after worker termination so it includes telemetry emitted by finishing tasks. |
| 269 | +Long-running Activities must still use heartbeats and cancellation handling so they can stop within |
| 270 | +the platform shutdown window. |
0 commit comments