From aaf1947b32d5ae41138cc0743513e23527c0db23 Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Mon, 13 Jul 2026 21:06:06 +0100 Subject: [PATCH] docs: turn remaining testkit pages into reference pages --- docs/how-to-testkit/index.md | 12 +- .../test-logs-emitted-by-your-code.md | 2 +- .../test-metrics-emitted-by-your-code.md | 2 +- .../test-traces-emitted-by-your-code.md | 2 +- docs/oteljava/testkit-logs.md | 134 +++---------- docs/oteljava/testkit-metrics.md | 181 ++++++++---------- docs/oteljava/testkit-traces.md | 122 ++++-------- docs/oteljava/testkit.md | 6 +- 8 files changed, 149 insertions(+), 312 deletions(-) diff --git a/docs/how-to-testkit/index.md b/docs/how-to-testkit/index.md index 2192ea82b..a3c87fbbe 100644 --- a/docs/how-to-testkit/index.md +++ b/docs/how-to-testkit/index.md @@ -14,9 +14,9 @@ telemetry it emits. - For an overview of the available signal-specific and combined testkits, see [Testkit](../oteljava/testkit.md). -- For the complete metrics expectation API, see - [Testkit | Metrics](../oteljava/testkit-metrics.md). -- For the complete traces expectation API, see - [Testkit | Traces](../oteljava/testkit-traces.md). -- For the complete logs expectation API, see - [Testkit | Logs](../oteljava/testkit-logs.md). +- For metrics expectation details, see + [Metrics testkit reference](../oteljava/testkit-metrics.md). +- For traces expectation details, see + [Traces testkit reference](../oteljava/testkit-traces.md). +- For logs expectation details, see + [Logs testkit reference](../oteljava/testkit-logs.md). diff --git a/docs/how-to-testkit/test-logs-emitted-by-your-code.md b/docs/how-to-testkit/test-logs-emitted-by-your-code.md index 4af736415..2d1e01518 100644 --- a/docs/how-to-testkit/test-logs-emitted-by-your-code.md +++ b/docs/how-to-testkit/test-logs-emitted-by-your-code.md @@ -107,4 +107,4 @@ def test: IO[Unit] = ## What's next For structured bodies, trace correlation, scope and resource matching, timestamps, and custom predicates, see -[Testkit | Logs](../oteljava/testkit-logs.md). +[Logs testkit reference](../oteljava/testkit-logs.md). diff --git a/docs/how-to-testkit/test-metrics-emitted-by-your-code.md b/docs/how-to-testkit/test-metrics-emitted-by-your-code.md index 4fc8d70ab..9f41980e7 100644 --- a/docs/how-to-testkit/test-metrics-emitted-by-your-code.md +++ b/docs/how-to-testkit/test-metrics-emitted-by-your-code.md @@ -101,4 +101,4 @@ def test: IO[Unit] = ## What's next For point attributes, histograms, collection-wide constraints, and custom numeric comparison, see -[Testkit | Metrics](../oteljava/testkit-metrics.md). +[Metrics testkit reference](../oteljava/testkit-metrics.md). diff --git a/docs/how-to-testkit/test-traces-emitted-by-your-code.md b/docs/how-to-testkit/test-traces-emitted-by-your-code.md index f5ed259c6..58cfa6b4f 100644 --- a/docs/how-to-testkit/test-traces-emitted-by-your-code.md +++ b/docs/how-to-testkit/test-traces-emitted-by-your-code.md @@ -100,4 +100,4 @@ timestamps and attributes, are ignored. ## What's next For flat span matching, attributes, events, links, status, and custom predicates, see -[Testkit | Traces](../oteljava/testkit-traces.md). +[Traces testkit reference](../oteljava/testkit-traces.md). diff --git a/docs/oteljava/testkit-logs.md b/docs/oteljava/testkit-logs.md index 75a1841cd..f9bd4fbf1 100644 --- a/docs/oteljava/testkit-logs.md +++ b/docs/oteljava/testkit-logs.md @@ -1,105 +1,42 @@ -# Testkit | Logs +# Logs testkit reference -The logs testkit provides a partial-matching expectation API for OpenTelemetry Java `LogRecordData`. +The logs testkit provides in-memory log collection and partial-matching expectation APIs for OpenTelemetry Java +`LogRecordData`. -This is useful in tests because exported log records contain more than just a message: +Use this page as an API reference for `LogsTestkit`, `LogRecordExpectation`, and `LogRecordExpectations`. -- structured body values -- severity and severity text -- trace and span correlation -- attributes -- instrumentation scope -- telemetry resource -- source and observed timestamps +For an end-to-end test setup, see +[Test logs emitted by your code](../how-to-testkit/test-logs-emitted-by-your-code.md). +For the overview of all signal testkits, see [Testkit](testkit.md). -The expectation API lets you match only the parts of a log record that matter for the test while still preserving -access to the raw SDK model when needed. +The examples below assume these imports: -## Getting started - -@:select(build-tool) - -@:choice(sbt) - -Add settings to `build.sbt`: - -```scala -libraryDependencies ++= Seq( - "org.typelevel" %% "otel4s-oteljava-testkit" % "@VERSION@" % Test, -) -``` - -@:choice(scala-cli) - -Add directives to the `*.scala` file: - -```scala -//> using test.dep "org.typelevel::otel4s-oteljava-testkit:@VERSION@" +```scala mdoc:silent +import io.opentelemetry.sdk.logs.data.LogRecordData +import org.typelevel.otel4s.{AnyValue, Attribute} +import org.typelevel.otel4s.logs.Severity +import org.typelevel.otel4s.oteljava.testkit.AttributesExpectation +import org.typelevel.otel4s.oteljava.testkit.{ + InstrumentationScopeExpectation, + TelemetryResourceExpectation +} +import org.typelevel.otel4s.oteljava.testkit.logs._ ``` -@:@ - -## Basic flow +## `LogsTestkit` -The usual flow is: +`LogsTestkit` is the signal-specific in-memory backend for logs. -1. run your program against `LogsTestkit` or `OtelJavaTestkit` -2. collect finished logs as OpenTelemetry Java `LogRecordData` -3. build `LogRecordExpectation` values -4. check them with `LogRecordExpectations` +| Member | Purpose | +| ------ | ------- | +| `LogsTestkit.inMemory[F]()` | Creates a `Resource[F, LogsTestkit[F]]` backed by an in-memory log exporter. | +| `LogsTestkit.builder[F]` | Creates a builder for customizing the underlying `SdkLoggerProviderBuilder`. | +| `loggerProvider` | The otel4s `LoggerProvider[F, Context]` used by code under test. | +| `finishedLogs` | Returns `List[LogRecordData]` from the in-memory log exporter. | +| `resetLogs` | Clears the in-memory log exporter. | -```scala mdoc:silent:reset -import cats.effect.IO -import io.opentelemetry.sdk.logs.data.LogRecordData -import org.typelevel.otel4s.AnyValue -import org.typelevel.otel4s.Attribute -import org.typelevel.otel4s.logs.{LoggerProvider, Severity} -import org.typelevel.otel4s.oteljava.context.Context -import org.typelevel.otel4s.oteljava.testkit.logs._ - -def program(loggerProvider: LoggerProvider[IO, Context]): IO[Unit] = - for { - logger <- loggerProvider.logger("service").withVersion("1.0.0").get - _ <- logger.logRecordBuilder - .withSeverity(Severity.error) - .withSeverityText("ERROR") - .withBody(AnyValue.string("request failed")) - .addAttributes( - Attribute("http.route", "/users"), - Attribute("error.type", "timeout") - ) - .emit - } yield () - -def assertExpected( - records: List[LogRecordData], - expected: LogRecordExpectation* -): Unit = - LogRecordExpectations.checkAllDistinct(records, expected: _*) match { - case Right(_) => - () - case Left(mismatches) => - sys.error(LogRecordExpectations.format(mismatches)) - } - -def test: IO[Unit] = - LogsTestkit.inMemory[IO]().use { testkit => - for { - _ <- program(testkit.loggerProvider) - records <- testkit.finishedLogs - } yield assertExpected( - records, - LogRecordExpectation - .message("request failed") - .severity(Severity.error) - .severityText("ERROR") - .attributesSubset( - Attribute("http.route", "/users"), - Attribute("error.type", "timeout") - ) - ) - } -``` +`OtelJavaTestkit` also exposes `loggerProvider`, `finishedLogs`, and `resetLogs` when a test needs logs together with +metrics or traces. ## Partial matching @@ -114,9 +51,6 @@ This means: For example: ```scala mdoc:silent -import org.typelevel.otel4s.logs.Severity -import org.typelevel.otel4s.oteljava.testkit.logs.LogRecordExpectation - LogRecordExpectation.message("request failed") LogRecordExpectation @@ -135,8 +69,6 @@ OpenTelemetry log body is not always a string. The testkit therefore exposes two - `body(AnyValue)` for exact raw body matching ```scala mdoc:silent -import org.typelevel.otel4s.AnyValue - LogRecordExpectation.message("request failed") LogRecordExpectation.body( @@ -160,8 +92,6 @@ Severity matching is intentionally shallow: - `severityText(...)` ```scala mdoc:silent -import org.typelevel.otel4s.logs.Severity - LogRecordExpectation .message("request failed") .severity(Severity.error) @@ -206,9 +136,6 @@ Log attributes use the same helpers as the metrics and traces expectation APIs: - `attributesEmpty` ```scala mdoc:silent -import org.typelevel.otel4s.Attribute -import org.typelevel.otel4s.oteljava.testkit.AttributesExpectation - LogRecordExpectation .message("request failed") .attributesExact( @@ -232,9 +159,6 @@ LogRecordExpectation Instrumentation scope and telemetry resource can be asserted directly: ```scala mdoc:silent -import org.typelevel.otel4s.Attribute -import org.typelevel.otel4s.oteljava.testkit.{InstrumentationScopeExpectation, TelemetryResourceExpectation} - LogRecordExpectation .message("request failed") .scope( diff --git a/docs/oteljava/testkit-metrics.md b/docs/oteljava/testkit-metrics.md index 42ab508f6..f854fbf01 100644 --- a/docs/oteljava/testkit-metrics.md +++ b/docs/oteljava/testkit-metrics.md @@ -1,104 +1,45 @@ -# Testkit | Metrics +# Metrics testkit reference -The metrics testkit provides a partial-matching expectation API for OpenTelemetry Java `MetricData`. +The metrics testkit provides in-memory metric collection and partial-matching expectation APIs for OpenTelemetry Java +`MetricData`. -This is useful in tests because metric data contains much more than the values you usually care about: -instrumentation scope, telemetry resource, point attributes, collection timestamps, exemplars, histogram buckets, -and so on. +Use this page as an API reference for `MetricsTestkit`, `MetricExpectation`, `PointExpectation`, +`PointSetExpectation`, `MetricExpectations`, and `NumberComparison`. -The expectation API lets you assert only the relevant parts of a metric while still preserving detail when you need -it. Metric point matching is expressed at the collection level, so a single metric expectation can accumulate -multiple point constraints. - -This guide documents the expectation-first testing style for metrics. +For an end-to-end test setup, see +[Test metrics emitted by your code](../how-to-testkit/test-metrics-emitted-by-your-code.md). For the overview of all signal testkits, see [Testkit](testkit.md). -## Getting started - -@:select(build-tool) - -@:choice(sbt) - -Add settings to the `build.sbt`: - -```scala -libraryDependencies ++= Seq( - "org.typelevel" %% "otel4s-oteljava-testkit" % "@VERSION@" % Test, -) -``` - -@:choice(scala-cli) - -Add directives to the `*.scala` file: - -```scala -//> using test.dep "org.typelevel::otel4s-oteljava-testkit:@VERSION@" -``` - -@:@ - -## Basic flow - -The usual flow is: - -1. Run your program against `MetricsTestkit` or `OtelJavaTestkit` -2. Collect metrics as `MetricData` -3. Build `MetricExpectation` values -4. Check them with `MetricExpectations.checkAll` or `MetricExpectations.checkAllDistinct` +The examples below assume these imports: ```scala mdoc:silent -import cats.effect.IO import io.opentelemetry.sdk.metrics.data.MetricData -import org.typelevel.otel4s.metrics.MeterProvider +import org.typelevel.otel4s.{Attribute, Attributes} +import org.typelevel.otel4s.oteljava.testkit.AttributesExpectation +import org.typelevel.otel4s.oteljava.testkit.{ + InstrumentationScopeExpectation, + TelemetryResourceExpectation +} import org.typelevel.otel4s.oteljava.testkit.metrics._ +``` -def program(meterProvider: MeterProvider[IO]): IO[Unit] = - for { - meter <- meterProvider.get("service") - counter <- meter.counter[Long]("service.counter").create - _ <- counter.inc() - gauge <- meter.gauge[Long]("service.gauge").create - _ <- gauge.record(42L) - } yield () - -def assertExpected(metrics: List[MetricData], expected: MetricExpectation*): Unit = - MetricExpectations.checkAllDistinct(metrics, expected: _*) match { - case Right(_) => - () - case Left(mismatches) => - // or use an assert function from the testing framework here - sys.error(MetricExpectations.format(mismatches)) - } +## `MetricsTestkit` -def test: IO[Unit] = - MetricsTestkit.inMemory[IO]().use { testkit => - for { - _ <- program(testkit.meterProvider) - metrics <- testkit.collectMetrics - } yield assertExpected( - metrics, - MetricExpectation.sum[Long]("service.counter").value(1L), - MetricExpectation.gauge[Long]("service.gauge").value(42L) - ) - } -``` +`MetricsTestkit` is the signal-specific in-memory backend for metrics. -`checkAll(...)` is non-consuming: each expectation is checked independently against the full collected metric list. -If you need to ensure that repeated expectations match different collected metrics, use -`MetricExpectations.checkAllDistinct(...)` instead. +| Member | Purpose | +| ------ | ------- | +| `MetricsTestkit.inMemory[F]()` | Creates a `Resource[F, MetricsTestkit[F]]` backed by an in-memory metric reader. | +| `MetricsTestkit.builder[F]` | Creates a builder for customizing the underlying `SdkMeterProviderBuilder`. | +| `meterProvider` | The otel4s `MeterProvider[F]` used by code under test. | +| `collectMetrics` | Collects and returns `List[MetricData]`. Metrics are recollected on each invocation. | -```scala mdoc:silent -def checkAllDistinct(metrics: List[MetricData]) = - MetricExpectations.checkAllDistinct( - metrics, - MetricExpectation.sum[Long]("service.counter").value(1L), - MetricExpectation.sum[Long]("service.counter").value(1L) - ) -``` +`OtelJavaTestkit` also exposes `meterProvider` and `collectMetrics` when a test needs metrics together with traces or +logs. ## Partial matching -All expectations are partial. +Metric expectations are partial. This means: - unspecified properties are ignored @@ -142,8 +83,6 @@ MetricExpectation.sum[Long]("service.requests").value(1L) If you also care about point attributes, there is a shorthand for exact point matching: ```scala mdoc:silent -import org.typelevel.otel4s.Attribute - MetricExpectation .sum[Long]("service.requests") .value(1L, Attribute("http.method", "GET")) @@ -176,10 +115,6 @@ Point attributes can be matched in three ways: Both `attributesExact(...)` and `attributesSubset(...)` support `Attributes` and varargs of `Attribute[_]`. ```scala mdoc:silent -import org.typelevel.otel4s.Attribute -import org.typelevel.otel4s.Attributes -import org.typelevel.otel4s.oteljava.testkit.AttributesExpectation - PointExpectation .numeric(1L) .attributesExact(Attribute("http.method", "GET")) @@ -206,6 +141,32 @@ Empty attributes can be matched with: PointExpectation.numeric(1L).attributesEmpty ``` +## Metric metadata and predicates + +Metric expectations can assert metadata on the `MetricData` itself: + +- `description(...)` +- `unit(...)` +- `scopeName(...)` +- `where(...)` + +```scala mdoc:silent +MetricExpectation + .sum[Long]("service.requests") + .description("Total requests") + .unit("1") + .scopeName("service") + +MetricExpectation + .name("service.requests") + .where("metric should have at least one point") { metric => + !metric.getData.getPoints.isEmpty + } +``` + +Use `where(...)` when the expectation API does not expose the field you need, or when a test needs a custom invariant +over the raw `MetricData`. + ## Scope and resource expectations Metric expectations can also assert the instrumentation scope and telemetry resource. @@ -213,12 +174,6 @@ Metric expectations can also assert the instrumentation scope and telemetry reso This is useful when you want tests to preserve the same level of detail as raw `MetricData`. ```scala mdoc:silent -import org.typelevel.otel4s.Attribute -import org.typelevel.otel4s.oteljava.testkit.{ - InstrumentationScopeExpectation, - TelemetryResourceExpectation -} - MetricExpectation .sum[Long]("service.requests") .points( @@ -532,10 +487,7 @@ MetricExpectation The API is framework-agnostic, so it does not provide assertions directly. Instead, it returns structured mismatches and a formatter. -```scala mdoc:silent:reset -import io.opentelemetry.sdk.metrics.data.MetricData -import org.typelevel.otel4s.oteljava.testkit.metrics.{MetricExpectation, MetricExpectations} - +```scala mdoc:silent def assertExpected(metrics: List[MetricData], expected: MetricExpectation*): Unit = MetricExpectations.checkAll(metrics, expected: _*) match { case Right(_) => @@ -555,14 +507,33 @@ There are two main failure cases: This is especially helpful when a metric name is correct but, for example, one scope attribute or one point attribute is wrong. +## Top-level matching + +Use `MetricExpectations` to match expectations against a collected list of exported metrics. + +Available helpers: + +- `exists` +- `find` +- `check` +- `checkAll` +- `checkAllDistinct` +- `missing` +- `missingDistinct` +- `allMatch` +- `allMatchDistinct` +- `format` + +`checkAll(...)` is non-consuming: the same exported metric may satisfy multiple expectations. + +`checkAllDistinct(...)` enforces distinct assignment and is the safer default when repeated expectations should match +different collected metrics. + ## Clues Metric, point, and point-set expectations support optional clues. ```scala mdoc:silent -import org.typelevel.otel4s.oteljava.testkit.metrics.{PointExpectation, PointSetExpectation} -import org.typelevel.otel4s.Attribute - MetricExpectation .sum[Long]("service.requests") .clue("the request counter must be emitted") @@ -596,7 +567,7 @@ This applies to: If needed, you can override the default `Double` comparison implicitly in a test suite: -```scala mdoc:silent +```scala mdoc:silent:reset import org.typelevel.otel4s.oteljava.testkit.metrics.NumberComparison implicit val cmp: NumberComparison[Double] = diff --git a/docs/oteljava/testkit-traces.md b/docs/oteljava/testkit-traces.md index 349acb8fc..f3bc51162 100644 --- a/docs/oteljava/testkit-traces.md +++ b/docs/oteljava/testkit-traces.md @@ -1,96 +1,44 @@ -# Testkit | Traces +# Traces testkit reference -The traces testkit provides structural and span-level expectation APIs for OpenTelemetry Java `SpanData`. +The traces testkit provides in-memory span collection and structural expectation APIs for OpenTelemetry Java +`SpanData`. -This is useful in tests because span data contains much more than just names and parent-child relationships: +Use this page as an API reference for `TracesTestkit`, `SpanExpectation`, `TraceExpectation`, +`TraceForestExpectation`, `SpanExpectations`, `TraceExpectations`, and the event, link, status, and span-context +expectation types. -- timestamps -- span kind and status -- parent context and span context -- attributes -- events -- links -- instrumentation scope -- telemetry resource +For an end-to-end test setup, see +[Test traces emitted by your code](../how-to-testkit/test-traces-emitted-by-your-code.md). +For the overview of all signal testkits, see [Testkit](testkit.md). -The expectation API lets you assert only the span properties and tree structure that matter for the test. +The examples below assume these imports: -## Getting started - -@:select(build-tool) - -@:choice(sbt) - -Add settings to `build.sbt`: - -```scala -libraryDependencies ++= Seq( - "org.typelevel" %% "otel4s-oteljava-testkit" % "@VERSION@" % Test, -) -``` - -@:choice(scala-cli) - -Add directives to the `*.scala` file: - -```scala -//> using test.dep "org.typelevel::otel4s-oteljava-testkit:@VERSION@" +```scala mdoc:silent +import io.opentelemetry.sdk.trace.data.SpanData +import org.typelevel.otel4s.Attribute +import org.typelevel.otel4s.oteljava.testkit.AttributesExpectation +import org.typelevel.otel4s.oteljava.testkit.{ + InstrumentationScopeExpectation, + TelemetryResourceExpectation +} +import org.typelevel.otel4s.oteljava.testkit.trace._ ``` -@:@ +## `TracesTestkit` -## Basic flow - -The usual flow is: - -1. run your program against `TracesTestkit` or `OtelJavaTestkit` -2. collect finished spans as OpenTelemetry Java `SpanData` -3. build `TraceExpectation` or `SpanExpectation` values -4. check them with `TraceExpectations` or `SpanExpectations` - -```scala mdoc:silent:reset -import cats.effect.IO -import io.opentelemetry.sdk.trace.data.SpanData -import org.typelevel.otel4s.oteljava.testkit.trace._ -import org.typelevel.otel4s.trace.TracerProvider - -def program(tracerProvider: TracerProvider[IO]): IO[Unit] = - for { - tracer <- tracerProvider.get("service") - _ <- tracer.span("app.span").surround { - tracer.span("app.nested.1").surround(IO.unit) >> - tracer.span("app.nested.2").surround(IO.unit) - } - } yield () - -def assertExpected( - spans: List[SpanData], - expected: TraceForestExpectation -): Unit = - TraceExpectations.check(spans, expected) match { - case Right(_) => - () - case Left(mismatches) => - sys.error(TraceExpectations.format(mismatches)) - } +`TracesTestkit` is the signal-specific in-memory backend for traces. -def test: IO[Unit] = - TracesTestkit.inMemory[IO]().use { testkit => - val expected = - TraceForestExpectation.unordered( - TraceExpectation.unordered( - SpanExpectation.name("app.span").noParentSpanContext, - TraceExpectation.leaf(SpanExpectation.name("app.nested.1")), - TraceExpectation.leaf(SpanExpectation.name("app.nested.2")) - ) - ) +| Member | Purpose | +| ------ | ------- | +| `TracesTestkit.inMemory[F]()` | Creates a `Resource[F, TracesTestkit[F]]` backed by an in-memory span exporter. | +| `TracesTestkit.builder[F]` | Creates a builder for customizing the underlying `SdkTracerProviderBuilder` and propagators. | +| `tracerProvider` | The otel4s `TracerProvider[F]` used by code under test. | +| `finishedSpans` | Returns `List[SpanData]` from the in-memory span exporter. | +| `resetSpans` | Clears the in-memory span exporter. | +| `propagators` | The context propagators used by the tracer provider. | +| `localContext` | The `LocalContext[F]` used by the tracer provider. | - for { - _ <- program(testkit.tracerProvider) - spans <- testkit.finishedSpans - } yield assertExpected(spans, expected) - } -``` +`OtelJavaTestkit` also exposes the same trace members when a test needs traces together with metrics or logs. ## Flat vs structural matching @@ -251,9 +199,6 @@ Span attributes follow the same conventions as the metrics testkit: - `attributesEmpty` ```scala mdoc:silent -import org.typelevel.otel4s.Attribute -import org.typelevel.otel4s.oteljava.testkit.AttributesExpectation - SpanExpectation .name("request") .attributesExact( @@ -331,12 +276,9 @@ SpanExpectation.name("request").spanContextExact(spanContext) ### Scope and resource -Instrumentation scope and telemetry resource are matched the same way as in the metrics guide: +Instrumentation scope and telemetry resource are matched the same way as in the metrics testkit reference: ```scala mdoc:silent -import org.typelevel.otel4s.Attribute -import org.typelevel.otel4s.oteljava.testkit.{InstrumentationScopeExpectation, TelemetryResourceExpectation} - SpanExpectation .name("request") .scope( diff --git a/docs/oteljava/testkit.md b/docs/oteljava/testkit.md index 8f967aeb7..ea6a0c60c 100644 --- a/docs/oteljava/testkit.md +++ b/docs/oteljava/testkit.md @@ -31,9 +31,9 @@ specified are ignored. | Signal | SDK model | Main expectation APIs | API reference | |---------|-----------------|-----------------------------------------------------------------------------------------|---------------------------------------| -| Metrics | `MetricData` | `MetricExpectation`, `PointExpectation`, `PointSetExpectation`, `MetricExpectations` | [Testkit \| Metrics](testkit-metrics.md) | -| Traces | `SpanData` | `SpanExpectation`, `TraceExpectation`, `TraceForestExpectation`, `SpanExpectations`, `TraceExpectations` | [Testkit \| Traces](testkit-traces.md) | -| Logs | `LogRecordData` | `LogRecordExpectation`, `LogRecordExpectations` | [Testkit \| Logs](testkit-logs.md) | +| Metrics | `MetricData` | `MetricExpectation`, `PointExpectation`, `PointSetExpectation`, `MetricExpectations` | [Metrics testkit reference](testkit-metrics.md) | +| Traces | `SpanData` | `SpanExpectation`, `TraceExpectation`, `TraceForestExpectation`, `SpanExpectations`, `TraceExpectations` | [Traces testkit reference](testkit-traces.md) | +| Logs | `LogRecordData` | `LogRecordExpectation`, `LogRecordExpectations` | [Logs testkit reference](testkit-logs.md) | Each top-level expectation API has a `format` method for rendering structured mismatches: