Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gateway): trace context header support #256

Merged
merged 4 commits into from
Apr 11, 2023
Merged
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
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].
hacdias marked this conversation as resolved.
Show resolved Hide resolved

### `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,
hacdias marked this conversation as resolved.
Show resolved Hide resolved
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"
hacdias marked this conversation as resolved.
Show resolved Hide resolved
)

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")
hacdias marked this conversation as resolved.
Show resolved Hide resolved

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