Observability - OpenTelemetry Go
Overview
Status and Releases
The logs signal is still experimental. Breaking changes may be introduced in future versions.
Traces | Metrics | Logs |
---|---|---|
Stable | Stable | Beta |
Getting Started
Example application
Setup
To begin, set up a go.mod in a new directory:
1 | $ go mod init dice |
Create and launch an HTTP server
In that same folder, create a file called main.go and add the following code to the file:
1 | package main |
Create another file called
rolldice.go
and add the following code to the file:
1 | package main |
Build and run the application with the following command:
1 | $ go run . |
Open http://localhost:8080/rolldice in your web browser to ensure it is working.
Add OpenTelemetry Instrumentation
Add Dependencies
Install the following packages - This installs OpenTelemetry SDK components and
net/http
instrumentation.
1 | $ go get "go.opentelemetry.io/otel" \ |
Initialize the OpenTelemetry SDK
First, we’ll initialize the OpenTelemetry SDK. This is required for any application that exports telemetry.
Create otel.go with OpenTelemetry SDK bootstrapping code
1 | package main |
If you’re only using tracing or metrics, you can omit the corresponding TracerProvider or MeterProvider initialization code.
Instrument the HTTP server
- Now that we have the OpenTelemetry SDK initialized, we can instrument the HTTP server.
- Modify main.go to include code that sets up OpenTelemetry SDK and instruments the HTTP server using the otelhttp instrumentation library:
1 | package main |
Add Custom Instrumentation
- Instrumentation libraries capture telemetry at the edges of your systems
- such as inbound and outbound HTTP requests, but they don’t capture what’s going on in your application.
- For that you’ll need to write some custom manual instrumentation.
- Modify rolldice.go to include custom instrumentation using OpenTelemetry API:
1 | package main |
Run the Application
Build and run the application with the following command:
1 | go mod tidy |
- Open http://localhost:8080/rolldice/Alice in your web browser
- When you send a request to the server, you’ll see two spans in the trace emitted to the console.
- The span generated by the instrumentation library tracks the lifetime of a request to the /rolldice/{player} route.
- The span called roll is created manually and it is a child of the previously mentioned span.
- Along with the trace, log messages are emitted to the console.
- Refresh the http://localhost:8080/rolldice/Alice page a few times
- and then either wait a little or terminate the app and you’ll see metrics as in the console output
- You’ll see the dice.rolls metric emitted to the console, with separate counts for each roll value
- as well as the HTTP metrics generated by the instrumentation library.
Instrumentation
Overview
Manual instrumentation for OpenTelemetry Go
- Instrumentation is the act of adding observability code to an app yourself.
- If you’re instrumenting an app, you need to use the OpenTelemetry SDK for your language.
- You’ll then use the SDK to initialize OpenTelemetry and the API to instrument your code
- This will emit telemetry from your app, and any library you installed that also comes with instrumentation.
- If you’re instrumenting a library, only install the OpenTelemetry API package for your language.
- Your library will not emit telemetry on its own
- It will only emit telemetry when it is part of an app that uses the OpenTelemetry SDK
Traces
Getting a Tracer
To create spans, you’ll need to acquire or initialize a tracer first. - Ensure you have the right packages installed:
1 | go get go.opentelemetry.io/otel \ |
Then initialize an exporter, resources, tracer provider, and finally a tracer.
1 | package app |
You can now access tracer to manually instrument your code.
Creating Spans
- Spans are created by tracers. If you don’t have one initialized, you’ll need to do that.
- To create a span with a tracer, you’ll also need a handle on a context.Context instance.
- These will typically come from things like a request object and may already contain a parent span from an instrumentation library.
- In Go, the context package is used to store the active span.
- When you start a span, you’ll get a handle on not only the span that’s created, but the modified context that contains it.
- Once a span has completed, it is immutable and can no longer be modified.
1 | func httpHandler(w http.ResponseWriter, r *http.Request) { |
Get the current span
To get the current span, you’ll need to pull it out of a
context.Context
you have a handle on:
1 | // This context needs contain the active span you plan to extract. |
This can be helpful if you’d like to add information to the current span at a point in time.
Create nested spans
- You can create a nested span to track work in a nested operation.
- If the current context.Context you have a handle on already contains a span inside of it, creating a new span makes it a nested span. For example:
1 | func parentFunction(ctx context.Context) { |
Once a span has completed, it is immutable and can no longer be modified.
Span Attributes
- Attributes are keys and values that are applied as metadata to your spans and are useful for aggregating, filtering, and grouping traces.
- Attributes can be added at span creation, or at any other time during the lifecycle of a span before it has completed.
1 | // setting attributes at creation... |
Attribute keys can be precomputed, as well:
1 | var myKey = attribute.Key("myCoolAttribute") |
Semantic Attributes
- Semantic Attributes are attributes that are defined by the OpenTelemetry Specification
- in order to provide a shared set of attribute keys across multiple languages, frameworks, and runtimes for common concepts
- like HTTP methods, status codes, user agents, and more.
Events
An event is a human-readable message on a span that represents “something happening” during it’s lifetime.
1 | span.AddEvent("Acquiring lock") |
- A useful characteristic of events is that their timestamps are displayed as offsets from the beginning of the span
- allowing you to easily see how much time elapsed between them.
Events can also have attributes of their own
1 | span.AddEvent("Cancelled wait due to external signal", trace.WithAttributes(attribute.Int("pid", 4328), attribute.String("signal", "SIGHUP"))) |
Set span status
- A Status can be set on a Span, typically used to specify that a Span has not completed successfully - Error.
- By default, all spans are Unset, which means a span completed without error.
The status can be set at any time before the span is finished.
1 | import ( |
Record errors
If you have an operation that failed and you wish to capture the error it produced, you can record that error. - Stack
1 | import ( |
- It is highly recommended that you also set a span’s status to Error when using RecordError
- unless you do not wish to consider the span tracking a failed operation as an error span.
- The RecordError function does not automatically set a span status when called.
Propagators and Context
- Traces can extend beyond a single process.
- This requires context propagation, a mechanism where identifiers for a trace are sent to remote processes
In order to propagate trace context over the wire, a propagator must be registered with the OpenTelemetry API.
1 | import ( |
- OpenTelemetry also supports the B3 header format, for compatibility with existing tracing systems (go.opentelemetry.io/contrib/propagators/b3)
- that do not support the W3C TraceContext standard.
Libraries
- When you develop an app, you might use third-party libraries and frameworks to accelerate your work.
- If you then instrument your app using OpenTelemetry, you might want to
- avoid spending additional time to manually add traces, logs, and metrics to the third-party libraries and frameworks you use.
- Many libraries and frameworks already support OpenTelemetry or are supported through OpenTelemetry instrumentation
- so that they can generate telemetry you can export to an observability back end.
Use natively instrumented libraries
As of today, we don’t know about any Go library that has OpenTelemetry natively integrated.
- If a library comes with OpenTelemetry support by default
- you can get traces, metrics, and logs emitted from that library by adding and setting up the OpenTelemetry SDK with your app.
- The library might require some additional configuration for the instrumentation.
Use instrumentation libraries
- If a library does not come with OpenTelemetry out of the box, you can use instrumentation libraries to generate telemetry data for a library or framework.
- For example, the instrumentation library for net/http automatically creates spans and metrics based on the HTTP requests.
Each instrumentation library is a package. In general, this means you need to
go get
the appropriate package.
1 | go get go.opentelemetry.io/contrib/instrumentation/{import-path}/otel{package-name} |
Exporters
- Send telemetry to the OpenTelemetry Collector to make sure it’s exported correctly.
- Using the Collector in production environments is a best practice.
- To visualize your telemetry, export it to a backend such as Jaeger, Zipkin, Prometheus, or a vendor-specific backend.
Available exporters
- Among exporters, OpenTelemetry Protocol (OTLP) exporters are designed with the OpenTelemetry data model in mind
- emitting OTel data without any loss of information.
- Furthermore, many tools that operate on telemetry data support OTLP (such as Prometheus, Jaeger, and most vendors)
- providing you with a high degree of flexibility when you need it.
OTLP
To send trace data to an OTLP endpoint (like the collector or Jaeger >= v1.35.0) you’ll want to configure an OTLP exporter that sends to your endpoint.
OTLP traces over HTTP
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
- contains an implementation of the OTLP trace exporter using HTTP with binary protobuf payloads
OTLP traces over gRPC
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
- contains an implementation of OTLP trace exporter using gRPC
Jaeger
To try out the OTLP exporter, since v1.35.0 you can run Jaeger as an OTLP endpoint and for trace visualization in a Docker container:
OTLP metrics over HTTP
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp
- contains an implementation of OTLP metrics exporter using HTTP with binary protobuf payloads
OTLP metrics over gRPC
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc
- contains an implementation of OTLP metrics exporter using gRPC
Resources
- A resource represents the entity producing telemetry as resource attributes.
- In your observability backend, you can use resource information to better investigate interesting behavior.
Resources should be assigned to a tracer, meter, and logger provider at its initialization, and are created much like attributes:
1 | res := resource.NewWithAttributes( |
- Note the use of the semconv package to provide conventional names for resource attributes.
- This helps ensure that consumers of telemetry produced with these semantic conventions
- can easily discover relevant attributes and understand their meaning.
Resources can also be detected automatically through
resource.Detector
implementations.
- These Detectors may discover information about
- the currently running process
- the operating system it is running on
- the cloud provider hosting that operating system instance
- or any number of other resource attributes.
1 | res, err := resource.New( |
Sampling
- Sampling is a process that restricts the amount of spans that are generated by a system.
- The exact sampler you should use depends on your specific needs
- but in general you should make a decision at the start of a trace, and allow the sampling decision to propagate to other services.
A Sampler can be set on the tracer provider using the WithSampler option, as follows:
1 | provider := trace.NewTracerProvider( |
AlwaysSample
andNeverSample
are self-explanatory values.- AlwaysSample means that every span is sampled, while NeverSample means that no span is sampled
- Other samplers include
- TraceIDRatioBased
- which samples a fraction of spans, based on the fraction given to the sampler.
- If you set .5, half of all the spans are sampled.
- ParentBased
- is a sampler decorator which behaves differently, based on the parent of the span.
- If the span has no parent, the decorated sampler is used to make sampling decision based on the parent of the span.
- By default, ParentBased samples spans that have parents that were sampled, and doesn’t sample spans whose parents were not sampled.
- TraceIDRatioBased
- By default, the tracer provider uses a ParentBased sampler with the AlwaysSample sampler.
- When in a production environment, consider using the ParentBased sampler with the TraceIDRatioBased sampler.