Temporal
Monitor Temporal workflows and activities with Last9 using Prometheus and OpenTelemetry
Monitor your Temporal workflow orchestration platform with Last9. This integration covers metrics from both the Temporal Server (self-hosted) and Temporal SDKs, so you can see how your durable workflow executions behave.
Temporal is a durable execution platform that helps developers build scalable, reliable applications. Monitor Temporal to measure workflow performance, to find bottlenecks, and to confirm reliable task processing.
Last9 supports two methods for collecting Temporal telemetry:
- Prometheus Remote Write: Scrape metrics from Temporal Server and SDKs, then forward to Last9
- OpenTelemetry: Send traces and metrics directly to Last9 via OTLP
Prerequisites
Before setting up Temporal monitoring, ensure you have:
- Temporal Cluster: Running Temporal Server (self-hosted) or Temporal Cloud account
- Last9 Account: With integration credentials from Getting Started
- Prometheus (for metrics): Running instance with remote write capability
- Network Access: Collector can reach Temporal services and Last9 endpoints
Keep the following Last9 credentials handy:
$last9_remote_write_url- Last9’s Remote write endpoint$last9_remote_write_username- Cluster ID$last9_remote_write_password- Write token created for the cluster$last9_otlp_endpoint- Last9’s OTLP endpoint (for OpenTelemetry)$last9_otlp_auth_header- OTLP authorization header
Temporal Server Metrics
-
Configure Temporal Server Metrics Endpoint
Update your Temporal Server configuration to expose Prometheus metrics. Add the following to your Temporal Server configuration file:
global:metrics:prometheus:framework: "opentelemetry"listenAddress: "0.0.0.0:9090"handlerPath: "/metrics"For Docker Compose deployments, ensure the metrics port is exposed:
services:temporal:image: temporalio/auto-setup:latestports:- "7233:7233" # gRPC frontend- "9090:9090" # Metrics endpointenvironment:- PROMETHEUS_ENDPOINT=0.0.0.0:9090 -
Configure Prometheus to Scrape Temporal
Add Temporal as a scrape target in your
prometheus.yaml:global:scrape_interval: 15sscrape_configs:# Temporal Server metrics- job_name: "temporal-server"static_configs:- targets:- "temporal-frontend:9090"- "temporal-history:9090"- "temporal-matching:9090"- "temporal-worker:9090"metrics_path: /metrics# Temporal SDK Worker metrics (if exposed)- job_name: "temporal-workers"static_configs:- targets:- "your-worker-host:8077"- "your-worker-host:8078" -
Configure Prometheus Remote Write to Last9
Add the remote write configuration to forward metrics to Last9:
remote_write:- url: "$last9_remote_write_url"basic_auth:username: "$last9_remote_write_username"password: "$last9_remote_write_password"remote_timeout: 60s -
Restart Services
Restart Prometheus to apply the configuration changes:
# For systemdsudo systemctl restart prometheus# For Dockerdocker-compose restart prometheus
SDK Metrics Configuration
Temporal SDKs emit metrics for Workers, Workflows, and Activities. Configure your SDK to expose these metrics.
contrib/opentelemetry and contrib/tally are separate Go modules from the main SDK — add them with go get go.temporal.io/sdk/contrib/opentelemetry and go get go.temporal.io/sdk/contrib/tally. Configure metrics and tracing together in one client.Dial call — if they’re set up in separate calls, the second call overwrites the first client’s config entirely:
package main
import ( "log" "time"
"github.com/uber-go/tally/v4" "github.com/uber-go/tally/v4/prometheus" "go.temporal.io/sdk/client" "go.temporal.io/sdk/contrib/opentelemetry" sdktally "go.temporal.io/sdk/contrib/tally" "go.temporal.io/sdk/interceptor" "go.temporal.io/sdk/worker")
func main() { // Configure Prometheus metrics reporter — serve its handler yourself, // e.g. http.Handle("/metrics", reporter.HTTPHandler()) reporter := prometheus.NewReporter(prometheus.Options{})
scope, closer := tally.NewRootScope(tally.ScopeOptions{ CachedReporter: reporter, }, time.Second) defer closer.Close()
// Create OpenTelemetry tracing interceptor tracingInterceptor, err := opentelemetry.NewTracingInterceptor( opentelemetry.TracerOptions{}, ) if err != nil { log.Fatal(err) }
// Create Temporal client with BOTH metrics and tracing c, err := client.Dial(client.Options{ HostPort: "temporal:7233", MetricsHandler: sdktally.NewMetricsHandler(scope), Interceptors: []interceptor.ClientInterceptor{tracingInterceptor}, }) if err != nil { log.Fatal(err) } defer c.Close()
// Create worker with metrics w := worker.New(c, "your-task-queue", worker.Options{})
// Register workflows and activities w.RegisterWorkflow(YourWorkflow) w.RegisterActivity(YourActivity)
// Start worker if err := w.Run(worker.InterruptCh()); err != nil { log.Fatal(err) }}import { NativeConnection, Worker } from "@temporalio/worker";import { Runtime } from "@temporalio/worker";
// Configure metrics endpointRuntime.install({ telemetryOptions: { metrics: { prometheus: { bindAddress: "0.0.0.0:8077", }, }, },});
async function run() { const connection = await NativeConnection.connect({ address: "temporal:7233", });
const worker = await Worker.create({ connection, namespace: "default", taskQueue: "your-task-queue", workflowsPath: require.resolve("./workflows"), activities: require("./activities"), });
await worker.run();}
run().catch((err) => { console.error(err); process.exit(1);});For OpenTelemetry tracing, use the @temporalio/interceptors-opentelemetry package. Pin your @opentelemetry/* packages to the major version this package bundles (v1.x as of @temporalio/interceptors-opentelemetry@1.23.0) — mixing major versions causes type and runtime incompatibilities:
import { OpenTelemetryActivityInboundInterceptor, makeWorkflowExporter,} from "@temporalio/interceptors-opentelemetry/lib/worker";import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";import { Resource } from "@opentelemetry/resources";
const resource = new Resource({ "service.name": "your-worker" });const spanProcessor = new BatchSpanProcessor(new OTLPTraceExporter());
const worker = await Worker.create({ connection, namespace: "default", taskQueue: "your-task-queue", workflowsPath: require.resolve("./workflows"), activities: require("./activities"), interceptors: { // `activityInbound` factories receive the Activity Context; // pass it through to the interceptor's constructor activityInbound: [ (ctx) => new OpenTelemetryActivityInboundInterceptor(ctx), ], workflowModules: [ require.resolve("@temporalio/interceptors-opentelemetry/lib/workflow"), ], }, sinks: { exporter: makeWorkflowExporter(spanProcessor, resource), },});Install the SDK with the OpenTelemetry extra:
pip install "temporalio[opentelemetry]"Configure Prometheus metrics and OpenTelemetry tracing together on the same client. Pass both runtime (metrics) and interceptors (tracing) in the same Client.connect() call. If you use two separate calls, the second call drops the runtime from the first call:
import asynciofrom temporalio.client import Clientfrom temporalio.worker import Workerfrom temporalio.runtime import Runtime, TelemetryConfig, PrometheusConfigfrom temporalio.contrib.opentelemetry import TracingInterceptorfrom opentelemetry import tracefrom opentelemetry.sdk.trace import TracerProvider
# Set up OpenTelemetry tracing (skip this if opentelemetry-instrument# already configured a global TracerProvider for this process)provider = TracerProvider()trace.set_tracer_provider(provider)
# Configure runtime with Prometheus metricsruntime = Runtime( telemetry=TelemetryConfig( metrics=PrometheusConfig(bind_address="0.0.0.0:8077") ))
async def main(): client = await Client.connect( "temporal:7233", runtime=runtime, interceptors=[TracingInterceptor()], )
worker = Worker( client, task_queue="your-task-queue", workflows=[YourWorkflow], activities=[your_activity], )
await worker.run()
if __name__ == "__main__": asyncio.run(main())If your process already runs opentelemetry-instrument for HTTP auto-instrumentation, TracingInterceptor() uses the same global TracerProvider by default. Worker spans then join the same trace as the HTTP request that started the workflow. Add TracingInterceptor after auto-instrumentation sets up the provider.
OpenTelemetry Collector Setup
Use the OpenTelemetry Collector to receive metrics and traces from Temporal in one pipeline.
-
Install OpenTelemetry Collector
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.118.0/otelcol-contrib_0.118.0_linux_amd64.debsudo dpkg -i otelcol-contrib_0.118.0_linux_amd64.debwget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.118.0/otelcol-contrib_0.118.0_linux_amd64.rpmsudo rpm -ivh otelcol-contrib_0.118.0_linux_amd64.rpmservices:otel-collector:image: otel/opentelemetry-collector-contrib:0.118.0command: ["--config=/etc/otelcol/config.yaml"]volumes:- ./otel-config.yaml:/etc/otelcol/config.yamlports:- "4317:4317" # OTLP gRPC- "4318:4318" # OTLP HTTP- "8889:8889" # Prometheus metrics -
Configure the Collector
Create
/etc/otelcol-contrib/config.yaml:receivers:otlp:protocols:grpc:endpoint: 0.0.0.0:4317http:endpoint: 0.0.0.0:4318prometheus:config:scrape_configs:- job_name: "temporal-server"scrape_interval: 15sstatic_configs:- targets:- "temporal-frontend:9090"- "temporal-history:9090"- "temporal-matching:9090"- job_name: "temporal-workers"scrape_interval: 15sstatic_configs:- targets:- "worker-host:8077"processors:batch:timeout: 15ssend_batch_size: 10000resource:attributes:- key: service.namevalue: temporalaction: upsert- key: deployment.environmentvalue: productionaction: upsertexporters:otlp/last9:endpoint: "$last9_otlp_endpoint"headers:Authorization: "$last9_otlp_auth_header"prometheusremotewrite:endpoint: "$last9_remote_write_url"auth:authenticator: basicauth/last9extensions:basicauth/last9:client_auth:username: "$last9_remote_write_username"password: "$last9_remote_write_password"service:extensions: [basicauth/last9]pipelines:metrics:receivers: [prometheus, otlp]processors: [batch, resource]exporters: [prometheusremotewrite]traces:receivers: [otlp]processors: [batch, resource]exporters: [otlp/last9] -
Start the Collector
sudo systemctl daemon-reloadsudo systemctl enable otelcol-contribsudo systemctl start otelcol-contrib
Key Metrics to Monitor
Workflow Metrics
| Metric | Description | Alert Threshold |
|---|---|---|
temporal_workflow_completed | Count of completed workflows | Track trends |
temporal_workflow_failed | Count of failed workflows | > 0 (investigate) |
temporal_workflow_canceled | Count of canceled workflows | Track trends |
temporal_workflow_continue_as_new | Workflows that continued as new | Track trends |
temporal_workflow_task_schedule_to_start_latency | Time from the task being placed on the task queue to a worker’s poller picking it up (the Started event) | P95 > 1s |
Activity Metrics
| Metric | Description | Alert Threshold |
|---|---|---|
temporal_activity_schedule_to_start_latency | Time from the task being placed on the task queue to a worker’s poller picking it up (the Started event) | P95 > 1s |
temporal_activity_execution_latency | Time to complete activity execution | Based on SLO |
temporal_activity_execution_failed | Count of failed activity executions | > 0 (investigate) |
Worker Metrics
| Metric | Description | Alert Threshold |
|---|---|---|
temporal_worker_task_slots_available | Available task slots on worker | < 10% of max |
temporal_sticky_cache_hit | Count of sticky workflow cache hits | Track trends |
temporal_sticky_cache_miss | Count of sticky workflow cache misses | Track trends |
temporal_poller_start | Poller start events | Should be stable |
temporal_num_pollers | Number of active pollers (gauge) | Should be stable |
Server Metrics
| Metric | Description | Alert Threshold |
|---|---|---|
temporal_service_requests | Total service requests by operation | Track trends |
temporal_service_latency | Service request latency | P99 > 500ms |
temporal_persistence_latency | Database operation latency | P99 > 100ms |
Example Prometheus Queries
Query workflow completion rate:
sum(rate(temporal_workflow_completed_total[5m])) by (namespace, workflow_type)Monitor Schedule-To-Start latency (P95):
histogram_quantile(0.95, sum(rate(temporal_workflow_task_schedule_to_start_latency_bucket[5m])) by (le, namespace))Track activity failure rate:
sum(rate(temporal_activity_execution_failed_total[5m])) by (namespace, activity_type)/sum(rate(temporal_activity_execution_latency_count[5m])) by (namespace, activity_type)Worker utilization:
temporal_worker_task_slots_used/(temporal_worker_task_slots_used + temporal_worker_task_slots_available)Sticky cache hit ratio (alert if this drops below 80%):
sum(rate(temporal_sticky_cache_hit_total[5m])) by (namespace)/(sum(rate(temporal_sticky_cache_hit_total[5m])) by (namespace) + sum(rate(temporal_sticky_cache_miss_total[5m])) by (namespace))Verification
-
Check Prometheus Targets
Navigate to your Prometheus instance at
http://localhost:9090/targetsand verify that Temporal targets are being scraped successfully. -
Query Temporal Metrics
Run a test query in Prometheus:
temporal_service_requests -
Verify Last9 Ingestion
Log into your Last9 account and open Metrics Explorer at
https://app.last9.io/v2/organizations/<org_slug>/metrics(replace<org_slug>with your organization slug). Search for Temporal metrics:temporal_workflow_completed_total(counters land with a_totalsuffix when forwarded through the Collector; see the note in Example Prometheus Queries)temporal_activity_execution_latencytemporal_worker_task_slots_available
-
Generate Test Workflows
Run some test workflows to generate metrics:
# Using the Temporal CLI (tctl is deprecated; use `temporal`)temporal workflow start \--workflow-id your-workflow-id \--type YourWorkflow \--task-queue your-task-queue \--input '{"key": "value"}'
Next Steps
- Open Metrics Explorer (
https://app.last9.io/v2/organizations/<org_slug>/metrics) to visualize Temporal metrics - Set up Alerts for workflow failures and latency thresholds
- Explore Log Management for Temporal server logs
Troubleshooting
-
High schedule-to-start latency
If
temporal_workflow_task_schedule_to_start_latencyortemporal_activity_schedule_to_start_latencyis high, check these causes:- Insufficient workers: Scale up the number of workers, or increase the task slots.
- Worker overload: Check
temporal_worker_task_slots_availablefor capacity limits. - Network latency: Deploy the workers close to the Temporal cluster.
-
Missing metrics
- Verify that the metrics endpoint is accessible:
curl http://temporal-frontend:9090/metrics - Check the scrape targets in the Prometheus configuration.
- Make sure that the firewall rules allow metrics traffic.
- Verify that the metrics endpoint is accessible:
-
SDK metrics do not appear
- Verify that the SDK metrics bind address is accessible.
- Check that the metrics handler is configured in your worker code.
- Make sure that Prometheus can reach the metrics endpoint of the worker.
Please get in touch with us on Discord or Email if you have any questions.