Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Some examples require extra dependencies. See each sample's directory for specif
* [env_config](env_config) - Load client configuration from TOML files with programmatic overrides.
* [external_storage](external_storage) - Offload large payloads to S3-compatible object storage, plus a codec server for the Web UI and CLI.
* [external_storage_redis](external_storage_redis) - Redis driver for external storage
* [gcp/cloud_run/opentelemetry](gcp/cloud_run/opentelemetry) - Run a Temporal Worker on a Google Cloud Run worker pool with OpenTelemetry traces and metrics exported to a Google-Built Collector sidecar.
* [gevent_async](gevent_async) - Combine gevent and Temporal.
* [google_adk_agents](google_adk_agents) - Run Google ADK agents as durable Temporal workflows (model calls, tools, multi-agent, MCP, streaming).
* [google_genai](google_genai) - Run the Google Gemini SDK inside durable Temporal workflows.
Expand Down
Empty file added gcp/__init__.py
Empty file.
Empty file added gcp/cloud_run/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions gcp/cloud_run/opentelemetry/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
*
!Dockerfile
!pyproject.toml
!__init__.py
!settings.py
!worker.py
!workflow.py
30 changes: 30 additions & 0 deletions gcp/cloud_run/opentelemetry/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# syntax=docker/dockerfile:1

FROM python:3.13-slim AS builder

COPY --from=ghcr.io/astral-sh/uv:0.8.15 /uv /uvx /bin/

ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy

WORKDIR /app
COPY pyproject.toml ./
RUN uv sync --no-dev

FROM python:3.13-slim

ENV PATH=/app/.venv/bin:$PATH \
PYTHONUNBUFFERED=1

RUN groupadd --system app \
&& useradd --system --gid app --create-home app

WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
# The build context is this sample directory (flat); recreate the package path.
COPY --chown=app:app __init__.py settings.py worker.py workflow.py /app/gcp/cloud_run/opentelemetry/
RUN touch /app/gcp/__init__.py /app/gcp/cloud_run/__init__.py \
&& chown -R app:app /app/gcp

USER app
CMD ["python", "-m", "gcp.cloud_run.opentelemetry.worker"]
80 changes: 80 additions & 0 deletions gcp/cloud_run/opentelemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Google Cloud Run OpenTelemetry Worker

Run a Temporal Worker on a [Google Cloud Run worker
pool](https://cloud.google.com/run/docs/worker-pools) with
`temporalio.contrib.gcp.cloud_run.opentelemetry.OpenTelemetryPlugin`. It exports
Temporal Core metrics and traces over OTLP/gRPC to a Google-Built OpenTelemetry
Collector sidecar, which forwards traces to the Cloud Telemetry API and metrics
to Google Managed Service for Prometheus. Endpoint, service name (from
`CLOUD_RUN_WORKER_POOL`), tracer provider, and 60s metric export are plugin
defaults; `worker.py` opts into `add_temporal_spans=True` for operation spans.

Prerequisites: a Temporal Cloud namespace and API key; a Google Cloud project
with billing and an authenticated `gcloud` CLI; `envsubst` (`gettext` package).
Worker pools bill continuously, so run scale-to-zero (step 6) after testing.

## Deploy

Run from the repository root, with your own values:

```bash
export PROJECT_ID=your-project-id REGION=us-central1
export REPOSITORY=temporal-workers WORKER_POOL=temporal-gcp-cloud-run
export SERVICE_ACCOUNT_EMAIL="cloud-run-worker@${PROJECT_ID}.iam.gserviceaccount.com"
export TEMPORAL_NAMESPACE=your-namespace.account-id
export TEMPORAL_ADDRESS="${TEMPORAL_NAMESPACE}.tmprl.cloud:7233"
export TEMPORAL_TASK_QUEUE=gcp-cloud-run
export TEMPORAL_API_KEY_FILE=/secure/path/to/temporal-api-key
export TEMPORAL_API_KEY_SECRET=temporal-api-key TEMPORAL_API_KEY_SECRET_VERSION=1
export COLLECTOR_CONFIG_SECRET=temporal-otel-collector COLLECTOR_CONFIG_SECRET_VERSION=1
export WORKER_IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/gcp-cloud-run:v1"
export INSTANCE_COUNT=1

# 1. Enable APIs, create the Artifact Registry repo and the runtime service account.
gcloud services enable artifactregistry.googleapis.com cloudbuild.googleapis.com \
monitoring.googleapis.com run.googleapis.com secretmanager.googleapis.com \
telemetry.googleapis.com --project "$PROJECT_ID"
gcloud artifacts repositories create "$REPOSITORY" --location "$REGION" \
--repository-format docker --project "$PROJECT_ID"

# 2. Grant the service account the collector's telemetry roles.
for role in roles/logging.logWriter roles/monitoring.metricWriter roles/telemetry.tracesWriter; do
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member "serviceAccount:${SERVICE_ACCOUNT_EMAIL}" --role "$role"
done

# 3. Store the API key and collector config as secrets the service account can read.
gcloud secrets create "$TEMPORAL_API_KEY_SECRET" --data-file "$TEMPORAL_API_KEY_FILE" --project "$PROJECT_ID"
gcloud secrets create "$COLLECTOR_CONFIG_SECRET" \
--data-file gcp/cloud_run/opentelemetry/collector-config.yaml --project "$PROJECT_ID"
for secret in "$TEMPORAL_API_KEY_SECRET" "$COLLECTOR_CONFIG_SECRET"; do
gcloud secrets add-iam-policy-binding "$secret" \
--member "serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
--role roles/secretmanager.secretAccessor --project "$PROJECT_ID"
done

# 4. Build the image (build context is the sample dir, keeping credentials out).
gcloud builds submit gcp/cloud_run/opentelemetry --region "$REGION" \
--tag "$WORKER_IMAGE" --project "$PROJECT_ID"

# 5. Render and deploy the two-container worker pool.
envsubst < gcp/cloud_run/opentelemetry/worker-pool.yaml > /tmp/worker-pool.yaml
gcloud run worker-pools replace /tmp/worker-pool.yaml --project "$PROJECT_ID"

# 6. Scale to zero when done to stop compute charges.
gcloud run worker-pools update "$WORKER_POOL" --instances 0 \
--region "$REGION" --project "$PROJECT_ID"
```

## Run a Workflow and verify

```bash
uv sync --group gcp-cloud-run-opentelemetry
TEMPORAL_API_KEY="$(cat "$TEMPORAL_API_KEY_FILE")" \
uv run --group gcp-cloud-run-opentelemetry python -m gcp.cloud_run.opentelemetry.starter
```

The starter prints `Hello, Temporal!`. In Google Cloud, Trace Explorer then shows
`RunWorkflow:GreetingWorkflow` (with `service.name` = the worker-pool name) and
Metrics Explorer shows
`prometheus.googleapis.com/temporal_workflow_completed_total/counter`.
1 change: 1 addition & 0 deletions gcp/cloud_run/opentelemetry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Google Cloud Run worker-pool OpenTelemetry sample."""
68 changes: 68 additions & 0 deletions gcp/cloud_run/opentelemetry/collector-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# @@@SNIPSTART python-cloud-run-otel-collector-config
receivers:
otlp:
protocols:
grpc:
endpoint: localhost:4317

processors:
batch/traces:
send_batch_max_size: 200
send_batch_size: 200
timeout: 5s
memory_limiter:
check_interval: 1s
limit_percentage: 65
spike_limit_percentage: 20
resource_detection:
detectors: [gcp]
timeout: 10s
transform/collision:
metric_statements:
- context: datapoint
statements:
- set(attributes["exported_location"], attributes["location"])
- delete_key(attributes, "location")
- set(attributes["exported_cluster"], attributes["cluster"])
- delete_key(attributes, "cluster")
- set(attributes["exported_namespace"], attributes["namespace"])
- delete_key(attributes, "namespace")
- set(attributes["exported_job"], attributes["job"])
- delete_key(attributes, "job")
- set(attributes["exported_instance"], attributes["instance"])
- delete_key(attributes, "instance")
- set(attributes["exported_project_id"], attributes["project_id"])
- delete_key(attributes, "project_id")
transform/set_project_id:
error_mode: ignore
trace_statements:
- set(resource.attributes["gcp.project_id"], resource.attributes["gcp.project.id"]) where resource.attributes["gcp.project.id"] != nil
- set(resource.attributes["gcp.project_id"], resource.attributes["cloud.account.id"]) where resource.attributes["gcp.project_id"] == nil and resource.attributes["cloud.account.id"] != nil

exporters:
googlemanagedprometheus:
otlp_grpc:
endpoint: telemetry.googleapis.com:443
compression: none
balancer_name: pick_first
auth:
authenticator: googleclientauth

extensions:
googleclientauth:
health_check:
endpoint: 0.0.0.0:13133

service:
extensions: [googleclientauth, health_check]
pipelines:
metrics:
receivers: [otlp]
processors: [memory_limiter, resource_detection, transform/collision]
exporters: [googlemanagedprometheus]
traces:
receivers: [otlp]
processors:
[memory_limiter, resource_detection, transform/set_project_id, batch/traces]
exporters: [otlp_grpc]
# @@@SNIPEND
9 changes: 9 additions & 0 deletions gcp/cloud_run/opentelemetry/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
name = "temporalio-gcp-cloud-run-opentelemetry-sample"
version = "0.1.0"
description = "Temporal Worker on a Google Cloud Run worker pool with OpenTelemetry"
requires-python = ">=3.10"
dependencies = ["temporalio[cloud-run-worker-otel]>=1.33.0"]

[tool.uv]
package = false
34 changes: 34 additions & 0 deletions gcp/cloud_run/opentelemetry/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Temporal connection settings shared by the worker and the starter.

Set ``TEMPORAL_API_KEY`` to connect to Temporal Cloud (which enables TLS); leave
it unset for a plaintext dev server.
"""

from __future__ import annotations

import os
from dataclasses import dataclass


@dataclass(frozen=True)
class Settings:
address: str
namespace: str
task_queue: str
api_key: str | None

@property
def tls(self) -> bool:
return self.api_key is not None


def load_settings() -> Settings:
namespace = os.environ.get("TEMPORAL_NAMESPACE") or "default"
api_key = os.environ.get("TEMPORAL_API_KEY")
return Settings(
address=os.environ.get("TEMPORAL_ADDRESS") or f"{namespace}.tmprl.cloud:7233",
namespace=namespace,
task_queue=os.environ.get("TEMPORAL_TASK_QUEUE") or "gcp-cloud-run",
# Secret managers frequently preserve a trailing newline; strip it.
api_key=api_key.strip() if api_key else None,
)
34 changes: 34 additions & 0 deletions gcp/cloud_run/opentelemetry/starter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Start the sample Workflow against the Cloud Run worker's task queue."""

from __future__ import annotations

import asyncio
from uuid import uuid4

from temporalio.client import Client

from gcp.cloud_run.opentelemetry.settings import load_settings
from gcp.cloud_run.opentelemetry.workflow import GreetingWorkflow


async def main() -> None:
settings = load_settings()
client = await Client.connect(
settings.address,
namespace=settings.namespace,
api_key=settings.api_key,
tls=settings.tls,
)

workflow_id = f"gcp-cloud-run-{uuid4()}"
result = await client.execute_workflow(
GreetingWorkflow.run,
"Temporal",
id=workflow_id,
task_queue=settings.task_queue,
)
print(f"Workflow {workflow_id} result: {result}")


if __name__ == "__main__":
asyncio.run(main())
58 changes: 58 additions & 0 deletions gcp/cloud_run/opentelemetry/worker-pool.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
apiVersion: run.googleapis.com/v1
kind: WorkerPool
metadata:
annotations:
run.googleapis.com/manualInstanceCount: "${INSTANCE_COUNT}"
run.googleapis.com/scalingMode: manual
labels:
cloud.googleapis.com/location: "${REGION}"
name: "${WORKER_POOL}"
spec:
template:
metadata:
annotations:
run.googleapis.com/container-dependencies: '{"worker":["collector"]}'
run.googleapis.com/execution-environment: gen2
spec:
containerConcurrency: 0
containers:
- name: worker
image: "${WORKER_IMAGE}"
env:
- name: TEMPORAL_NAMESPACE
value: "${TEMPORAL_NAMESPACE}"
- name: TEMPORAL_ADDRESS
value: "${TEMPORAL_ADDRESS}"
- name: TEMPORAL_TASK_QUEUE
value: "${TEMPORAL_TASK_QUEUE}"
- name: TEMPORAL_API_KEY
valueFrom:
secretKeyRef:
key: "${TEMPORAL_API_KEY_SECRET_VERSION}"
name: "${TEMPORAL_API_KEY_SECRET}"
resources:
limits:
cpu: "1"
memory: 512Mi
- name: collector
image: us-docker.pkg.dev/cloud-ops-agents-artifacts/google-cloud-opentelemetry-collector/otelcol-google:0.156.0
args:
- --config=env:OTELCOL_CONFIG
env:
- name: OTELCOL_CONFIG
valueFrom:
secretKeyRef:
key: "${COLLECTOR_CONFIG_SECRET_VERSION}"
name: "${COLLECTOR_CONFIG_SECRET}"
startupProbe:
httpGet:
path: /
port: 13133
timeoutSeconds: 1
periodSeconds: 2
failureThreshold: 30
resources:
limits:
cpu: "1"
memory: 512Mi
serviceAccountName: "${SERVICE_ACCOUNT_EMAIL}"
Loading
Loading