diff --git a/Directory.Packages.props b/Directory.Packages.props
index 38177b2..4123496 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -24,6 +24,7 @@
+
diff --git a/README.md b/README.md
index a09c161..125eaaf 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ Prerequisites:
* [EagerWorkflowStart](src/EagerWorkflowStart) - Demonstrates usage of Eager Workflow Start to reduce latency for workflows that start with a local activity.
* [Encryption](src/Encryption) - End-to-end encryption with Temporal payload codecs.
* [EnvConfig](src/EnvConfig) - Load client configuration from TOML files with programmatic overrides
+* [Gcp/CloudRun/OpenTelemetry](src/Gcp/CloudRun/OpenTelemetry) - Run a Temporal Worker on a Google Cloud Run worker pool, exporting OpenTelemetry metrics and traces to a collector sidecar.
* [LambdaWorker](src/LambdaWorker) - Run a Temporal Worker inside an AWS Lambda function.
* [Mutex](src/Mutex) - How to implement a mutex as a workflow. Demonstrates how to avoid race conditions or parallel mutually exclusive operations on the same resource.
* [NexusCancellation](src/NexusCancellation) - Demonstrates how to cancel a running Nexus operation from a caller workflow.
diff --git a/TemporalioSamples.sln b/TemporalioSamples.sln
index c945182..f6dbf08 100644
--- a/TemporalioSamples.sln
+++ b/TemporalioSamples.sln
@@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1A647B41-53D
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.ActivityWorker", "src\ActivityWorker\TemporalioSamples.ActivityWorker.csproj", "{7AECC7C6-9A21-4B8A-84D9-AFC4F5840CAF}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.Gcp.CloudRun.OpenTelemetry", "src\Gcp\CloudRun\OpenTelemetry\TemporalioSamples.Gcp.CloudRun.OpenTelemetry.csproj", "{E0F934F9-10A7-41A0-A85E-EE6D0E267367}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemporalioSamples.Tests", "tests\TemporalioSamples.Tests.csproj", "{3FA7E5DF-03B7-4586-A980-85C155B376C5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNet", "AspNet", "{E431D279-E02B-4670-B934-3DB9F15D8CCC}"
@@ -167,6 +169,18 @@ Global
{7AECC7C6-9A21-4B8A-84D9-AFC4F5840CAF}.Release|x64.Build.0 = Release|Any CPU
{7AECC7C6-9A21-4B8A-84D9-AFC4F5840CAF}.Release|x86.ActiveCfg = Release|Any CPU
{7AECC7C6-9A21-4B8A-84D9-AFC4F5840CAF}.Release|x86.Build.0 = Release|Any CPU
+ {E0F934F9-10A7-41A0-A85E-EE6D0E267367}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E0F934F9-10A7-41A0-A85E-EE6D0E267367}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E0F934F9-10A7-41A0-A85E-EE6D0E267367}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E0F934F9-10A7-41A0-A85E-EE6D0E267367}.Debug|x64.Build.0 = Debug|Any CPU
+ {E0F934F9-10A7-41A0-A85E-EE6D0E267367}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E0F934F9-10A7-41A0-A85E-EE6D0E267367}.Debug|x86.Build.0 = Debug|Any CPU
+ {E0F934F9-10A7-41A0-A85E-EE6D0E267367}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E0F934F9-10A7-41A0-A85E-EE6D0E267367}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E0F934F9-10A7-41A0-A85E-EE6D0E267367}.Release|x64.ActiveCfg = Release|Any CPU
+ {E0F934F9-10A7-41A0-A85E-EE6D0E267367}.Release|x64.Build.0 = Release|Any CPU
+ {E0F934F9-10A7-41A0-A85E-EE6D0E267367}.Release|x86.ActiveCfg = Release|Any CPU
+ {E0F934F9-10A7-41A0-A85E-EE6D0E267367}.Release|x86.Build.0 = Release|Any CPU
{3FA7E5DF-03B7-4586-A980-85C155B376C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3FA7E5DF-03B7-4586-A980-85C155B376C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3FA7E5DF-03B7-4586-A980-85C155B376C5}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -821,6 +835,7 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7AECC7C6-9A21-4B8A-84D9-AFC4F5840CAF} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
+ {E0F934F9-10A7-41A0-A85E-EE6D0E267367} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
{E431D279-E02B-4670-B934-3DB9F15D8CCC} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC}
{31EC2647-6A5A-42D1-B7B5-02804B340726} = {E431D279-E02B-4670-B934-3DB9F15D8CCC}
{AFFA4143-DC28-4FBE-A33B-D6414F541EA4} = {E431D279-E02B-4670-B934-3DB9F15D8CCC}
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..fc9f9a7
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Gcp/CloudRun/OpenTelemetry/CloudRunWorkerSample.cs b/src/Gcp/CloudRun/OpenTelemetry/CloudRunWorkerSample.cs
new file mode 100644
index 0000000..de5c3b5
--- /dev/null
+++ b/src/Gcp/CloudRun/OpenTelemetry/CloudRunWorkerSample.cs
@@ -0,0 +1,12 @@
+namespace TemporalioSamples.Gcp.CloudRun.OpenTelemetry;
+
+using Temporalio.Worker;
+
+// Shared so the entrypoint and tests register the same workflow and activities.
+public static class CloudRunWorkerSample
+{
+ public static TemporalWorkerOptions ConfigureOptions(TemporalWorkerOptions options) =>
+ options.
+ AddWorkflow().
+ AddActivity(GreetingActivities.SayHello);
+}
diff --git a/src/Gcp/CloudRun/OpenTelemetry/Dockerfile b/src/Gcp/CloudRun/OpenTelemetry/Dockerfile
new file mode 100644
index 0000000..59c32f2
--- /dev/null
+++ b/src/Gcp/CloudRun/OpenTelemetry/Dockerfile
@@ -0,0 +1,14 @@
+# syntax=docker/dockerfile:1
+# Build from the repo root: docker build -f src/Gcp/CloudRun/OpenTelemetry/Dockerfile -t .
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+WORKDIR /src
+COPY global.json Directory.Build.props Directory.Packages.props .editorconfig nuget.config ./
+COPY src/Gcp/CloudRun/OpenTelemetry/ ./src/Gcp/CloudRun/OpenTelemetry/
+RUN dotnet publish src/Gcp/CloudRun/OpenTelemetry/TemporalioSamples.Gcp.CloudRun.OpenTelemetry.csproj -c Release -o /app
+
+FROM mcr.microsoft.com/dotnet/runtime:8.0
+RUN useradd --create-home --uid 10001 worker
+WORKDIR /app
+COPY --from=build --chown=worker:worker /app ./
+USER 10001
+ENTRYPOINT ["dotnet", "TemporalioSamples.Gcp.CloudRun.OpenTelemetry.dll"]
diff --git a/src/Gcp/CloudRun/OpenTelemetry/GreetingActivities.cs b/src/Gcp/CloudRun/OpenTelemetry/GreetingActivities.cs
new file mode 100644
index 0000000..a04bf65
--- /dev/null
+++ b/src/Gcp/CloudRun/OpenTelemetry/GreetingActivities.cs
@@ -0,0 +1,14 @@
+namespace TemporalioSamples.Gcp.CloudRun.OpenTelemetry;
+
+using Microsoft.Extensions.Logging;
+using Temporalio.Activities;
+
+public static class GreetingActivities
+{
+ [Activity]
+ public static string SayHello(string name)
+ {
+ ActivityExecutionContext.Current.Logger.LogInformation("SayHello activity: {Name}", name);
+ return $"Hello, {name}!";
+ }
+}
diff --git a/src/Gcp/CloudRun/OpenTelemetry/GreetingWorkflow.workflow.cs b/src/Gcp/CloudRun/OpenTelemetry/GreetingWorkflow.workflow.cs
new file mode 100644
index 0000000..98e4463
--- /dev/null
+++ b/src/Gcp/CloudRun/OpenTelemetry/GreetingWorkflow.workflow.cs
@@ -0,0 +1,19 @@
+namespace TemporalioSamples.Gcp.CloudRun.OpenTelemetry;
+
+using Microsoft.Extensions.Logging;
+using Temporalio.Workflows;
+
+[Workflow]
+public class GreetingWorkflow
+{
+ [WorkflowRun]
+ public async Task RunAsync(string name)
+ {
+ Workflow.Logger.LogInformation("GreetingWorkflow started: {Name}", name);
+ var result = await Workflow.ExecuteActivityAsync(
+ () => GreetingActivities.SayHello(name),
+ new() { StartToCloseTimeout = TimeSpan.FromSeconds(10) });
+ Workflow.Logger.LogInformation("GreetingWorkflow completed: {Result}", result);
+ return result;
+ }
+}
diff --git a/src/Gcp/CloudRun/OpenTelemetry/Program.cs b/src/Gcp/CloudRun/OpenTelemetry/Program.cs
new file mode 100644
index 0000000..03cd7df
--- /dev/null
+++ b/src/Gcp/CloudRun/OpenTelemetry/Program.cs
@@ -0,0 +1,67 @@
+using System.Runtime.InteropServices;
+using Microsoft.Extensions.Logging;
+using Temporalio.Client;
+using Temporalio.Common.EnvConfig;
+using Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry;
+using Temporalio.Worker;
+using TemporalioSamples.Gcp.CloudRun.OpenTelemetry;
+
+// Connect from environment config (TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, TEMPORAL_API_KEY, ...).
+var connectOptions = ClientEnvConfig.LoadClientConnectOptions();
+connectOptions.TargetHost ??= "localhost:7233";
+connectOptions.LoggerFactory = LoggerFactory.Create(builder =>
+ builder.
+ AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] ").
+ SetMinimumLevel(LogLevel.Information));
+
+var taskQueue = Environment.GetEnvironmentVariable("TEMPORAL_TASK_QUEUE") ?? "cloud-run-worker";
+
+// --starter runs a single workflow, e.g. to kick off work against the same server the worker polls.
+if (args.Contains("--starter"))
+{
+ using var starterTelemetry = connectOptions.ApplyGoogleCloudRunOpenTelemetryDefaults();
+ var starterClient = await TemporalClient.ConnectAsync(connectOptions);
+ var greeting = await starterClient.ExecuteWorkflowAsync(
+ (GreetingWorkflow wf) => wf.RunAsync("Temporal"),
+ new($"cloud-run-worker-{Guid.NewGuid():N}", taskQueue));
+ Console.WriteLine("Workflow result: {0}", greeting);
+ await starterTelemetry.FlushAsync(TimeSpan.FromSeconds(2));
+ return;
+}
+
+// Adds the tracing interceptor and a runtime exporting Core metrics + traces over OTLP to the
+// collector sidecar; the returned handle owns the tracer provider.
+using var telemetry = connectOptions.ApplyGoogleCloudRunOpenTelemetryDefaults();
+
+var client = await TemporalClient.ConnectAsync(connectOptions);
+
+using var cts = new CancellationTokenSource();
+Console.CancelKeyPress += (_, eventArgs) =>
+{
+ eventArgs.Cancel = true;
+ cts.Cancel();
+};
+
+// Cloud Run sends SIGTERM ~10s before SIGKILL.
+using var sigterm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, _ => cts.Cancel());
+
+using var worker = new TemporalWorker(
+ client, CloudRunWorkerSample.ConfigureOptions(new(taskQueue)));
+
+Console.WriteLine(
+ "Worker running: taskQueue={0} address={1} namespace={2}",
+ taskQueue,
+ connectOptions.TargetHost,
+ connectOptions.Namespace ?? "default");
+try
+{
+ await worker.ExecuteAsync(cts.Token);
+}
+catch (OperationCanceledException)
+{
+ Console.WriteLine("Worker shutting down");
+}
+
+// Flush traces within the shutdown grace window (Core metrics export periodically, no explicit flush).
+await telemetry.FlushAsync(TimeSpan.FromSeconds(2));
+Console.WriteLine("Worker stopped");
diff --git a/src/Gcp/CloudRun/OpenTelemetry/README.md b/src/Gcp/CloudRun/OpenTelemetry/README.md
new file mode 100644
index 0000000..786088b
--- /dev/null
+++ b/src/Gcp/CloudRun/OpenTelemetry/README.md
@@ -0,0 +1,52 @@
+# Cloud Run OpenTelemetry Worker
+
+Run a Temporal Worker on a
+[Google Cloud Run worker pool](https://cloud.google.com/run/docs/deploy-worker-pools) that exports
+Core SDK metrics and traces to a
+[Google-Built OpenTelemetry Collector](https://cloud.google.com/stackdriver/docs/instrumentation/opentelemetry-collector-cloud-run)
+sidecar (metrics to Managed Service for Prometheus, traces to Cloud Trace) via the
+`Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry` extension.
+
+`Program.cs` calls `ApplyGoogleCloudRunOpenTelemetryDefaults()`, which adds the tracing interceptor
+and an OTLP exporter aimed at the sidecar, then runs a greeting workflow and activity until SIGTERM.
+
+> The extension is not on nuget.org yet, so the sample restores it from the committed
+> `local-packages/` feed (see `nuget.config`) until it ships.
+
+## Prerequisites
+
+- A Temporal server the worker pool can reach (`TEMPORAL_ADDRESS` / `TEMPORAL_NAMESPACE`)
+- A Google Cloud project with the Cloud Run and Artifact Registry APIs enabled
+- [`gcloud`](https://cloud.google.com/sdk/docs/install), authenticated with the project set
+- The [Temporal CLI](https://docs.temporal.io/cli) and .NET 8
+
+## Deploy
+
+Set the placeholders used by `worker-pool.yaml`, then build the image, store the collector config as
+a secret, and deploy. Run from the repo root:
+
+```bash
+export REGION=us-central1 WORKER_POOL=temporal-otel-worker INSTANCE_COUNT=1
+export SERVICE_ACCOUNT_EMAIL=@.iam.gserviceaccount.com
+export WORKER_IMAGE=$REGION-docker.pkg.dev/$(gcloud config get-value project)/samples/cloud-run-otel
+export TEMPORAL_ADDRESS= TEMPORAL_NAMESPACE= TEMPORAL_TASK_QUEUE=cloud-run-worker
+export COLLECTOR_CONFIG_SECRET=otel-collector-config COLLECTOR_CONFIG_SECRET_VERSION=latest
+
+docker build -f src/Gcp/CloudRun/OpenTelemetry/Dockerfile -t "$WORKER_IMAGE" . && docker push "$WORKER_IMAGE"
+gcloud secrets create "$COLLECTOR_CONFIG_SECRET" --data-file=src/Gcp/CloudRun/OpenTelemetry/collector-config.yaml
+
+envsubst < src/Gcp/CloudRun/OpenTelemetry/worker-pool.yaml > /tmp/worker-pool.yaml
+gcloud run worker-pools replace /tmp/worker-pool.yaml --region "$REGION"
+```
+
+The service account needs the `monitoring.metricWriter`, `cloudtrace.agent`, and
+`secretmanager.secretAccessor` roles.
+
+## Run a workflow
+
+```bash
+temporal workflow execute --task-queue cloud-run-worker --type GreetingWorkflow --input '"Temporal"'
+```
+
+Metrics appear in Metrics Explorer and traces in Trace Explorer. Delete the pool with
+`gcloud run worker-pools delete "$WORKER_POOL" --region "$REGION"`.
diff --git a/src/Gcp/CloudRun/OpenTelemetry/TemporalioSamples.Gcp.CloudRun.OpenTelemetry.csproj b/src/Gcp/CloudRun/OpenTelemetry/TemporalioSamples.Gcp.CloudRun.OpenTelemetry.csproj
new file mode 100644
index 0000000..bec8e3e
--- /dev/null
+++ b/src/Gcp/CloudRun/OpenTelemetry/TemporalioSamples.Gcp.CloudRun.OpenTelemetry.csproj
@@ -0,0 +1,11 @@
+
+
+
+ Exe
+
+
+
+
+
+
+
diff --git a/src/Gcp/CloudRun/OpenTelemetry/collector-config.yaml b/src/Gcp/CloudRun/OpenTelemetry/collector-config.yaml
new file mode 100644
index 0000000..dc7f7fe
--- /dev/null
+++ b/src/Gcp/CloudRun/OpenTelemetry/collector-config.yaml
@@ -0,0 +1,82 @@
+# Google-Built OpenTelemetry Collector config for the sidecar: metrics -> Managed Service for Prometheus, traces -> Cloud Trace. Auth via the worker-pool service account (ADC).
+receivers:
+ otlp:
+ protocols:
+ grpc:
+ endpoint: localhost:4317
+
+processors:
+ # Batch traces for throughput. Do NOT add a batch processor to the cumulative-metrics pipeline: a
+ # shutdown flush could be batched with a recent periodic export of the same series and rejected as
+ # a duplicate time series.
+ 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
+ resourcedetection:
+ detectors: [gcp]
+ timeout: 10s
+ # Rename Temporal datapoint labels that collide with the target labels Managed Service for Prometheus injects (e.g. `namespace`).
+ 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:
+ otlp:
+ endpoint: telemetry.googleapis.com:443
+ compression: none
+ balancer_name: pick_first
+ auth:
+ authenticator: googleclientauth
+
+extensions:
+ health_check:
+ endpoint: 0.0.0.0:13133
+ googleclientauth:
+
+service:
+ extensions:
+ - health_check
+ - googleclientauth
+ pipelines:
+ metrics:
+ 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]
+ telemetry:
+ metrics:
+ readers:
+ - periodic:
+ exporter:
+ otlp:
+ protocol: grpc
+ endpoint: http://localhost:4317
+ insecure: true
diff --git a/src/Gcp/CloudRun/OpenTelemetry/local-packages/Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry.1.19.0.nupkg b/src/Gcp/CloudRun/OpenTelemetry/local-packages/Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry.1.19.0.nupkg
new file mode 100644
index 0000000..36b7498
Binary files /dev/null and b/src/Gcp/CloudRun/OpenTelemetry/local-packages/Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry.1.19.0.nupkg differ
diff --git a/src/Gcp/CloudRun/OpenTelemetry/worker-pool.yaml b/src/Gcp/CloudRun/OpenTelemetry/worker-pool.yaml
new file mode 100644
index 0000000..8688311
--- /dev/null
+++ b/src/Gcp/CloudRun/OpenTelemetry/worker-pool.yaml
@@ -0,0 +1,56 @@
+# Cloud Run WorkerPool: a Temporal Worker + a Collector sidecar. Render placeholders with `envsubst` (see README) then `gcloud run worker-pools replace`. For Temporal Cloud, add TEMPORAL_API_KEY from a Secret Manager secretKeyRef.
+apiVersion: run.googleapis.com/v1
+kind: WorkerPool
+metadata:
+ name: "${WORKER_POOL}"
+ labels:
+ cloud.googleapis.com/location: "${REGION}"
+ annotations:
+ run.googleapis.com/scalingMode: manual
+ run.googleapis.com/manualInstanceCount: "${INSTANCE_COUNT}"
+spec:
+ template:
+ metadata:
+ annotations:
+ run.googleapis.com/container-dependencies: '{"worker":["collector"]}'
+ run.googleapis.com/execution-environment: gen2
+ spec:
+ containerConcurrency: 0
+ serviceAccountName: "${SERVICE_ACCOUNT_EMAIL}"
+ containers:
+ - name: worker
+ image: "${WORKER_IMAGE}"
+ env:
+ - name: TEMPORAL_ADDRESS
+ value: "${TEMPORAL_ADDRESS}"
+ - name: TEMPORAL_NAMESPACE
+ value: "${TEMPORAL_NAMESPACE}"
+ - name: TEMPORAL_TASK_QUEUE
+ value: "${TEMPORAL_TASK_QUEUE}"
+ - 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: "${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
diff --git a/tests/Gcp/CloudRun/OpenTelemetry/CloudRunWorkerTests.cs b/tests/Gcp/CloudRun/OpenTelemetry/CloudRunWorkerTests.cs
new file mode 100644
index 0000000..a24889c
--- /dev/null
+++ b/tests/Gcp/CloudRun/OpenTelemetry/CloudRunWorkerTests.cs
@@ -0,0 +1,33 @@
+namespace TemporalioSamples.Tests.Gcp.CloudRun.OpenTelemetry;
+
+using Temporalio.Client;
+using Temporalio.Testing;
+using Temporalio.Worker;
+using TemporalioSamples.Gcp.CloudRun.OpenTelemetry;
+using Xunit;
+using Xunit.Abstractions;
+
+public class CloudRunWorkerTests : TestBase
+{
+ public CloudRunWorkerTests(ITestOutputHelper output)
+ : base(output)
+ {
+ }
+
+ [TimeSkippingServerFact]
+ public async Task GreetingWorkflow_SimpleRun_Succeeds()
+ {
+ await using var env = await WorkflowEnvironment.StartTimeSkippingAsync();
+ using var worker = new TemporalWorker(
+ env.Client,
+ CloudRunWorkerSample.ConfigureOptions(
+ new TemporalWorkerOptions("cloud-run-worker-test-task-queue")));
+ await worker.ExecuteAsync(async () =>
+ {
+ var result = await env.Client.ExecuteWorkflowAsync(
+ (GreetingWorkflow wf) => wf.RunAsync("Cloud Run"),
+ new(id: $"workflow-{Guid.NewGuid()}", taskQueue: worker.Options.TaskQueue!));
+ Assert.Equal("Hello, Cloud Run!", result);
+ });
+ }
+}
diff --git a/tests/TemporalioSamples.Tests.csproj b/tests/TemporalioSamples.Tests.csproj
index 66b6b0a..988d31d 100644
--- a/tests/TemporalioSamples.Tests.csproj
+++ b/tests/TemporalioSamples.Tests.csproj
@@ -22,6 +22,7 @@
+