> ## 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 Kotlin logs and traces to Bronto via OpenTelemetry

> Instrument Kotlin and JVM applications with the OpenTelemetry Java SDK to send logs and traces to Bronto over OTLP via a local Collector.

This page covers instrumenting a Kotlin application with the OpenTelemetry Java SDK to send **logs and traces** to Bronto over OTLP/HTTP via a local OTel Collector. Both signals are co-equal: logs are bridged via the Logback appender — 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

* Kotlin 1.8 or later, JVM 8 or later
* Gradle or Maven
* A running OTel Collector configured to forward logs and traces to Bronto — see [Connect Open Telemetry to Bronto](/agent-setup/open-telemetry)
* [OpenTelemetry Java SDK documentation](https://opentelemetry.io/docs/languages/java/) (the Java SDK is used for Kotlin)

## Install dependencies

<Tip>
  Use the [OpenTelemetry BOM](https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-bom) to manage all SDK versions in one place and avoid version conflicts.
</Tip>

<CodeGroup>
  ```kotlin Gradle (build.gradle.kts) theme={"dark"}
  // check https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-bom for latest BOM version
  implementation(platform("io.opentelemetry:opentelemetry-bom:LATEST"))

  dependencies {
      implementation("io.opentelemetry:opentelemetry-api")
      implementation("io.opentelemetry:opentelemetry-sdk")
      implementation("io.opentelemetry:opentelemetry-exporter-otlp")
      // check https://mvnrepository.com/artifact/io.opentelemetry.instrumentation/opentelemetry-logback-appender-1.0 for latest
      implementation("io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0:LATEST")
  }
  ```

  ```xml Maven (pom.xml) theme={"dark"}
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-bom</artifactId>
        <!-- check https://mvnrepository.com/artifact/io.opentelemetry/opentelemetry-bom for latest -->
        <version>LATEST</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-api</artifactId>
    </dependency>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-sdk</artifactId>
    </dependency>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-exporter-otlp</artifactId>
    </dependency>
    <!-- check https://mvnrepository.com/artifact/io.opentelemetry.instrumentation/opentelemetry-logback-appender-1.0 for latest -->
    <dependency>
      <groupId>io.opentelemetry.instrumentation</groupId>
      <artifactId>opentelemetry-logback-appender-1.0</artifactId>
      <version>LATEST</version>
    </dependency>
  </dependencies>
  ```
</CodeGroup>

## Configure the log bridge

Add the `OpenTelemetryAppender` to your `logback.xml`. Every log record Logback handles will be forwarded to the OTel pipeline.

```xml logback.xml theme={"dark"}
<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <appender name="OpenTelemetry"
            class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
    <captureExperimentalAttributes>true</captureExperimentalAttributes>
    <captureCodeAttributes>true</captureCodeAttributes>
  </appender>

  <root level="INFO">
    <appender-ref ref="CONSOLE" />
    <appender-ref ref="OpenTelemetry" />
  </root>
</configuration>
```

## Configure the OTLP exporter

Create an `OpenTelemetrySdk` instance and call `OpenTelemetryAppender.install()` to wire the Logback appender to the SDK.

```kotlin OtelConfig.kt theme={"dark"}
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender
import io.opentelemetry.sdk.OpenTelemetrySdk
import io.opentelemetry.sdk.logs.SdkLoggerProvider
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor
import io.opentelemetry.sdk.resources.Resource
import io.opentelemetry.semconv.ResourceAttributes

fun configureOtelLogging() {
    val exporter = OtlpHttpLogRecordExporter.builder()
        .setEndpoint("http://localhost:4318/v1/logs")
        .build()

    val resource = Resource.getDefault().merge(
        Resource.create(
            Attributes.of(
                ResourceAttributes.SERVICE_NAME, "my-service",
                ResourceAttributes.SERVICE_NAMESPACE, "my-team",
                ResourceAttributes.DEPLOYMENT_ENVIRONMENT, "production"
            )
        )
    )

    val loggerProvider = SdkLoggerProvider.builder()
        .setResource(resource)
        .addLogRecordProcessor(BatchLogRecordProcessor.builder(exporter).build())
        .build()

    val openTelemetry = OpenTelemetrySdk.builder()
        .setLoggerProvider(loggerProvider)
        .buildAndRegisterGlobal()

    OpenTelemetryAppender.install(openTelemetry)
}
```

Call `configureOtelLogging()` once at application startup, before the first log statement.

## 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.create()` in the SDK configuration above.

## Complete example

```kotlin main.kt theme={"dark"}
import org.slf4j.LoggerFactory

fun main() {
    configureOtelLogging()

    val logger = LoggerFactory.getLogger("main")
    logger.info("Application started")
    logger.warn("Low disk space, free_gb={}", 2.1)
    logger.error("Database connection failed", RuntimeException("timeout"))
}
```

Existing SLF4J / Logback log statements require no changes.

## 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).
* `OpenTelemetryAppender.install()` is called before the first log statement.
* `BatchLogRecordProcessor` exports on a background thread — for short-lived programs, add a shutdown call: `loggerProvider.shutdown()`.

## Traces

<Tip>
  The [OpenTelemetry Java Agent](https://opentelemetry.io/docs/zero-code/java/agent/) works for Kotlin applications too — attach it with `-javaagent:opentelemetry-javaagent.jar` to auto-instrument Spring Boot, Hibernate, gRPC, Kafka, and many more frameworks with no code changes.
</Tip>

### Configure the tracer provider

No additional packages are needed — `opentelemetry-sdk` and `opentelemetry-exporter-otlp` already include tracing support.

Create a `SdkTracerProvider` and combine it with the `SdkLoggerProvider` in the same `OpenTelemetrySdk` builder:

```kotlin OtelConfig.kt theme={"dark"}
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter
import io.opentelemetry.sdk.trace.SdkTracerProvider
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor

val traceExporter = OtlpHttpSpanExporter.builder()
    .setEndpoint("http://localhost:4318/v1/traces")
    .build()

val tracerProvider = SdkTracerProvider.builder()
    .setResource(resource)
    .addSpanProcessor(BatchSpanProcessor.builder(traceExporter).build())
    .build()

val openTelemetry = OpenTelemetrySdk.builder()
    .setLoggerProvider(loggerProvider)
    .setTracerProvider(tracerProvider)
    .buildAndRegisterGlobal()

OpenTelemetryAppender.install(openTelemetry)
```

The shared `resource` ensures `service.name` and `service.namespace` are identical on both logs and traces.

### Creating spans

```kotlin theme={"dark"}
val tracer = GlobalOpenTelemetry.getTracer("my-service")

val span = tracer.spanBuilder("process-payment").startSpan()
span.makeCurrent().use {
    span.setAttribute("payment.amount", 99.99)
    logger.info("Processing payment")  // trace_id and span_id injected automatically
}
span.end()
```

Any Logback statement inside an active span will automatically have `trace_id` and `span_id` attached.

## 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). Kotlin runs on the JVM, so the same [OpenTelemetry Java Agent](https://opentelemetry.io/docs/zero-code/java/agent/) covers Kotlin applications — no separate package. See [Java: GenAI semantic conventions](/opentelemetry/java#genai-semantic-conventions) for current auto-instrumentation coverage, content-capture configuration, and the manual-span pattern.

<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>

See [LLM Observability](/ai-features/llm-observability) for the full recommended `gen_ai.*` 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:

```kotlin theme={"dark"}
val logExporter = OtlpHttpLogRecordExporter.builder()
    .setEndpoint("https://ingestion.eu.bronto.io/v1/logs") // or ingestion.us.bronto.io
    .addHeader("x-bronto-api-key", "<YOUR_API_KEY>")
    .build()

val traceExporter = OtlpHttpSpanExporter.builder()
    .setEndpoint("https://ingestion.eu.bronto.io/v1/traces") // or ingestion.us.bronto.io
    .addHeader("x-bronto-api-key", "<YOUR_API_KEY>")
    .build()
```

| 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.
