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.health;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023import org.apache.juneau.commons.inject.*;
024import org.apache.juneau.rest.server.*;
025import org.apache.juneau.rest.server.health.HealthAggregator.*;
026import org.apache.juneau.rest.server.servlet.*;
027
028/**
029 * Servlet flavor of the built-in health/readiness/liveness probe capability.
030 *
031 * <p>
032 * Mounts as a standalone top-level servlet at {@code /healthz}, {@code /readyz}, and {@code /livez},
033 * delegating to a shared {@link HealthAggregator} worker bean. The {@link HealthMixin} mixin flavor
034 * delegates to the same bean, so the two forms cannot drift.
035 *
036 * <h5 class='section'>See Also:</h5><ul>
037 *    <li class='jc'>{@link HealthMixin}
038 *    <li class='jc'>{@link HealthAggregator}
039 * </ul>
040 *
041 * @serial exclude
042 * @since 10.0.0
043 */
044@Rest(paths={"/healthz","/readyz","/livez"})
045public class HealthServlet extends BasicRestServlet {
046   private static final long serialVersionUID = 1L;
047
048   private final transient HealthAggregator aggregator = new HealthAggregator();
049
050   @SuppressWarnings({
051      "java:S2226", // Mutable by design: the embedded-server lifecycle publishes this post-construction, from a
052                    // thread other than the request-handling threads that read it via probe()/initReadinessState()
053      "java:S3077"  // volatile is used only to publish the reference itself (write-once, read-many); ReadinessState
054                    // is treated as an opaque handle here, never mutated through this field
055   })
056   private transient volatile ReadinessState readinessState;
057
058   /**
059    * Publishes the lifecycle-owned {@link ReadinessState} that this probe should observe.
060    *
061    * <p>
062    * Called by the embedded-server lifecycle component (e.g. {@code JettyServerComponent},
063    * {@code TomcatServerComponent}) before the server starts, so the per-service instance that component flips
064    * on shutdown is registered into this resource's own bean store &mdash; see {@link #initReadinessState}
065    * &mdash; closing the gap between the microservice's bean store and this servlet's bean store that
066    * {@link HealthAggregator} consults.
067    *
068    * @param state The per-service readiness state.  Ignored if <jk>null</jk>.
069    * @return This object.
070    */
071   public HealthServlet publishReadinessState(ReadinessState state) {
072      this.readinessState = state;
073      return this;
074   }
075
076   /**
077    * Registers the {@linkplain #publishReadinessState(ReadinessState) published} readiness state (if any) into
078    * this resource's own bean store, so {@link HealthAggregator#aggregate} resolves the lifecycle-owned
079    * instance instead of falling back to {@link ReadinessState#shared()}.
080    *
081    * @param beanStore This resource's bean store.
082    */
083   @RestInit
084   @SuppressWarnings({
085      "resource" // addBean returns this; the discarded return is the store the caller already holds
086   })
087   public void initReadinessState(WritableBeanStore beanStore) {
088      if (readinessState != null)
089         beanStore.addBean(ReadinessState.class, readinessState);
090   }
091
092   /**
093    * Health/readiness/liveness probe endpoint.
094    *
095    * <p>
096    * Serves all three mount paths ({@code /healthz}, {@code /readyz}, {@code /livez}) through one
097    * {@code /*} operation, dispatching on the request's last path segment.  A single zero-part,
098    * {@code hasRemainder=true} matcher is required because this servlet is auto-mounted at three <i>exact-match</i>
099    * top-level path-specs: a real container delivers a bare {@code GET /readyz} with a zero-segment
100    * {@code pathInfo} that no 1-segment matcher can satisfy, so three separate {@code @RestGet(path="/{probe}")}
101    * operations all 404.  The last-path-segment key ({@link #probeFor}) is the only thing that differs per mount,
102    * so it is the disambiguator.
103    *
104    * <p>
105    * This replaces the former {@code healthz(...)}/{@code readyz(...)}/{@code livez(...)} entry points; subclasses
106    * that overrode any of those must override {@link #probeFor(RestRequest)} (or this method) instead.
107    *
108    * @param req The request (its last path segment selects the probe).
109    * @param res The response.
110    * @return Aggregated health payload.
111    */
112   @RestGet(path="/*")  // Sibling-servlet pin (matches FaviconServlet/VersionServlet): a zero-part, hasRemainder=true
113                        // matcher that resolves BOTH a container's zero-segment bare-mount hit AND a mock/remount remainder.
114   public HealthResponse probe(RestRequest req, RestResponse res) {
115      return aggregator.aggregate(getContext(), indicators(), probeFor(req), res);
116   }
117
118   /**
119    * Resolves which probe this request is for, keyed off the LAST PATH SEGMENT.
120    *
121    * <p>
122    * Prefers {@link RestRequest#getPathInfo()} when it carries a real (non-root) value &mdash; this is what
123    * {@code MockRestClient} and any remainder-based remount populate ({@code servletPath="", pathInfo="/readyz"}).
124    * Falls back to {@link RestRequest#getServletPath()} for a real container's bare exact-match hit, where
125    * {@code pathInfo} is {@code null} ({@code servletPath="/readyz", pathInfo=null}).
126    *
127    * @param req The request.
128    * @return The resolved probe, or {@code null} for {@code /healthz} (the overall aggregate).
129    */
130   protected HealthProbe probeFor(RestRequest req) {
131      var hint = req.getPathInfo();
132      if (hint == null || hint.isEmpty() || eq(hint, "/"))
133         hint = req.getServletPath();
134      var last = lastSegment(hint);
135      if (eqa(last, "readyz", "ready"))
136         return HealthProbe.READY;
137      if (eqa(last, "livez", "live"))
138         return HealthProbe.LIVE;
139      return null; // "/healthz" (or anything else this instance is mounted at) -- overall aggregate.
140   }
141
142   private static String lastSegment(String path) {
143      if (path == null)
144         return "";
145      var p = path;
146      while (p.length() > 1 && p.endsWith("/"))
147         p = p.substring(0, p.length() - 1);
148      var i = p.lastIndexOf('/');
149      return i < 0 ? p : p.substring(i + 1);
150   }
151
152   /**
153    * Returns the indicators to evaluate for this request.
154    *
155    * <p>
156    * Defaults to every {@link HealthIndicator} bean registered in this resource's bean store.
157    * Subclasses may override to supply a fixed indicator set.
158    *
159    * @return Indicator map keyed by bean name.
160    */
161   protected Map<String,HealthIndicator> indicators() {
162      return aggregator.indicators(getContext());
163   }
164}