# Scala

> Use OpenTelemetry to instrument your Scala application and send telemetry data to Last9

Source: https://last9.io/docs/integrations/languages/scala/

Use OpenTelemetry to instrument your Scala application and send traces, metrics, and logs to Last9.
Scala runs on the JVM, so the [OpenTelemetry Java agent](https://github.com/open-telemetry/opentelemetry-java-instrumentation)
works out of the box — no Scala-specific library required.

For a complete working example, see the
[scala/akka-http](https://github.com/last9/opentelemetry-examples/tree/main/scala/akka-http) example in the Last9 OTel examples repository.

## Prerequisites

- Scala 2.13 or 3.x
- Java 11 or later
- sbt (or Maven/Gradle)
- Last9 account with OTLP endpoint configured

## Step 1: Add OpenTelemetry API dependency

Add only the OTel **API** to your build — the SDK is provided by the Java agent at runtime.

**sbt**

```scala
// build.sbt
libraryDependencies ++= Seq(
  "io.opentelemetry" % "opentelemetry-api" % "1.44.0",

  // Logback appender — ships log records to the collector via OTLP
  "io.opentelemetry.instrumentation" % "opentelemetry-logback-appender-1.0" % "2.10.0-alpha" % Runtime,
  "ch.qos.logback" % "logback-classic" % "1.5.12",
)
```

If building a fat JAR with `sbt-assembly`, add the merge strategy to avoid losing OTel SPI registrations:

```scala
assembly / assemblyMergeStrategy := {
  case PathList("META-INF", "services", _*) => MergeStrategy.concat
  case PathList("META-INF", _*)             => MergeStrategy.discard
  case PathList("reference.conf")           => MergeStrategy.concat
  case _                                    => MergeStrategy.first
}
```

**Maven**

```xml
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-api</artifactId>
    <version>1.44.0</version>
</dependency>
<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-logback-appender-1.0</artifactId>
    <version>2.10.0-alpha</version>
    <scope>runtime</scope>
</dependency>
```

## Step 2: Download the Java agent

```bash
curl -L https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.30.0/opentelemetry-javaagent.jar \
  -o opentelemetry-javaagent.jar
```

## Step 3: Configure log correlation (optional)

Add the `OpenTelemetryAppender` to `logback.xml` to ship log records via OTLP and correlate them with traces:

```xml
<!-- src/main/resources/logback.xml -->
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} trace_id=%X{trace_id} span_id=%X{span_id} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="OTEL" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
        <captureExperimentalAttributes>true</captureExperimentalAttributes>
        <captureMdcAttributes>*</captureMdcAttributes>
    </appender>

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

## Step 4: Add manual spans (optional)

For business logic not covered by auto-instrumentation, create spans using the OTel API:

```scala
import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.api.trace.{SpanKind, StatusCode}

val tracer = GlobalOpenTelemetry.getTracer("my-service", "1.0.0")

// Wrap a block in a span. makeCurrent() ensures child spans (JDBC, Kafka, etc.)
// created inside the block are linked as children of this span.
val span  = tracer.spanBuilder("process-order").setSpanKind(SpanKind.INTERNAL).startSpan()
val scope = span.makeCurrent()
try {
  span.setAttribute("order.id", orderId)
  // ... your code ...
} catch {
  case e: Exception =>
    span.recordException(e)
    span.setStatus(StatusCode.ERROR, e.getMessage)
    throw e
} finally {
  scope.close()
  span.end()
}
```

:::caution[Async code]
If the block returns a `Future`, do **not** use the try/finally pattern above — the span will end before
the `Future` resolves. Instead, end the span explicitly inside the `onComplete` / `map` / `recover` callbacks.
:::

## Step 5: Set environment variables

```bash
export OTEL_SERVICE_NAME="my-scala-service"
export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_TRACES_EXPORTER="otlp"
export OTEL_METRICS_EXPORTER="otlp"
export OTEL_LOGS_EXPORTER="otlp"
export OTEL_TRACES_SAMPLER="parentbased_always_on"
```

Find your OTLP endpoint and auth header in the Last9 console under **Integrations → OpenTelemetry**.

## Step 6: Start the application with the agent

```bash
java -javaagent:opentelemetry-javaagent.jar -jar myapp.jar
```

Or set `JAVA_TOOL_OPTIONS` so the agent is added automatically by your runtime/container:

```bash
export JAVA_TOOL_OPTIONS="-javaagent:/path/to/opentelemetry-javaagent.jar"
```

## What gets instrumented automatically

| Library                                   | Signal                             |
| ----------------------------------------- | ---------------------------------- |
| Akka HTTP / Pekko HTTP (server + client)  | Traces                             |
| JDBC (PostgreSQL, MySQL, etc.) + HikariCP | Traces                             |
| Redis (Lettuce, Jedis)                    | Traces                             |
| Kafka producer / consumer                 | Traces + W3C header propagation    |
| gRPC (ScalaPB, Armeria)                   | Traces                             |
| JVM (GC, heap, threads, CPU)              | Metrics                            |
| SLF4J / Logback                           | Logs (via `OpenTelemetryAppender`) |

### Cloud resource detection

```bash
# AWS (EC2, ECS, EKS, Lambda)
export OTEL_RESOURCE_PROVIDERS_AWS_ENABLED=true

# GCP (GCE, GKE, Cloud Run, App Engine)
export OTEL_RESOURCE_PROVIDERS_GCP_ENABLED=true
```

### Sampling for high-traffic services

```bash
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1   # Sample 10% of new traces
```

## Kubernetes: OTel Operator

On Kubernetes, the [OpenTelemetry Operator](https://opentelemetry.io/docs/kubernetes/operator/) can
inject the Java agent automatically — no `-javaagent` flag in your Dockerfile required.

Add the annotation to your Deployment pod template:

```yaml
annotations:
  instrumentation.opentelemetry.io/inject-java: "true"
```

And create an `Instrumentation` CRD pointing at your Last9 collector endpoint. The operator sets
all `OTEL_*` environment variables on the pod before your container starts.

See the [Akka HTTP guide](/docs/integrations/frameworks/scala/akka-http/#kubernetes-otel-operator-no-dockerfile-changes)
for a full example.

## Verify the integration

After starting your application and sending a few requests, you should see:

1. **Traces** in Last9 with spans for HTTP requests, DB queries, and Kafka messages
2. **Logs** with `trace_id` and `span_id` fields linking to traces
3. **JVM metrics** (memory, GC, CPU) under your service name

For troubleshooting, set `OTEL_LOG_LEVEL=debug` to see SDK initialization details.
