This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/docs by this push:
     new c425cffed0 docs: Observability topic page + 9.5.0 release-notes entry 
(TODO-67)
c425cffed0 is described below

commit c425cffed0e1cad3734285e0ada764b7ee9bb24c
Author: James Bognar <[email protected]>
AuthorDate: Tue May 26 18:30:49 2026 -0400

    docs: Observability topic page + 9.5.0 release-notes entry (TODO-67)
    
    Co-authored-by: Cursor <[email protected]>
---
 pages/release-notes/9.5.0.md                   | 129 ++++++++++
 pages/topics/10.20g.RestServerObservability.md | 316 +++++++++++++++++++++++++
 sidebars.ts                                    |   5 +
 3 files changed, 450 insertions(+)

diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index d72d68df95..5e32179106 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -2291,6 +2291,44 @@ A new JUnit 5 extension and `@TestBean` annotation 
enable Spring-style test-time
 
 - **New constant.** `RestServerConstants.REQUEST_ID` (`"requestId"`) — the 
canonical request-attribute key used by `RequestIdFilter`.
 
+#### Observability — Micrometer + OpenTelemetry hooks (TODO-67)
+
+**Metrics and tracing are opt-in and disabled by default.** Same 
non-negotiable contract as Bean Validation (TODO-68): a fresh `RestContext` 
built with no `MetricsRecorder` / `TracerHook` bean registered never records a 
sample, never opens a span, and never adds a per-request cost — even when 
`io.micrometer:micrometer-core` and `io.opentelemetry:opentelemetry-api` are on 
the classpath. Opt in by registering a `@Bean MetricsRecorder` / `@Bean 
TracerHook` on the resource.
+
+##### New SPIs in `juneau-rest-server`
+
+- **`org.apache.juneau.rest.metrics.MetricsRecorder`** — 
`@FunctionalInterface` with a single method `record(String opName, String 
httpMethod, String uriTemplate, int statusCode, Duration elapsed, Throwable 
error)`. Called once per `@RestOp` invocation (happy path and exception path). 
The `uriTemplate` is the `@RestOp` path template (`/users/{id}`) — not the raw 
concrete URI — so tag cardinality stays bounded.
+- **`org.apache.juneau.rest.metrics.NoOpMetricsRecorder`** — process-wide 
singleton `INSTANCE`; resolved when no bean is registered. Drops events on the 
floor with zero allocations.
+- **`org.apache.juneau.rest.tracing.TracerHook`** — `@FunctionalInterface` 
with `Scope startSpan(RestRequest request)`. The framework opens the scope 
before parameter binding and always closes it in a `finally` block; the 
resolved HTTP status and any thrown throwable are delivered via 
`Scope.setStatusCode(int)` / `Scope.setError(Throwable)` before `Scope.close()`.
+- **`org.apache.juneau.rest.tracing.Scope`** — `AutoCloseable` with 
`setStatusCode(int)`, `setError(Throwable)`, and `close()`. The framework 
guarantees `close()` runs exactly once per call (including parameter-resolution 
failures and 5xx paths).
+- **`org.apache.juneau.rest.tracing.NoOpTracerHook`** — process-wide singleton 
`INSTANCE` (and `NoOpTracerHook.NoOpScope.INSTANCE`); resolved when no bean is 
registered.
+
+##### Where the hooks fire
+
+The hooks wrap the `@RestOp` handler invocation in 
`RestOpInvoker.invokeOp(...)` — they cover parameter binding, the user method 
body, and the framework's standard exception-mapping path. Pre/post-call 
lifecycle methods (`@RestPreCall` / `@RestPostCall`) are intentionally outside 
the observability boundary so the metric / span describes the user-facing 
operation only. Resolution goes through the `RestContext` bean store: the first 
`MetricsRecorder` / `TracerHook` bean wins; absent any re [...]
+
+##### Wiring
+
+```java
+@Rest
+public class MyResource extends BasicRestServlet {
+
+    @Bean
+    public MetricsRecorder metrics(MeterRegistry r) {
+        return new MicrometerMetricsRecorder(r);   // see 
juneau-rest-server-micrometer below
+    }
+
+    @Bean
+    public TracerHook tracer(OpenTelemetry otel) {
+        return new OtelTracerHook(otel);           // see 
juneau-rest-server-otel below
+    }
+
+    @RestGet("/users/{id}") public User get(@Path String id) { ... }
+}
+```
+
+See [REST Server — Observability (Micrometer + 
OpenTelemetry)](/docs/topics/RestServerObservability) for the full topic.
+
 #### Health Probe SPI + Resource (TODO-65)
 
 `juneau-rest-server` now includes a built-in probe SPI and aggregation 
resource under
@@ -3841,6 +3879,97 @@ Requests without an `id` are treated as JSON-RPC 
notifications: handlers run, ex
 
 See [juneau-rest-server-mcp](/docs/topics/JuneauRestServerMcpBasics) for the 
full topic.
 
+### juneau-rest-server-micrometer (new module)
+
+A new opt-in REST module, `juneau-rest-server-micrometer`, bridges the new 
`MetricsRecorder` SPI (see [juneau-rest-server](#juneau-rest-server)) into a 
Micrometer `MeterRegistry` so a Juneau REST service can drop into existing 
Prometheus / StatsD / JMX scrape pipelines with no hand-rolled instrumentation. 
Engine-agnostic POM stance (TODO-67 resolved decision #1, mirroring TODO-68 / 
TODO-78 / TODO-82 / TODO-83 / TODO-84): `io.micrometer:micrometer-core` is 
declared in `provided` scope on  [...]
+
+`mvn -pl juneau-rest/juneau-rest-server dependency:tree | grep -i micrometer` 
returns nothing — the containment requirement is verified at build time.
+
+#### New Classes
+
+- **`org.apache.juneau.rest.metrics.micrometer.MicrometerMetricsRecorder`** — 
`MetricsRecorder` implementation. Builds a Micrometer `Timer` named 
`http.server.requests` (overridable via the two-arg constructor) with four tags 
per Spring Boot's `WebMvcMetricsFilter` convention: `method` (uppercased HTTP 
method), `uri` (the `@RestOp` path template — bounded cardinality), `status` 
(HTTP response status), `exception` (the thrown exception's simple-name, or 
`None` on the happy path). Records  [...]
+
+#### Naming convention
+
+Spring Boot's `http.server.requests` + `{method, uri, status, exception}` was 
chosen (TODO-67 resolved decision #2) over the OTel-native 
`http.server.duration` shape because the wider Prometheus / Grafana 
scrape-config and dashboard ecosystem already standardizes on it. The 
OTel-native attribute names live in the sibling `juneau-rest-server-otel` 
module (see below).
+
+#### Wiring
+
+```java
+@Configuration
+public class ObservabilityConfig {
+    @Bean MeterRegistry registry() { return new 
PrometheusMeterRegistry(PrometheusConfig.DEFAULT); }
+    @Bean MetricsRecorder recorder(MeterRegistry r) { return new 
MicrometerMetricsRecorder(r); }
+}
+```
+
+#### Dependency
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-micrometer</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<dependency>
+    <groupId>io.micrometer</groupId>
+    <artifactId>micrometer-core</artifactId>
+    <version>1.13.6</version>          <!-- consumer-supplied; provided scope 
on juneau-rest-server-micrometer -->
+</dependency>
+```
+
+See [REST Server — Observability (Micrometer + 
OpenTelemetry)](/docs/topics/RestServerObservability) for the full topic.
+
+### juneau-rest-server-otel (new module)
+
+A new opt-in REST module, `juneau-rest-server-otel`, bridges the new 
`TracerHook` SPI (see [juneau-rest-server](#juneau-rest-server)) into an 
OpenTelemetry `Tracer` so each `@RestOp` invocation becomes a server span with 
HTTP semantic-convention attributes and W3C trace-context propagation. 
Engine-agnostic POM stance: `io.opentelemetry:opentelemetry-api` is declared in 
`provided` scope on the module's POM, so consumers explicitly pick the 
OpenTelemetry version they want.
+
+`mvn -pl juneau-rest/juneau-rest-server dependency:tree | grep -i 
opentelemetry` returns nothing — the containment requirement is verified at 
build time.
+
+#### New Classes
+
+- **`org.apache.juneau.rest.tracing.otel.OtelTracerHook`** — `TracerHook` 
implementation. Each request becomes a single `SpanKind.SERVER` span named 
after the HTTP method (`GET`, `POST`, …) per the OpenTelemetry HTTP semantic 
conventions, with attributes `http.request.method`, 
`http.response.status_code`, and `http.route` (the `@RestOp` path template, 
bounded cardinality). On the exception path, the throwable is recorded via 
`Span.recordException(...)`, the span status is set to `StatusC [...]
+- **`org.apache.juneau.rest.tracing.otel.RestRequestTextMapGetter`** — 
`TextMapGetter<RestRequest>` for OpenTelemetry context extraction. Reads the 
standard W3C `traceparent` / `tracestate` headers (and any other 
propagator-defined headers, e.g. baggage) from the incoming request so the 
server span is created as a child of the caller's trace.
+
+#### Attribute naming convention
+
+OpenTelemetry HTTP semantic conventions (`http.request.method`, 
`http.response.status_code`, `http.route`) were chosen (TODO-67 resolved 
decision #2) for the tracer hook to keep the spans interoperable with any OTel 
collector / backend. The Spring-Boot-style `http.server.requests` metric shape 
lives in the sibling `juneau-rest-server-micrometer` module (see above).
+
+#### Wiring
+
+```java
+@Configuration
+public class ObservabilityConfig {
+
+    // Option A: rely on the JVM-wide GlobalOpenTelemetry (recommended).
+    @Bean TracerHook tracer() { return new OtelTracerHook(); }
+
+    // Option B: pass a specific OpenTelemetry instance (e.g. for tests).
+    @Bean TracerHook tracer(OpenTelemetry otel) { return new 
OtelTracerHook(otel); }
+}
+```
+
+#### W3C trace-context propagation
+
+Incoming `traceparent` / `tracestate` request headers are extracted via the 
configured `TextMapPropagator` (default: `W3CTraceContextPropagator`) so the 
server span continues a caller-supplied distributed trace. Downstream HTTP 
calls made from inside the handler pick up the active span's context 
automatically when the outbound HTTP client honors the OTel `Context.current()` 
(the standard OTel client instrumentations do).
+
+#### Dependency
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-otel</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<dependency>
+    <groupId>io.opentelemetry</groupId>
+    <artifactId>opentelemetry-api</artifactId>
+    <version>1.43.0</version>          <!-- consumer-supplied; provided scope 
on juneau-rest-server-otel -->
+</dependency>
+```
+
+See [REST Server — Observability (Micrometer + 
OpenTelemetry)](/docs/topics/RestServerObservability) for the full topic.
+
 ### juneau-rest-server-view-jsp (new module)
 
 A new opt-in REST module, `juneau-rest-server-view-jsp`, adds JSP 
view-rendering to `juneau-rest-server` without bleeding the JSP-engine 
dependency (Apache Jasper) into the core. The new `View` interface (see 
[juneau-rest-server](#juneau-rest-server)) lives in core; this module ships the 
JSP-specific implementation. Engine-agnostic POM stance: the bridge module 
declares the JSP API + JSTL impl in `provided` scope only — consumers add the 
engine matching their container (Jetty 12 EE11's ` [...]
diff --git a/pages/topics/10.20g.RestServerObservability.md 
b/pages/topics/10.20g.RestServerObservability.md
new file mode 100644
index 0000000000..40671d97d4
--- /dev/null
+++ b/pages/topics/10.20g.RestServerObservability.md
@@ -0,0 +1,316 @@
+---
+title: "Observability — Micrometer + OpenTelemetry"
+slug: RestServerObservability
+---
+
+**Observability is opt-in and disabled by default.** A fresh `RestContext` 
built with no `MetricsRecorder` or `TracerHook` bean registered never records a 
metric sample, never opens a span, and never adds a per-request cost — even 
when `io.micrometer:micrometer-core` and `io.opentelemetry:opentelemetry-api` 
are sitting on the classpath. Opt in by registering a `@Bean MetricsRecorder` 
and / or `@Bean TracerHook` on the resource. Same non-negotiable contract as 
[Jakarta Bean Validation](/d [...]
+
+This is a deliberate departure from Spring Boot's auto-configuration, where 
`WebMvcMetricsFilter` is wired up at startup and a `MeterRegistry` bean is 
enough to turn on per-request metrics. Juneau's contract is the opposite: zero 
classpath cost, zero per-request cost, and zero startup cost until the user 
explicitly opts in.
+
+## Motivation
+
+Juneau REST already tracks per-method execution statistics via 
`MethodExecStats` / `RestContextStats`. The data is there — only the wiring to 
the dominant observability stacks (Prometheus / Grafana via Micrometer; 
distributed tracing via OpenTelemetry) was missing. The new SPIs 
(`MetricsRecorder`, `TracerHook`) plus two opt-in bridge modules 
(`juneau-rest-server-micrometer`, `juneau-rest-server-otel`) close that gap 
without dragging either runtime into the core.
+
+End-state developer experience:
+
+```java
+// pom: add juneau-rest-server-micrometer + your preferred MeterRegistry (e.g. 
micrometer-registry-prometheus).
+@Configuration
+public class ObservabilityConfig {
+    @Bean MeterRegistry registry() { return new 
PrometheusMeterRegistry(PrometheusConfig.DEFAULT); }
+    @Bean MetricsRecorder recorder(MeterRegistry r) { return new 
MicrometerMetricsRecorder(r); }
+}
+// → Each @RestOp call records a Timer sample on the registry; scrape via 
/actuator/prometheus.
+
+// pom: add juneau-rest-server-otel + OTel SDK.
+@Bean OpenTelemetry otel() { return GlobalOpenTelemetry.get(); }
+@Bean TracerHook hook(OpenTelemetry otel) { return new OtelTracerHook(otel); }
+// → Each @RestOp call becomes a SERVER span; incoming traceparent continues 
the upstream trace.
+```
+
+## The off-by-default contract
+
+The off-by-default contract has six tangible guarantees:
+
+1. **Cold-start default.** A fresh `RestContext` built with no 
`MetricsRecorder` / `TracerHook` bean registered never resolves a non-`NoOp` 
recorder or tracer. The framework looks up the SPI through the `RestContext`'s 
`BeanStore` and falls back to `NoOpMetricsRecorder.INSTANCE` / 
`NoOpTracerHook.INSTANCE` — both stateless singletons whose `record(...)` / 
`startSpan(...)` methods short-circuit immediately.
+2. **Per-resource granularity.** A `@Bean MetricsRecorder` on one `@Rest` 
class never opts in a sibling resource, an unrelated resource, or the global 
JVM. Bean resolution is scoped to the resource's `BeanStore`.
+3. **No global "on" switch.** There is no 
`RestContext.Builder.observability(true)`, no 
`juneau.observability.enabled=true` system property, and no resource-level 
boolean attribute that enables observability across every operation in a class. 
Bean registration is the only opt-in surface.
+4. **No silent enablement via classpath presence.** Even if `micrometer-core` 
and `opentelemetry-api` are on the classpath, no metric is recorded and no span 
is opened unless the user has registered a `@Bean MetricsRecorder` / `@Bean 
TracerHook`.
+5. **Lifecycle hooks are outside the observability boundary.** `@RestPreCall` 
and `@RestPostCall` lifecycle methods are not wrapped — only the `@RestOp` 
handler invocation (the user-facing operation) is. This keeps the metric / span 
scoped to the call as the API consumer perceives it.
+6. **Hot-path overhead is short-circuited.** The 
`NoOpMetricsRecorder.record(...)` and `NoOpTracerHook.NoOpScope.close()` 
methods compile to empty bodies, so the `RestOpInvoker` observability block 
costs at most one `BeanStore.getBean(...)` lookup plus two no-op virtual calls 
per request when nothing is wired up.
+
+## SPIs in `juneau-rest-server`
+
+Two functional interfaces live in core `juneau-rest-server`. They're stable 
enough for users to implement directly — the Micrometer / OpenTelemetry bridges 
shipped in the sibling sub-modules are not the only valid implementations.
+
+### `MetricsRecorder`
+
+```java
+@FunctionalInterface
+public interface MetricsRecorder {
+    void record(String opName, String httpMethod, String uriTemplate,
+                int statusCode, Duration elapsed, Throwable error);
+}
+```
+
+Called exactly once per `@RestOp` invocation (happy path and exception path). 
Arguments:
+
+| Field | Meaning |
+|---|---|
+| `opName` | Fully-qualified Java method name of the handler (e.g. 
`com.acme.MyResource.findUser(java.lang.String)`). |
+| `httpMethod` | Uppercased HTTP method (e.g. `GET`, `POST`). |
+| `uriTemplate` | The `@RestOp` path template (e.g. `/users/{id}`) — **not** 
the raw concrete URI. Using the template keeps metric / tag cardinality 
bounded; raw URIs (`/users/123`, `/users/124`, …) would explode the registry's 
series count. Empty string when the operation has no path pattern. |
+| `statusCode` | HTTP response status as resolved by the framework after the 
handler runs (200 on success; 4xx / 5xx on the exception path). |
+| `elapsed` | Wall-clock duration from just-before parameter binding to 
just-after framework exception mapping. |
+| `error` | The thrown throwable on the exception path, or `null` on the happy 
path. |
+
+### `TracerHook`
+
+```java
+@FunctionalInterface
+public interface TracerHook {
+    Scope startSpan(RestRequest request);
+}
+
+public interface Scope extends AutoCloseable {
+    void setStatusCode(int statusCode);
+    void setError(Throwable error);
+    @Override void close();
+}
+```
+
+`startSpan` opens a per-request scope before parameter binding. The framework 
always closes the scope in a `finally` block, delivering the resolved HTTP 
status and any thrown throwable via `setStatusCode(int)` / 
`setError(Throwable)` first. `close()` runs exactly once per call (including 
parameter-resolution failures and 5xx paths) so consumers can safely end an 
OTel span or finalize an in-memory record inside `close()`.
+
+### `NoOp` defaults
+
+- `NoOpMetricsRecorder.INSTANCE` — drops events on the floor with zero 
allocations.
+- `NoOpTracerHook.INSTANCE` — returns `NoOpTracerHook.NoOpScope.INSTANCE`. All 
three `Scope` methods are empty.
+
+Both are process-wide singletons. The framework picks them up automatically 
when no `@Bean` of the corresponding type is registered.
+
+## Where the hooks fire
+
+The hooks wrap the handler invocation in `RestOpInvoker.invokeOp(...)`. The 
sequence per call:
+
+1. Resolve `MetricsRecorder` / `TracerHook` from the `RestContext`'s 
`BeanStore` (one lookup; falls back to the `NoOp` singletons).
+2. `tracer.startSpan(request)` → `Scope`.
+3. `System.nanoTime()` → `startNanos`.
+4. Bind parameters, invoke the user method, run the framework's standard 
exception-mapping path.
+5. In a `finally` block:
+   - `elapsed` = `Duration.ofNanos(System.nanoTime() - startNanos)`.
+   - `status` = `res.getStatus()` (falling back to 200 / 500 if the response 
status is still 0).
+   - `scope.setStatusCode(status)`; `scope.setError(observed)` if a throwable 
was caught.
+   - `scope.close()`.
+   - `recorder.record(opName, httpMethod, uriTemplate, status, elapsed, 
observed)`.
+
+Pre / post-call lifecycle methods (`@RestPreCall` / `@RestPostCall`) are 
intentionally outside the observability boundary — the metric / span describes 
the user-facing operation only.
+
+## Micrometer bridge — `juneau-rest-server-micrometer`
+
+The Micrometer bridge ships a single `MetricsRecorder` implementation that 
records to any `MeterRegistry`:
+
+```java
+public class MicrometerMetricsRecorder implements MetricsRecorder {
+    public MicrometerMetricsRecorder(MeterRegistry registry) { ... }
+    public MicrometerMetricsRecorder(MeterRegistry registry, String timerName) 
{ ... }
+
+    @Override
+    public void record(String opName, String httpMethod, String uriTemplate,
+                       int statusCode, Duration elapsed, Throwable error) {
+        Timer.builder(timerName)        // default: "http.server.requests"
+            .tag("method", httpMethod)
+            .tag("uri",    uriTemplate) // bounded cardinality
+            .tag("status", Integer.toString(statusCode))
+            .tag("exception", error == null ? "None" : 
error.getClass().getSimpleName())
+            .register(registry)
+            .record(elapsed);
+    }
+}
+```
+
+### Metric shape
+
+The default tag set mirrors Spring Boot's `WebMvcMetricsFilter` convention 
(`http.server.requests` with `{method, uri, status, exception}`) so existing 
Prometheus / Grafana dashboards built for Spring Boot services keep working 
when those services are reimplemented on Juneau:
+
+```text
+http_server_requests_seconds_count{method="GET",uri="/users/{id}",status="200",exception="None"}
 1
+http_server_requests_seconds_sum  
{method="GET",uri="/users/{id}",status="200",exception="None"} 0.007
+```
+
+### Why Spring's naming convention (not OTel-native)
+
+OpenTelemetry's metric semantic conventions specify `http.server.duration` 
with `http.request.method` / `http.response.status_code` / `http.route` 
attribute names. We chose Spring Boot's older `http.server.requests` shape for 
the **Micrometer** bridge because the wider Prometheus / Grafana scrape-config 
and dashboard ecosystem already standardizes on it. The OTel-native attribute 
names live on the **tracing** side — see [OpenTelemetry bridge — 
`juneau-rest-server-otel`](#opentelemetry-br [...]
+
+To override the timer name, use the two-arg constructor:
+
+```java
+@Bean public MetricsRecorder recorder(MeterRegistry r) {
+    return new MicrometerMetricsRecorder(r, "myservice.http.timer");
+}
+```
+
+### Dependency
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-micrometer</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<dependency>
+    <groupId>io.micrometer</groupId>
+    <artifactId>micrometer-core</artifactId>
+    <version>1.13.6</version>          <!-- consumer-supplied; provided scope 
on the bridge module -->
+</dependency>
+<!-- pick the registry you want -->
+<dependency>
+    <groupId>io.micrometer</groupId>
+    <artifactId>micrometer-registry-prometheus</artifactId>
+    <version>1.13.6</version>
+</dependency>
+```
+
+The bridge declares `micrometer-core` in `provided` scope so the consumer 
picks the version. `mvn -pl juneau-rest/juneau-rest-server dependency:tree | 
grep -i micrometer` returns nothing — the containment requirement is verified 
at build time.
+
+## OpenTelemetry bridge — `juneau-rest-server-otel`
+
+The OpenTelemetry bridge ships a single `TracerHook` implementation that opens 
a span per request and propagates W3C trace context:
+
+```java
+public class OtelTracerHook implements TracerHook {
+
+    // Option A — use the JVM-wide GlobalOpenTelemetry (recommended).
+    public OtelTracerHook() { this(GlobalOpenTelemetry.get()); }
+
+    // Option B — explicit OpenTelemetry instance (e.g. multi-tenant or test).
+    public OtelTracerHook(OpenTelemetry otel) { ... }
+
+    // Option C — explicit Tracer + TextMapPropagator (typically for tests).
+    public OtelTracerHook(Tracer tracer, TextMapPropagator propagator) { ... }
+
+    @Override
+    public Scope startSpan(RestRequest request) {
+        // Extract incoming W3C traceparent / tracestate.
+        Context extracted = propagator.extract(Context.current(), request, 
RestRequestTextMapGetter.INSTANCE);
+
+        Span span = tracer.spanBuilder(request.getMethod())   // "GET", 
"POST", ...
+            .setSpanKind(SpanKind.SERVER)
+            .setParent(extracted)
+            .setAttribute("http.request.method", request.getMethod())
+            .setAttribute("http.route",          /* @RestOp path template */)
+            .startSpan();
+
+        return new OtelScope(span, span.makeCurrent());
+    }
+}
+```
+
+### Span shape
+
+Each `@RestOp` invocation becomes one span with:
+
+| Attribute | Meaning |
+|---|---|
+| Span kind | `SpanKind.SERVER` |
+| Span name | The uppercased HTTP method (`GET`, `POST`, …) per the OTel HTTP 
semantic conventions. |
+| `http.request.method` | Uppercased HTTP method. |
+| `http.response.status_code` | HTTP response status (set in `Scope.close()`, 
so error paths get the framework's resolved status). |
+| `http.route` | The `@RestOp` path template (e.g. `/users/{id}`) — bounded 
cardinality, same reasoning as the Micrometer `uri` tag. |
+
+On the exception path:
+
+- `Span.recordException(throwable)` adds an event carrying the stack trace.
+- The span status is set to `StatusCode.ERROR`.
+- An `exception.type` attribute carries the throwable's simple-name (e.g. 
`IllegalStateException`).
+
+A 2xx status leaves the span status at `StatusCode.UNSET` per the OTel HTTP 
semconv (servers should not pre-color successful spans as `OK`).
+
+### W3C trace-context propagation
+
+Incoming `traceparent` / `tracestate` headers are extracted via the configured 
`TextMapPropagator` (default: `W3CTraceContextPropagator`) so the server span 
continues a caller-supplied distributed trace:
+
+```text
+Request: traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
+
+Server span: traceId=0af7651916cd43dd8448eb211c80319c
+             parentSpanId=b7ad6b7169203331
+             spanId=<freshly-minted>
+```
+
+Downstream HTTP calls made from inside the handler pick up the active span's 
context automatically when the outbound HTTP client honors the OTel 
`Context.current()` (the standard OTel client instrumentations do).
+
+### Wiring
+
+```java
+@Configuration
+public class ObservabilityConfig {
+
+    // Option A: rely on GlobalOpenTelemetry (typically wired by the 
otel-java-agent).
+    @Bean TracerHook tracer() { return new OtelTracerHook(); }
+
+    // Option B: explicit instance (e.g. for tests).
+    @Bean TracerHook tracer(OpenTelemetry otel) { return new 
OtelTracerHook(otel); }
+}
+```
+
+### Dependency
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-otel</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<dependency>
+    <groupId>io.opentelemetry</groupId>
+    <artifactId>opentelemetry-api</artifactId>
+    <version>1.43.0</version>          <!-- consumer-supplied; provided scope 
on the bridge module -->
+</dependency>
+<!-- typically also the SDK + exporter of your choice -->
+<dependency>
+    <groupId>io.opentelemetry</groupId>
+    <artifactId>opentelemetry-sdk</artifactId>
+    <version>1.43.0</version>
+</dependency>
+```
+
+The bridge declares `opentelemetry-api` in `provided` scope so the consumer 
picks the version. `mvn -pl juneau-rest/juneau-rest-server dependency:tree | 
grep -i opentelemetry` returns nothing — the containment requirement is 
verified at build time.
+
+## Custom `MetricsRecorder` / `TracerHook` implementations
+
+The SPIs are deliberately tiny — implement them directly when you need a 
backend the shipped bridges don't cover (Dropwizard Metrics, an internal 
time-series store, a structured-log appender, …):
+
+```java
+@Bean public MetricsRecorder recorder() {
+    return (opName, method, uri, status, elapsed, error) -> {
+        myInternalMetricsBus.recordHttpCall(method, uri, status, 
elapsed.toMillis(), error);
+    };
+}
+```
+
+```java
+@Bean public TracerHook tracer() {
+    return request -> {
+        var startedAt = System.nanoTime();
+        return new Scope() {
+            @Override public void setStatusCode(int s) { /* stash */ }
+            @Override public void setError(Throwable t) { /* stash */ }
+            @Override public void close() {
+                myInternalTraceBus.recordSpan(request.getMethod(), 
System.nanoTime() - startedAt);
+            }
+        };
+    };
+}
+```
+
+## What's out of scope (v1)
+
+- **Structured-logging bridges** (SLF4J / Log4j2 structured appender) — 
TODO-20 owns the call-logger rework; the OTel bridge can publish a `Logs` event 
later if there's demand.
+- **Custom tag schemes per resource.** v1 uses fixed Spring-style tags on the 
Micrometer side and fixed OTel HTTP semantic-convention attribute names on the 
OTel side. Open a follow-on TODO if you need per-resource customization.
+- **Histogram percentile config from annotations.** Let the user configure the 
`MeterRegistry` directly — that's already a Micrometer-native surface 
(`@Timed`, `MeterFilter`).
+- **StatsD / Datadog / NewRelic native bridges.** Use Micrometer's registries 
— that's the whole point of `MetricsRecorder` going through `MeterRegistry`.
+
+## See also
+
+- [REST Server — Jakarta Bean Validation](/docs/topics/RestServerValidation) — 
same off-by-default contract precedent (TODO-68).
+- [REST Server — Rate-Limiting and Request-Id 
Propagation](/docs/topics/RestServerRateLimitAndRequestId) — sibling 
operational primitives (TODO-66). The `requestId` from TODO-66 is the natural 
log-correlation key alongside the OTel span id.
diff --git a/sidebars.ts b/sidebars.ts
index 0eb3832853..54a80ba6de 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1556,6 +1556,11 @@ const sidebars: SidebarsConfig = {
                                                        id: 
'topics/10.20f.RestServerValidation',
                                                        label: '10.20f. Jakarta 
Bean Validation',
                                                },
+                                               {
+                                                       type: 'doc',
+                                                       id: 
'topics/10.20g.RestServerObservability',
+                                                       label: '10.20g. 
Observability — Micrometer + OpenTelemetry',
+                                               },
                                                {
                                                        type: 'doc',
                                                        id: 
'topics/10.21.BuiltInParameters',

Reply via email to