001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.tracing; 018 019import static org.apache.juneau.commons.utils.StringUtils.*; 020 021import java.io.*; 022import java.util.logging.*; 023 024import org.apache.juneau.http.response.*; 025import org.apache.juneau.rest.server.*; 026import org.apache.juneau.rest.server.processor.*; 027 028/** 029 * Writes the W3C Trace Context response headers ({@code traceparent} and, when present, 030 * {@code tracestate}) so a client calling this server can read the resulting trace identifiers back 031 * off the response. 032 * 033 * <h5 class='topic'>How it works</h5> 034 * 035 * <p> 036 * A {@link TracerHook} bridge (e.g. the OpenTelemetry bridge in {@code juneau-rest-server-tracing-otel}) 037 * renders the W3C {@code traceparent} / {@code tracestate} values from the <i>server-started</i> span 038 * context at {@link TracerHook#startSpan(RestRequest) span-start} time — the only point where 039 * that context is reliably active — and stashes them as the request attributes 040 * {@link #ATTR_TRACEPARENT} / {@link #ATTR_TRACESTATE}. This processor runs later, during response 041 * rendering (after the span scope has already closed), reads those stashed strings, and writes them 042 * as response headers. 043 * 044 * <p> 045 * This capture-at-span-start / write-at-response-time split is deliberate: the framework opens and 046 * closes the tracing scope around the {@code @RestOp} handler in 047 * {@link RestOpInvoker#invokeOp(RestOpSession)}, which completes <i>before</i> response processors 048 * run, so {@code Span.current()} is no longer valid here. Keeping the rendered values on the request 049 * also keeps this processor free of any OpenTelemetry (or other tracer) dependency — it works 050 * with any {@code TracerHook} bridge that populates the two attributes. 051 * 052 * <h5 class='topic'>Off-by-default behavior</h5> 053 * 054 * <p> 055 * The first action in {@link #process(RestOpSession)} is a single request-attribute read. When no 056 * non-no-op {@code TracerHook} ran for the request, no {@link #ATTR_TRACEPARENT} attribute is present 057 * and the processor returns {@link #NEXT} immediately with no allocations — matching the 058 * off-by-default, zero-cost contract of the observability hooks. Registration of this processor is 059 * itself gated on {@code RestContext.responseTraceparent} (default {@code true}); setting it to 060 * {@code false} keeps the processor out of the chain entirely. 061 * 062 * <h5 class='topic'>Already-committed responses</h5> 063 * 064 * <p> 065 * If the response has already been committed (for example a streaming / SSE handler that flushed its 066 * headers before returning), the headers can no longer be set. In that case the processor logs a 067 * {@link Level#FINE FINE} message and skips the write — it never throws. 068 * 069 * <h5 class='section'>See Also:</h5><ul> 070 * <li class='jc'>{@link TracerHook} 071 * <li class='link'><a class="doclink" href="https://www.w3.org/TR/trace-context/">W3C Trace Context</a> 072 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerObservability">Observability — Micrometer + OpenTelemetry</a> 073 * </ul> 074 * 075 * @since 10.0.0 076 */ 077public class TraceContextResponseProcessor implements ResponseProcessor { 078 079 /** 080 * Request-attribute key under which a {@link TracerHook} bridge stashes the rendered W3C 081 * {@code traceparent} value (e.g. {@code 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01}) 082 * for this processor to emit as a response header. 083 */ 084 public static final String ATTR_TRACEPARENT = "juneau.traceparent"; 085 086 /** 087 * Request-attribute key under which a {@link TracerHook} bridge stashes the rendered W3C 088 * {@code tracestate} value (a comma-separated {@code key=value} list). Only set when the active 089 * trace state is non-empty. 090 */ 091 public static final String ATTR_TRACESTATE = "juneau.tracestate"; 092 093 /** 094 * Request-attribute key under which a {@link TracerHook} bridge stashes the rendered, non-empty W3C 095 * {@code baggage} value. 096 * 097 * <p> 098 * Unlike {@link #ATTR_TRACEPARENT} / {@link #ATTR_TRACESTATE}, this processor does <b>not</b> read 099 * this attribute and never emits a {@code baggage} response header from it — {@code baggage} is 100 * stashed purely so a dated MCP adapter can echo it into a successful result's {@code _meta} (added in 101 * a later task). Ordinary HTTP response-header behavior for {@code traceparent} / {@code tracestate} is 102 * unaffected by this constant's existence. 103 * 104 * @since 10.0.0 105 */ 106 public static final String ATTR_BAGGAGE = "juneau.baggage"; 107 108 /** 109 * Request-attribute key under which {@link RestOpInvoker} stashes the active {@link Scope} for the 110 * duration of one {@code @RestOp} handler invocation. 111 * 112 * <p> 113 * Populated only on the active-tracer path — see the no-tracer fast-path contract documented on 114 * {@link TracerHook} — and cleared once that invocation's synchronous or deferred observability 115 * lifecycle closes the scope. A dated MCP adapter running inside the handler already resolves this 116 * same {@link RestRequest} through its own request-scoped {@code BeanStore} entry; reading this 117 * attribute off that request lets it call {@link Scope#recordRpcError(int, String)} before returning a 118 * JSON-RPC error response — a JSON-RPC error is dispatched over HTTP {@code 200}, so it never 119 * otherwise reaches {@link Scope#setStatusCode(int)} with anything but the adapter's uniform success 120 * status. 121 * 122 * @since 10.0.0 123 */ 124 public static final String ATTR_SCOPE = "juneau.tracerScope"; 125 126 private static final String HEADER_TRACEPARENT = "traceparent"; 127 private static final String HEADER_TRACESTATE = "tracestate"; 128 129 private static final Logger LOG = Logger.getLogger(TraceContextResponseProcessor.class.getName()); 130 131 @Override /* Overridden from ResponseProcessor */ 132 public int process(RestOpSession opSession) throws IOException, BasicHttpException { 133 var req = opSession.getRequest(); 134 135 // NoOp short-circuit: no active tracer stashed a trace context for this request. 136 var traceparent = req.getAttribute(ATTR_TRACEPARENT).as(String.class).orElse(null); 137 if (isEmpty(traceparent)) 138 return NEXT; 139 140 var res = opSession.getResponse(); 141 142 // Headers already flushed (e.g. streaming / SSE response) — body was also written; chain is done. 143 if (res.getHttpServletResponse().isCommitted()) { 144 LOG.log(Level.FINE, () -> "Response already committed; skipping traceparent/tracestate header injection."); 145 return FINISHED; 146 } 147 148 res.setHeader(HEADER_TRACEPARENT, traceparent); 149 150 var tracestate = req.getAttribute(ATTR_TRACESTATE).as(String.class).orElse(null); 151 if (tracestate != null && ! tracestate.isEmpty()) 152 res.setHeader(HEADER_TRACESTATE, tracestate); 153 154 // Headers only — let the downstream processors render the body. 155 return NEXT; 156 } 157}