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 &mdash; 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()} &mdash; 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}