Files
temporal/docs/development/tracing.md
Stephan Behnke c19b51a743 Grafana Tempo (#7006)
## What changed?
<!-- Describe what has changed in this PR -->

Added Grafana Tempo - an OTEL collector - to the developer setup.

Since we're already using Grafana, Tempo seemed like a logical choice.

## Why?
<!-- Tell your future self why have you made these changes -->

It allows developers to see all the RPC calls they are making (e.g. when
running the server locally or a functional test).

Tempo allows filtering by workflow ID. _(note that not _all_ places are
tagged yet, but lots of them)_

## How did you test it?
<!-- How have you verified this change? Tested locally? Added a unit
test? Checked in staging env? -->

```
make start-grafana-tempo
make OTEL=true start
```
```
tctl --ns default namespace register
tctl wf start --tq test --wid test -wt test
```

<img width="3352" alt="image"
src="https://github.com/user-attachments/assets/5809691a-da32-4a04-8e80-700f8acd47e5"
/>

And running a functional test from the IDE:

<img width="3346" alt="image"
src="https://github.com/user-attachments/assets/061bfaf3-0a34-4b4e-a06c-497dfa3a948a"
/>


## Potential risks
<!-- Assuming the worst case, what can be broken when deploying this
change to production? -->

## Documentation
<!-- Have you made sure this change doesn't falsify anything currently
stated in `docs/`? If significant
new behavior is added, have you described that in `docs/`? -->

Updated docs/development/tracing.md

## Is hotfix candidate?
<!-- Is this PR a hotfix candidate or does it require a notification to
be sent to the broader community? (Yes/No) -->
2024-12-19 17:33:13 -08:00

234 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tracing Temporal Services with OTEL
The Temporal server supports ability to configure OTEL trace exporters to
support emitting spans and traces for observability. More specifically, the
server uses the [Go Open Telemetry library](https://github.com/open-telemetry/opentelemetry-go)
for instrumentation and multi-protocol multi-model telemetry exporting. This document is intended to
help developers understand how to configure exporters and instrument their code.
A full exploration of tracing and telemetry is out of scope of this document and
the reader is referred to [external reference
material](https://opentelemetry.io/docs/concepts/signals/traces/), [third party
descriptions](https://lightstep.com/opentelemetry/tracing), and the
[specification
itself](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#tracing-signal).
## Quickstart
1. Run `make start-dependencies` (which starts Grafana Tempo)
2. Start the server using `make OTEL=true start` (or any other start-x command)
3. Visit http://localhost:3000/explore and select "Tempo" from the datasource dropdown.
tip: use [TraceQL](https://grafana.com/docs/tempo/latest/traceql/)
`{ .temporalWorkflowID =~ "<WF-ID>.*" }` to find the traces for your workflow
## Configuring
No trace exporters are configured by default and thus trace data is neither
collected nor emitted without additional configuration.
In OpenTelemetry, the concept of an "exporter" is
abstract. The concrete implementation of an exporter is determined by a
3-tuple of values: the exporter signal, model, and protocol:
- a "signal" is one of traces, metrics, or logs (in this document we will only deal with traces),
- "model" indicates the abstract data model for the span and trace data being exported,
- and the "protocol" specifies the concrete application protocol binding for the indicated model.
Temporal is known to support exporting trace data as defined by otlp over grpc.
### Configuration File
The server supports an `otel` YAML stanza which is used to configure a
set of process-wide exporters.
A common configuration is to emit tracing data to an agent such as the
[otel-collector](https://opentelemetry.io/docs/collector/) running locally. To
configure such a system add the stanza below to your configuration yaml file(s).
```
otel:
exporters:
- kind:
signal: traces
model: otlp
protocol: grpc
spec:
connection:
insecure: true
endpoint: localhost:4317
```
Another example is pointing Temporal directly at the Honeycomb hosted OTLP
collection service. To achieve such a configuration you will need an API key
from the upstream Honeycomb service and the stanza below.
```
otel:
exporters:
- kind:
signal: traces
model: otlp
protocol: grpc
spec:
connection:
endpoint: api.honeycomb.io:443
headers:
x-honeycomb-team: <a honeycomb API key>
```
Note that the configuration parser supports defining multiple exporters by
supplying additional `kind` and `spec` declarations. Additional configuration
fields can be found in [config_test.go](../../common/telemetry/config_test.go)
and are mostly related to the underlying gRPC client configuration (retries,
timeouts, etc).
### Environment Variables
#### Creating Exporter
An OTEL span exporter can also be configured via environment variables: [OTEL_TRACES_EXPORTER](
https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#exporter-selection)
creates a span exporter.
```
OTEL_TRACES_EXPORTER=otlp
```
Note that if the configuration file already defines a traces exporter, no additional exporter
will be created.
#### Configuring Exporter
The Go OTEL SDK will also read a well-known set of environment variables for the configuration
of the exporter. So if you prefer setting environment variables to writing YAML then you can use the
[variables defined in the OTEL spec](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/).
For example:
```
OTEL_SERVICE_NAME=my-service OTEL_EXPORTER_OTLP_TRACES_INSECURE=true
```
**NOTE: If an environment variable conflicts with YAML-provided configuration then the environment
variable takes precedence.**
## Instrumenting
While the exporter configuration described above is executed and set up at
process startup time, instrumentation code - the creation and termination of
spans - is inserted inline (like logging statements) into normal server
processing code. Spans are created by `go.opentelemetry.io/otel/trace.Tracer`
objects which are themselves created by
`go.opentelemetry.io/otel/trace.TracerProvider` instances. The `TracerProvider`
instances are bound to a single logical service and as such a single Temporal
process will have up to four such instances (for worker, mathcing, history, and
frontend services respectively). The `Tracer` object is bound to a single
logical _library_ which is different than a _service_. Consider that a history
_service_ instance might run code from the temporal common library, gRPC
library, and gocql library.
`Tracer` and `TracerProvider` object management has been added to the server's
`fx` [DI configuration](https://github.com/temporalio/temporal/blob/f86b8d2c5f43907eaea4ad53ea082d70692c38cf/temporal/fx.go#L787-L889)
and thus they are available to be added to any fx-enabled object constructors.
Due the possibility of multiple services being coresident within a single
process, we do not use the OTEL library's capability to host and access a single
global `TracerProvider`.
By default, gRPC clients and servers are instrumented via the open source
[otelgrpc](https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/instrumentation/google.golang.org/grpc/otelgrpc)
library.
## Instrumentation Tips
### Follow the OTEL attribute naming guidelines
The OpenTelemetry project has published a non-normative set of [guidelines for
attribute naming](https://opentelemetry.io/docs/reference/specification/common/attribute-naming/).
If nothing else, please
1. Always check for an appropriate attribute in
[semconv](https://pkg.go.dev/go.opentelemetry.io/otel/semconv) before
creating your own
1. Always prefix Temporal attributes with `io.temporal`
### Create shared package-appropriate attribute keys
Do not create a single file in common for all attributes
Do not create packages _just_ for OTEL attributes
Do create a set of `attribute.Key`s in the semantically appropriate package and
re-use those to create `attribute.KeyValue`s as needed.
Do create a set of utility functions that can transform frequently used
aggregate types (Tasks, WorkflowExecutions, TaskQueues, etc) into an
`[]attribute.KeyValue`. The association of `attribute.KeyValue`s to a
`trace.Span` can be verbose in terms of the number of lines of code needed so
any reduction in that noise will be a good idea. Not to mention the consistency
benefit of sharing a single mapping function.
### Start a span in `common` or other non-service-specific code
*Q:* Given that common code can be called from any service, how can I start a span
in common library code that is bound the appropriate service (frontend/history/matching/worker)?
*A:* The `TracerProvider` that created the currently active Span can be retrieved
from that Span itself and the currently active Span can be received from the `context.Context`.
```
// DoFoo is a function in the common package
func DoFoo(ctx context.Context, x int, y string) string {
var span trace.Span
ctx, span = trace.SpanFromContext(ctx).TracerProvider().Tracer("go.temporal.io/server/common").Start("DoFoo")
defer span.End()
return fmt.Sprintf("%v-%v", y, x)
}
```
### `RecordError` does not imply Span failure
Using `Span.RecordError` is a good idea but not all errors imply failure. Thus,
if you want to capture an error _and also_ capture that a span failed, you must
additionally call `Span.SetStatus(codes.Error, err.Error())`. A
`FailSpanWithError` utility function might be a good idea.
### Propagate TraceContext across things other than function calls
This is taken care of by default for gRPC calls via the otelgrpc interceptors.
However, you may want to propagate tracing information between goroutines or
other places where the `context.Context` is not passed such as handoffs through
a Go channel or an external datastore. There are two broad approaches that are
applicable in different situations:
1. If the object being transferred is not externally durable (e.g. an object put
into a Go channel but _not_ spooled to a database) then you can pull the
`trace.SpanContext` out of the current `trace.Span` with
`trace.SpanContextFromContext(context.Context)` or `Span.SpanContext()` and
pass that object along with the data being transferred. The consuming side
can restore the tracing state with
`trace.ContextWithSpanContext(trace.SpanContext)`.
1. If the tracing state needs to be serialized, the OTEL library provides the
[propagation](https://pkg.go.dev/go.opentelemetry.io/otel@v1.7.0/propagation)
package to convert trace state into a more serialization-friendly type such
as a `map[string]string`. The `propagation.TraceContext` type can be used to
inject and extract trace state into a key-value-ish object.
```
carrier := propagation.MapCarrier(map[string]string{})
propagation.TraceContext{}.Inject(ctx, carrier)
// write the carrier object to a durable store
```
### Trace individual tasks that are processed together in batches
OpenTelemetry Spans can be _linked_ together to form a non-parent-child
relationship. One of the main use cases for linking is so that a batch process
(e.g. a database read that fills a large buffer of work items) can create Spans
for each of the individual work items it creates and those Spans can be linked
back to the parent batch Span without that span becoming their logical parent.
### Still want to log things?
Use `Span.AddEvent` to write messages that will be associated with that `Span`.
From the OTEL manual
> An event is a human-readable message on a span that represents “something happening” during its lifetime