> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bronto.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Send Go logs and traces to Bronto via OpenTelemetry

> Instrument Go applications with the OpenTelemetry Go SDK to send logs and traces to Bronto over OTLP/gRPC or OTLP/HTTP via a local Collector.

This page covers instrumenting a Go application with the OpenTelemetry SDK to send **logs and traces** to Bronto over OTLP/HTTP via a local OTel Collector. Both signals are co-equal: logs are bridged from `log/slog` — no log statement changes needed — and traces are emitted via a tracer provider.

<Tip>
  If you don't have a Collector and want to export directly from your application to Bronto, see [Direct export to Bronto](#direct-export-to-bronto) at the bottom of this page.
</Tip>

## Prerequisites

* Go 1.21 or later (required for `log/slog`)
* A running OTel Collector configured to forward logs and traces to Bronto — see [Connect Open Telemetry to Bronto](/agent-setup/open-telemetry)
* [OpenTelemetry Go SDK documentation](https://opentelemetry.io/docs/languages/go/)

## Install dependencies

```bash theme={"dark"}
go get go.opentelemetry.io/otel/sdk/log
go get go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp
go get go.opentelemetry.io/contrib/bridges/otelslog
go get go.opentelemetry.io/otel/sdk/resource
go get go.opentelemetry.io/otel/semconv/v1.26.0
```

| Package                              | Purpose                          |
| ------------------------------------ | -------------------------------- |
| `sdk/log`                            | `LoggerProvider` and processors  |
| `exporters/otlp/otlplog/otlploghttp` | OTLP/HTTP exporter               |
| `contrib/bridges/otelslog`           | Bridges `log/slog` into OTel     |
| `sdk/resource`                       | Resource builder                 |
| `semconv/v1.26.0`                    | Standard attribute key constants |

## Configure the log bridge

The `otelslog` bridge replaces the default `slog` handler. All `slog.Info()`, `slog.Warn()`, and `slog.Error()` calls are forwarded to the OTel pipeline automatically.

```go otel_logging.go theme={"dark"}
package logging

import (
    "context"
    "log/slog"

    "go.opentelemetry.io/contrib/bridges/otelslog"
    "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
    "go.opentelemetry.io/otel/sdk/log"
    "go.opentelemetry.io/otel/sdk/resource"
    semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

func Configure(ctx context.Context) (shutdown func(context.Context) error, err error) {
    res, err := resource.New(ctx,
        resource.WithAttributes(
            semconv.ServiceName("my-service"),
            semconv.ServiceNamespace("my-team"),
            semconv.DeploymentEnvironment("production"),
        ),
    )
    if err != nil {
        return nil, err
    }

    exporter, err := otlploghttp.New(ctx,
        otlploghttp.WithEndpoint("localhost:4318"),
        otlploghttp.WithInsecure(),
    )
    if err != nil {
        return nil, err
    }

    provider := log.NewLoggerProvider(
        log.WithResource(res),
        log.WithProcessor(log.NewBatchProcessor(exporter)),
    )

    // Replace the default slog handler with the OTel bridge
    slog.SetDefault(slog.New(otelslog.NewHandler("my-service",
        otelslog.WithLoggerProvider(provider))))

    return provider.Shutdown, nil
}
```

## Configure the OTLP exporter

The `otlploghttp.New` call in the snippet above connects to the OTel Collector on `localhost:4318` without TLS. If your Collector runs on a different host or port, update `WithEndpoint` accordingly.

To use TLS, remove `WithInsecure()` and ensure your Collector is configured with a valid certificate.

## Set resource attributes

Resource attributes are attached to every log record exported from this process. Two attributes drive how Bronto organises incoming logs:

| OTel attribute      | Bronto concept | Description                                  |
| ------------------- | -------------- | -------------------------------------------- |
| `service.name`      | Dataset        | Groups logs from one service                 |
| `service.namespace` | Collection     | Groups related services or a team's services |

These are set via `resource.WithAttributes` in the setup above.

## Complete example

```go main.go theme={"dark"}
package main

import (
    "context"
    "log/slog"
    "os"
    "os/signal"

    "myapp/logging"
)

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
    defer stop()

    shutdown, err := logging.Configure(ctx)
    if err != nil {
        slog.Error("failed to configure OTel logging", "error", err)
        os.Exit(1)
    }
    defer shutdown(context.Background())

    slog.Info("Application started")
    slog.Warn("Low disk space", "disk_free_gb", 2.1)
    slog.Error("Database connection failed", "error", "timeout")
}
```

`defer shutdown(context.Background())` flushes any buffered log records before the process exits.

## Verify delivery

After running your application, check both signals in Bronto:

* **Logs**: open the [Search](https://app.bronto.io/search) page and filter by the dataset name you set in `service.name` — your log records should appear within a few seconds.
* **Traces**: open the Explore Traces page and filter by the same `service.name` — your spans should appear within a few seconds.

If no logs appear, check:

* The OTel Collector is running and reachable at the configured endpoint.
* The Collector's pipeline includes a `logs` pipeline with an `otlp` receiver and the Bronto exporter — see [Connect Open Telemetry to Bronto](/agent-setup/open-telemetry).
* `logging.Configure` is called before the first `slog` statement.
* The `shutdown` function is called before process exit to flush buffered records.

## Traces

<Tip>
  The [`go.opentelemetry.io/contrib/instrumentation`](https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation) packages provide instrumentation for `net/http`, gRPC, database drivers, and more with minimal setup. For example, wrap your HTTP handler with `otelhttp.NewHandler` for automatic span creation per request. See [Go instrumentation libraries](https://opentelemetry.io/docs/languages/go/libraries/) for the full list.
</Tip>

### Install tracing dependencies

```bash theme={"dark"}
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk/trace
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
```

### Configure the tracer provider

```go otel_tracing.go theme={"dark"}
package logging

import (
    "context"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

func ConfigureTracing(ctx context.Context, res *resource.Resource) (shutdown func(context.Context) error, err error) {
    exporter, err := otlptracehttp.New(ctx,
        otlptracehttp.WithEndpoint("localhost:4318"),
        otlptracehttp.WithInsecure(),
    )
    if err != nil {
        return nil, err
    }

    provider := sdktrace.NewTracerProvider(
        sdktrace.WithResource(res),
        sdktrace.WithBatcher(exporter),
    )

    otel.SetTracerProvider(provider)
    return provider.Shutdown, nil
}
```

Pass the same `res` from `Configure` so both signals share `service.name` and `service.namespace`.

### Creating spans

```go theme={"dark"}
tracer := otel.Tracer("my-service")

ctx, span := tracer.Start(ctx, "process-payment")
defer span.End()

span.SetAttributes(attribute.Float64("payment.amount", 99.99))
slog.InfoContext(ctx, "Processing payment")  // trace_id and span_id injected automatically
```

Use `slog.InfoContext` (with the span context) to ensure the active span's IDs are propagated to the log record.

### Complete example

```go main.go theme={"dark"}
package main

import (
    "context"
    "log/slog"
    "os"
    "os/signal"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "myapp/logging"
)

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
    defer stop()

    logShutdown, res, err := logging.Configure(ctx)
    if err != nil {
        slog.Error("failed to configure logging", "error", err)
        os.Exit(1)
    }
    defer logShutdown(context.Background())

    traceShutdown, err := logging.ConfigureTracing(ctx, res)
    if err != nil {
        slog.Error("failed to configure tracing", "error", err)
        os.Exit(1)
    }
    defer traceShutdown(context.Background())

    tracer := otel.Tracer("my-service")
    ctx, span := tracer.Start(ctx, "handle-request")
    defer span.End()

    span.SetAttributes(attribute.String("http.method", "GET"))
    slog.InfoContext(ctx, "Handling request")  // trace_id + span_id attached automatically
}
```

## GenAI semantic conventions

If your application calls an LLM, OpenTelemetry defines [GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/gen-ai-spans.md). There is no first-party GenAI auto-instrumentation package for Go yet — set the attributes yourself around each LLM call:

<Note>
  This page was verified on **July 17, 2026**. GenAI libraries and semantic conventions are evolving rapidly, so package configuration and emitted attributes can change between releases. Keep your instrumentation current and verify the fields emitted by the version you deploy.
</Note>

```go theme={"dark"}
tracer := otel.Tracer("my-service")

ctx, span := tracer.Start(ctx, "chat gpt-4o-mini")
defer span.End()

span.SetAttributes(
    attribute.String("gen_ai.provider.name", "openai"),
    attribute.String("gen_ai.request.model", "gpt-4o-mini"),
    attribute.Int("gen_ai.usage.input_tokens", 33),
    attribute.Int("gen_ai.usage.output_tokens", 74),
    attribute.StringSlice("gen_ai.response.finish_reasons", []string{"stop"}),
)
```

<Note>
  Message content (`gen_ai.input.messages` / `gen_ai.output.messages`) is opt-in by convention in languages with auto-instrumentation, off by default. Since you're setting attributes by hand here, apply the same discipline — gate prompt/response content behind your own config flag rather than always sending it.
</Note>

See [LLM Observability](/ai-features/llm-observability) for the full recommended attribute set and how to search and aggregate GenAI fields in Bronto.

## Direct export to Bronto

If you are not using an OTel Collector, export directly to Bronto by replacing the exporter configurations with the Bronto OTLP endpoints and your API key:

```go theme={"dark"}
// Logs
logExporter, err := otlploghttp.New(ctx,
    otlploghttp.WithEndpoint("ingestion.eu.bronto.io"), // or ingestion.us.bronto.io
    otlploghttp.WithURLPath("/v1/logs"),
    otlploghttp.WithHeaders(map[string]string{"x-bronto-api-key": "<YOUR_API_KEY>"}),
)

// Traces
traceExporter, err := otlptracehttp.New(ctx,
    otlptracehttp.WithEndpoint("ingestion.eu.bronto.io"), // or ingestion.us.bronto.io
    otlptracehttp.WithURLPath("/v1/traces"),
    otlptracehttp.WithHeaders(map[string]string{"x-bronto-api-key": "<YOUR_API_KEY>"}),
)
```

| Region | Logs endpoint                            | Traces endpoint                            |
| ------ | ---------------------------------------- | ------------------------------------------ |
| EU     | `https://ingestion.eu.bronto.io/v1/logs` | `https://ingestion.eu.bronto.io/v1/traces` |
| US     | `https://ingestion.us.bronto.io/v1/logs` | `https://ingestion.us.bronto.io/v1/traces` |

See [API Keys](/Account-Management/API-Keys) for how to create a key with ingestion permissions. No other changes to the rest of the setup are required.
