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