Skip to content

Commit

Permalink
feat(gateway): trace context header support (#256)
Browse files Browse the repository at this point in the history
* feat(examples): wrap handler with OTel propagation
* feat: add tracing sub-package based on Kubo
* docs: update tracing according to comments
* docs(tracing): clarify that kubo is an example

Context:
ipfs-inactive/bifrost-gateway#68
https://www.w3.org/TR/trace-context/

---------

Co-authored-by: Marcin Rataj <lidel@lidel.org>
  • Loading branch information
hacdias and lidel authored Apr 11, 2023
1 parent 999d939 commit 5d6c73c
Show file tree
Hide file tree
Showing 14 changed files with 636 additions and 28 deletions.
142 changes: 142 additions & 0 deletions docs/tracing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Tracing

Tracing across the stack follows, as much as possible, the [Open Telemetry]
specifications. Configuration environment variables are specified in the
[OpenTelemetry Environment Variable Specification].

We use the [opentelemtry-go] package, which currently does not have default support
for the `OTEL_TRACES_EXPORTER` environment variables. Therefore, we provide some
helper functions under [`boxo/tracing`](../tracing/) to support these.

In this document, we document the quirks of our custom support for the `OTEL_TRACES_EXPORTER`,
as well as examples on how to use tracing, create traceable headers, and how
to use the Jaeger UI. The [Gateway examples](../examples/gateway/) fully support Tracing.

- [Environment Variables](#environment-variables)
- [`OTEL_TRACES_EXPORTER`](#otel_traces_exporter)
- [`OTLP Exporter`](#otlp-exporter)
- [`Jaeger Exporter`](#jaeger-exporter)
- [`Zipkin Exporter`](#zipkin-exporter)
- [`File Exporter`](#file-exporter)
- [`OTEL_PROPAGATORS`](#otel_propagators)
- [Using Jaeger UI](#using-jaeger-ui)
- [Generate `traceparent` Header](#generate-traceparent-header)

## Environment Variables

For advanced configurations, such as ratio-based sampling, please see also the
[OpenTelemetry Environment Variable Specification].

### `OTEL_TRACES_EXPORTER`

Specifies the exporters to use as a comma-separated string. Each exporter has a
set of additional environment variables used to configure it. The following values
are supported:

- `otlp`
- `jaeger`
- `zipkin`
- `stdout`
- `file` -- appends traces to a JSON file on the filesystem

Default: `""` (no exporters)

### `OTLP Exporter`

Unless specified in this section, the OTLP exporter uses the environment variables
documented in [OpenTelemetry Protocol Exporter].

#### `OTEL_EXPORTER_OTLP_PROTOCOL`
Specifies the OTLP protocol to use, which is one of:

- `grpc`
- `http/protobuf`

Default: `"grpc"`

### `Jaeger Exporter`

See [Jaeger Exporter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#jaeger-exporter).

### `Zipkin Exporter`

See [Zipkin Exporter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#zipkin-exporter).

### `File Exporter`

#### `OTEL_EXPORTER_FILE_PATH`

Specifies the filesystem path for the JSON file.

Default: `"$PWD/traces.json"`

### `OTEL_PROPAGATORS`

See [General SDK Configuration](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#general-sdk-configuration).

## Using Jaeger UI

One can use the `jaegertracing/all-in-one` Docker image to run a full Jaeger stack
and configure the Kubo daemon, or gateway examples, to publish traces to it. Here, in an
ephemeral container:

```console
$ docker run --rm -it --name jaeger \
-e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
-p 5775:5775/udp \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 14268:14268 \
-p 14269:14269 \
-p 14250:14250 \
-p 9411:9411 \
jaegertracing/all-in-one
```

Then, in other terminal, start the app that uses `boxo/tracing` internally (e.g., a Kubo daemon), with Jaeger exporter enabled:

```
$ OTEL_TRACES_EXPORTER=jaeger ipfs daemon
```

Finally, the [Jaeger UI] is available at http://localhost:16686.

## Generate `traceparent` Header

If you want to trace a specific request and want to have its tracing ID, you can
generate a `Traceparent` header. According to the [Trace Context] specification,
the header is formed as follows:

> ```
> version-format = trace-id "-" parent-id "-" trace-flags
> trace-id = 32HEXDIGLC ; 16 bytes array identifier. All zeroes forbidden
> parent-id = 16HEXDIGLC ; 8 bytes array identifier. All zeroes forbidden
> trace-flags = 2HEXDIGLC ; 8 bit flags. Currently, only one bit is used. See below for details
> ```
To generate a valid `Traceparent` header value, the following script can be used:

```bash
version="00" # fixed in spec at 00
trace_id="$(cat /dev/urandom | tr -dc 'a-f0-9' | fold -w 32 | head -n 1)"
parent_id="00$(cat /dev/urandom | tr -dc 'a-f0-9' | fold -w 14 | head -n 1)"
trace_flag="01" # sampled
traceparent="$version-$trace_id-$parent_id-$trace_flag"
echo $traceparent
```

**NOTE**: the `tr` command behaves differently on macOS. You may want to install
the GNU `tr` (`gtr`) and use it instead.

Then, the value can be passed onto the request with `curl -H "Traceparent: $traceparent" URL`.
If using Jaeger, you can now search by the trace with ID `$trace_id` and see
the complete trace of this request.

[Open Telemetry]: https://opentelemetry.io/
[opentelemetry-go]: https://github.com/open-telemetry/opentelemetry-go
[Trace Context]: https://www.w3.org/TR/trace-context
[OpenTelemetry Environment Variable Specification]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md
[OpenTelemetry Protocol Exporter]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md
[Jaeger UI]: https://github.com/jaegertracing/jaeger-ui
14 changes: 14 additions & 0 deletions examples/gateway/car/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"flag"
"io"
"log"
Expand All @@ -17,16 +18,29 @@ import (
)

func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

carFilePtr := flag.String("c", "", "path to CAR file to back this gateway from")
port := flag.Int("p", 8040, "port to run this gateway from")
flag.Parse()

// Setups up tracing. This is optional and only required if the implementer
// wants to be able to enable tracing.
tp, err := common.SetupTracing(ctx, "CAR Gateway Example")
if err != nil {
log.Fatal(err)
}
defer (func() { _ = tp.Shutdown(ctx) })()

// Sets up a block service based on the CAR file.
blockService, roots, f, err := newBlockServiceFromCAR(*carFilePtr)
if err != nil {
log.Fatal(err)
}
defer f.Close()

// Creates the gateway API with the block service.
gwAPI, err := gateway.NewBlocksGateway(blockService)
if err != nil {
log.Fatal(err)
Expand Down
9 changes: 8 additions & 1 deletion examples/gateway/common/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/ipfs/boxo/gateway"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func NewHandler(gwAPI gateway.IPFSBackend) http.Handler {
Expand Down Expand Up @@ -58,10 +59,16 @@ func NewHandler(gwAPI gateway.IPFSBackend) http.Handler {
var handler http.Handler
handler = gateway.WithHostname(mux, gwAPI, publicGateways, noDNSLink)

// Finally, wrap with the withConnect middleware. This is required since we use
// Then, wrap with the withConnect middleware. This is required since we use
// http.ServeMux which does not support CONNECT by default.
handler = withConnect(handler)

// Finally, wrap with the otelhttp handler. This will allow the tracing system
// to work and for correct propagation of tracing headers. This step is optional
// and only required if you want to use tracing. Note that OTel must be correctly
// setup in order for this to have an effect.
handler = otelhttp.NewHandler(handler, "Gateway.Request")

return handler
}

Expand Down
61 changes: 61 additions & 0 deletions examples/gateway/common/tracing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package common

import (
"context"

"github.com/ipfs/boxo/tracing"
"go.opentelemetry.io/contrib/propagators/autoprop"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)

// SetupTracing sets up the tracing based on the OTEL_* environment variables,
// and the provided service name. It returns a trace.TracerProvider.
func SetupTracing(ctx context.Context, serviceName string) (*trace.TracerProvider, error) {
tp, err := NewTracerProvider(ctx, serviceName)
if err != nil {
return nil, err
}

// Sets the default trace provider for this process. If this is not done, tracing
// will not be enabled. Please note that this will apply to the entire process
// as it is set as the default tracer, as per OTel recommendations.
otel.SetTracerProvider(tp)

// Configures the default propagators used by the Open Telemetry library. By
// using autoprop.NewTextMapPropagator, we ensure the value of the environmental
// variable OTEL_PROPAGATORS is respected, if set. By default, Trace Context
// and Baggage are used. More details on:
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md
otel.SetTextMapPropagator(autoprop.NewTextMapPropagator())

return tp, nil
}

// NewTracerProvider creates and configures a TracerProvider.
func NewTracerProvider(ctx context.Context, serviceName string) (*trace.TracerProvider, error) {
exporters, err := tracing.NewSpanExporters(ctx)
if err != nil {
return nil, err
}

options := []trace.TracerProviderOption{}

for _, exporter := range exporters {
options = append(options, trace.WithBatcher(exporter))
}

r, err := resource.Merge(
resource.Default(),
resource.NewSchemaless(
semconv.ServiceNameKey.String(serviceName),
),
)
if err != nil {
return nil, err
}
options = append(options, trace.WithResource(r))
return trace.NewTracerProvider(options...), nil
}
18 changes: 8 additions & 10 deletions examples/gateway/proxy/blockstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import (
"fmt"
"io"
"net/http"
"net/url"

"github.com/ipfs/boxo/exchange"
blocks "github.com/ipfs/go-block-format"
"github.com/ipfs/go-cid"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

type proxyExchange struct {
Expand All @@ -19,7 +19,9 @@ type proxyExchange struct {

func newProxyExchange(gatewayURL string, client *http.Client) exchange.Interface {
if client == nil {
client = http.DefaultClient
client = &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
}

return &proxyExchange{
Expand All @@ -29,17 +31,13 @@ func newProxyExchange(gatewayURL string, client *http.Client) exchange.Interface
}

func (e *proxyExchange) fetch(ctx context.Context, c cid.Cid) (blocks.Block, error) {
u, err := url.Parse(fmt.Sprintf("%s/ipfs/%s?format=raw", e.gatewayURL, c))
urlStr := fmt.Sprintf("%s/ipfs/%s?format=raw", e.gatewayURL, c)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
if err != nil {
return nil, err
}
resp, err := e.httpClient.Do(&http.Request{
Method: http.MethodGet,
URL: u,
Header: http.Header{
"Accept": []string{"application/vnd.ipld.raw"},
},
})
req.Header.Set("Accept", "application/vnd.ipld.raw")
resp, err := e.httpClient.Do(req)
if err != nil {
return nil, err
}
Expand Down
12 changes: 12 additions & 0 deletions examples/gateway/proxy/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"flag"
"log"
"net/http"
Expand All @@ -15,10 +16,21 @@ import (
)

func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

gatewayUrlPtr := flag.String("g", "", "gateway to proxy to")
port := flag.Int("p", 8040, "port to run this gateway from")
flag.Parse()

// Setups up tracing. This is optional and only required if the implementer
// wants to be able to enable tracing.
tp, err := common.SetupTracing(ctx, "CAR Gateway Example")
if err != nil {
log.Fatal(err)
}
defer (func() { _ = tp.Shutdown(ctx) })()

// Sets up a blockstore to hold the blocks we request from the gateway
// Note: in a production environment you would likely want to choose a more efficient datastore implementation
// as well as one that has a way of pruning storage so as not to hold data in memory indefinitely.
Expand Down
Loading

0 comments on commit 5d6c73c

Please sign in to comment.