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.microservice.logging; 018 019import java.lang.reflect.*; 020import java.util.logging.*; 021 022/** 023 * Reflective accessor for the currently-active OpenTelemetry trace / span id, used by 024 * {@link LogEntryFormatter} to populate the optional {@code {traceId}} / {@code {spanId}} log fields. 025 * 026 * <h5 class='topic'>Optional / reflective coupling</h5> 027 * 028 * <p> 029 * OpenTelemetry is <b>not</b> a compile-time dependency of {@code juneau-microservice}. This class 030 * probes for {@code io.opentelemetry.api.trace.Span} via {@link Class#forName(String)} at class-load 031 * time — mirroring how {@link org.apache.juneau.rest.server.processor.MdcAsyncListener} reflectively 032 * handles SLF4J. When OTel is absent from the runtime classpath the class gracefully degrades: 033 * {@link #isAvailable()} returns {@code false} and {@link #currentTraceId()} / {@link #currentSpanId()} 034 * return the empty string, so the {@code {traceId}} / {@code {spanId}} placeholders render empty. 035 * 036 * <h5 class='topic'>Where the id comes from</h5> 037 * 038 * <p> 039 * The id is read from {@code Span.current().getSpanContext()} — the OTel context active on the 040 * calling thread. Inside a {@code @RestOp} handler (or a custom 041 * {@link org.apache.juneau.rest.server.observation.Observations observation}) wrapped by the OTel 042 * {@code TracerHook}, that is the request / observation span. An invalid (no active trace) context 043 * yields the empty string. 044 * 045 * @since 10.0.0 046 */ 047public final class TraceContext { 048 049 private static final Logger LOG = Logger.getLogger(TraceContext.class.getName()); 050 051 /** {@code true} if {@code io.opentelemetry.api.trace.Span} was found on the classpath at class-load time. */ 052 private static final boolean AVAILABLE; 053 054 private static final Method SPAN_CURRENT; 055 private static final Method SPAN_GET_SPAN_CONTEXT; 056 private static final Method CTX_IS_VALID; 057 private static final Method CTX_GET_TRACE_ID; 058 private static final Method CTX_GET_SPAN_ID; 059 060 static { 061 boolean available = false; 062 Method spanCurrent = null; 063 Method getSpanContext = null; 064 Method isValid = null; 065 Method getTraceId = null; 066 Method getSpanId = null; 067 try { 068 Class<?> span = Class.forName("io.opentelemetry.api.trace.Span"); 069 Class<?> spanContext = Class.forName("io.opentelemetry.api.trace.SpanContext"); 070 spanCurrent = span.getMethod("current"); 071 getSpanContext = span.getMethod("getSpanContext"); 072 isValid = spanContext.getMethod("isValid"); 073 getTraceId = spanContext.getMethod("getTraceId"); 074 getSpanId = spanContext.getMethod("getSpanId"); 075 available = true; 076 } catch (Exception e) { 077 LOG.fine(() -> "OpenTelemetry API not found on classpath — trace-id log fields render empty: " + e.getMessage()); 078 } 079 AVAILABLE = available; 080 SPAN_CURRENT = spanCurrent; 081 SPAN_GET_SPAN_CONTEXT = getSpanContext; 082 CTX_IS_VALID = isValid; 083 CTX_GET_TRACE_ID = getTraceId; 084 CTX_GET_SPAN_ID = getSpanId; 085 } 086 087 private TraceContext() {} 088 089 /** 090 * Returns whether the OpenTelemetry trace API is available on the runtime classpath. 091 * 092 * <p> 093 * When {@code false}, {@link #currentTraceId()} and {@link #currentSpanId()} always return the empty 094 * string. 095 * 096 * @return {@code true} if {@code io.opentelemetry.api.trace.Span} was found at class-load time. 097 */ 098 public static boolean isAvailable() { 099 return AVAILABLE; 100 } 101 102 /** 103 * Returns the trace id of the OTel span active on the current thread. 104 * 105 * @return The 32-char hex trace id, or the empty string when OTel is absent or no valid span is active. 106 */ 107 public static String currentTraceId() { 108 return field(CTX_GET_TRACE_ID); 109 } 110 111 /** 112 * Returns the span id of the OTel span active on the current thread. 113 * 114 * @return The 16-char hex span id, or the empty string when OTel is absent or no valid span is active. 115 */ 116 public static String currentSpanId() { 117 return field(CTX_GET_SPAN_ID); 118 } 119 120 @SuppressWarnings({ 121 "java:S3011" // Reflective access to the OTel trace API — intentional; OpenTelemetry is not a compile dep. 122 }) 123 private static String field(Method accessor) { 124 if (! AVAILABLE) 125 return ""; 126 try { 127 Object span = SPAN_CURRENT.invoke(null); 128 if (span == null) 129 return ""; 130 Object ctx = SPAN_GET_SPAN_CONTEXT.invoke(span); 131 if (ctx == null || ! ((Boolean) CTX_IS_VALID.invoke(ctx)).booleanValue()) 132 return ""; 133 Object v = accessor.invoke(ctx); 134 return v == null ? "" : v.toString(); 135 } catch (Exception e) { 136 LOG.fine(() -> "OTel Span.current() trace-context read failed: " + e.getMessage()); 137 return ""; 138 } 139 } 140}