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.logger;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Utils.*;
021
022import java.util.function.*;
023import java.util.logging.*;
024
025import org.apache.juneau.*;
026import org.apache.juneau.commons.collections.*;
027import org.apache.juneau.cp.*;
028
029import jakarta.servlet.http.*;
030
031/**
032 * Represents a logging rule used by {@link CallLogger}.
033 *
034 * <h5 class='section'>See Also:</h5><ul>
035 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerLoggingAndDebugging">Logging / Debugging</a>
036 * </ul>
037 */
038public class CallLoggerRule {
039   /**
040    * Builder class.
041    */
042   public static class Builder extends BeanBuilder<CallLoggerRule> {
043
044      Predicate<Integer> statusFilter;
045      Predicate<HttpServletRequest> requestFilter;
046      Predicate<HttpServletResponse> responseFilter;
047      Predicate<Throwable> exceptionFilter;
048      Enablement enabled;
049      Predicate<HttpServletRequest> enabledTest;
050      Level level;
051      CallLoggingDetail requestDetail, responseDetail;
052      boolean logStackTrace;
053
054      /**
055       * Constructor.
056       *
057       * @param beanStore The bean store to use for creating beans.
058       */
059      protected Builder(BeanStore beanStore) {
060         super(CallLoggerRule.class, beanStore);
061      }
062
063      /**
064       * Shortcut for calling <c>enabled(<jsf>NEVER</jsf>)</c>.
065       *
066       * @return This object.
067       */
068      public Builder disabled() {
069         return this.enabled(Enablement.NEVER);
070      }
071
072      /**
073       * Specifies whether logging is enabled when using this rule.
074       *
075       * <h5 class='section'>Example:</h5>
076       * <p class='bjava'>
077       *    <jc>// Create a logger rule where logging is only enabled if the query string contains "foobar".</jc>
078       *    RestLogger
079       *       .<jsm>createRule</jsm>()
080       *       .enabled(<jsf>CONDITIONALLY</jsf>)
081       *       .enabledPredicate(<jv>x</jv> -&gt; <jv>x</jv>.getQueryString().contains(<js>"foobar"</js>))
082       *       .build();
083       * </p>
084       *
085       * <ul class='values'>
086       *    <li>{@link Enablement#ALWAYS ALWAYS} - Logging is enabled.
087       *    <li>{@link Enablement#NEVER NEVER} - Logging is disabled.
088       *    <li>{@link Enablement#CONDITIONAL CONDITIONALLY} - Logging is enabled if it passes the {@link #enabledPredicate(Predicate)} test.
089       * </ul>
090       *
091       * @param value
092       *    The enablement flag value, or <jk>null</jk> to inherit from the call logger whose default value is {@link Enablement#ALWAYS ALWAYS}
093       *    unless overridden via a <js>"juneau.restCallLogger.enabled"</js> system property or <js>"JUNEAU_RESTCALLLOGGER_ENABLED"</js> environment variable.
094       * @return This object.
095       */
096      public Builder enabled(Enablement value) {
097         enabled = value;
098         return this;
099      }
100
101      /**
102       * Specifies the predicate test to use when the enabled setting is set to {@link Enablement#CONDITIONAL CONDITIONALLY}.
103       *
104       * <p>
105       * This setting has no effect if the enablement value is not {@link Enablement#CONDITIONAL CONDITIONALLY}.
106       *
107       * <h5 class='section'>Example:</h5>
108       * <p class='bjava'>
109       *    <jc>// Create a logger rule where logging is only enabled if the query string contains "foobar".</jc>
110       *    RestLogger
111       *       .<jsm>createRule</jsm>()
112       *       .enabled(<jsf>CONDITIONALLY</jsf>)
113       *       .enabledPredicate(<jv>x</jv> -&gt; <jv>x</jv>.getQueryString().contains(<js>"foobar"</js>))
114       *       .build();
115       * </p>
116       *
117       * @param value
118       *    The enablement predicate test, or <jk>null</jk> to inherit from the call logger whose default value is <c><jv>x</jv> -&gt; <jk>false</jk></c>.
119       * @return This object.
120       */
121      public Builder enabledPredicate(Predicate<HttpServletRequest> value) {
122         enabledTest = value;
123         return this;
124      }
125
126      /**
127       * Apply a throwable-based predicate check for this rule to match against.
128       *
129       * <h5 class='section'>Example:</h5>
130       * <p class='bjava'>
131       *    <jc>// Create a logger rule that only matches if a NotFound exception was thrown.</jc>
132       *    RestLogger
133       *       .<jsm>createRule</jsm>()
134       *       .exceptionFilter(<jv>x</jv> -&gt; <jv>x</jv> <jk>instanceof</jk> NotFound)
135       *       .build();
136       * </p>
137       *
138       * <p>
139       * This check is only performed if an actual throwable was thrown.  Therefore it's not necessary to perform a
140       * null check in the predicate.
141       *
142       * @param value
143       *    The predicate check, or <jk>null</jk> to not use any filtering based on exceptions.
144       * @return This object.
145       */
146      public Builder exceptionFilter(Predicate<Throwable> value) {
147         exceptionFilter = value;
148         return this;
149      }
150
151      @Override /* Overridden from BeanBuilder */
152      public Builder impl(Object value) {
153         super.impl(value);
154         return this;
155      }
156
157      /**
158       * The logging level to use for logging the request/response.
159       *
160       * <p>
161       * The default value is {@link Level#INFO}.
162       *
163       * @param value
164       *    The new value for this property, or <jk>null</jk> to inherit from the call logger.
165       * @return This object.
166       */
167      public Builder level(Level value) {
168         level = value;
169         return this;
170      }
171
172      /**
173       * Log a stack trace as part of the log entry.
174       *
175       * <p>
176       * The default value is <jk>false</jk>.
177       *
178       * @return This object.
179       */
180      public Builder logStackTrace() {
181         this.logStackTrace = true;
182         return this;
183      }
184
185      /**
186       * The level of detail to log on a request.
187       *
188       * <ul class='values'>
189       *    <li>{@link CallLoggingDetail#STATUS_LINE STATUS_LINE} - Log only the status line.
190       *    <li>{@link CallLoggingDetail#HEADER HEADER} - Log the status line and headers.
191       *    <li>{@link CallLoggingDetail#ENTITY ENTITY} - Log the status line and headers and content if available.
192       * </ul>
193       *
194       * @param value
195       *    The new value for this property, or <jk>null</jk> to inherit from the call logger.
196       * @return This object.
197       */
198      public Builder requestDetail(CallLoggingDetail value) {
199         requestDetail = value;
200         return this;
201      }
202
203      /**
204       * Apply a request-based predicate check for this rule to match against.
205       *
206       * <h5 class='section'>Example:</h5>
207       * <p class='bjava'>
208       *    <jc>// Create a logger rule that only matches if the servlet path contains "foobar".</jc>
209       *    RestLogger
210       *       .<jsm>createRule</jsm>()
211       *       .requestFilter(<jv>x</jv> -&gt; <jv>x</jv>.getServletPath().contains(<js>"foobar"</js>))
212       *       .build();
213       * </p>
214       *
215       * @param value
216       *    The predicate check, or <jk>null</jk> to not use any filtering based on the request.
217       * @return This object.
218       */
219      public Builder requestFilter(Predicate<HttpServletRequest> value) {
220         requestFilter = value;
221         return this;
222      }
223
224      /**
225       * The level of detail to log on a response.
226       *
227       * <ul class='values'>
228       *    <li>{@link CallLoggingDetail#STATUS_LINE STATUS_LINE} - Log only the status line.
229       *    <li>{@link CallLoggingDetail#HEADER HEADER} - Log the status line and headers.
230       *    <li>{@link CallLoggingDetail#ENTITY ENTITY} - Log the status line and headers and content if available.
231       * </ul>
232       *
233       * @param value
234       *    The new value for this property, or <jk>null</jk> to inherit from the call logger.
235       * @return This object.
236       */
237      public Builder responseDetail(CallLoggingDetail value) {
238         responseDetail = value;
239         return this;
240      }
241
242      /**
243       * Apply a response-based predicate check for this rule to match against.
244       *
245       * <h5 class='section'>Example:</h5>
246       * <p class='bjava'>
247       *    <jc>// Create a logger rule that only matches if the servlet path contains "foobar".</jc>
248       *    RestLogger
249       *       .<jsm>createRule</jsm>()
250       *       .responseFilter(<jv>x</jv> -&gt; <jv>x</jv>.getStatus() &gt;= 500)
251       *       .build();
252       * </p>
253       *
254       * <p>
255       * Note that the {@link #statusFilter(Predicate)} and {@link #exceptionFilter(Predicate)} methods are simply
256       * convenience response filters.
257       *
258       * @param value
259       *    The predicate check, or <jk>null</jk> to not use any filtering based on the response.
260       * @return This object.
261       */
262      public Builder responseFilter(Predicate<HttpServletResponse> value) {
263         responseFilter = value;
264         return this;
265      }
266
267      /**
268       * Apply a status-based predicate check for this rule to match against.
269       *
270       * <h5 class='section'>Example:</h5>
271       * <p class='bjava'>
272       *    <jc>// Create a logger rule that only matches if the status code is greater than or equal to 500.</jc>
273       *    RestLogger
274       *       .<jsm>createRule</jsm>()
275       *       .statusFilter(<jv>x</jv> -&gt; <jv>x</jv> &gt;= 500)
276       *       .build();
277       * </p>
278       *
279       * @param value
280       *    The predicate check, or <jk>null</jk> to not use any filtering based on status code.
281       * @return This object.
282       */
283      public Builder statusFilter(Predicate<Integer> value) {
284         statusFilter = value;
285         return this;
286      }
287
288      @Override /* Overridden from BeanBuilder */
289      public Builder type(Class<?> value) {
290         super.type(value);
291         return this;
292      }
293
294      @Override /* Overridden from BeanBuilder */
295      protected CallLoggerRule buildDefault() {
296         return new CallLoggerRule(this);
297      }
298   }
299
300   /**
301    * Static creator.
302    *
303    * @param beanStore The bean store to use for creating beans.
304    * @return A new builder for this object.
305    */
306   public static Builder create(BeanStore beanStore) {
307      return new Builder(beanStore);
308   }
309
310   private final Predicate<Integer> statusFilter;
311   private final Predicate<HttpServletRequest> requestFilter;
312   private final Predicate<HttpServletResponse> responseFilter;
313   private final Predicate<Throwable> exceptionFilter;
314   private final Level level;
315   private final Enablement enabled;
316   private final Predicate<HttpServletRequest> enabledTest;
317   private final CallLoggingDetail requestDetail, responseDetail;
318
319   /**
320    * Constructor.
321    *
322    * @param b Builder
323    */
324   CallLoggerRule(Builder b) {
325      statusFilter = b.statusFilter;
326      exceptionFilter = b.exceptionFilter;
327      requestFilter = b.requestFilter;
328      responseFilter = b.responseFilter;
329      level = b.level;
330      enabled = b.enabled;
331      enabledTest = b.enabledTest;
332      requestDetail = b.requestDetail;
333      responseDetail = b.responseDetail;
334   }
335
336   /**
337    * Returns the enablement flag value on this rule.
338    *
339    * @return The enablement flag value on this rule, or <jk>null</jk> if it's not set.
340    */
341   public Enablement getEnabled() { return enabled; }
342
343   /**
344    * Returns the enablement predicate test on this rule.
345    *
346    * @return The enablement predicate test on this rule, or <jk>null</jk> if it's not set.
347    */
348   public Predicate<HttpServletRequest> getEnabledTest() { return enabledTest; }
349
350   /**
351    * Returns the log level on this rule.
352    *
353    * @return The log level on this rule, or <jk>null</jk> if it's not set.
354    */
355   public Level getLevel() { return level; }
356
357   /**
358    * Returns the detail level for HTTP requests.
359    *
360    * @return the detail level for HTTP requests, or <jk>null</jk> if it's not set.
361    */
362   public CallLoggingDetail getRequestDetail() { return requestDetail; }
363
364   /**
365    * Returns the detail level for HTTP responses.
366    *
367    * @return the detail level for HTTP responses, or <jk>null</jk> if it's not set.
368    */
369   public CallLoggingDetail getResponseDetail() { return responseDetail; }
370
371   /**
372    * Returns <jk>true</jk> if this rule matches the specified parameters.
373    *
374    * @param req The HTTP request being logged.  Never <jk>null</jk>.
375    * @param res The HTTP response being logged.  Never <jk>null</jk>.
376    * @return <jk>true</jk> if this rule matches the specified parameters.
377    */
378   public boolean matches(HttpServletRequest req, HttpServletResponse res) {
379
380      if ((nn(requestFilter) && ! requestFilter.test(req)) || (nn(responseFilter) && ! responseFilter.test(res)))
381         return false;
382
383      if (nn(statusFilter) && ! statusFilter.test(res.getStatus()))
384         return false;
385
386      var e = (Throwable)req.getAttribute("Exception");
387      if (nn(e) && nn(exceptionFilter) && ! exceptionFilter.test(e))
388         return false;
389
390      return true;
391   }
392
393   protected FluentMap<String,Object> properties() {
394      // @formatter:off
395      return filteredBeanPropertyMap()
396         .a("codeFilter", statusFilter)
397         .a("enabled", enabled)
398         .a("enabledTest", enabledTest)
399         .a("exceptionFilter", exceptionFilter)
400         .a("level", level)
401         .a("requestDetail", requestDetail)
402         .a("requestFilter", requestFilter)
403         .a("responseDetail", responseDetail)
404         .a("responseFilter", responseFilter);
405      // @formatter:on
406   }
407
408   @Override /* Overridden from Object */
409   public String toString() {
410      return r(properties());
411   }
412}