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 java.util.logging.Level.*;
020import static org.apache.juneau.rest.logger.CallLoggingDetail.*;
021
022import java.util.concurrent.atomic.*;
023import java.util.logging.*;
024
025import org.apache.juneau.assertions.*;
026import org.apache.juneau.cp.*;
027
028/**
029 *
030 * Implementation of a {@link CallLogger} that captures log entries for testing logging itself.
031 *
032 * <p>
033 * Instead of logging messages to a log file, messages are simply kept in an internal atomic string reference.
034 * Once a message has been logged, you can use the {@link #getMessage()} or {@link #assertMessage()} methods
035 * to access and evaluate it.
036 *
037 * <p>
038 * The following example shows how the capture logger can be associated with a REST class so that logged messages can
039 * be examined.
040 *
041 * <p class='bjava'>
042 *    <jk>public class</jk> MyTests {
043 *
044 *       <jk>private static final</jk> CaptureLogger <jsf>LOGGER</jsf> = <jk>new</jk> CaptureLogger();
045 *
046 *       <jk>public static class</jk> CaptureLogger <jk>extends</jk> BasicTestCaptureCallLogger {
047 *          <jc>// How our REST class will get the logger.</jc>
048 *          <jk>public static</jk> CaptureLogger <jsm>getInstance</jsm>() {
049 *             <jk>return</jk> <jsf>LOGGER</jsf>;
050 *          }
051 *       }
052 *
053 *       <ja>@Rest</ja>(callLogger=CaptureLogger.<jk>class</jk>)
054 *       <jk>public static class</jk> TestRest <jk>implements</jk> BasicRestServlet {
055 *          <ja>@RestGet</ja>
056 *          <jk>public boolean</jk> bad() <jk>throws</jk> InternalServerError {
057 *             <jk>throw new</jk> InternalServerError(<js>"foo"</js>);
058 *          }
059 *       }
060 *
061 *       <ja>@Test</ja>
062 *       <jk>public void</jk> testBadRequestLogging() <jk>throws</jk> Exception {
063 *          <jc>// Create client that won't throw exceptions.</jc>
064 *          RestClient <jv>client</jv> = MockRestClient.<jsm>create</jsm>(TestRest.<jk>class</jk>).ignoreErrors().build();
065 *          <jc>// Make the REST call.</jc>
066 *          <jv>client</jv>.get(<js>"/bad"</js>).run().assertStatusCode(500).assertContent().contains(<js>"foo"</js>);
067 *          <jc>// Make sure the message was logged in our expected format.</jc>
068 *          <jsf>LOGGER</jsf>.assertMessageAndReset().contains(<js>"[500] HTTP GET /bad"</js>);
069 *       }
070 * }
071 * </p>
072 *
073 * <h5 class='section'>See Also:</h5><ul>
074 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerLoggingAndDebugging">Logging / Debugging</a>
075 * </ul>
076 */
077public class BasicTestCaptureCallLogger extends CallLogger {
078
079   private AtomicReference<LogRecord> lastRecord = new AtomicReference<>();
080
081   /**
082    * Constructor using default settings.
083    * <p>
084    * Uses the same settings as {@link CallLogger}.
085    */
086   public BasicTestCaptureCallLogger() {
087      super(BeanStore.INSTANCE);
088   }
089
090   /**
091    * Constructor using specific settings.
092    *
093    * @param beanStore The bean store containing injectable beans for this logger.
094    */
095   public BasicTestCaptureCallLogger(BeanStore beanStore) {
096      super(beanStore);
097   }
098
099   /**
100    * Returns an assertion of the last logged message.
101    *
102    * @return The last logged message as an assertion object.  Never <jk>null</jk>.
103    */
104   public StringAssertion assertMessage() {
105      return new StringAssertion(getMessage());
106   }
107
108   /**
109    * Returns an assertion of the last logged message and then deletes it internally.
110    *
111    * @return The last logged message as an assertion object.  Never <jk>null</jk>.
112    */
113   public StringAssertion assertMessageAndReset() {
114      return new StringAssertion(getMessageAndReset());
115   }
116
117   /**
118    * Returns the last logged message level.
119    *
120    * @return The last logged message level, or <jk>null</jk> if nothing was logged.
121    */
122   public ThrowableAssertion<Throwable> assertThrown() {
123      return new ThrowableAssertion<>(getThrown());
124   }
125
126   /**
127    * Returns the last logged message level.
128    *
129    * @return The last logged message level, or <jk>null</jk> if nothing was logged.
130    */
131   public Level getLevel() {
132      LogRecord r = lastRecord.get();
133      return r == null ? null : r.getLevel();
134   }
135
136   /**
137    * Returns the last logged message.
138    *
139    * @return The last logged message, or <jk>null</jk> if nothing was logged.
140    */
141   public String getMessage() {
142      LogRecord r = lastRecord.get();
143      return r == null ? null : r.getMessage();
144   }
145
146   /**
147    * Returns the last logged message and then deletes it internally.
148    *
149    * @return The last logged message.
150    */
151   public String getMessageAndReset() {
152      var msg = getMessage();
153      reset();
154      return msg;
155   }
156
157   /**
158    * Returns the last logged message level.
159    *
160    * @return The last logged message level, or <jk>null</jk> if nothing was logged.
161    */
162   public Throwable getThrown() {
163      LogRecord r = lastRecord.get();
164      return r == null ? null : r.getThrown();
165   }
166
167   /**
168    * Resets the internal message buffer.
169    *
170    * @return This object.
171    */
172   public BasicTestCaptureCallLogger reset() {
173      this.lastRecord.set(null);
174      return this;
175   }
176
177   @Override
178   protected Builder init(BeanStore beanStore) {
179      // @formatter:off
180      return super.init(beanStore)
181         .normalRules(  // Rules when debugging is not enabled.
182            CallLoggerRule.create(beanStore)  // Log 500+ errors with status-line and header information.
183               .statusFilter(x -> x >= 500)
184               .level(SEVERE)
185               .requestDetail(HEADER)
186               .responseDetail(HEADER)
187               .build(),
188            CallLoggerRule.create(beanStore)  // Log 400-500 errors with just status-line information.
189               .statusFilter(x -> x >= 400)
190               .level(WARNING)
191               .requestDetail(STATUS_LINE)
192               .responseDetail(STATUS_LINE)
193               .build()
194         )
195         .debugRules(  // Rules when debugging is enabled.
196            CallLoggerRule.create(beanStore)  // Log everything with full details.
197               .level(SEVERE)
198               .requestDetail(ENTITY)
199               .responseDetail(ENTITY)
200               .build()
201         )
202      ;
203      // @formatter:on
204   }
205
206   @Override
207   protected void log(Level level, String msg, Throwable e) {
208      var r = new LogRecord(level, msg);
209      r.setThrown(e);
210      this.lastRecord.set(r);
211   }
212}