Skip to content

Adding Custom Spans and Metrics

Auto-instrumentation covers HTTP, database, and messaging calls out of the box. To trace your own business logic or track application-specific events, add custom spans and metrics using the OpenTelemetry SDK.

Prerequisites

1. Add custom spans

Custom spans let you trace operations not covered by auto-instrumentation — business logic, batch jobs, or complex workflows.

Kotlin with @WithSpan

The @WithSpan annotation is the simplest way to trace a method. The OpenTelemetry Java agent picks it up automatically.

Add the dependency:

dependencies {
    implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.14.0")
}
<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-instrumentation-annotations</artifactId>
    <version>2.14.0</version>
</dependency>

Then annotate methods you want to trace:

import io.opentelemetry.instrumentation.annotations.WithSpan
import io.opentelemetry.instrumentation.annotations.SpanAttribute

class PaymentService {

    @WithSpan("process-payment")
    fun processPayment(
        @SpanAttribute("payment.amount") amount: Long,
        @SpanAttribute("payment.currency") currency: String
    ): PaymentResult {
        // This method call becomes a span in your traces.
        // @SpanAttribute adds the parameter values as span attributes.
        validate(amount, currency)
        return execute(amount, currency)
    }

    @WithSpan
    private fun validate(amount: Long, currency: String) {
        // Nested span — appears as a child of "process-payment"
    }
}

Java with the Tracer API

For more control (dynamic span names, adding events, setting status), use the Tracer API directly:

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.GlobalOpenTelemetry;

public class MyService {
    private static final Tracer tracer =
        GlobalOpenTelemetry.getTracer("my-app");

    public void doWork() {
        Span span = tracer.spanBuilder("custom-operation").startSpan();
        try (var scope = span.makeCurrent()) {
            // Your business logic here
            span.addEvent("checkpoint-reached");
        } catch (Exception e) {
            span.recordException(e);
            span.setStatus(io.opentelemetry.api.trace.StatusCode.ERROR);
            throw e;
        } finally {
            span.end();
        }
    }
}

Node.js

import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("my-app");

async function processOrder(orderId) {
  return tracer.startActiveSpan("process-order", async (span) => {
    try {
      span.setAttribute("order.id", orderId);
      const result = await executeOrder(orderId);
      return result;
    } catch (error) {
      span.recordException(error);
      span.setStatus({ code: 2 }); // ERROR
      throw error;
    } finally {
      span.end();
    }
  });
}

2. Add custom metrics

Custom metrics track application-specific counters, gauges, or histograms. These are exported to Mimir and available in Grafana.

Kotlin / Java

import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.api.metrics.LongCounter

object AppMetrics {
    private val meter = GlobalOpenTelemetry.getMeter("my-app")

    val ordersProcessed: LongCounter = meter
        .counterBuilder("orders_processed_total")
        .setDescription("Number of orders processed")
        .build()
}

// Usage:
AppMetrics.ordersProcessed.add(1, Attributes.of(
    AttributeKey.stringKey("status"), "completed"
))

OTel Metrics API

Use GlobalOpenTelemetry.getMeter() (not GlobalMeterProvider.getMeter(), which was removed in OpenTelemetry Java 2.x).

Node.js

import { metrics } from "@opentelemetry/api";

const meter = metrics.getMeter("my-app");
const ordersProcessed = meter.createCounter("orders_processed_total", {
  description: "Number of orders processed",
});

// Usage:
ordersProcessed.add(1, { status: "completed" });

3. Verify in APM

  1. Deploy your application
  2. Open the Nais APM and find your service
  3. Check the Operations tab — your custom spans appear as operations
  4. In Grafana Explore, query your custom metrics with PromQL

Next steps