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.metrics; 018 019import java.time.*; 020 021/** 022 * SPI for receiving per-request metric events from {@code juneau-rest-server}. 023 * 024 * <p> 025 * One {@code record(...)} call fires after every {@code @RestOp} handler invocation — including 026 * exception paths. Bridge implementations translate the event into the format their downstream 027 * registry expects (Micrometer {@code io.micrometer.core.instrument.Timer}, OpenTelemetry 028 * metric instruments, Dropwizard {@code MetricRegistry}, etc.). 029 * 030 * <h5 class='topic'>Off-by-default contract</h5> 031 * 032 * <p> 033 * {@code juneau-rest-server} resolves the {@code MetricsRecorder} via 034 * {@code RestContext.getBeanStore().getBean(MetricsRecorder.class)}. When no bean is supplied, 035 * {@link NoOpMetricsRecorder#INSTANCE} is used and the framework never allocates a downstream 036 * registry, never reaches Micrometer / OpenTelemetry on its own, and never adds any per-request 037 * cost beyond a single static-field read. To opt in, the consumer registers a {@code MetricsRecorder} 038 * bean — typically via {@code @Bean MetricsRecorder} on the resource or its parent. 039 * 040 * <h5 class='topic'>Usage</h5> 041 * 042 * <p class='bjava'> 043 * <ja>@Rest</ja> 044 * <jk>public class</jk> MyResource <jk>extends</jk> RestServlet { 045 * 046 * <ja>@Bean</ja> 047 * <jk>public</jk> MetricsRecorder recorder(MeterRegistry <jv>registry</jv>) { 048 * <jk>return new</jk> MicrometerMetricsRecorder(<jv>registry</jv>); 049 * } 050 * } 051 * </p> 052 * 053 * <h5 class='topic'>Threading</h5> 054 * 055 * <p> 056 * Implementations <b>must</b> be thread-safe — the same {@code MetricsRecorder} bean is invoked 057 * concurrently from every request thread for the lifetime of the {@code RestContext}. The framework 058 * calls {@code record(...)} synchronously inside the request thread immediately after the response 059 * status is known, so implementations should keep the call cheap (constant-time tag computation, 060 * non-blocking registry writes). 061 * 062 * <h5 class='topic'>Event ordering</h5> 063 * 064 * <p> 065 * For a given {@code @RestOp} invocation, {@code record(...)} fires exactly once after the operation 066 * completes — whether the handler returned normally, threw a {@code BasicHttpException}, or 067 * triggered an internal-server-error. The {@code elapsed} parameter measures wall-clock time spent 068 * inside the framework's per-op invocation boundary (from just before parameter resolution to just 069 * after the handler returns or throws). It does <b>not</b> include response serialization performed 070 * later in the request pipeline. 071 * 072 * <h5 class='section'>See Also:</h5><ul> 073 * <li class='jc'>{@link NoOpMetricsRecorder} 074 * <li class='jc'>{@link org.apache.juneau.rest.server.tracing.TracerHook} 075 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerObservability">Observability — Micrometer + OpenTelemetry</a> 076 * </ul> 077 * 078 * @since 10.0.0 079 */ 080@FunctionalInterface 081@SuppressWarnings({ 082 "java:S6213" // 'record' is the established SPI method name; renaming would break the public observability API. 083}) 084public interface MetricsRecorder { 085 086 /** 087 * Records one per-request metric event. 088 * 089 * @param opName The fully-qualified {@code @RestOp} method name (e.g. 090 * {@code "com.example.OrderResource.create(java.lang.String,com.example.Order)"}). 091 * Never <jk>null</jk>; never blank. 092 * @param httpMethod The uppercased HTTP method of the request (e.g. {@code "GET"}, {@code "POST"}). 093 * Never <jk>null</jk>; never blank. 094 * @param uriTemplate The {@code @RestOp} path template (e.g. {@code "/users/{id}"}), <b>not</b> the 095 * raw concrete URI. Bridges should use this as the {@code uri} tag to keep cardinality bounded. 096 * May be empty for resource-level operations that match {@code /*}; never <jk>null</jk>. 097 * @param statusCode The HTTP response status code (e.g. {@code 200}, {@code 404}, {@code 500}). 098 * If the handler threw before the status was set, this is the framework's inferred status (typically 099 * {@code 500} for an unmapped exception, or the exception's status for a {@code BasicHttpException}). 100 * @param elapsed Wall-clock duration of the {@code @RestOp} invocation. Never <jk>null</jk>; never 101 * negative. 102 * @param error The exception thrown by the handler, or <jk>null</jk> if the call completed normally. 103 * Bridges typically derive an {@code exception} tag from the throwable's simple class name, or 104 * {@code "None"} when null (mirrors Spring Boot's {@code WebMvcMetricsFilter} convention). 105 * @param metricName Per-op metric name override from {@link org.apache.juneau.rest.server.RestOp#metricName()}. 106 * Empty string (default) means the implementation should use its own default name derivation. 107 * Never <jk>null</jk>. 108 * @param metricTags Per-op additional metric tags from {@link org.apache.juneau.rest.server.RestOp#metricTags()}. 109 * Format: comma-separated {@code key=value} pairs (e.g. {@code "team=payments,region=us-east"}). 110 * Empty string (default) means no additional tags. Never <jk>null</jk>. 111 */ 112 @SuppressWarnings({ 113 "java:S107" // The 8 parameters form the stable per-request metric event contract; a holder object would obscure this SPI signature. 114 }) 115 void record(String opName, String httpMethod, String uriTemplate, int statusCode, Duration elapsed, Throwable error, String metricName, String metricTags); 116 117 /** 118 * Records one custom (non-request) observation event. 119 * 120 * <p> 121 * Unlike {@link #record(String, String, String, int, Duration, Throwable, String, String)}, this 122 * entry point is <b>not</b> tied to an HTTP request — it is the substrate for the explicit 123 * programmatic observation API ({@link org.apache.juneau.rest.server.observation.Observations}) so 124 * application code can time an arbitrary block of work. Bridge implementations record a timer named 125 * {@code metricName} carrying {@code metricTags} (plus an {@code exception} tag derived from 126 * {@code error}). 127 * 128 * <p> 129 * The default implementation does nothing — a bridge that does not override it simply does not 130 * record custom observations, and the no-backend path stays zero-allocation. The shipped Micrometer 131 * bridge overrides it. 132 * 133 * @param metricName The timer name (e.g. {@code "order.load"}). Never <jk>null</jk>; never blank. 134 * @param metricTags Additional tags as comma-separated {@code key=value} pairs (e.g. 135 * {@code "team=payments,region=us-east"}). Empty string means no additional tags. Never <jk>null</jk>. 136 * @param elapsed Wall-clock duration of the observed block. Never <jk>null</jk>; never negative. 137 * @param error The exception thrown by the observed block, or <jk>null</jk> if it completed normally. 138 */ 139 default void record(String metricName, String metricTags, Duration elapsed, Throwable error) { 140 // Default no-op: bridges that support custom observations override this. 141 } 142}