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
16 changes: 9 additions & 7 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.10" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.VisualStudio.Threading.Analyzers" Version="18.7.23" />
<PackageVersion Include="NexusRpc" Version="0.3.0" />
<!-- Temporalio 1.18.0 requires NexusRpc >= 0.4.0. -->
<PackageVersion Include="NexusRpc" Version="0.4.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.16.0" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.435" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="Temporalio" Version="1.17.0" />
<PackageVersion Include="Temporalio.Extensions.Aws.Lambda" Version="1.17.0" />
<PackageVersion Include="Temporalio.Extensions.Aws.Lambda.OpenTelemetry" Version="1.17.0" />
<PackageVersion Include="Temporalio.Extensions.DiagnosticSource" Version="1.17.0" />
<PackageVersion Include="Temporalio.Extensions.Hosting" Version="1.17.0" />
<PackageVersion Include="Temporalio.Extensions.OpenTelemetry" Version="1.17.0" />
<PackageVersion Include="Temporalio" Version="1.18.0" />
<PackageVersion Include="Temporalio.Extensions.Aws.Lambda" Version="1.18.0" />
<PackageVersion Include="Temporalio.Extensions.Aws.Lambda.OpenTelemetry" Version="1.18.0" />
<PackageVersion Include="Temporalio.Extensions.DiagnosticSource" Version="1.18.0" />
<PackageVersion Include="Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry" Version="1.18.0" />
<PackageVersion Include="Temporalio.Extensions.Hosting" Version="1.18.0" />
<PackageVersion Include="Temporalio.Extensions.OpenTelemetry" Version="1.18.0" />
<PackageVersion Include="TemporalCommunity.Aspire.Hosting" Version="0.1.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.console" Version="2.9.3" />
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Prerequisites:
* [AspNet](src/AspNet) - Demonstration of a generic host worker and an ASP.NET workflow starter.
* [Bedrock](src/Bedrock) - Orchestrate a chatbot with Amazon Bedrock.
* [ClientMtls](src/ClientMtls) - How to use client certificate authentication, e.g. for Temporal Cloud.
* [Gcp/CloudRun/OpenTelemetry](src/Gcp/CloudRun/OpenTelemetry) - Run a continuously-polling worker in a Google Cloud Run worker pool, exporting OpenTelemetry metrics and traces to a collector sidecar.
* [ContextPropagation](src/ContextPropagation) - Context propagation via interceptors.
* [CounterInterceptor](src/CounterInterceptor/) - Simple Workflow and Client Interceptors example.
* [DependencyInjection](src/DependencyInjection) - How to inject dependencies in activities and use generic hosts for workers
Expand Down
15 changes: 15 additions & 0 deletions TemporalioSamples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -163,6 +165,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
Expand Down Expand Up @@ -805,6 +819,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}
Expand Down
22 changes: 22 additions & 0 deletions nuget.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TEMPORARY test scaffolding. Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry is not on NuGet.org
yet, so this restores it (for the whole solution incl. tests) from a local folder feed. Everything
else restores from nuget.org. Remove this file (and src/Gcp/CloudRun/OpenTelemetry/local-packages/)
once the package is published.
-->
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="local" value="./src/Gcp/CloudRun/OpenTelemetry/local-packages" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="local">
<package pattern="Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry" />
</packageSource>
</packageSourceMapping>
</configuration>
3 changes: 3 additions & 0 deletions src/Gcp/CloudRun/OpenTelemetry/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Temporary test scaffolding for the unpublished GCP Cloud Run package (remove once published).
local-packages/
nuget.config
20 changes: 20 additions & 0 deletions src/Gcp/CloudRun/OpenTelemetry/CloudRunWorkerSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace TemporalioSamples.Gcp.CloudRun.OpenTelemetry;

using Temporalio.Worker;

/// <summary>
/// Shared worker configuration so both the entrypoint and the tests register the same
/// workflow and activities.
/// </summary>
public static class CloudRunWorkerSample
{
/// <summary>
/// Register the sample workflow and activities on the given worker options.
/// </summary>
/// <param name="options">Worker options to configure.</param>
/// <returns>The same options, for chaining.</returns>
public static TemporalWorkerOptions ConfigureOptions(TemporalWorkerOptions options) =>
options.
AddWorkflow<GreetingWorkflow>().
AddActivity(GreetingActivities.SayHello);
}
22 changes: 22 additions & 0 deletions src/Gcp/CloudRun/OpenTelemetry/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# syntax=docker/dockerfile:1
#
# Builds the Cloud Run worker image. Temporalio (with its bundled native bridge for linux) is
# restored from NuGet; the GCP Cloud Run OpenTelemetry package is restored from the local folder
# feed under this sample until it is published (see nuget.config / local-packages). No SDK-from-
# source or Rust build is needed.
#
# Build context is the samples-dotnet repo root (so the shared Directory.*.props / global.json are
# available):
# docker build -f src/Gcp/CloudRun/OpenTelemetry/Dockerfile -t <image> .
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY global.json Directory.Build.props Directory.Packages.props .editorconfig ./
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"]
14 changes: 14 additions & 0 deletions src/Gcp/CloudRun/OpenTelemetry/GreetingActivities.cs
Original file line number Diff line number Diff line change
@@ -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}!";
}
}
19 changes: 19 additions & 0 deletions src/Gcp/CloudRun/OpenTelemetry/GreetingWorkflow.workflow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace TemporalioSamples.Gcp.CloudRun.OpenTelemetry;

using Microsoft.Extensions.Logging;
using Temporalio.Workflows;

[Workflow]
public class GreetingWorkflow
{
[WorkflowRun]
public async Task<string> 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;
}
}
75 changes: 75 additions & 0 deletions src/Gcp/CloudRun/OpenTelemetry/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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;

// Build client connection options from environment configuration (TEMPORAL_ADDRESS,
// TEMPORAL_NAMESPACE, TEMPORAL_API_KEY, ...). With no API key and no TLS block this connects in
// plaintext, which is what a local dev server (reached over an ngrok TCP tunnel) needs.
var connectOptions = ClientEnvConfig.LoadClientConnectOptions();
connectOptions.TargetHost ??= "localhost:7233";

// Send all Temporal logs to stdout so Cloud Run captures them in Cloud Logging.
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";

// The --starter mode runs a single workflow (useful for kicking off work locally against the same
// server the deployed worker polls). Applying the defaults here too propagates a trace context into
// the workflow so the deployed worker's spans join the same distributed trace.
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;
}

// Apply the Google Cloud Run OpenTelemetry defaults: adds the tracing interceptor and configures a
// Temporal runtime that exports Core metrics + traces over OTLP to the local collector sidecar. The
// returned handle owns the tracer provider and is flushed on shutdown.
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 signals shutdown with SIGTERM (about 10 seconds 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 buffered traces within the Cloud Run shutdown grace window. Core metrics are exported
// periodically by the runtime and have no explicit flush.
await telemetry.FlushAsync(TimeSpan.FromSeconds(2));
Console.WriteLine("Worker stopped");
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry" />
</ItemGroup>

</Project>
88 changes: 88 additions & 0 deletions src/Gcp/CloudRun/OpenTelemetry/collector-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Google-Built OpenTelemetry Collector config for the Cloud Run worker-pool sidecar.
# metrics -> Google Managed Service for Prometheus (googlemanagedprometheus)
# traces -> Cloud Trace via the Telemetry API (OTLP), authenticated with the runtime SA (ADC)
# The worker exports OTLP/gRPC to localhost:4317; the collector detects GCP resource attributes and
# fans out. Auth uses the worker-pool service account's Application Default Credentials via the
# googleclientauth extension (no key files).
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 Google Managed Service for
# Prometheus injects (e.g. Temporal emits a `namespace` label).
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
Loading
Loading