# OpenTelemetry

Tilebox uses OpenTelemetry data models for workflow telemetry. Built-in Tilebox observability works without any OpenTelemetry setup, but you can add OTLP export when you need the same logs and traces in another backend.

## Configure OTLP export

Call the configuration functions when the runner process starts, before creating the client or runner.

**Python**

```python title="Python"
from tilebox.workflows import Client
from tilebox.workflows.observability.logging import configure_otel_logging
from tilebox.workflows.observability.tracing import configure_otel_tracing

from my_workflow import ProcessScene

configure_otel_tracing(
    service="sentinel-2-runner",
    endpoint="http://localhost:4318/v1/traces",
    headers={"Authorization": "Bearer <token>"},
)
configure_otel_logging(
    service="sentinel-2-runner",
    endpoint="http://localhost:4318/v1/logs",
    headers={"Authorization": "Bearer <token>"},
)

client = Client(name="sentinel-2-runner")
runner = client.runner(tasks=[ProcessScene])
runner.run_forever()
```

**Go**

```go title="Go"
package main

import (
	"context"
	"log/slog"

	"github.com/tilebox/tilebox-go/observability"
	"github.com/tilebox/tilebox-go/observability/logger"
	"github.com/tilebox/tilebox-go/observability/tracer"
	"github.com/tilebox/tilebox-go/workflows/v1"
	"go.opentelemetry.io/otel"
)

type ProcessScene struct{}

func (t *ProcessScene) Execute(ctx context.Context) error {
	slog.InfoContext(ctx, "processing scene")
	return nil
}

func main() {
	ctx := context.Background()
	service := &observability.Service{Name: "sentinel-2-runner"}

	traceProvider, shutdownTracer, err := tracer.NewOtelProvider(ctx, service,
		tracer.WithEndpointURL("http://localhost:4318/v1/traces"),
		tracer.WithHeaders(map[string]string{"Authorization": "Bearer <token>"}),
	)
	if err != nil {
		slog.ErrorContext(ctx, "failed to configure tracing", slog.Any("error", err))
		return
	}
	defer shutdownTracer(ctx)
	otel.SetTracerProvider(traceProvider)

	logHandler, shutdownLogger, err := logger.NewOtelHandler(ctx, service,
		logger.WithEndpointURL("http://localhost:4318/v1/logs"),
		logger.WithHeaders(map[string]string{"Authorization": "Bearer <token>"}),
		logger.WithLevel(slog.LevelInfo),
	)
	if err != nil {
		slog.ErrorContext(ctx, "failed to configure logging", slog.Any("error", err))
		return
	}
	defer shutdownLogger(ctx)
	slog.SetDefault(logger.New(logHandler))

	client := workflows.NewClient()
	runner, err := client.NewTaskRunner(ctx)
	if err != nil {
		slog.ErrorContext(ctx, "failed to create runner", slog.Any("error", err))
		return
	}

	if err := runner.RegisterTasks(&ProcessScene{}); err != nil {
		slog.ErrorContext(ctx, "failed to register tasks", slog.Any("error", err))
		return
	}

	runner.Run(ctx)
}
```

If the endpoint does not include `/v1/traces` or `/v1/logs`, the Python SDK adds the correct path automatically.

## Environment variables

You can omit endpoint and interval arguments by setting environment variables:

| Variable               | Used by                       |
| ---------------------- | ----------------------------- |
| `OTEL_TRACES_ENDPOINT` | `configure_otel_tracing()`    |
| `OTEL_LOGS_ENDPOINT`   | `configure_otel_logging()`    |
| `OTEL_EXPORT_INTERVAL` | tracing and logging exporters |

`OTEL_EXPORT_INTERVAL` accepts durations such as `5s`, `30s`, or `2m`.

## Local collector example

For local trace testing, run Jaeger with OTLP HTTP enabled:

```bash
docker run --rm --name jaeger \
  -p 16686:16686 \
  -p 4318:4318 \
  jaegertracing/jaeger:2.9.0
```

Then configure tracing with `endpoint="http://localhost:4318"` and open the Jaeger UI at <http://localhost:16686>.
