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