Skip to content
Closed
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
51 changes: 51 additions & 0 deletions contrib/temporal-gcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Temporal Google Cloud module

This module provides an OpenTelemetry plugin with defaults for Temporal Java SDK workers running on Google Cloud Run services and worker pools.

> **Collector required by default:** The plugin exports metrics and traces to an OTLP collector at `http://localhost:4317`. It does not export directly to Google Cloud. Deploy the Google-Built OpenTelemetry Collector as a sidecar, configure another collector endpoint, or provide an application-owned `OpenTelemetry` instance. Without a collector at the configured endpoint, telemetry is not delivered to Google Cloud.

This integration is for container-based Cloud Run workloads. It does not implement a Cloud Run functions invocation lifecycle.

## Usage

Add `temporal-gcp` next to your Temporal SDK dependency, then install the plugin on service stubs options before creating clients and workers:

```java
GcpOpenTelemetryPlugin plugin = GcpOpenTelemetryPlugin.newBuilder().build();

WorkflowServiceStubs service =
WorkflowServiceStubs.newServiceStubs(
WorkflowServiceStubsOptions.newBuilder()
.setPlugins(plugin)
.build());
WorkflowClient client = WorkflowClient.newInstance(service);
WorkerFactory factory = WorkerFactory.newInstance(client);
```

The plugin configures the SDK metrics scope, tracing interceptors, OTLP metric and trace exporters, and worker-factory shutdown flushing through `temporal-opentelemetry`. Do not install both `GcpOpenTelemetryPlugin` and `OpenTelemetryPlugin` on the same service stubs.

The OTLP endpoint is resolved in this order:

1. `Builder.setEndpoint(...)`.
2. `OTEL_EXPORTER_OTLP_ENDPOINT`.
3. `http://localhost:4317`.

The OpenTelemetry service name is resolved in this order:

1. `Builder.setServiceName(...)`.
2. `OTEL_SERVICE_NAME`.
3. `CLOUD_RUN_WORKER_POOL` for a Cloud Run worker pool.
4. `K_SERVICE` for a Cloud Run service.
5. `temporal-worker`.

The collector should use its GCP resource detector to add the Google Cloud project, location, revision, and monitored-resource attributes. This module does not call the Google Cloud metadata server and adds no Google Cloud client libraries or exporters to the worker process.

## Collector sidecar

Google publishes the Google-Built OpenTelemetry Collector as a container image. Configure it as a second Cloud Run container, listen for OTLP gRPC on `localhost:4317`, and use its GCP exporters for metrics and traces. For the image, recommended collector configuration, IAM roles, health check, and Secret Manager mount, see [Deploy Google-Built OpenTelemetry Collector on Cloud Run](https://cloud.google.com/stackdriver/docs/instrumentation/opentelemetry-collector-cloud-run).

Cloud Run worker pools support sidecar containers over localhost and are intended for continuous background work. The deployment should start the collector before the Temporal worker and use the collector health extension as its startup probe.

To use an external collector instead, set `OTEL_EXPORTER_OTLP_ENDPOINT` or call `Builder.setEndpoint(...)`.

To use an application-owned provider, call `Builder.setOpenTelemetry(...)`. In that path, no exporters are created; the plugin installs the Temporal metrics scope, tracing interceptors, and shutdown flush hook around the supplied provider.
17 changes: 17 additions & 0 deletions contrib/temporal-gcp/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
description = '''Temporal Java SDK Google Cloud Support Module'''

dependencies {
// This module shouldn't carry temporal-sdk with it, especially for situations when users may
// be using a shaded artifact.
compileOnly project(':temporal-serviceclient')
compileOnly project(':temporal-sdk')
compileOnly "javax.annotation:javax.annotation-api:$annotationApiVersion"

api project(':temporal-opentelemetry')

testImplementation project(':temporal-sdk')
testImplementation project(':temporal-serviceclient')
testImplementation "junit:junit:${junitVersion}"

testRuntimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package io.temporal.gcp;

import io.opentelemetry.api.OpenTelemetry;
import io.temporal.client.WorkflowClientOptions;
import io.temporal.common.Experimental;
import io.temporal.common.SimplePlugin;
import io.temporal.opentelemetry.OpenTelemetryPlugin;
import io.temporal.opentelemetry.OpenTelemetryWorker;
import io.temporal.opentelemetry.TimedShutdownHook;
import io.temporal.serviceclient.WorkflowServiceStubsOptions;
import io.temporal.worker.WorkerFactory;
import io.temporal.worker.WorkerFactoryOptions;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import javax.annotation.Nonnull;

/** OpenTelemetry plugin with defaults for Temporal workers running on Google Cloud Run. */
@Experimental
public final class GcpOpenTelemetryPlugin extends SimplePlugin {
public static final String NAME = OpenTelemetryPlugin.NAME;
public static final String OTEL_EXPORTER_OTLP_ENDPOINT =
OpenTelemetryWorker.OTEL_EXPORTER_OTLP_ENDPOINT;
public static final String OTEL_SERVICE_NAME = OpenTelemetryWorker.OTEL_SERVICE_NAME;
public static final String CLOUD_RUN_WORKER_POOL = "CLOUD_RUN_WORKER_POOL";
public static final String K_SERVICE = "K_SERVICE";
public static final String DEFAULT_OTLP_ENDPOINT = OpenTelemetryWorker.DEFAULT_OTLP_ENDPOINT;
public static final String DEFAULT_SERVICE_NAME = OpenTelemetryWorker.DEFAULT_SERVICE_NAME;

private final OpenTelemetryPlugin delegate;

private GcpOpenTelemetryPlugin(Builder builder) {
super(NAME);
this.delegate = builder.buildDelegate();
}

public static Builder newBuilder() {
return new Builder(System.getenv());
}

public static Builder newBuilder(@Nonnull Map<String, String> env) {
return new Builder(env);
}

public String getEndpoint() {
return delegate.getEndpoint();
}

public String getServiceName() {
return delegate.getServiceName();
}

public OpenTelemetry getOpenTelemetry() {
return delegate.getOpenTelemetry();
}

/**
* Creates a flush hook that reports buffered Temporal metrics before force-flushing OpenTelemetry
* providers.
*/
public TimedShutdownHook newFlushHook() {
return delegate.newFlushHook();
}

@Override
public void configureServiceStubs(@Nonnull WorkflowServiceStubsOptions.Builder builder) {
delegate.configureServiceStubs(builder);
}

@Override
public void configureWorkflowClient(@Nonnull WorkflowClientOptions.Builder builder) {
delegate.configureWorkflowClient(builder);
}

@Override
public void configureWorkerFactory(@Nonnull WorkerFactoryOptions.Builder builder) {
delegate.configureWorkerFactory(builder);
}

@Override
public void shutdownWorkerFactory(
@Nonnull WorkerFactory factory, @Nonnull Consumer<WorkerFactory> next) {
delegate.shutdownWorkerFactory(factory, next);
}

/** Builder for {@link GcpOpenTelemetryPlugin}. */
public static final class Builder {
private final Map<String, String> env;
private final OpenTelemetryPlugin.Builder delegate;
private String serviceName;

private Builder(Map<String, String> env) {
this.env = Objects.requireNonNull(env, "env");
this.delegate = OpenTelemetryPlugin.newBuilder(env);
}

/**
* Uses an application-owned OpenTelemetry instance instead of creating an SDK and exporters.
*/
public Builder setOpenTelemetry(@Nonnull OpenTelemetry openTelemetry) {
delegate.setOpenTelemetry(openTelemetry);
return this;
}

/** Sets the OTLP metric and trace exporter endpoint used by the default SDK setup. */
public Builder setEndpoint(@Nonnull String endpoint) {
delegate.setEndpoint(endpoint);
return this;
}

/** Sets the service name used by the default SDK resource and Temporal metrics reporter. */
public Builder setServiceName(@Nonnull String serviceName) {
this.serviceName = Objects.requireNonNull(serviceName, "serviceName");
return this;
}

/** Sets the interval used by the Temporal metrics scope and periodic metric reader. */
public Builder setMetricsReportInterval(@Nonnull Duration metricsReportInterval) {
delegate.setMetricsReportInterval(metricsReportInterval);
return this;
}

/** Sets how long the OpenTelemetry flush hook waits for provider flushing. */
public Builder setFlushTimeout(@Nonnull Duration flushTimeout) {
delegate.setFlushTimeout(flushTimeout);
return this;
}

/** Overrides the OpenTelemetry provider flush hook. */
public Builder setFlushHook(@Nonnull Runnable flushHook) {
delegate.setFlushHook(flushHook);
return this;
}

public String getEndpoint() {
return delegate.getEndpoint();
}

public String getServiceName() {
return serviceName == null ? resolveServiceName(env) : serviceName;
}

public OpenTelemetry createOpenTelemetry() {
applyServiceNameDefault();
return delegate.createOpenTelemetry();
}

public GcpOpenTelemetryPlugin build() {
return new GcpOpenTelemetryPlugin(this);
}

private OpenTelemetryPlugin buildDelegate() {
applyServiceNameDefault();
return delegate.build();
}

private void applyServiceNameDefault() {
delegate.setServiceName(getServiceName());
}
}

static String resolveServiceName(Map<String, String> env) {
String value = nonEmptyEnv(env, OTEL_SERVICE_NAME);
if (value != null) {
return value;
}
value = nonEmptyEnv(env, CLOUD_RUN_WORKER_POOL);
if (value != null) {
return value;
}
value = nonEmptyEnv(env, K_SERVICE);
return value == null ? DEFAULT_SERVICE_NAME : value;
}

private static String nonEmptyEnv(Map<String, String> env, String name) {
String value = env.get(name);
return value == null || value.trim().isEmpty() ? null : value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package io.temporal.gcp;

import static org.junit.Assert.*;

import io.opentelemetry.api.OpenTelemetry;
import io.temporal.client.WorkflowClientOptions;
import io.temporal.opentelemetry.OpenTelemetryPlugin;
import io.temporal.serviceclient.WorkflowServiceStubsOptions;
import io.temporal.worker.WorkerFactoryOptions;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;

public class GcpOpenTelemetryPluginTest {
@Test
public void defaultsToLocalCollectorAndGenericServiceName() {
GcpOpenTelemetryPlugin.Builder builder = GcpOpenTelemetryPlugin.newBuilder(new HashMap<>());

assertEquals("http://localhost:4317", builder.getEndpoint());
assertEquals("temporal-worker", builder.getServiceName());
}

@Test
public void resolvesCloudRunServiceNameWithExpectedPrecedence() {
Map<String, String> env = new HashMap<>();
env.put(GcpOpenTelemetryPlugin.K_SERVICE, "cloud-run-service");
assertEquals("cloud-run-service", GcpOpenTelemetryPlugin.newBuilder(env).getServiceName());

env.put(GcpOpenTelemetryPlugin.CLOUD_RUN_WORKER_POOL, "worker-pool");
assertEquals("worker-pool", GcpOpenTelemetryPlugin.newBuilder(env).getServiceName());

env.put(GcpOpenTelemetryPlugin.OTEL_SERVICE_NAME, "otel-service");
assertEquals("otel-service", GcpOpenTelemetryPlugin.newBuilder(env).getServiceName());

assertEquals(
"builder-service",
GcpOpenTelemetryPlugin.newBuilder(env).setServiceName("builder-service").getServiceName());
}

@Test
public void ignoresEmptyEnvironmentValues() {
Map<String, String> env = new HashMap<>();
env.put(GcpOpenTelemetryPlugin.OTEL_SERVICE_NAME, " ");
env.put(GcpOpenTelemetryPlugin.CLOUD_RUN_WORKER_POOL, "");
env.put(GcpOpenTelemetryPlugin.K_SERVICE, "cloud-run-service");

assertEquals("cloud-run-service", GcpOpenTelemetryPlugin.newBuilder(env).getServiceName());
}

@Test
public void buildAppliesResolvedEndpointAndServiceName() {
Map<String, String> env = new HashMap<>();
env.put(GcpOpenTelemetryPlugin.OTEL_EXPORTER_OTLP_ENDPOINT, "http://collector:4317");
env.put(GcpOpenTelemetryPlugin.CLOUD_RUN_WORKER_POOL, "worker-pool");

GcpOpenTelemetryPlugin plugin =
GcpOpenTelemetryPlugin.newBuilder(env).setOpenTelemetry(OpenTelemetry.noop()).build();

assertEquals("http://collector:4317", plugin.getEndpoint());
assertEquals("worker-pool", plugin.getServiceName());
}

@Test
public void installsMetricsScopeAndTracingInterceptors() {
GcpOpenTelemetryPlugin plugin =
GcpOpenTelemetryPlugin.newBuilder(new HashMap<>())
.setOpenTelemetry(OpenTelemetry.noop())
.build();
WorkflowServiceStubsOptions.Builder serviceOptions = WorkflowServiceStubsOptions.newBuilder();
WorkflowClientOptions.Builder clientOptions = WorkflowClientOptions.newBuilder();
WorkerFactoryOptions.Builder factoryOptions = WorkerFactoryOptions.newBuilder();

plugin.configureServiceStubs(serviceOptions);
plugin.configureWorkflowClient(clientOptions);
plugin.configureWorkerFactory(factoryOptions);

assertEquals(OpenTelemetryPlugin.NAME, plugin.getName());
assertNotNull(serviceOptions.build().getMetricsScope());
assertEquals(1, clientOptions.build().getInterceptors().length);
assertEquals(1, factoryOptions.build().getWorkerInterceptors().length);
}

@Test
public void workerFactoryShutdownFlushesByDefault() {
AtomicInteger flushes = new AtomicInteger();
AtomicInteger shutdowns = new AtomicInteger();
GcpOpenTelemetryPlugin plugin =
GcpOpenTelemetryPlugin.newBuilder(new HashMap<>())
.setOpenTelemetry(OpenTelemetry.noop())
.setFlushHook(flushes::incrementAndGet)
.build();

plugin.shutdownWorkerFactory(null, factory -> shutdowns.incrementAndGet());

assertEquals(1, shutdowns.get());
assertEquals(1, flushes.get());
}
}
2 changes: 2 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ include 'temporal-opentracing'
project(':temporal-opentracing').projectDir = file('contrib/temporal-opentracing')
include 'temporal-opentelemetry'
project(':temporal-opentelemetry').projectDir = file('contrib/temporal-opentelemetry')
include 'temporal-gcp'
project(':temporal-gcp').projectDir = file('contrib/temporal-gcp')
include 'temporal-kotlin'
include 'temporal-spring-ai'
project(':temporal-spring-ai').projectDir = file('contrib/temporal-spring-ai')
Expand Down
1 change: 1 addition & 0 deletions temporal-bom/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ description = '''Temporal Java BOM'''
dependencies {
constraints {
api project(':temporal-kotlin')
api project(':temporal-gcp')
api project(':temporal-opentelemetry')
api project(':temporal-opentracing')
api project(':temporal-aws-lambda')
Expand Down
Loading