diff --git a/README.md b/README.md index 1465a9cc..a84cc61a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Temporal using the [Java SDK](https://github.com/temporalio/sdk-java). It contains the following modules: * [Core](/core): showcases many different SDK features. +* [Google Cloud OpenTelemetry](/gcp-opentelemetry): runs a continuously polling worker in a Cloud Run worker pool and exports metrics and traces through the Google-Built OpenTelemetry Collector. * [SpringBoot](/springboot): showcases SpringBoot autoconfig integration. * [SpringBoot Basic](/springboot-basic): Minimal sample showing SpringBoot autoconfig integration without any extra external dependencies. * [Spring AI](/springai): demonstrates the Temporal Spring AI integration — durable AI agents with chat models, tools, MCP servers, vector stores, and embeddings. diff --git a/gcp-opentelemetry/Dockerfile b/gcp-opentelemetry/Dockerfile new file mode 100644 index 00000000..33a909e4 --- /dev/null +++ b/gcp-opentelemetry/Dockerfile @@ -0,0 +1,19 @@ +FROM eclipse-temurin:17-jdk-jammy AS build + +WORKDIR /workspace +COPY . . + +# Replace this snapshot with the first released version that contains temporal-gcp. +ARG TEMPORAL_GCP_VERSION=1.37.0-SNAPSHOT +RUN ./gradlew --no-daemon :gcp-opentelemetry:installDist \ + -PtemporalGcpVersion=${TEMPORAL_GCP_VERSION} + +FROM eclipse-temurin:17-jre-jammy + +RUN useradd --create-home --uid 10001 temporal +WORKDIR /app +COPY --from=build --chown=temporal:temporal \ + /workspace/gcp-opentelemetry/build/install/gcp-opentelemetry/ /app/ + +USER 10001 +ENTRYPOINT ["/app/bin/gcp-opentelemetry"] diff --git a/gcp-opentelemetry/README.md b/gcp-opentelemetry/README.md new file mode 100644 index 00000000..8b3f4e60 --- /dev/null +++ b/gcp-opentelemetry/README.md @@ -0,0 +1,270 @@ +# Temporal Google Cloud OpenTelemetry worker + +This sample runs a continuously polling Temporal worker in a **Cloud Run worker pool** and +exports Temporal SDK metrics and traces through a +[Google-Built OpenTelemetry Collector](https://cloud.google.com/stackdriver/docs/instrumentation/opentelemetry-collector-cloud-run) +sidecar. + +It is intentionally not a Cloud Run function or request-driven Cloud Run service. Worker pools +keep CPU allocated while the Temporal worker performs continuous background polling. + +```text +Temporal worker + │ OTLP/gRPC, localhost:4317 + ▼ +Google-Built OpenTelemetry Collector sidecar + ├── metrics ──► Google Managed Service for Prometheus + └── traces ──► Telemetry (OTLP) API ──► Cloud Trace storage +``` + +The Java process uses `GcpOpenTelemetryPlugin`, which configures the Temporal SDK metrics scope, +tracing interceptors, OTLP exporters, and shutdown flushing. The plugin defaults to +`http://localhost:4317` and derives `service.name` from the Cloud Run-provided +`CLOUD_RUN_WORKER_POOL` environment variable. It reports and exports metrics every 60 seconds by +default, matching the upstream OpenTelemetry SDK default and the coordinated Temporal GCP plugin +default across Java, Go, and Python. This sample deliberately does not override that interval. + +## Unreleased SDK dependency + +At the time this sample was added, `io.temporal:temporal-gcp` had not been released. The Gradle +build and Dockerfile therefore use `1.37.0-SNAPSHOT` as an explicit placeholder. A normal build +from Maven Central and the production Docker build remain blocked until a version containing the +module is published. Do not replace the plugin with hand-written OpenTelemetry configuration; that +would stop this sample from exercising the supported SDK API. + +You can compile and test against an unmodified local `sdk-java` checkout with Gradle composite +substitution. The sample-level plugin-default test requires the coordinated 60-second SDK change, +so an older checkout fails instead of silently testing a different cadence: + +```bash +./gradlew \ + -PtemporalSdkPath=/path/to/sdk-java \ + :gcp-opentelemetry:test \ + :gcp-opentelemetry:installDist +``` + +Alternatively, after publishing all required `1.37.0-SNAPSHOT` SDK modules to Maven Local, add +`-PuseMavenLocal=true`. Once `temporal-gcp` is released, update the default +`temporalGcpVersion` in `build.gradle` and `TEMPORAL_GCP_VERSION` in `Dockerfile` to that released +version. + +## Files + +- `src/main/java/io/temporal/samples/gcp/GcpOpenTelemetryWorker.java` creates the plugin, client, + and long-lived worker and performs a bounded shutdown on `SIGTERM`. +- `collector-config.yaml` adapts Google's Cloud Run collector configuration for cumulative + Prometheus metrics and batched traces. +- `worker-pool.yaml` deploys the worker and collector as two containers sharing localhost and + injects the collector configuration from Secret Manager. +- `Dockerfile` packages the Gradle application as the worker container. + +## Metric cadence and collector batching + +`GcpOpenTelemetryPlugin` defaults to a 60-second metric reporting and export interval. Applications +can still override it with `Builder.setMetricsReportInterval(...)`; custom intervals must remain +above Google Cloud's five-second minimum. When an application supplies its own `OpenTelemetry` +instance, it must configure that instance's metric-reader cadence separately. + +Metric cadence and collector batching solve different problems. This collector does **not** put its +cumulative OTLP metrics through a batch processor: a forced shutdown flush can arrive immediately +after a periodic export, and a metric batch could combine both points for the same Prometheus time +series even when the periodic interval is much longer than the batch timeout. Managed Service for +Prometheus rejects that request as `Duplicate TimeSeries`. + +The five-second ingestion minimum and metric batching are separate constraints. Meeting the +minimum does not make batching cumulative metrics safe during shutdown. + +The dedicated `batch/traces` processor retains a five-second timeout because trace batching is +independently useful and does not have the cumulative-series collision behavior. Do not add +`batch/traces` to `metrics/otlp` or introduce another metric batch processor as a substitute for +choosing an application metric cadence. + +## Required Google Cloud APIs + +Enable these APIs in the project that hosts the worker pool: + +```bash +gcloud services enable \ + run.googleapis.com \ + artifactregistry.googleapis.com \ + secretmanager.googleapis.com \ + iam.googleapis.com \ + cloudresourcemanager.googleapis.com \ + monitoring.googleapis.com \ + telemetry.googleapis.com \ + cloudtrace.googleapis.com \ + --project="$PROJECT_ID" +``` + +`monitoring.googleapis.com` is required for Google Managed Service for Prometheus ingestion. +Traces are sent using authenticated OTLP to `telemetry.googleapis.com`; the Cloud Trace API must +also be enabled or Google Cloud discards trace data received by the Telemetry API. +The IAM API is used to create the runtime service account. The declarative worker-pool replacement +workflow can require the Cloud Resource Manager API to resolve the target project. + +The account enabling APIs needs `roles/serviceusage.serviceUsageAdmin` (or equivalent +permissions). + +## IAM + +Use a user-managed service account as the Cloud Run worker pool service identity. The collector +uses that identity through Application Default Credentials; do not set +`GOOGLE_APPLICATION_CREDENTIALS` in Cloud Run. + +Grant the worker-pool service account: + +- `roles/monitoring.metricWriter` on the telemetry project, for the + `googlemanagedprometheus` exporter. +- `roles/telemetry.tracesWriter` on the telemetry project, for OTLP traces sent to the Telemetry + API. `roles/cloudtrace.agent` also contains the write permission, but the narrower Telemetry role + is preferred here. +- `roles/serviceusage.serviceUsageConsumer` on the quota project (the same project in this + example). +- `roles/secretmanager.secretAccessor` on the collector-config and Temporal API-key secrets. + +For example: + +```bash +gcloud iam service-accounts create temporal-gcp-worker --project="$PROJECT_ID" + +SERVICE_ACCOUNT="temporal-gcp-worker@${PROJECT_ID}.iam.gserviceaccount.com" +for ROLE in \ + roles/monitoring.metricWriter \ + roles/telemetry.tracesWriter \ + roles/serviceusage.serviceUsageConsumer \ + roles/secretmanager.secretAccessor +do + gcloud projects add-iam-policy-binding "$PROJECT_ID" \ + --member="serviceAccount:${SERVICE_ACCOUNT}" \ + --role="$ROLE" +done +``` + +The deployer needs `roles/run.admin` (or the documented worker-pool deployment permissions) and +`roles/iam.serviceAccountUser` on this service account. Creating the service account, Artifact +Registry repository, secrets, and their IAM bindings also requires the corresponding administrative +permissions. The runtime service account does not need those administrative roles. + +The collector configuration does not export OTLP logs, so it does not require +`roles/logging.logWriter`. Cloud Run still captures the worker and collector containers' stdout and +stderr through its platform logging. + +## Build and deploy + +The commands below assume an existing Temporal Cloud namespace and API key. + +1. Create the secrets. Pin the API key to a numbered version in `worker-pool.yaml`; the collector + configuration is also pinned to a numbered version and injected as `OTELCOL_CONFIG`. + + ```bash + printf '%s' "$TEMPORAL_API_KEY" | \ + gcloud secrets create temporal-api-key --data-file=- --project="$PROJECT_ID" + + gcloud secrets create temporal-gcp-otel-config \ + --data-file=gcp-opentelemetry/collector-config.yaml \ + --project="$PROJECT_ID" + ``` + + If either secret already exists, add a version with `gcloud secrets versions add` instead. + The manifest loads the collector YAML with `--config=env:OTELCOL_CONFIG`. This is intentional: + secret-backed file volumes have been rejected by some Cloud Run worker-pool rollouts even where + current documentation advertises support. + +2. After `temporal-gcp` is released, create an Artifact Registry repository and build and push the + worker image from the repository root: + + ```bash + REGION=us-central1 + RELEASED_TEMPORAL_GCP_VERSION=REPLACE_AFTER_RELEASE + IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/temporal-samples/gcp-opentelemetry:latest" + + gcloud artifacts repositories create temporal-samples \ + --repository-format=docker \ + --location="$REGION" \ + --project="$PROJECT_ID" + gcloud auth configure-docker "${REGION}-docker.pkg.dev" + + docker build \ + -f gcp-opentelemetry/Dockerfile \ + --build-arg "TEMPORAL_GCP_VERSION=${RELEASED_TEMPORAL_GCP_VERSION}" \ + -t "$IMAGE" \ + . + docker push "$IMAGE" + ``` + +3. Edit the placeholders in `worker-pool.yaml`: + + - `PROJECT_ID` and `REGION`. + - `NAMESPACE_ID.ACCOUNT_ID` and the matching Temporal Cloud address. + - The Temporal API-key and collector-config secret versions if either is not version `1`. + - Image tag, task queue, instance count, and container resources as appropriate. + +4. Deploy the worker pool: + + ```bash + gcloud run worker-pools replace gcp-opentelemetry/worker-pool.yaml \ + --dry-run \ + --project="$PROJECT_ID" + + gcloud run worker-pools replace gcp-opentelemetry/worker-pool.yaml \ + --project="$PROJECT_ID" + ``` + +Worker pools use manual instance counts. This manifest starts one continuously allocated instance; +setting the count to zero disables the worker pool. + +## Collector startup and health requirements + +The collector is a required dependency, not an optional observability add-on: + +- `run.googleapis.com/container-dependencies` declares that `worker` depends on `collector`. +- The collector enables `health_check` on `0.0.0.0:13133` and has a startup probe on `/`. + Cloud Run worker pools do not supply a default startup probe. Without this probe Cloud Run can + start the worker even when the collector failed to load its configuration. +- The collector configuration is a Secret Manager-backed environment variable loaded through the + collector's `env` configuration provider. Keep the YAML below the Cloud Run secret-environment + size limit; this sample configuration is intentionally small. +- The worker starts only after the collector startup probe succeeds. A liveness probe restarts the + collector if it later becomes unhealthy. +- The OTLP receiver listens on `localhost:4317`, which is reachable by both containers because + containers in a worker-pool instance share a network namespace. +- A successful health probe confirms that the collector is running and accepted its configuration; + it does not prove that Google Cloud ingestion and IAM are working. Check collector logs for + exporter errors and verify both signals after deployment. + +If the collector is unavailable after startup, the Temporal worker continues processing work but +telemetry delivery can be delayed or lost. Treat collector liveness and exporter failures as +operational alerts. + +## Generate and view telemetry + +Start `GreetingWorkflow` on task queue `gcp-opentelemetry` with a single string argument. For +example, with an already configured Temporal CLI: + +```bash +temporal workflow start \ + --workflow-id gcp-otel-greeting \ + --type GreetingWorkflow \ + --task-queue gcp-opentelemetry \ + --input '"Google Cloud"' +``` + +Temporal SDK metrics appear as Prometheus metrics in Cloud Monitoring. Traces appear in Trace +Explorer after passing through the Telemetry API. The OpenTelemetry service name defaults to the +value of `CLOUD_RUN_WORKER_POOL`; set `OTEL_SERVICE_NAME` on the worker container only if you need +an explicit override. + +For end-to-end metric verification, observe at least one normal 60-second periodic export, then +terminate or replace a worker-pool revision to exercise the forced shutdown flush. Confirm that +both exports reach Managed Service for Prometheus and that the collector logs contain no +`Duplicate TimeSeries` rejection. A short smoke test that exercises only one of these paths is not +sufficient evidence for the cumulative-metric pipeline. + +## Shutdown + +Cloud Run sends `SIGTERM` and allows 10 seconds before `SIGKILL`. The shutdown hook reserves six +seconds for graceful worker shutdown, one second for forced shutdown if necessary, and two seconds +for the plugin's Temporal-metrics and OpenTelemetry flush before closing the service stubs. The +flush runs after worker termination so it includes telemetry emitted by finishing tasks. +Long-running Activities must still use heartbeats and cancellation handling so they can stop within +the platform shutdown window. diff --git a/gcp-opentelemetry/build.gradle b/gcp-opentelemetry/build.gradle new file mode 100644 index 00000000..dd3c1474 --- /dev/null +++ b/gcp-opentelemetry/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'application' +} + +def temporalGcpVersion = findProperty('temporalGcpVersion') ?: '1.37.0-SNAPSHOT' + +repositories { + if (findProperty('useMavenLocal') == 'true') { + mavenLocal() + } +} + +dependencies { + implementation "io.temporal:temporal-sdk:$temporalGcpVersion" + implementation "io.temporal:temporal-envconfig:$temporalGcpVersion" + implementation "io.temporal:temporal-gcp:$temporalGcpVersion" + + runtimeOnly 'ch.qos.logback:logback-classic:1.5.6' + + testImplementation "io.temporal:temporal-testing:$temporalGcpVersion" + testImplementation 'junit:junit:4.13.2' + testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.10.3' + + errorproneJavac 'com.google.errorprone:javac:9+181-r4173-1' + errorprone 'com.google.errorprone:error_prone_core:2.28.0' +} + +application { + mainClass = 'io.temporal.samples.gcp.GcpOpenTelemetryWorker' +} diff --git a/gcp-opentelemetry/collector-config.yaml b/gcp-opentelemetry/collector-config.yaml new file mode 100644 index 00000000..801471de --- /dev/null +++ b/gcp-opentelemetry/collector-config.yaml @@ -0,0 +1,85 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: localhost:4317 + +processors: + # Batch traces for throughput. Do not add this processor to the cumulative metrics pipeline: + # a shutdown flush can otherwise be batched with a recent periodic export of the same series. + batch/traces: + send_batch_max_size: 200 + send_batch_size: 200 + timeout: 5s + memory_limiter: + # This is the collector's memory polling cadence, not the SDK metric export interval. + check_interval: 1s + limit_percentage: 65 + spike_limit_percentage: 20 + resourcedetection: + detectors: [gcp] + timeout: 10s + # Avoid collisions with labels that Google Managed Service for Prometheus adds. + 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") + # The Telemetry API expects the Google Cloud project in gcp.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: + # Google Cloud's supported OTLP path for traces is the Telemetry API. + otlp: + endpoint: telemetry.googleapis.com:443 + compression: none + balancer_name: pick_first + auth: + authenticator: googleclientauth + +extensions: + # Cloud Run container dependencies require a startup probe. This endpoint is also used for the + # collector liveness probe in worker-pool.yaml. + health_check: + endpoint: 0.0.0.0:13133 + googleclientauth: + +service: + extensions: + - health_check + - googleclientauth + pipelines: + metrics/otlp: + receivers: [otlp] + processors: [memory_limiter, resourcedetection, transform/collision] + exporters: [googlemanagedprometheus] + traces: + receivers: [otlp] + processors: [memory_limiter, resourcedetection, transform/set_project_id, batch/traces] + exporters: [otlp] + # Feed collector self-metrics back through the metrics pipeline. + telemetry: + metrics: + readers: + - periodic: + exporter: + otlp: + protocol: grpc + endpoint: http://localhost:4317 + insecure: true diff --git a/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GcpOpenTelemetryWorker.java b/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GcpOpenTelemetryWorker.java new file mode 100644 index 00000000..68560a19 --- /dev/null +++ b/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GcpOpenTelemetryWorker.java @@ -0,0 +1,69 @@ +package io.temporal.samples.gcp; + +import io.temporal.client.WorkflowClient; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.gcp.GcpOpenTelemetryPlugin; +import io.temporal.serviceclient.WorkflowServiceStubs; +import io.temporal.serviceclient.WorkflowServiceStubsOptions; +import io.temporal.worker.Worker; +import io.temporal.worker.WorkerFactory; +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** A continuously polling Temporal worker for a Cloud Run worker pool. */ +public final class GcpOpenTelemetryWorker { + public static final String DEFAULT_TASK_QUEUE = "gcp-opentelemetry"; + + private GcpOpenTelemetryWorker() {} + + public static void main(String[] args) throws IOException { + ClientConfigProfile profile = ClientConfigProfile.load(); + GcpOpenTelemetryPlugin telemetryPlugin = GcpOpenTelemetryPlugin.newBuilder().build(); + + WorkflowServiceStubsOptions serviceOptions = + WorkflowServiceStubsOptions.newBuilder(profile.toWorkflowServiceStubsOptions()) + .setPlugins(telemetryPlugin) + .build(); + WorkflowServiceStubs service = WorkflowServiceStubs.newServiceStubs(serviceOptions); + WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions()); + WorkerFactory factory = WorkerFactory.newInstance(client); + + String taskQueue = taskQueue(); + Worker worker = factory.newWorker(taskQueue); + worker.registerWorkflowImplementationTypes(GreetingWorkflowImpl.class); + worker.registerActivitiesImplementations(new GreetingActivitiesImpl()); + + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> shutdown(factory, service, telemetryPlugin), "temporal-worker-shutdown")); + + factory.start(); + System.out.printf( + "Temporal worker started: taskQueue=%s, otelEndpoint=%s, serviceName=%s%n", + taskQueue, telemetryPlugin.getEndpoint(), telemetryPlugin.getServiceName()); + + // Cloud Run worker pools are continuous workloads. Keep the process alive until SIGTERM. + factory.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); + } + + private static String taskQueue() { + String configured = System.getenv("TEMPORAL_TASK_QUEUE"); + return configured == null || configured.trim().isEmpty() ? DEFAULT_TASK_QUEUE : configured; + } + + private static void shutdown( + WorkerFactory factory, WorkflowServiceStubs service, GcpOpenTelemetryPlugin telemetryPlugin) { + // Cloud Run allows 10 seconds between SIGTERM and SIGKILL. Flush only after the asynchronous + // worker shutdown so telemetry produced by finishing tasks is included. + factory.shutdown(); + factory.awaitTermination(6, TimeUnit.SECONDS); + if (!factory.isTerminated()) { + factory.shutdownNow(); + factory.awaitTermination(1, TimeUnit.SECONDS); + } + telemetryPlugin.newFlushHook().run(Duration.ofSeconds(2)); + service.shutdown(); + } +} diff --git a/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GreetingActivities.java b/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GreetingActivities.java new file mode 100644 index 00000000..4bcc0097 --- /dev/null +++ b/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GreetingActivities.java @@ -0,0 +1,8 @@ +package io.temporal.samples.gcp; + +import io.temporal.activity.ActivityInterface; + +@ActivityInterface +public interface GreetingActivities { + String composeGreeting(String name); +} diff --git a/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GreetingActivitiesImpl.java b/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GreetingActivitiesImpl.java new file mode 100644 index 00000000..1b5e6ad3 --- /dev/null +++ b/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GreetingActivitiesImpl.java @@ -0,0 +1,8 @@ +package io.temporal.samples.gcp; + +public final class GreetingActivitiesImpl implements GreetingActivities { + @Override + public String composeGreeting(String name) { + return "Hello " + name + "!"; + } +} diff --git a/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GreetingWorkflow.java b/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GreetingWorkflow.java new file mode 100644 index 00000000..282442cf --- /dev/null +++ b/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GreetingWorkflow.java @@ -0,0 +1,10 @@ +package io.temporal.samples.gcp; + +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; + +@WorkflowInterface +public interface GreetingWorkflow { + @WorkflowMethod + String getGreeting(String name); +} diff --git a/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GreetingWorkflowImpl.java b/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GreetingWorkflowImpl.java new file mode 100644 index 00000000..646de83e --- /dev/null +++ b/gcp-opentelemetry/src/main/java/io/temporal/samples/gcp/GreetingWorkflowImpl.java @@ -0,0 +1,17 @@ +package io.temporal.samples.gcp; + +import io.temporal.activity.ActivityOptions; +import io.temporal.workflow.Workflow; +import java.time.Duration; + +public final class GreetingWorkflowImpl implements GreetingWorkflow { + private final GreetingActivities activities = + Workflow.newActivityStub( + GreetingActivities.class, + ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build()); + + @Override + public String getGreeting(String name) { + return activities.composeGreeting(name); + } +} diff --git a/gcp-opentelemetry/src/test/java/io/temporal/samples/gcp/GcpOpenTelemetryPluginDefaultsTest.java b/gcp-opentelemetry/src/test/java/io/temporal/samples/gcp/GcpOpenTelemetryPluginDefaultsTest.java new file mode 100644 index 00000000..6babf874 --- /dev/null +++ b/gcp-opentelemetry/src/test/java/io/temporal/samples/gcp/GcpOpenTelemetryPluginDefaultsTest.java @@ -0,0 +1,14 @@ +package io.temporal.samples.gcp; + +import static org.junit.Assert.assertEquals; + +import io.temporal.gcp.GcpOpenTelemetryPlugin; +import java.time.Duration; +import org.junit.Test; + +public class GcpOpenTelemetryPluginDefaultsTest { + @Test + public void usesCoordinatedMetricExportInterval() { + assertEquals(Duration.ofSeconds(60), GcpOpenTelemetryPlugin.DEFAULT_METRICS_REPORT_INTERVAL); + } +} diff --git a/gcp-opentelemetry/src/test/java/io/temporal/samples/gcp/GreetingWorkflowTest.java b/gcp-opentelemetry/src/test/java/io/temporal/samples/gcp/GreetingWorkflowTest.java new file mode 100644 index 00000000..900ea32c --- /dev/null +++ b/gcp-opentelemetry/src/test/java/io/temporal/samples/gcp/GreetingWorkflowTest.java @@ -0,0 +1,32 @@ +package io.temporal.samples.gcp; + +import static org.junit.Assert.assertEquals; + +import io.temporal.client.WorkflowOptions; +import io.temporal.testing.TestWorkflowRule; +import org.junit.Rule; +import org.junit.Test; + +public class GreetingWorkflowTest { + @Rule + public TestWorkflowRule testWorkflowRule = + TestWorkflowRule.newBuilder() + .setWorkflowTypes(GreetingWorkflowImpl.class) + .setDoNotStart(true) + .build(); + + @Test + public void completesGreeting() { + testWorkflowRule.getWorker().registerActivitiesImplementations(new GreetingActivitiesImpl()); + testWorkflowRule.getTestEnvironment().start(); + + GreetingWorkflow workflow = + testWorkflowRule + .getWorkflowClient() + .newWorkflowStub( + GreetingWorkflow.class, + WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build()); + + assertEquals("Hello Google Cloud!", workflow.getGreeting("Google Cloud")); + } +} diff --git a/gcp-opentelemetry/worker-pool.yaml b/gcp-opentelemetry/worker-pool.yaml new file mode 100644 index 00000000..fd4bc491 --- /dev/null +++ b/gcp-opentelemetry/worker-pool.yaml @@ -0,0 +1,68 @@ +apiVersion: run.googleapis.com/v1 +kind: WorkerPool +metadata: + name: temporal-gcp-worker + labels: + cloud.googleapis.com/location: REGION + annotations: + run.googleapis.com/scalingMode: manual + run.googleapis.com/manualInstanceCount: "1" +spec: + template: + metadata: + annotations: + # Cloud Run starts the worker only after the collector startup probe succeeds. + run.googleapis.com/container-dependencies: '{"worker":["collector"]}' + run.googleapis.com/execution-environment: gen2 + spec: + containerConcurrency: 0 + serviceAccountName: temporal-gcp-worker@PROJECT_ID.iam.gserviceaccount.com + containers: + - name: worker + image: REGION-docker.pkg.dev/PROJECT_ID/temporal-samples/gcp-opentelemetry:latest + env: + - name: TEMPORAL_ADDRESS + value: NAMESPACE_ID.tmprl.cloud:7233 + - name: TEMPORAL_NAMESPACE + value: NAMESPACE_ID.ACCOUNT_ID + - name: TEMPORAL_API_KEY + valueFrom: + secretKeyRef: + key: "1" + name: temporal-api-key + - name: TEMPORAL_TASK_QUEUE + value: gcp-opentelemetry + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: http://localhost:4317 + 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: "1" + name: temporal-gcp-otel-config + startupProbe: + httpGet: + path: / + port: 13133 + timeoutSeconds: 5 + periodSeconds: 10 + failureThreshold: 12 + livenessProbe: + httpGet: + path: / + port: 13133 + timeoutSeconds: 5 + periodSeconds: 30 + failureThreshold: 3 + resources: + limits: + cpu: "1" + memory: 512Mi diff --git a/settings.gradle b/settings.gradle index b99ad281..cafe5650 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,4 +6,10 @@ include 'springai:multimodel' include 'springai:rag' include 'springboot' include 'springboot-basic' +include 'gcp-opentelemetry' +// Allows unreleased SDK modules to be tested from a local SDK checkout without changing it. +def temporalSdkPath = gradle.startParameter.projectProperties['temporalSdkPath'] +if (temporalSdkPath) { + includeBuild temporalSdkPath +}