Skip to main content
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. Logs are bridged via the Logback appender — no log statement changes needed. Traces are added by configuring a tracer provider alongside the log provider.
If you don’t have a Collector and want to export directly from your application to Bronto, see Direct export to Bronto at the bottom of this page.

Prerequisites

Install dependencies

Use the OpenTelemetry BOM to manage all SDK versions in one place and avoid version conflicts.
// 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")
}

Configure the log bridge

Add the OpenTelemetryAppender to your logback.xml. Every log record Logback handles will be forwarded to the OTel pipeline.
logback.xml
<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.
OtelConfig.kt
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 attributeBronto conceptDescription
service.nameDatasetGroups logs from one service
service.namespaceCollectionGroups related services or a team’s services
These are set via Resource.create() in the SDK configuration above.

Complete example

main.kt
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 log collection

After running your application, open the Search page in Bronto. Filter by the dataset name you set in service.name — your log records 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.
  • 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

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

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:
OtelConfig.kt
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

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.

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:
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()
RegionLogs endpointTraces endpoint
EUhttps://ingestion.eu.bronto.io/v1/logshttps://ingestion.eu.bronto.io/v1/traces
UShttps://ingestion.us.bronto.io/v1/logshttps://ingestion.us.bronto.io/v1/traces
See API Keys for how to create a key with ingestion permissions. No other changes to the rest of the setup are required.