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.observation; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.time.*; 022 023import org.apache.juneau.rest.server.metrics.*; 024import org.apache.juneau.rest.server.tracing.*; 025 026/** 027 * Factory for custom (non-request) {@link Observation}s, composing a {@link MetricsRecorder} and a 028 * {@link TracerHook}. 029 * 030 * <p> 031 * This is the explicit programmatic observation substrate for {@code juneau-rest-server}: it reuses the 032 * exact same two SPIs the request path uses ({@link MetricsRecorder}, {@link TracerHook}) and the same 033 * {@code metricName} / {@code metricTags} comma-separated tag model, but is decoupled from 034 * {@code RestRequest} so application code can instrument an arbitrary block of work. Juneau deliberately 035 * has no method-interception / AOP substrate, so observation is explicit (a try-with-resources block) 036 * rather than annotation-driven — consistent with the framework's explicit-over-magic philosophy. 037 * 038 * <h5 class='topic'>No-backend contract</h5> 039 * 040 * <p> 041 * An {@code Observer} built from the no-op SPIs ({@link #NOOP}) never takes a timestamp and hands back 042 * {@link Observation#NOOP}, so an observation in an unconfigured process costs nothing beyond the 043 * try-with-resources scaffolding. {@link #start(String, String)} short-circuits to the no-op handle 044 * whenever <i>both</i> backends are the no-op singletons. 045 * 046 * <h5 class='topic'>Wiring</h5> 047 * 048 * <p> 049 * Typically obtained from a {@code RestContext}'s bean store — register the same 050 * {@code @Bean MetricsRecorder} / {@code @Bean TracerHook} you already register for request-path 051 * observability, then build an {@code Observer} from them (or install one as the {@link Observations} 052 * default). Construct directly for tests: 053 * 054 * <p class='bjava'> 055 * Observer <jv>o</jv> = <jk>new</jk> Observer(<jk>new</jk> MicrometerMetricsRecorder(<jv>registry</jv>), <jk>new</jk> OtelTracerHook(<jv>otel</jv>)); 056 * </p> 057 * 058 * <h5 class='section'>See Also:</h5><ul> 059 * <li class='jc'>{@link Observations} 060 * <li class='jc'>{@link Observation} 061 * <li class='jc'>{@link MetricsRecorder} 062 * <li class='jc'>{@link TracerHook} 063 * </ul> 064 * 065 * @since 10.0.0 066 */ 067@SuppressWarnings({ 068 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 069 "resource" // start(...) returns the Observation for the caller's try-with-resources; ActiveObservation holds the tracer Scope it closes in its own close(). 070}) 071public final class Observer { 072 073 /** 074 * Observer backed by the no-op metrics recorder and tracer — produces only 075 * {@link Observation#NOOP}. 076 */ 077 public static final Observer NOOP = new Observer(NoOpMetricsRecorder.INSTANCE, NoOpTracerHook.INSTANCE); 078 079 private final MetricsRecorder recorder; 080 private final TracerHook tracer; 081 private final boolean active; 082 083 /** 084 * Constructor. 085 * 086 * @param recorder The metrics recorder to time observations against. Must not be <jk>null</jk>; 087 * use {@link NoOpMetricsRecorder#INSTANCE} to disable metrics. 088 * @param tracer The tracer to open spans against. Must not be <jk>null</jk>; use 089 * {@link NoOpTracerHook#INSTANCE} to disable tracing. 090 */ 091 public Observer(MetricsRecorder recorder, TracerHook tracer) { 092 this.recorder = reqnn("recorder", recorder); 093 this.tracer = reqnn("tracer", tracer); 094 this.active = recorder != NoOpMetricsRecorder.INSTANCE || tracer != NoOpTracerHook.INSTANCE; 095 } 096 097 /** 098 * Starts a new observation. 099 * 100 * <p> 101 * When both backends are the no-op singletons this returns {@link Observation#NOOP} without taking a 102 * timestamp or opening a span. Otherwise it opens a span via {@link TracerHook#startSpan(String)} and 103 * captures a start timestamp; the paired timer sample is recorded on {@link Observation#close()}. 104 * 105 * @param name The observation name — used as both the span name and the metric timer name. 106 * Must not be <jk>null</jk> or blank. 107 * @param tags Additional metric tags as comma-separated {@code key=value} pairs (e.g. 108 * {@code "team=payments,region=us-east"}), or <jk>null</jk> / empty for none. 109 * @return The started {@link Observation}. Never <jk>null</jk>. 110 */ 111 public Observation start(String name, String tags) { 112 reqnb("name", name); 113 if (! active) 114 return Observation.NOOP; 115 var scope = tracer.startSpan(name); 116 return new ActiveObservation(recorder, scope, name, tags == null ? "" : tags); 117 } 118 119 /** 120 * Returns whether this observer has at least one live backend (metrics or tracing). 121 * 122 * @return <jk>true</jk> if either the metrics recorder or the tracer is non-no-op. 123 */ 124 public boolean isActive() { 125 return active; 126 } 127 128 private static final class ActiveObservation implements Observation { 129 130 private final MetricsRecorder recorder; 131 private final Scope scope; 132 private final String name; 133 private final String tags; 134 private final long startNanos; 135 private Throwable error; 136 137 ActiveObservation(MetricsRecorder recorder, Scope scope, String name, String tags) { 138 this.recorder = recorder; 139 this.scope = scope; 140 this.name = name; 141 this.tags = tags; 142 this.startNanos = System.nanoTime(); 143 } 144 145 @Override /* Observation */ 146 public void setError(Throwable error) { 147 this.error = error; 148 } 149 150 @Override /* Observation */ 151 public void close() { 152 var elapsed = Duration.ofNanos(System.nanoTime() - startNanos); 153 try { 154 if (nn(error)) 155 scope.setError(error); 156 } finally { 157 try { 158 scope.close(); 159 } finally { 160 recorder.record(name, tags, elapsed, error); 161 } 162 } 163 } 164 } 165}