Skip to main content
Semantic conventions standardize attribute and metric names across instrumentation so telemetry is comparable across libraries and services. The semconv modules provide generated, typesafe attribute keys and metric specs that match the OpenTelemetry semantic conventions specification.

Modules Overview

Otel4s provides four semantic convention modules:
ModuleStabilityPackage prefix
otel4s-semconvStableorg.typelevel.otel4s.semconv.attributes
otel4s-semconv-metricsStableorg.typelevel.otel4s.semconv.metrics
otel4s-semconv-experimentalIncubatingorg.typelevel.otel4s.semconv.experimental.attributes
otel4s-semconv-metrics-experimentalIncubatingorg.typelevel.otel4s.semconv.experimental.metrics
Stable modules track the stable spec, while experimental modules track incubating conventions. Use stable modules by default.
The *-experimental modules have no binary-compatibility guarantees between releases and may introduce binary breaking changes at any time.

Getting Started

Add the semantic convention modules to your project:
libraryDependencies ++= Seq(
  "org.typelevel" %% "otel4s-semconv"         % "0.15.0", // stable attributes
  "org.typelevel" %% "otel4s-semconv-metrics" % "0.15.0", // stable metric specs
)
If you need incubating conventions, add the -experimental variants instead of the stable ones.

Using Semantic Attribute Keys

The generated attribute keys are typesafe and can be used to build an attribute.
import org.typelevel.otel4s.Attributes
import org.typelevel.otel4s.semconv.attributes.{ExceptionAttributes, HttpAttributes}

val attrs = Attributes(
  HttpAttributes.HttpRequestMethod("GET"),
  HttpAttributes.HttpResponseStatusCode(200),
  ExceptionAttributes.ExceptionType("java.lang.RuntimeException")
)
The same keys work for spans, log records, or metric attributes because they are plain AttributeKey[A] values.

Common Attribute Categories

Semantic conventions are organized by domain:
import org.typelevel.otel4s.semconv.attributes.HttpAttributes

val httpAttrs = Attributes(
  HttpAttributes.HttpRequestMethod("POST"),
  HttpAttributes.HttpResponseStatusCode(201),
  HttpAttributes.HttpRoute("/api/users"),
  HttpAttributes.UrlFull("https://api.example.com/users")
)

Using Semantic Metric Specs

Semantic metric specs provide the canonical metric name, unit, description, and attribute expectations.

Method 1: Manual Creation

import org.typelevel.otel4s.metrics.{BucketBoundaries, Histogram, Meter}
import org.typelevel.otel4s.semconv.metrics.HttpMetrics

def createHttpClientDuration[F[_]: Meter](
  boundaries: BucketBoundaries
): F[Histogram[F, Double]] =
  Meter[F]
    .histogram[Double](HttpMetrics.ClientRequestDuration.name)
    .withUnit(HttpMetrics.ClientRequestDuration.unit)
    .withDescription(HttpMetrics.ClientRequestDuration.description)
    .withExplicitBucketBoundaries(boundaries)
    .create

Method 2: Using the Convenience Method

For convenience, every metric also has the generated create method that creates an instrument with the spec’s name, unit, and description:
import org.typelevel.otel4s.metrics.{BucketBoundaries, Histogram, Meter}
import org.typelevel.otel4s.semconv.metrics.HttpMetrics

def createHttpClientDuration[F[_]: Meter](
  boundaries: BucketBoundaries
): F[Histogram[F, Double]] =
  HttpMetrics.ClientRequestDuration.create[F, Double](boundaries)

Validating Metrics in Tests

Metric specs are also useful for validating exported metrics. The example below checks that expected server metrics exist and that each exported metric matches the semantic name, unit, description, and required attributes.
import cats.effect.IO
import org.typelevel.otel4s.metrics.Meter
import org.typelevel.otel4s.semconv.MetricSpec
import org.typelevel.otel4s.semconv.metrics.HttpMetrics
import org.typelevel.otel4s.semconv.Requirement
import org.typelevel.otel4s.oteljava.testkit.metrics.MetricsTestkit
import org.typelevel.otel4s.oteljava.testkit.metrics.data.Metric

def semanticTest(scenario: Meter[IO] => IO[Unit]): IO[Unit] = {
  // the set of metrics to check
  val specs = List(
    HttpMetrics.ServerRequestDuration
  )

  MetricsTestkit.inMemory[IO]().use { testkit =>
    testkit.meterProvider.get("meter").flatMap { meter =>
      for {
        // run a scenario to generate metrics 
        _       <- scenario(meter)
        // collect metrics
        metrics <- testkit.collectMetrics[Metric]
        // ensure the expected metrics exist and match the spec
      } yield specs.foreach(spec => specTest(metrics, spec))
    }
  }
}

def specTest(metrics: List[Metric], spec: MetricSpec): Unit = {
  val metric = metrics.find(_.name == spec.name)
  assert(
    metric.isDefined,
    s"${spec.name} metric is missing. Available [${metrics.map(_.name).mkString(", ")}]",
  )

  val clue = s"[${spec.name}] has a mismatched property"

  metric.foreach { md =>
    assert(md.name == spec.name, clue)
    assert(md.description == Some(spec.description), clue)
    assert(md.unit == Some(spec.unit), clue)

    val required = spec.attributeSpecs
      .filter(_.requirement.level == Requirement.Level.Required)
      .map(_.key)
      .toSet

    val current = md.data.points.toVector
      .flatMap(_.attributes.map(_.key))
      .filter(key => required.contains(key))
      .toSet

    assert(current == required, clue)
  }
}

Best Practices

1

Prefer stable conventions

Use stable modules (otel4s-semconv and otel4s-semconv-metrics) by default. Only use experimental modules when you need incubating conventions.
2

Use typesafe attributes

Always use the generated attribute keys instead of raw strings to ensure type safety and consistency.
3

Follow naming conventions

Stick to the OpenTelemetry semantic conventions for attribute and metric names to ensure compatibility across services.
4

Validate in tests

Use metric specs in your tests to ensure your instrumentation follows the conventions correctly.
5

Document custom attributes

If you need custom attributes not covered by semantic conventions, document them clearly in your codebase.

Common Patterns

Combining Semantic and Custom Attributes

import org.typelevel.otel4s.Attributes
import org.typelevel.otel4s.semconv.attributes.HttpAttributes

val combinedAttrs = Attributes(
  HttpAttributes.HttpRequestMethod("GET"),
  HttpAttributes.HttpResponseStatusCode(200),
  // Custom application-specific attribute
  Attribute("app.tenant_id", "tenant-123"),
  Attribute("app.feature_flag", "beta-enabled")
)

Using Semantic Conventions with Spans

import org.typelevel.otel4s.trace.Tracer
import org.typelevel.otel4s.semconv.attributes.HttpAttributes

def handleHttpRequest[F[_]: Tracer](method: String, statusCode: Int): F[Unit] =
  Tracer[F]
    .span(
      "http.request",
      HttpAttributes.HttpRequestMethod(method),
      HttpAttributes.HttpResponseStatusCode(statusCode)
    )
    .use { span =>
      // Your request handling logic
      ???
    }

Build docs developers (and LLMs) love