JUNEAU-106 Improved REST debugging.
diff --git a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/StackTraceDatabaseTest.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/StackTraceDatabaseTest.java
new file mode 100644
index 0000000..967e989
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/StackTraceDatabaseTest.java
@@ -0,0 +1,56 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.utils;
+
+import static org.junit.Assert.*;
+
+import org.junit.*;
+
+public class StackTraceDatabaseTest {
+
+ @Test
+ public void testBasic() {
+ Throwable t1 = new Throwable();
+ t1.fillInStackTrace();
+ Throwable t2 = new Throwable();
+ t2.fillInStackTrace();
+
+ StackTraceDatabase db = new StackTraceDatabase();
+ StackTraceInfo t1a = db.getStackTraceInfo(t1, Integer.MAX_VALUE);
+ StackTraceInfo t1b = db.getStackTraceInfo(t1, Integer.MAX_VALUE);
+ StackTraceInfo t2a = db.getStackTraceInfo(t2, Integer.MAX_VALUE);
+ assertEquals(t1a.getHash(), t1b.getHash());
+ assertNotEquals(t1a.getHash(), t2a.getHash());
+ assertEquals(1, t1a.getCount());
+ assertEquals(2, t1b.getCount());
+ assertEquals(1, t2a.getCount());
+ }
+
+ @Test
+ public void testTimeout() {
+ Throwable t1 = new Throwable();
+ t1.fillInStackTrace();
+ Throwable t2 = new Throwable();
+ t2.fillInStackTrace();
+
+ StackTraceDatabase db = new StackTraceDatabase();
+ StackTraceInfo t1a = db.getStackTraceInfo(t1, -1);
+ StackTraceInfo t1b = db.getStackTraceInfo(t1, -1);
+ StackTraceInfo t2a = db.getStackTraceInfo(t2, -1);
+ assertEquals(t1a.getHash(), t1b.getHash());
+ assertNotEquals(t1a.getHash(), t2a.getHash());
+ assertEquals(1, t1a.getCount());
+ assertEquals(1, t1b.getCount());
+ assertEquals(1, t2a.getCount());
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceDatabase.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceDatabase.java
new file mode 100644
index 0000000..7d4e042
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceDatabase.java
@@ -0,0 +1,64 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.utils;
+
+import java.util.concurrent.*;
+
+/**
+ * An in-memory cache of stack traces.
+ *
+ * <p>
+ * Used for preventing duplication of stack traces in log files and replacing them with small hashes.
+ */
+public class StackTraceDatabase {
+
+ private final ConcurrentHashMap<Integer,StackTraceInfo> DB = new ConcurrentHashMap<>();
+
+ /**
+ * Retrieves the stack trace information for the specified exception.
+ *
+ * @param e The exception.
+ * @param timeout The timeout in milliseconds to cache the hash for this stack trace.
+ * @return The stack trace info, never <jk>null</jk>.
+ */
+ public StackTraceInfo getStackTraceInfo(Throwable e, int timeout) {
+ int hash = hash(e);
+ StackTraceInfo stc = DB.get(hash);
+ if (stc != null && stc.timeout > System.currentTimeMillis()) {
+ stc.incrementAndClone();
+ return stc.clone();
+ }
+ synchronized (DB) {
+ stc = new StackTraceInfo(timeout, hash);
+ DB.put(hash, stc);
+ return stc.clone();
+ }
+ }
+
+ private static int hash(Throwable t) {
+ int i = 0;
+ while (t != null) {
+ for (StackTraceElement e : t.getStackTrace())
+ i ^= e.hashCode();
+ t = t.getCause();
+ }
+ return i;
+ }
+
+ /**
+ * Clears out the stack trace cache.
+ */
+ public void reset() {
+ DB.clear();
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceInfo.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceInfo.java
new file mode 100644
index 0000000..9d14436
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StackTraceInfo.java
@@ -0,0 +1,63 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.utils;
+
+import java.util.concurrent.atomic.*;
+
+/**
+ * Represents an entry in {@link StackTraceDatabase}.
+ */
+public class StackTraceInfo {
+ AtomicInteger count;
+ long timeout;
+ String hash;
+
+ StackTraceInfo(long timeout, int hash) {
+ this.count = new AtomicInteger(1);
+ this.timeout = System.currentTimeMillis() + timeout;
+ this.hash = Integer.toHexString(hash);
+ }
+
+ private StackTraceInfo(int count, long timeout, String hash) {
+ this.count = new AtomicInteger(count);
+ this.timeout = timeout;
+ this.hash = hash;
+ }
+
+ @Override
+ public StackTraceInfo clone() {
+ return new StackTraceInfo(count.intValue(), timeout, hash);
+ }
+
+ /**
+ * Returns the number of times this stack trace was encountered.
+ *
+ * @return The number of times this stack trace was encountered.
+ */
+ public int getCount() {
+ return count.intValue();
+ }
+
+ /**
+ * Returns an 8-byte hash of the stack trace.
+ *
+ * @return An 8-byte hash of the stack trace.
+ */
+ public String getHash() {
+ return hash;
+ }
+
+ StackTraceInfo incrementAndClone() {
+ return new StackTraceInfo(count.incrementAndGet(), timeout, hash);
+ }
+}
diff --git a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockRest.java b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockRest.java
index 37b6add..15c4a7c 100644
--- a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockRest.java
+++ b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockRest.java
@@ -219,6 +219,17 @@
*/
public Builder debug() {
this.debug = true;
+ header("X-Debug", true);
+ return this;
+ }
+
+ /**
+ * Enable no-trace mode.
+ *
+ * @return This object (for method chaining).
+ */
+ public Builder noTrace() {
+ header("X-NoTrace", true);
return this;
}
diff --git a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockServletRequest.java b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockServletRequest.java
index 8978f65..9f63c4d 100644
--- a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockServletRequest.java
+++ b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockServletRequest.java
@@ -39,7 +39,7 @@
public class MockServletRequest implements HttpServletRequest, MockHttpRequest {
private String method = "GET";
- private Map<String,String[]> queryData;
+ private Map<String,String[]> queryDataMap = new LinkedHashMap<>();
private Map<String,String[]> formDataMap;
private Map<String,String[]> headerMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private Map<String,Object> attributeMap = new LinkedHashMap<>();
@@ -223,7 +223,16 @@
*/
@Override /* MockHttpRequest */
public MockServletRequest uri(String uri) {
- this.uri = emptyIfNull(uri);
+ uri = emptyIfNull(uri);
+ this.uri = uri;
+
+ if (uri.indexOf('?') != -1) {
+ String qs = uri.substring(uri.indexOf('?') + 1);
+ if (qs.indexOf('#') != -1)
+ qs = qs.substring(0, qs.indexOf('#'));
+ queryDataMap.putAll(RestUtils.parseQuery(qs));
+ }
+
return this;
}
@@ -675,21 +684,17 @@
@Override /* HttpServletRequest */
public Map<String,String[]> getParameterMap() {
- if (queryData == null) {
- try {
- if ("POST".equalsIgnoreCase(method)) {
- if (formDataMap != null)
- queryData = formDataMap;
- else
- queryData = RestUtils.parseQuery(IOUtils.read(body));
- } else {
- queryData = RestUtils.parseQuery(getQueryString());
+ if ("POST".equalsIgnoreCase(method)) {
+ if (formDataMap == null) {
+ try {
+ formDataMap = RestUtils.parseQuery(IOUtils.read(body));
+ } catch (IOException e) {
+ e.printStackTrace();
}
- } catch (Exception e) {
- throw new RuntimeException(e);
}
+ return formDataMap;
}
- return queryData;
+ return queryDataMap;
}
@Override /* HttpServletRequest */
@@ -897,11 +902,16 @@
@Override /* HttpServletRequest */
public String getQueryString() {
if (queryString == null) {
- queryString = "";
- if (uri.indexOf('?') != -1) {
- queryString = uri.substring(uri.indexOf('?') + 1);
- if (queryString.indexOf('#') != -1)
- queryString = queryString.substring(0, queryString.indexOf('#'));
+ if (queryDataMap.isEmpty())
+ queryString = "";
+ else {
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry<String,String[]> e : queryDataMap.entrySet())
+ if (e.getValue() == null)
+ sb.append(sb.length() == 0 ? "" : "&").append(urlEncode(e.getKey()));
+ else for (String v : e.getValue())
+ sb.append(sb.length() == 0 ? "" : "&").append(urlEncode(e.getKey())).append('=').append(urlEncode(v));
+ queryString = sb.toString();
}
}
return isEmpty(queryString) ? null : queryString;
@@ -1120,15 +1130,14 @@
* @return This object (for method chaining).
*/
public MockServletRequest query(String key, Object value) {
- if (queryData == null)
- queryData = new LinkedHashMap<>();
String s = stringify(value);
- String[] existing = queryData.get(key);
+ String[] existing = queryDataMap.get(key);
if (existing == null)
existing = new String[]{s};
else
existing = new AList<>().appendAll(Arrays.asList(existing)).append(s).toArray(new String[0]);
- queryData.put(key, existing);
+ queryDataMap.put(key, existing);
+ queryString = null;
return this;
}
@@ -1435,8 +1444,7 @@
* @return This object (for method chaining).
*/
public MockServletRequest debug() {
- this.debug = true;
- return this;
+ return debug(true);
}
/**
@@ -1450,6 +1458,33 @@
*/
public MockServletRequest debug(boolean value) {
this.debug = value;
+ header("X-Debug", value ? true : null);
+ return this;
+ }
+
+ /**
+ * Enabled no-trace on this request.
+ *
+ * <p>
+ * Prevents errors from being logged on the server side if no-trace per-request is enabled.
+ *
+ * @return This object (for method chaining).
+ */
+ public MockServletRequest noTrace() {
+ return noTrace(true);
+ }
+
+ /**
+ * Enabled debug mode on this request.
+ *
+ * <p>
+ * Prevents errors from being logged on the server side if no-trace per-request is enabled.
+ *
+ * @param value The enable flag value.
+ * @return This object (for method chaining).
+ */
+ public MockServletRequest noTrace(boolean value) {
+ header("X-NoTrace", true);
return this;
}
}
diff --git a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockServletResponse.java b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockServletResponse.java
index b289bec..ec18b0e 100644
--- a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockServletResponse.java
+++ b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockServletResponse.java
@@ -97,11 +97,33 @@
this.characterEncoding = charset;
}
+ /**
+ * Fluent setter for {@link #setCharacterEncoding(String)}.
+ *
+ * @param value The new property value.
+ * @return This object (for method chaining).
+ */
+ public MockServletResponse characterEncoding(String value) {
+ setCharacterEncoding(value);
+ return this;
+ }
+
@Override /* HttpServletResponse */
public void setContentLength(int len) {
this.contentLength = len;
}
+ /**
+ * Fluent setter for {@link #setContentLength(int)}.
+ *
+ * @param value The new property value.
+ * @return This object (for method chaining).
+ */
+ public MockServletResponse contentLength(int value) {
+ setContentLength(value);
+ return this;
+ }
+
@Override /* HttpServletResponse */
public void setContentLengthLong(long len) {
this.contentLength = len;
@@ -112,11 +134,33 @@
setHeader("Content-Type", type);
}
+ /**
+ * Fluent setter for {@link #setContentType(String)}.
+ *
+ * @param value The new property value.
+ * @return This object (for method chaining).
+ */
+ public MockServletResponse contentType(String value) {
+ setContentType(value);
+ return this;
+ }
+
@Override /* HttpServletResponse */
public void setBufferSize(int size) {
this.bufferSize = size;
}
+ /**
+ * Fluent setter for {@link #bufferSize(int)}.
+ *
+ * @param value The new property value.
+ * @return This object (for method chaining).
+ */
+ public MockServletResponse bufferSize(int value) {
+ setBufferSize(value);
+ return this;
+ }
+
@Override /* HttpServletResponse */
public int getBufferSize() {
return bufferSize;
@@ -144,6 +188,17 @@
this.locale = loc;
}
+ /**
+ * Fluent setter for {@link #setLocale(Locale)}.
+ *
+ * @param value The new property value.
+ * @return This object (for method chaining).
+ */
+ public MockServletResponse locale(Locale value) {
+ setLocale(value);
+ return this;
+ }
+
@Override /* HttpServletResponse */
public Locale getLocale() {
return locale;
@@ -215,6 +270,18 @@
headerMap.put(name, new String[] {value});
}
+ /**
+ * Fluent setter for {@link #setHeader(String,String)}.
+ *
+ * @param name The header name.
+ * @param value The new header value.
+ * @return This object (for method chaining).
+ */
+ public MockServletResponse header(String name, String value) {
+ setHeader(name, value);
+ return this;
+ }
+
@Override /* HttpServletResponse */
public void setIntHeader(String name, int value) {
headerMap.put(name, new String[] {String.valueOf(value)});
@@ -230,6 +297,17 @@
this.sc = sc;
}
+ /**
+ * Fluent setter for {@link #setStatus(int)}.
+ *
+ * @param value The new property value.
+ * @return This object (for method chaining).
+ */
+ public MockServletResponse status(int value) {
+ setStatus(value);
+ return this;
+ }
+
@Override /* HttpServletResponse */
public void setStatus(int sc, String sm) {
this.sc = sc;
diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/BasicRestCallLoggerTest.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/BasicRestCallLoggerTest.java
new file mode 100644
index 0000000..611f10a
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/BasicRestCallLoggerTest.java
@@ -0,0 +1,592 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.rest;
+
+import static org.junit.Assert.*;
+import static org.apache.juneau.rest.RestCallLoggingDetail.*;
+import static java.util.logging.Level.*;
+
+import java.util.logging.*;
+import java.util.regex.*;
+
+import org.apache.juneau.internal.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.rest.mock2.*;
+import org.junit.*;
+import org.junit.runners.*;
+
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class BasicRestCallLoggerTest {
+
+ static class TestLogger extends Logger {
+ Level level;
+ String msg;
+ Throwable t;
+
+ protected TestLogger() {
+ super(null, null);
+ }
+
+ @Override
+ public void log(Level level, String msg, Throwable t) {
+ this.level = level;
+ this.msg = msg;
+ this.t = t;
+ }
+
+ public void check(Level level, String msg, boolean hasThrowable) {
+ boolean isNot = (msg != null && msg.length() > 0 && msg.charAt(0) == '!');
+ if (isNot)
+ msg = msg.substring(1);
+ if (msg != null && msg.indexOf('*') != -1) {
+ Pattern p = StringUtils.getMatchPattern(msg, Pattern.DOTALL);
+ boolean eq = p.matcher(this.msg).matches();
+ if (isNot ? eq : ! eq)
+ fail("Message text didn't match [2].\nExpected=["+msg+"]\nActual=["+this.msg+"]");
+ } else {
+ boolean eq = StringUtils.isEquals(this.msg, msg);
+ if (isNot ? eq : ! eq)
+ fail("Message text didn't match [1].\nExpected=["+msg+"]\nActual=["+this.msg+"]");
+ }
+
+ assertEquals("Message level didn't match.", level, this.level);
+ if (hasThrowable && t == null)
+ fail("Throwable not present");
+ if (t != null && ! hasThrowable)
+ fail("Throwable present.");
+ }
+ }
+
+ private RestCallLoggerConfig.Builder config() {
+ return RestCallLoggerConfig.create();
+ }
+
+ private BasicRestCallLogger logger(Logger l) {
+ return new BasicRestCallLogger(null, l);
+ }
+
+ private RestCallLoggerRule.Builder rule() {
+ return RestCallLoggerRule.create();
+ }
+
+ private MockServletRequest req() {
+ return MockServletRequest.create();
+ }
+
+ private MockServletResponse res() {
+ return MockServletResponse.create();
+ }
+
+ private MockServletResponse res(int status) {
+ return MockServletResponse.create().status(status);
+ }
+
+ private String[] strings(String...s) {
+ return s;
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // No logging
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void a01_noRules() {
+ RestCallLoggerConfig lc = config().build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc);
+ MockServletRequest req = req();
+ MockServletResponse res = res();
+
+ cl.log(lc, req, res);
+ tc.check(null, null, false);
+ }
+
+ @Test
+ public void a02_levelOff() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").level(OFF).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc);
+ MockServletRequest req = req();
+ MockServletResponse res = res();
+
+ cl.log(lc, req, res);
+ tc.check(null, null, false);
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Basic logging
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void b01_short_short() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").req(SHORT).res(SHORT).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc);
+ MockServletRequest req = req().uri("/foo").query("bar", "baz");
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200] HTTP GET /foo", false);
+ }
+
+ @Test
+ public void b02_short_short_default() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc);
+ MockServletRequest req = req().uri("/foo").query("bar", "baz");
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200] HTTP GET /foo", false);
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Stack trace hashing.
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void c01_stackTraceHashing_on() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").req(SHORT).res(SHORT).build()
+ )
+ .stackTraceHashing()
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ Exception e = new StringIndexOutOfBoundsException();
+ MockServletRequest req = req().uri("/foo").query("bar", "baz").attribute("Exception", e);
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200,*.1] HTTP GET /foo", true);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200,*.2] HTTP GET /foo", false);
+ }
+
+ @Test
+ public void c02_stackTraceHashing_on_explicit() {
+
+ for (String s : strings("true", "TRUE")) {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").req(SHORT).res(SHORT).build()
+ )
+ .stackTraceHashing(s)
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ Exception e = new StringIndexOutOfBoundsException();
+ MockServletRequest req = req().uri("/foo").query("bar", "baz").attribute("Exception", e);
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200,*.1] HTTP GET /foo", true);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200,*.2] HTTP GET /foo", false);
+ }
+ }
+
+ @Test
+ public void c03_stackTraceHashing_off() {
+ for (String s : strings("false", "FALSE", "foo", null)) {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").req(SHORT).res(SHORT).build()
+ )
+ .stackTraceHashing(s)
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ Exception e = new StringIndexOutOfBoundsException();
+ MockServletRequest req = req().uri("/foo").query("bar", "baz").attribute("Exception", e);
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200] HTTP GET /foo", true);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200] HTTP GET /foo", true);
+ }
+ }
+
+ @Test
+ public void c04_stackTraceHashing_default() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").req(SHORT).res(SHORT).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ Exception e = new StringIndexOutOfBoundsException();
+ MockServletRequest req = req().uri("/foo").query("bar", "baz").attribute("Exception", e);
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200] HTTP GET /foo", true);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200] HTTP GET /foo", true);
+ }
+
+ @Test
+ public void c05_stackTraceHashing_timeout_on() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").req(SHORT).res(SHORT).build()
+ )
+ .stackTraceHashing("100000")
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ Exception e = new StringIndexOutOfBoundsException();
+ MockServletRequest req = req().uri("/foo").query("bar", "baz").attribute("Exception", e);
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200,*.1] HTTP GET /foo", true);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200,*.2] HTTP GET /foo", false);
+ }
+
+ @Test
+ public void c06_stackTraceHashing_timeout_off() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").req(SHORT).res(SHORT).build()
+ )
+ .stackTraceHashing("-1")
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ Exception e = new StringIndexOutOfBoundsException();
+ MockServletRequest req = req().uri("/foo").query("bar", "baz").attribute("Exception", e);
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200,*.1] HTTP GET /foo", true);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "[200,*.1] HTTP GET /foo", true);
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Various logging options.
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void d01a_requestLength_on() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").req(MEDIUM).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req().attribute("RequestBody", "foo".getBytes());
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "*\tRequest length: 3 bytes\n*", false);
+ }
+
+ @Test
+ public void d01b_requestLength_off() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").req(SHORT).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req().attribute("RequestBody", "foo".getBytes());
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "!*\tRequest length: 3 bytes\n*", false);
+ }
+
+ @Test
+ public void d02a_responseCode_on() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").res(MEDIUM).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req();
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "*\tResponse code: 200\n*", false);
+ }
+
+ @Test
+ public void d02b_responseCode_off() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").res(SHORT).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req();
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "!*\tResponse code: 200\n*", false);
+ }
+
+ @Test
+ public void d03a_responseLength_on() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").res(MEDIUM).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req().attribute("ResponseBody", "foo".getBytes());
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "*\tResponse length: 3 bytes\n*", false);
+ }
+
+ @Test
+ public void d03b_responseLength_off() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").res(SHORT).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req().attribute("ResponseBody", "foo".getBytes());
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "!*\tResponse length: 3 bytes\n*", false);
+ }
+
+ @Test
+ public void d04a_execTime_on() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").res(MEDIUM).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req().attribute("ExecTime", 123l);
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "*\tExec time: 123ms\n*", false);
+ }
+
+ @Test
+ public void d04b_execTime_off() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").res(SHORT).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req().attribute("ExecTime", 123l);
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "!*\tExec time: 123ms\n*", false);
+ }
+
+ @Test
+ public void d05a_requestHeaders_on() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").req(MEDIUM).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req().header("Foo", "bar");
+ MockServletResponse res = res(200);
+
+ SimpleJsonSerializer.DEFAULT.println(req.getHeaderNames());
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "*---Request Headers---\n\tFoo: bar\n*", false);
+ }
+
+ @Test
+ public void d05b_requestHeaders_off() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").req(SHORT).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req().header("Foo", "bar");
+ MockServletResponse res = res(200);
+
+ SimpleJsonSerializer.DEFAULT.println(req.getHeaderNames());
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "!*---Request Headers---*", false);
+ }
+
+ @Test
+ public void d06a_responseHeaders_on() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").res(MEDIUM).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req();
+ MockServletResponse res = res(200).header("Foo", "bar");;
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "*---Response Headers---\n\tFoo: bar\n*", false);
+ }
+
+ @Test
+ public void d06b_responseHeaders_off() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").res(SHORT).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req();
+ MockServletResponse res = res(200).header("Foo", "bar");;
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "!*---Response Headers---*", false);
+ }
+
+ @Test
+ public void d07a_requestBody_on() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").req(LONG).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req().attribute("RequestBody", "foo".getBytes());
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "*---Request Body UTF-8---\nfoo\n*", false);
+ tc.check(INFO, "*---Request Body Hex---\n66 6F 6F\n*", false);
+ }
+
+ @Test
+ public void d07b_requestBody_off() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").req(MEDIUM).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req().attribute("RequestBody", "foo".getBytes());
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "!*---Request Body UTF-8---\nfoo\n*", false);
+ tc.check(INFO, "!*---Request Body Hex---\n66 6F 6F\n*", false);
+ }
+
+ @Test
+ public void d08a_responseBody_on() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").res(LONG).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req().attribute("ResponseBody", "foo".getBytes());
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "*---Response Body UTF-8---\nfoo\n*", false);
+ tc.check(INFO, "*---Response Body Hex---\n66 6F 6F\n*", false);
+ }
+
+ @Test
+ public void d08b_responseBody_off() {
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule().codes("*").res(MEDIUM).build()
+ )
+ .build();
+ TestLogger tc = new TestLogger();
+ BasicRestCallLogger cl = logger(tc).resetStackTraces();
+ MockServletRequest req = req().attribute("ResponseBody", "foo".getBytes());
+ MockServletResponse res = res(200);
+
+ cl.log(lc, req, res);
+ tc.check(INFO, "!*---Response Body UTF-8---\nfoo\n*", false);
+ tc.check(INFO, "!*---Response Body Hex---\n66 6F 6F\n*", false);
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/RestCallLoggerConfigTest.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/RestCallLoggerConfigTest.java
new file mode 100644
index 0000000..cf0b5c8
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/RestCallLoggerConfigTest.java
@@ -0,0 +1,543 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.rest;
+
+import static org.junit.Assert.*;
+
+import java.util.logging.*;
+
+import javax.servlet.http.*;
+
+import org.apache.juneau.rest.mock2.*;
+import org.junit.*;
+import org.junit.runners.*;
+
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class RestCallLoggerConfigTest {
+
+ private String[] strings(String...s) {
+ return s;
+ }
+
+ private MockServletRequest req() {
+ return MockServletRequest.create();
+ }
+
+ private MockServletResponse res() {
+ return MockServletResponse.create();
+ }
+
+ private MockServletResponse res(int status) {
+ return MockServletResponse.create().status(status);
+ }
+
+ private RestCallLoggerConfig.Builder config() {
+ return RestCallLoggerConfig.create();
+ }
+
+ private RestCallLoggerRule.Builder rule() {
+ return RestCallLoggerRule.create();
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Basic matching
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void a01_basicMatching_noRules() {
+ HttpServletRequest req = req();
+ HttpServletResponse res = res();
+
+ RestCallLoggerConfig lc = config().build();
+
+ assertNull(lc.getRule(req, res));
+ }
+
+ @Test
+ public void a02_basicMatching_codeMatchingRule() {
+ MockServletRequest req = req();
+ MockServletResponse res = res(200);
+
+ RestCallLoggerConfig lc =
+ config()
+ .rules(
+ rule()
+ .codes("200")
+ .build()
+ )
+ .build();
+
+ assertNotNull(lc.getRule(req, res));
+
+ res.status(201);
+ assertNull(lc.getRule(req, res));
+ }
+
+ @Test
+ public void a03_basicMatching_exceptionMatchingRule() {
+ MockServletRequest req = req();
+ MockServletResponse res = res();
+
+ RestCallLoggerConfig lc =
+ config()
+ .rule(
+ rule()
+ .exceptions("IndexOutOfBounds*")
+ .build()
+ )
+ .build();
+
+ assertNull(lc.getRule(req, res));
+
+ req.attribute("Exception", new IndexOutOfBoundsException());
+ assertNotNull(lc.getRule(req, res));
+ }
+
+ @Test
+ public void a04_basicMatching_debugMatching() {
+ MockServletRequest req = req();
+ MockServletResponse res = res();
+
+ RestCallLoggerConfig lc =
+ config()
+ .debug("per-request")
+ .rule(
+ rule()
+ .exceptions("IndexOutOfBounds*")
+ .debugOnly()
+ .build()
+ )
+ .build();
+
+ assertNull(lc.getRule(req, res));
+
+ req.attribute("Exception", new IndexOutOfBoundsException());
+ assertNull(lc.getRule(req, res));
+
+ req.attribute("Debug", true);
+ assertNotNull(lc.getRule(req, res));
+
+ req.attribute("Debug", null);
+ req.header("X-Debug", true);
+ assertNotNull(lc.getRule(req, res));
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Parent matching
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void b01_parentMatching() {
+ MockServletRequest req = req();
+ MockServletResponse res = res();
+
+ RestCallLoggerConfig lc =
+ config()
+ .debug("per-request")
+ .rule(
+ rule()
+ .exceptions("IndexOutOfBounds*")
+ .debugOnly()
+ .build()
+ )
+ .build();
+ lc = RestCallLoggerConfig.create().parent(lc).build();
+
+ assertNull(lc.getRule(req, res));
+
+ req.attribute("Exception", new IndexOutOfBoundsException());
+ assertNull(lc.getRule(req, res));
+
+ req.attribute("Debug", true);
+ assertNotNull(lc.getRule(req, res));
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Disabled
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void c01_disabled() {
+ MockServletRequest req = req();
+ MockServletResponse res = res(200);
+
+ RestCallLoggerConfig lc =
+ config()
+ .disabled()
+ .rule(
+ rule()
+ .codes("*")
+ .build()
+ )
+ .build();
+
+ assertNull(lc.getRule(req, res));
+ }
+
+ @Test
+ public void c02_disabled_trueValues() {
+ MockServletRequest req = req();
+ MockServletResponse res = res(200);
+
+ for (String s : strings("true", "TRUE")) {
+ RestCallLoggerConfig lc =
+ config()
+ .disabled(s)
+ .rule(
+ rule()
+ .codes("*")
+ .build()
+ )
+ .build();
+
+ assertNull(lc.getRule(req, res));
+ }
+ }
+
+ @Test
+ public void c03_disabled_falseValues() {
+ MockServletRequest req = req();
+ MockServletResponse res = res(200);
+
+ for (String s : strings("false", "FALSE", "foo", null)) {
+ RestCallLoggerConfig lc =
+ config()
+ .disabled(s)
+ .rule(
+ rule()
+ .codes("*")
+ .build()
+ )
+ .build();
+
+ assertNotNull(lc.getRule(req, res));
+ }
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Debug
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void d01_debugAlways() {
+ MockServletRequest req = req();
+
+ RestCallLoggerConfig lc =
+ config()
+ .debugAlways()
+ .build();
+
+ assertTrue(lc.isDebug(req));
+ }
+
+ @Test
+ public void d02_debug_trueValues() {
+ MockServletRequest req = req();
+
+ for (String s : strings("always", "ALWAYS")) {
+ RestCallLoggerConfig lc =
+ config()
+ .debug(s)
+ .build();
+
+ assertTrue(lc.isDebug(req));
+ }
+ }
+
+ @Test
+ public void d03_debug_falseValues() {
+ MockServletRequest req = req();
+
+ for (String s : strings("never", "NEVER", "foo", null)) {
+ RestCallLoggerConfig lc =
+ config()
+ .debug(s)
+ .build();
+
+ assertFalse(lc.isDebug(req));
+ }
+ }
+
+ @Test
+ public void d04_debug_perRequest() {
+ MockServletRequest req = req();
+ MockServletRequest reqDebug = req().debug();
+ MockServletRequest reqDebugAttrTrue = req().attribute("Debug", true);
+ MockServletRequest reqDebugAttrFalse = req().attribute("Debug", false);
+ MockServletRequest reqDebugAttrOther = req().attribute("Debug", "foo");
+
+ for (String s : strings("per-request", "PER-REQUEST")) {
+ RestCallLoggerConfig lc =
+ config()
+ .debug(s)
+ .build();
+
+ assertFalse(lc.isDebug(req));
+ assertTrue(lc.isDebug(reqDebug));
+ assertTrue(lc.isDebug(reqDebugAttrTrue));
+ assertFalse(lc.isDebug(reqDebugAttrFalse));
+ assertFalse(lc.isDebug(reqDebugAttrOther));
+ }
+ }
+
+ @Test
+ public void d05_debugPerRequest() {
+ MockServletRequest req = req();
+ MockServletRequest reqDebug = req().debug();
+ MockServletRequest reqDebugAttrTrue = req().attribute("Debug", true);
+ MockServletRequest reqDebugAttrFalse = req().attribute("Debug", false);
+ MockServletRequest reqDebugAttrOther = req().attribute("Debug", "foo");
+
+ RestCallLoggerConfig lc =
+ config()
+ .debugPerRequest()
+ .build();
+
+ assertFalse(lc.isDebug(req));
+ assertTrue(lc.isDebug(reqDebug));
+ assertTrue(lc.isDebug(reqDebugAttrTrue));
+ assertFalse(lc.isDebug(reqDebugAttrFalse));
+ assertFalse(lc.isDebug(reqDebugAttrOther));
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // No-trace
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void e01_noTraceAlways() {
+ MockServletRequest req = req();
+ MockServletResponse res = res();
+
+ RestCallLoggerConfig lc =
+ config()
+ .noTraceAlways()
+ .rule(
+ rule()
+ .codes("*")
+ .build()
+ )
+ .build();
+
+ assertNull(lc.getRule(req, res));
+ }
+
+ @Test
+ public void e02_noTrace_trueValues() {
+ MockServletRequest req = req();
+ MockServletResponse res = res();
+
+ for (String s : strings("always", "ALWAYS")) {
+ RestCallLoggerConfig lc =
+ config()
+ .noTrace(s)
+ .rule(
+ rule()
+ .codes("*")
+ .build()
+ )
+ .build();
+
+ assertNull(lc.getRule(req, res));
+ }
+ }
+
+ @Test
+ public void e03_noTrace_falseValues() {
+ MockServletRequest req = req();
+ MockServletResponse res = res();
+
+ for (String s : strings("never", "NEVER", "foo", null)) {
+ RestCallLoggerConfig lc =
+ config()
+ .noTrace(s)
+ .rule(
+ rule()
+ .codes("*")
+ .build()
+ )
+ .build();
+
+ assertNotNull(lc.getRule(req, res));
+ }
+ }
+
+ @Test
+ public void e04_noTrace_perRequest() {
+ MockServletRequest req = req();
+ MockServletRequest reqNoTrace = req().noTrace();
+ MockServletRequest reqNoTraceAttrTrue = req().attribute("NoTrace", true);
+ MockServletRequest reqNoTraceAttrFalse = req().attribute("NoTrace", false);
+ MockServletRequest reqNoTraceAttrOther = req().attribute("NoTrace", "foo");
+ MockServletResponse res = res();
+
+ for (String s : strings("per-request", "PER-REQUEST")) {
+ RestCallLoggerConfig lc =
+ config()
+ .noTrace(s)
+ .rule(
+ rule()
+ .codes("*")
+ .build()
+ )
+ .build();
+
+ assertNotNull(lc.getRule(req, res));
+ assertNull(lc.getRule(reqNoTrace, res));
+ assertNull(lc.getRule(reqNoTraceAttrTrue, res));
+ assertNotNull(lc.getRule(reqNoTraceAttrFalse, res));
+ assertNotNull(lc.getRule(reqNoTraceAttrOther, res));
+ }
+ }
+
+ @Test
+ public void e05_noTracePerRequest() {
+ MockServletRequest req = req();
+ MockServletRequest reqNoTrace = req().noTrace();
+ MockServletRequest reqNoTraceAttrTrue = req().attribute("NoTrace", true);
+ MockServletRequest reqNoTraceAttrFalse = req().attribute("NoTrace", false);
+ MockServletRequest reqNoTraceAttrOther = req().attribute("NoTrace", "foo");
+ MockServletResponse res = res();
+
+ RestCallLoggerConfig lc =
+ config()
+ .noTracePerRequest()
+ .rule(
+ rule()
+ .codes("*")
+ .build()
+ )
+ .build();
+
+ assertNotNull(lc.getRule(req, res));
+ assertNull(lc.getRule(reqNoTrace, res));
+ assertNull(lc.getRule(reqNoTraceAttrTrue, res));
+ assertNotNull(lc.getRule(reqNoTraceAttrFalse, res));
+ assertNotNull(lc.getRule(reqNoTraceAttrOther, res));
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Use stack trace hashing
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void f01_stackTraceHashing() {
+ RestCallLoggerConfig lc =
+ config()
+ .stackTraceHashing()
+ .build();
+
+ assertTrue(lc.useStackTraceHashing());
+ assertEquals(Integer.MAX_VALUE, lc.getStackTraceHashingTimeout());
+ }
+
+ @Test
+ public void f02_stackTraceHashing_trueValues() {
+ for (String s : strings("true", "TRUE")) {
+ RestCallLoggerConfig lc =
+ config()
+ .stackTraceHashing(s)
+ .build();
+
+ assertTrue(lc.useStackTraceHashing());
+ assertEquals(Integer.MAX_VALUE, lc.getStackTraceHashingTimeout());
+ }
+ }
+
+ @Test
+ public void f03_stackTraceHashing_falseValues() {
+ for (String s : strings("false", "FALSE", "foo", null)) {
+ RestCallLoggerConfig lc =
+ config()
+ .stackTraceHashing(s)
+ .build();
+
+ assertFalse(lc.useStackTraceHashing());
+ }
+ }
+
+ @Test
+ public void f04_stackTraceHashing_numericValues() {
+ RestCallLoggerConfig lc =
+ config()
+ .stackTraceHashing("1")
+ .build();
+
+ assertTrue(lc.useStackTraceHashing());
+ assertEquals(1, lc.getStackTraceHashingTimeout());
+ }
+
+ @Test
+ public void f05_stackTraceHashingTimeout() {
+ RestCallLoggerConfig lc =
+ config()
+ .stackTraceHashingTimeout(1)
+ .build();
+
+ assertTrue(lc.useStackTraceHashing());
+ assertEquals(1, lc.getStackTraceHashingTimeout());
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Level
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void g01_level_default() {
+ RestCallLoggerConfig lc =
+ config()
+ .build();
+
+ assertEquals(Level.INFO, lc.getLevel());
+ }
+
+ @Test
+ public void g02_level_warningLevel() {
+ RestCallLoggerConfig lc =
+ config()
+ .level(Level.WARNING)
+ .build();
+
+ assertEquals(Level.WARNING, lc.getLevel());
+ }
+
+ @Test
+ public void g03_level_warningString() {
+ RestCallLoggerConfig lc =
+ config()
+ .level("WARNING")
+ .build();
+
+ assertEquals(Level.WARNING, lc.getLevel());
+ }
+
+ @Test
+ public void g04_level_nullLevel() {
+ RestCallLoggerConfig lc =
+ config()
+ .level((Level)null)
+ .build();
+
+ assertEquals(Level.INFO, lc.getLevel());
+ }
+
+ @Test
+ public void g05_level_nullString() {
+ RestCallLoggerConfig lc =
+ config()
+ .level((String)null)
+ .build();
+
+ assertEquals(Level.INFO, lc.getLevel());
+ }
+}
diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/RestCallLoggerRuleTest.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/RestCallLoggerRuleTest.java
new file mode 100644
index 0000000..e4058d7
--- /dev/null
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/RestCallLoggerRuleTest.java
@@ -0,0 +1,319 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.rest;
+
+import static org.junit.Assert.*;
+
+import java.util.logging.*;
+
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+import org.junit.*;
+import org.junit.runners.*;
+
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class RestCallLoggerRuleTest {
+
+ static final Throwable T1 = new IndexOutOfBoundsException();
+ static final Throwable T2 = new NoSuchMethodError();
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Status code matching
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void a01_matchingCodes_ignoreOtherFields() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().codes("200").build();
+ assertTrue(r.matches(200, true, null));
+ assertTrue(r.matches(200, false, null));
+ assertTrue(r.matches(200, true, T1));
+ assertFalse(r.matches(201, true, null));
+ assertFalse(r.matches(199, true, null));
+ assertFalse(r.matches(201, false, null));
+ assertFalse(r.matches(199, false, null));
+ }
+
+ @Test
+ public void a02_matchingCodes_singleValue() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().codes("200").build();
+ assertTrue(r.matches(200, true, null));
+ assertFalse(r.matches(201, true, null));
+ assertFalse(r.matches(199, true, null));
+ }
+
+ @Test
+ public void a03_matchingCodes_range() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().codes("200-299").build();
+ assertTrue(r.matches(200, true, null));
+ assertTrue(r.matches(201, true, null));
+ assertTrue(r.matches(299, true, null));
+ assertFalse(r.matches(199, true, null));
+ assertFalse(r.matches(300, true, null));
+ }
+
+ @Test
+ public void a04_matchingCodes_openEnded() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().codes(">=200").build();
+ assertTrue(r.matches(200, true, null));
+ assertTrue(r.matches(201, true, null));
+ assertTrue(r.matches(299, true, null));
+ assertTrue(r.matches(300, true, null));
+ assertFalse(r.matches(199, true, null));
+ }
+
+ @Test
+ public void a05_matchingCodes_matchAll() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().codes("*").build();
+ assertTrue(r.matches(200, true, null));
+ }
+
+ @Test
+ public void a06_matchingCodes_null() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().codes(null).build();
+ assertTrue(r.matches(200, true, null));
+ }
+
+ @Test
+ public void a07_matchingCodes_empty() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().codes("").build();
+ assertTrue(r.matches(200, true, null));
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Exception matching
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void b01_matchingException_ignoreOtherFields() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().exceptions("IndexOutOfBoundsException").build();
+ assertTrue(r.matches(200, true, T1));
+ assertTrue(r.matches(200, false, T1));
+ assertTrue(r.matches(200, true, T1));
+ assertFalse(r.matches(201, true, null));
+ assertFalse(r.matches(199, true, null));
+ assertFalse(r.matches(201, false, null));
+ assertFalse(r.matches(199, false, null));
+ }
+
+ @Test
+ public void b02_matchingException_simpleClassName() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().exceptions("IndexOutOfBoundsException").build();
+ assertTrue(r.matches(200, true, T1));
+ assertFalse(r.matches(200, true, T2));
+ }
+
+ @Test
+ public void b03_matchingException_fullClassName() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().exceptions("java.lang.IndexOutOfBoundsException").build();
+ assertTrue(r.matches(200, true, T1));
+ assertFalse(r.matches(200, true, T2));
+ }
+
+ @Test
+ public void b04_matchingException_simpleClassName_pattern() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().exceptions("IndexOutOfBounds*").build();
+ assertTrue(r.matches(200, true, T1));
+ assertFalse(r.matches(200, true, T2));
+ }
+
+ @Test
+ public void b05_matchingException_fullClassName_pattern() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().exceptions("java.lang.IndexOutOfBounds*").build();
+ assertTrue(r.matches(200, true, T1));
+ assertFalse(r.matches(200, true, T2));
+ }
+
+ @Test
+ public void b06_matchingException_null() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().exceptions(null).build();
+ assertTrue(r.matches(200, true, null));
+ assertTrue(r.matches(201, false, T1));
+ }
+
+ @Test
+ public void b07_matchingException_empty() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().exceptions("").build();
+ assertTrue(r.matches(200, true, null));
+ assertTrue(r.matches(201, false, T1));
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Debug-only matching
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void c01_debugOnly_true() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().debugOnly().build();
+ assertTrue(r.matches(200, true, T1));
+ assertFalse(r.matches(200, false, T1));
+ }
+
+ @Test
+ public void c02_debugOnly_false() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().debugOnly(false).build();
+ assertTrue(r.matches(200, true, T1));
+ assertTrue(r.matches(200, false, T1));
+ }
+
+ @Test
+ public void c03_debugOnly_null() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().debugOnly(null).build();
+ assertTrue(r.matches(200, true, T1));
+ assertTrue(r.matches(200, false, T1));
+ }
+
+ @Test
+ public void c04_debugOnly_default() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().debugOnly(null).build();
+ assertTrue(r.matches(200, true, T1));
+ assertTrue(r.matches(200, false, T1));
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Level
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void d01_level() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().level(Level.WARNING).build();
+ assertEquals(Level.WARNING, r.getLevel());
+ }
+
+ @Test
+ public void d02_level_null() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().level(null).build();
+ assertNull(r.getLevel());
+ }
+
+ @Test
+ public void d03_level_default() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().build();
+ assertNull(r.getLevel());
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Request detail
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void e01_reqDetail_small() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().req(RestCallLoggingDetail.SHORT).build();
+ assertEquals(RestCallLoggingDetail.SHORT, r.getReqDetail());
+ }
+
+ @Test
+ public void e02_reqDetail_medium() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().req(RestCallLoggingDetail.MEDIUM).build();
+ assertEquals(RestCallLoggingDetail.MEDIUM, r.getReqDetail());
+ }
+
+ @Test
+ public void e03_reqDetail_large() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().req(RestCallLoggingDetail.LONG).build();
+ assertEquals(RestCallLoggingDetail.LONG, r.getReqDetail());
+ }
+
+ @Test
+ public void e05_reqDetail_null() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().req(null).build();
+ assertEquals(RestCallLoggingDetail.SHORT, r.getReqDetail());
+ }
+
+ @Test
+ public void e06_reqDetail_default() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().build();
+ assertEquals(RestCallLoggingDetail.SHORT, r.getReqDetail());
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Response detail
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void f01_resDetail_small() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().res(RestCallLoggingDetail.SHORT).build();
+ assertEquals(RestCallLoggingDetail.SHORT, r.getResDetail());
+ }
+
+ @Test
+ public void f02_resDetail_medium() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().res(RestCallLoggingDetail.MEDIUM).build();
+ assertEquals(RestCallLoggingDetail.MEDIUM, r.getResDetail());
+ }
+
+ @Test
+ public void f03_resDetail_large() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().res(RestCallLoggingDetail.LONG).build();
+ assertEquals(RestCallLoggingDetail.LONG, r.getResDetail());
+ }
+
+ @Test
+ public void f05_resDetail_null() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().res(null).build();
+ assertEquals(RestCallLoggingDetail.SHORT, r.getResDetail());
+ }
+
+ @Test
+ public void f06_resDetail_default() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().build();
+ assertEquals(RestCallLoggingDetail.SHORT, r.getResDetail());
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Verbose
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void f01_verbose_true() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().verbose().build();
+ assertEquals(RestCallLoggingDetail.LONG, r.getReqDetail());
+ assertEquals(RestCallLoggingDetail.LONG, r.getResDetail());
+ }
+
+ @Test
+ public void f02_verbose_false() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().verbose(false).build();
+ assertEquals(RestCallLoggingDetail.SHORT, r.getReqDetail());
+ assertEquals(RestCallLoggingDetail.SHORT, r.getResDetail());
+ }
+
+ @Test
+ public void f03_verbose_null() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().verbose(null).build();
+ assertEquals(RestCallLoggingDetail.SHORT, r.getReqDetail());
+ assertEquals(RestCallLoggingDetail.SHORT, r.getResDetail());
+ }
+
+ @Test
+ public void f04_verbose_true_override() {
+ RestCallLoggerRule r = RestCallLoggerRule.create().verbose(true).req(RestCallLoggingDetail.SHORT).res(RestCallLoggingDetail.SHORT).build();
+ assertEquals(RestCallLoggingDetail.LONG, r.getReqDetail());
+ assertEquals(RestCallLoggingDetail.LONG, r.getResDetail());
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Bean instantiation
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test
+ public void g01_beanInstantiation_defaultValues() throws ParseException {
+ RestCallLoggerRule r = JsonParser.DEFAULT.parse("{}", RestCallLoggerRule.class);
+ assertEquals("{matchAll:true,req:'SHORT',res:'SHORT'}", r.toString());
+ }
+
+ @Test
+ public void g02_beanInstantiation_allValues() throws ParseException {
+ RestCallLoggerRule r = JsonParser.DEFAULT.parse("{codes:'100-200',exceptions:'Foo*',level:'WARNING',req:'LONG',res:'LONG',debugOnly:'true'}", RestCallLoggerRule.class);
+ assertEquals("{codes:'100-200',exceptions:'Foo*',debugOnly:true,level:'WARNING',req:'LONG',res:'LONG'}", r.toString());
+ }
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
index d7d555d..f5728d7 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallHandler.java
@@ -236,8 +236,10 @@
} catch (Throwable e) {
e = convertThrowable(e);
- r1 = req.getInner();
- r2 = res.getInner();
+ if (req != null)
+ r1 = req.getInner();
+ if (res != null)
+ r2 = res.getInner();
r1.setAttribute("Exception", e);
r1.setAttribute("ExecTime", System.currentTimeMillis() - startTime);
logger.log(r1, r2);
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallLogger.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallLogger.java
new file mode 100644
index 0000000..0e62c99
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/BasicRestCallLogger.java
@@ -0,0 +1,245 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.rest;
+
+import static org.apache.juneau.internal.StringUtils.*;
+import static org.apache.juneau.internal.ObjectUtils.*;
+import static org.apache.juneau.rest.RestCallLoggingDetail.*;
+
+import java.util.*;
+import java.util.logging.*;
+
+import javax.servlet.http.*;
+
+import org.apache.juneau.internal.*;
+import org.apache.juneau.rest.util.*;
+import org.apache.juneau.utils.*;
+
+/**
+ * Default implementation of the {@link RestCallLogger} interface.
+ *
+ * <p>
+ * Subclasses can override these methods to tailor logging of HTTP requests.
+ * <br>Subclasses MUST implement a no-arg public constructor or constructor that takes in a {@link RestContext} arg.
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul>
+ * <li class='link'>{@doc juneau-rest-server.LoggingAndErrorHandling}
+ * </ul>
+ */
+public class BasicRestCallLogger implements RestCallLogger {
+
+ private static final StackTraceDatabase STACK_TRACE_DB = new StackTraceDatabase();
+
+ private final Logger logger;
+ private final RestContext context;
+
+ /**
+ * Constructor.
+ *
+ * @param context The context of the resource object.
+ */
+ public BasicRestCallLogger(RestContext context) {
+ this.context = context;
+ this.logger = Logger.getLogger(getLoggerName());
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param context The context of the resource object.
+ * @param logger The logger to use for logging.
+ */
+ public BasicRestCallLogger(RestContext context, Logger logger) {
+ this.context = context;
+ this.logger = logger;
+ }
+
+ /**
+ * Returns the logger name.
+ *
+ * <p>
+ * By default returns the class name of the servlet class passed in to the context.
+ *
+ * <p>
+ * Subclasses can override this to provide their own customized logger names.
+ *
+ * @return The logger name.
+ */
+ protected String getLoggerName() {
+ return context == null ? getClass().getName() : context.getResource().getClass().getName();
+ }
+
+ /**
+ * Returns the Java logger used for logging.
+ *
+ * <p>
+ * Subclasses can provide their own logger.
+ * The default implementation returns the logger created using <c>Logger.getLogger(getClass())</c>.
+ *
+ * @return The logger used for logging.
+ */
+ protected Logger getLogger() {
+ return logger;
+ }
+
+ /**
+ * Clears out the stack trace database.
+ *
+ * @return This object (for method chaining).
+ */
+ public BasicRestCallLogger resetStackTraces() {
+ STACK_TRACE_DB.reset();
+ return this;
+ }
+
+ @Override /* RestCallLogger */
+ public void log(RestCallLoggerConfig config, HttpServletRequest req, HttpServletResponse res) {
+
+ RestCallLoggerRule rule = config.getRule(req, res);
+ if (rule == null)
+ return;
+
+ Level level = rule.getLevel();
+ if (level == null)
+ level = config.getLevel();
+
+ if (level == Level.OFF)
+ return;
+
+ Throwable e = castOrNull(req.getAttribute("Exception"), Throwable.class);
+ Long execTime = castOrNull(req.getAttribute("ExecTime"), Long.class);
+
+ RestCallLoggingDetail reqd = rule.getReqDetail(), resd = rule.getResDetail();
+
+ String method = req.getMethod();
+ int status = res.getStatus();
+ String uri = req.getRequestURI();
+ byte[] reqBody = getRequestBody(req);
+ byte[] resBody = getResponseBody(req, res);
+
+ StringBuilder sb = new StringBuilder();
+
+ if (reqd != SHORT || resd != SHORT)
+ sb.append("\n=== HTTP Request (incoming) ===================================================\n");
+
+ StackTraceInfo sti = getStackTraceInfo(config, e);
+
+ sb.append('[').append(status);
+
+ if (sti != null) {
+ int count = sti.getCount();
+ sb.append(',').append(sti.getHash()).append('.').append(count);
+ if (count > 1)
+ e = null;
+ }
+
+ sb.append("] ");
+
+ sb.append("HTTP ").append(method).append(' ').append(uri);
+
+ if (reqd != SHORT || resd != SHORT) {
+
+ if (reqd.isOneOf(MEDIUM, LONG)) {
+ String qs = req.getQueryString();
+ sb.append('?').append(qs);
+ }
+
+ if (reqBody != null && reqd.isOneOf(MEDIUM ,LONG))
+ sb.append("\n\tRequest length: ").append(reqBody.length).append(" bytes");
+
+ if (resd.isOneOf(MEDIUM, LONG))
+ sb.append("\n\tResponse code: ").append(status);
+
+ if (resBody != null && resd.isOneOf(MEDIUM, LONG))
+ sb.append("\n\tResponse length: ").append(resBody.length).append(" bytes");
+
+ if (execTime != null && resd.isOneOf(MEDIUM, LONG))
+ sb.append("\n\tExec time: ").append(execTime).append("ms");
+
+ if (reqd.isOneOf(MEDIUM, LONG)) {
+ Enumeration<String> hh = req.getHeaderNames();
+ if (hh.hasMoreElements()) {
+ sb.append("\n---Request Headers---");
+ while (hh.hasMoreElements()) {
+ String h = hh.nextElement();
+ sb.append("\n\t").append(h).append(": ").append(req.getHeader(h));
+ }
+ }
+ }
+
+ if (context != null && reqd.isOneOf(MEDIUM, LONG)) {
+ Map<String,Object> hh = context.getDefaultRequestHeaders();
+ if (! hh.isEmpty()) {
+ sb.append("\n---Default Servlet Headers---");
+ for (Map.Entry<String,Object> h : hh.entrySet()) {
+ sb.append("\n\t").append(h.getKey()).append(": ").append(h.getValue());
+ }
+ }
+ }
+
+ if (resd.isOneOf(MEDIUM, LONG)) {
+ Collection<String> hh = res.getHeaderNames();
+ if (hh.size() > 0) {
+ sb.append("\n---Response Headers---");
+ for (String h : hh) {
+ sb.append("\n\t").append(h).append(": ").append(res.getHeader(h));
+ }
+ }
+ }
+
+ if (reqBody != null && reqBody.length > 0 && reqd == LONG) {
+ try {
+ sb.append("\n---Request Body UTF-8---");
+ sb.append("\n").append(new String(reqBody, IOUtils.UTF8));
+ sb.append("\n---Request Body Hex---");
+ sb.append("\n").append(toSpacedHex(reqBody));
+ } catch (Exception e1) {
+ sb.append("\n").append(e1.getLocalizedMessage());
+ }
+ }
+
+ if (resBody != null && resBody.length > 0 && resd == LONG) {
+ try {
+ sb.append("\n---Response Body UTF-8---");
+ sb.append("\n").append(new String(resBody, IOUtils.UTF8));
+ sb.append("\n---Response Body Hex---");
+ sb.append("\n").append(toSpacedHex(resBody));
+ } catch (Exception e1) {
+ sb.append(e1.getLocalizedMessage());
+ }
+ }
+ sb.append("\n=== END ===================================================================");
+ }
+
+ getLogger().log(level, sb.toString(), e);
+ }
+
+ private byte[] getRequestBody(HttpServletRequest req) {
+ if (req instanceof CachingHttpServletRequest)
+ return ((CachingHttpServletRequest)req).getBody();
+ return castOrNull(req.getAttribute("RequestBody"), byte[].class);
+ }
+
+ private byte[] getResponseBody(HttpServletRequest req, HttpServletResponse res) {
+ if (res instanceof CachingHttpServletResponse)
+ return ((CachingHttpServletResponse)res).getBody();
+ return castOrNull(req.getAttribute("ResponseBody"), byte[].class);
+ }
+
+ private StackTraceInfo getStackTraceInfo(RestCallLoggerConfig config, Throwable e) {
+ if (e == null || ! config.useStackTraceHashing())
+ return null;
+ return STACK_TRACE_DB.getStackTraceInfo(e, config.getStackTraceHashingTimeout());
+ }
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallLogger.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallLogger.java
new file mode 100644
index 0000000..c915009
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallLogger.java
@@ -0,0 +1,45 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.rest;
+
+import javax.servlet.http.*;
+
+/**
+ * Interface class used for logging HTTP requests to the log file.
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul>
+ * <li class='jf'>{@link RestContext#REST_logger}
+ * <li class='link'>{@doc juneau-rest-server.LoggingAndErrorHandling}
+ * </ul>
+ */
+public interface RestCallLogger {
+
+ /**
+ * Represents no RestLogger.
+ *
+ * <p>
+ * Used on annotation to indicate that the value should be inherited from the parent class, and
+ * ultimately {@link BasicRestLogger} if not specified at any level.
+ */
+ public interface Null extends RestCallLogger {}
+
+ /**
+ * Called at the end of a servlet request to log the request.
+ *
+ * @param config The logging configuration.
+ * @param req The servlet request.
+ * @param res The servlet response.
+ */
+ public void log(RestCallLoggerConfig config, HttpServletRequest req, HttpServletResponse res);
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallLoggerConfig.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallLoggerConfig.java
new file mode 100644
index 0000000..d09a462
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallLoggerConfig.java
@@ -0,0 +1,446 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.rest;
+
+import static org.apache.juneau.internal.StringUtils.*;
+
+import java.util.*;
+import java.util.logging.*;
+
+import javax.servlet.http.*;
+
+import org.apache.juneau.internal.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Represents a set of logging rules for how to handle logging of HTTP requests/responses.
+ */
+public class RestCallLoggerConfig {
+
+ private final RestCallLoggerRule[] rules;
+ private final boolean disabled, debugAlways, debugPerRequest, noTraceAlways, noTracePerRequest, useStackTraceHashing;
+ private final int stackTraceHashingTimeout;
+ private final Level level;
+
+ RestCallLoggerConfig(Builder b) {
+ RestCallLoggerConfig p = b.parent;
+
+ this.disabled = bool(b.disabled, p == null ? false : p.disabled);
+ this.debugAlways = always(b.debug, p == null ? false : p.debugAlways);
+ this.debugPerRequest = perRequest(b.debug, p == null ? false : p.debugPerRequest);
+ this.noTraceAlways = always(b.noTrace, p == null ? false : p.noTraceAlways);
+ this.noTracePerRequest = perRequest(b.noTrace, p == null ? false : p.noTracePerRequest);
+ this.useStackTraceHashing = boolOrNum(b.stackTraceHashing, p == null ? false : p.useStackTraceHashing);
+ this.stackTraceHashingTimeout = number(b.stackTraceHashing, p == null ? Integer.MAX_VALUE : p.stackTraceHashingTimeout);
+ this.level = level(b.level, p == null ? Level.INFO : p.level);
+
+ ArrayList<RestCallLoggerRule> rules = new ArrayList<>();
+ if (p != null)
+ rules.addAll(Arrays.asList(p.rules));
+ rules.addAll(b.rules);
+ this.rules = rules.toArray(new RestCallLoggerRule[rules.size()]);
+ }
+
+ private boolean always(String s, boolean def) {
+ if (s == null)
+ return def;
+ return "always".equalsIgnoreCase(s);
+ }
+
+ private boolean perRequest(String s, boolean def) {
+ if (s == null)
+ return def;
+ return "per-request".equalsIgnoreCase(s);
+ }
+
+ private boolean bool(String s, boolean def) {
+ if (s == null)
+ return def;
+ return "true".equalsIgnoreCase(s);
+ }
+
+ private boolean boolOrNum(String s, boolean def) {
+ if (s == null)
+ return def;
+ return "true".equalsIgnoreCase(s) || isNumeric(s);
+ }
+
+ private int number(String s, int def) {
+ if (StringUtils.isNumeric(s)) {
+ try {
+ return (Integer)parseNumber(s, Integer.class);
+ } catch (ParseException e) { /* Should never happen */ }
+ }
+ return def;
+ }
+
+ private Level level(String s, Level def) {
+ if (s == null)
+ return def;
+ return Level.parse(s);
+ }
+
+ /**
+ * Creates a builder for this class.
+ *
+ * @return A new builder for this class.
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ /**
+ * Builder for {@link RestCallLoggerConfig} objects.
+ */
+ public static class Builder {
+ List<RestCallLoggerRule> rules = new ArrayList<>();
+ RestCallLoggerConfig parent;
+ String stackTraceHashing, debug, noTrace, disabled, level;
+
+ /**
+ * Sets the parent logging config.
+ *
+ * @param parent The parent logging config.
+ * @return This object (for method chaining).
+ */
+ public Builder parent(RestCallLoggerConfig parent) {
+ this.parent = parent;
+ return this;
+ }
+
+ /**
+ * Adds a new logging rule to this config.
+ *
+ * @param rule The logging rule to add to this config.
+ * @return This object (for method chaining).
+ */
+ public Builder rule(RestCallLoggerRule rule) {
+ this.rules.add(rule);
+ return this;
+ }
+
+ /**
+ * Adds new logging rules to this config.
+ *
+ * @param rules The logging rules to add to this config.
+ * @return This object (for method chaining).
+ */
+ public Builder rules(RestCallLoggerRule...rules) {
+ for (RestCallLoggerRule rule : rules)
+ this.rules.add(rule);
+ return this;
+ }
+
+ /**
+ * Enables debug mode on this config.
+ *
+ * <p>
+ * Debug mode causes the HTTP bodies to be cached in memory so that they can be logged.
+ *
+ * <p>
+ * Possible values (case-insensitive):
+ * <ul>
+ * <li><js>"always"</js> - Debug mode enabled for all requests.
+ * <li><js>"never"</js> - Debug mode disabled for all requests.
+ * <li><js>"per-request"</js> - Debug mode enabled for requests that have a <js>"X-Debug: true"</js> header.
+ * </ul>
+ *
+ * @param value The value for this property.
+ * @return This object (for method chaining).
+ */
+ public Builder debug(String value) {
+ this.debug = value;
+ return this;
+ }
+
+ /**
+ * Shortcut for calling <c>debug(<js>"always"</js>);</c>.
+ *
+ * @return This object (for method chaining).
+ */
+ public Builder debugAlways() {
+ this.debug = "always";
+ return this;
+ }
+
+ /**
+ * Shortcut for calling <c>debug(<js>"per-request"</js>);</c>.
+ *
+ * @return This object (for method chaining).
+ */
+ public Builder debugPerRequest() {
+ this.debug = "per-request";
+ return this;
+ }
+
+ /**
+ * Enables no-trace mode on this config.
+ *
+ * <p>
+ * No-trace mode prevents logging of messages to the log file.
+ *
+ * <p>
+ * Possible values (case-insensitive):
+ * <ul>
+ * <li><js>"always"</js> - No-trace mode enabled for all requests.
+ * <li><js>"never"</js> - No-trace mode disabled for all requests.
+ * <li><js>"per-request"</js> - No-trace mode enabled for requests that have a <js>"X-NoTrace: true"</js> header.
+ * </ul>
+ *
+ * @param value The value for this property.
+ * @return This object (for method chaining).
+ */
+ public Builder noTrace(String value) {
+ this.noTrace = value;
+ return this;
+ }
+
+ /**
+ * Shortcut for calling <c>noTrace(<js>"always"</js>);</c>.
+ *
+ * @return This object (for method chaining).
+ */
+ public Builder noTraceAlways() {
+ this.noTrace = "always";
+ return this;
+ }
+
+ /**
+ * Shortcut for calling <c>noTrace(<js>"per-request"</js>);</c>.
+ *
+ * @return This object (for method chaining).
+ */
+ public Builder noTracePerRequest() {
+ this.noTrace = "per-request";
+ return this;
+ }
+
+ /**
+ * Enables the use of stacktrace hashing.
+ *
+ * <p>
+ * When enabled, stacktraces will be replaced with hashes in the log file.
+ *
+ * <p>
+ * Possible values (case-insensitive):
+ * <ul>
+ * <li><js>"true"</js> - Stacktrace-hash mode enabled for all requests.
+ * <li><js>"false"</js> - Stacktrace-hash mode disabled for all requests.
+ * <li>Numeric value - Same as <js>"true"</js> but identifies a time in milliseconds during which stack traces
+ * should be hashed before starting over.
+ * <br>Useful if you cycle your log files and want to make sure stack traces are logged at least one per day
+ * (for example).
+ * </ul>
+ *
+ * @param value The value for this property.
+ * @return This object (for method chaining).
+ */
+ public Builder stackTraceHashing(String value) {
+ this.stackTraceHashing = value;
+ return this;
+ }
+
+ /**
+ * Shortcut for calling <c>stackTraceHashing(<js>"true"</js>);</c>.
+ *
+ * @return This object (for method chaining).
+ */
+ public Builder stackTraceHashing() {
+ this.stackTraceHashing = "true";
+ return this;
+ }
+
+ /**
+ * Shortcut for calling <c>stackTraceHashing(String.<jsm>valueOf</jsm>(timeout));</c>.
+ *
+ * @param timeout Time in milliseconds to hash stack traces for.
+ * @return This object (for method chaining).
+ */
+ public Builder stackTraceHashingTimeout(int timeout) {
+ this.stackTraceHashing = String.valueOf(timeout);
+ return this;
+ }
+
+ /**
+ * Disable all logging.
+ *
+ * <p>
+ * This is equivalent to <c>noTrace(<js>"always"</js>)</c> and provided for convenience.
+ *
+ * <p>
+ * Possible values (case-insensitive):
+ * <ul>
+ * <li><js>"true"</js> - Stacktrace-hash mode enabled for all requests.
+ * <li><js>"false"</js> - Stacktrace-hash mode disabled for all requests.
+ * <li>Numeric value - Same as <js>"true"</js> but identifies a time in milliseconds during which stack traces
+ * should be hashed before starting over.
+ * <br>Useful if you cycle your log files and want to make sure stack traces are logged at least one per day
+ * (for example).
+ * </ul>
+ *
+ * @param value The value for this property.
+ * @return This object (for method chaining).
+ */
+ public Builder disabled(String value) {
+ this.disabled = value;
+ return this;
+ }
+
+ /**
+ * Shortcut for calling <c>disabled(<js>"true"</js>);</c>.
+ *
+ * @return This object (for method chaining).
+ */
+ public Builder disabled() {
+ this.disabled = "true";
+ return this;
+ }
+
+ /**
+ * The default logging level.
+ *
+ * <p>
+ * This defines the logging level for messages if they're not already defined on the matched rule.
+ *
+ * <p>
+ * If not specified, <js>"INFO"</js> is used.
+ *
+ * <p>
+ * See {@link Level} for possible values.
+ *
+ * @param value The value for this property.
+ * @return This object (for method chaining).
+ */
+ public Builder level(String value) {
+ this.level = value;
+ return this;
+ }
+
+ /**
+ * The default logging level.
+ *
+ * <p>
+ * This defines the logging level for messages if they're not already defined on the matched rule.
+ *
+ * <p>
+ * If not specified, <js>"INFO"</js> is used.
+ *
+ * <p>
+ * See {@link Level} for possible values.
+ *
+ * @param value The value for this property.
+ * @return This object (for method chaining).
+ */
+ public Builder level(Level value) {
+ this.level = value == null ? null : value.getName();
+ return this;
+ }
+
+ /**
+ * Creates the {@link RestCallLoggerConfig} object based on settings on this builder.
+ *
+ * @return A new {@link RestCallLoggerConfig} object.
+ */
+ public RestCallLoggerConfig build() {
+ return new RestCallLoggerConfig(this);
+ }
+ }
+
+ /**
+ * Given the specified servlet request/response, find the rule that applies to it.
+ *
+ * @param req The servlet request.
+ * @param res The servlet response.
+ * @return The applicable logging rule, or <jk>null<jk> if a match could not be found.
+ */
+ public RestCallLoggerRule getRule(HttpServletRequest req, HttpServletResponse res) {
+ if (isNoTrace(req))
+ return null;
+
+ int status = res.getStatus();
+ Throwable e = (Throwable)req.getAttribute("Exception");
+ boolean debug = isDebug(req);
+
+ for (RestCallLoggerRule r : rules)
+ if (r.matches(status, debug, e))
+ return r;
+
+ return null;
+ }
+
+ /**
+ * Returns <jk>true</jk> if the current request has debug enabled.
+ *
+ * @param req The request to check.
+ * @return <jk>true</jk> if the current request has debug enabled.
+ */
+ public boolean isDebug(HttpServletRequest req) {
+ if (debugAlways)
+ return true;
+ if (debugPerRequest) {
+ if ("true".equalsIgnoreCase(req.getHeader("X-Debug")))
+ return true;
+ Boolean b = boolAttr(req, "Debug");
+ if (b != null && b == true)
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isNoTrace(HttpServletRequest req) {
+ if (disabled || noTraceAlways)
+ return true;
+ if (noTracePerRequest) {
+ if ("true".equalsIgnoreCase(req.getHeader("X-NoTrace")))
+ return true;
+ Boolean b = boolAttr(req, "NoTrace");
+ if (b != null && b == true)
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the default logging level.
+ *
+ * @return The default logging level.
+ */
+ public Level getLevel() {
+ return level;
+ }
+
+ /**
+ * Returns <jk>true</jk> if stack traces should be hashed.
+ *
+ * @return <jk>true</jk> if stack traces should be hashed.
+ */
+ public boolean useStackTraceHashing() {
+ return useStackTraceHashing;
+ }
+
+ /**
+ * Returns the time in milliseconds that stacktrace hashes should be persisted.
+ *
+ * @return The time in milliseconds that stacktrace hashes should be persisted.
+ */
+ public int getStackTraceHashingTimeout() {
+ return stackTraceHashingTimeout;
+ }
+
+ private Boolean boolAttr(HttpServletRequest req, String name) {
+ Object o = req.getAttribute(name);
+ if (o == null || ! (o instanceof Boolean))
+ return null;
+ return (Boolean)o;
+ }
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallLoggerRule.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallLoggerRule.java
new file mode 100644
index 0000000..c4abc8e
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallLoggerRule.java
@@ -0,0 +1,269 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.rest;
+
+import static org.apache.juneau.internal.StringUtils.*;
+
+import java.util.logging.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.pojotools.*;
+
+/**
+ * Represents a logging rule used for determine how detailed requests should be logged at.
+ */
+public class RestCallLoggerRule {
+
+ private final Matcher codeMatcher;
+ private final Matcher exceptionMatcher;
+ private final boolean debugOnly, matchAll;
+ private final Level level;
+ private final RestCallLoggingDetail req, res;
+
+ /**
+ * Constructor.
+ *
+ * @param b Builder
+ */
+ RestCallLoggerRule(Builder b) {
+ this.codeMatcher = isEmpty(b.codes) || "*".equals(b.codes) ? null : NumberMatcherFactory.DEFAULT.create(b.codes);
+ this.exceptionMatcher = isEmpty(b.exceptions) ? null : StringMatcherFactory.DEFAULT.create(b.exceptions);
+ this.matchAll = "*".equals(b.codes) || (codeMatcher == null && exceptionMatcher == null);
+ boolean v = b.verbose == null ? false : b.verbose;
+ this.debugOnly = b.debugOnly == null ? false : b.debugOnly;
+ this.level = b.level;
+ this.req = v ? RestCallLoggingDetail.LONG : b.req != null ? b.req : RestCallLoggingDetail.SHORT;
+ this.res = v ? RestCallLoggingDetail.LONG : b.res != null ? b.res : RestCallLoggingDetail.SHORT;
+ }
+
+ /**
+ * Creates a new builder for this object.
+ *
+ * @return A new builder for this object.
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ /**
+ * Builder class for this object.
+ */
+ @Bean(fluentSetters=true)
+ public static class Builder {
+ String codes, exceptions;
+ Boolean verbose, debugOnly;
+ Level level;
+ RestCallLoggingDetail req, res;
+
+ /**
+ * The code ranges that this logging rule applies to.
+ *
+ * <p>
+ * See {@link NumberMatcherFactory} for format of values.
+ *
+ * <p>
+ * <js>"*"</js> can be used to represent all values.
+ *
+ * @param value
+ * The new value for this property.
+ * <br>Can be <jk>null</jk> or an empty string.
+ * @return This object (for method chaining).
+ */
+ public Builder codes(String value) {
+ this.codes = value;
+ return this;
+ }
+
+ /**
+ * The exception naming pattern that this rule applies to.
+ *
+ * <p>
+ * See {@link StringMatcherFactory} for format of values.
+ *
+ * <p>
+ * The pattern can be against either the fully-qualified or simple class name of the exception.
+ *
+ * @param value
+ * The new value for this property.
+ * <br>Can be <jk>null</jk> or an empty string.
+ * @return This object (for method chaining).
+ */
+ public Builder exceptions(String value) {
+ this.exceptions = value;
+ return this;
+ }
+
+ /**
+ * Shortcut for specifying {@link RestCallLoggingDetail#LONG} for {@link #req(RestCallLoggingDetail)} and {@link #res(RestCallLoggingDetail)}.
+ *
+ * @param value
+ * The new value for this property.
+ * <br>Can be <jk>null</jk>.
+ * @return This object (for method chaining).
+ */
+ public Builder verbose(Boolean value) {
+ this.verbose = value;
+ return this;
+ }
+
+ /**
+ * Shortcut for calling <c>verbose(<jk>true</jk>);</c>
+ *
+ * @return This object (for method chaining).
+ */
+ public Builder verbose() {
+ return this.verbose(true);
+ }
+
+ /**
+ * This match only applies when debug is enabled on the request.
+ *
+ * @param value The new value for this property.
+ * @return This object (for method chaining).
+ */
+ public Builder debugOnly(Boolean value) {
+ this.debugOnly = value;
+ return this;
+ }
+
+ /**
+ * Shortcut for calling <c>debugOnly(<jk>true</jk>);</c>
+ *
+ * @return This object (for method chaining).
+ */
+ public Builder debugOnly() {
+ return this.debugOnly(true);
+ }
+
+ /**
+ * The level of detail to log on a request.
+ *
+ * <p>
+ * The default value is {@link RestCallLoggingDetail#SHORT}.
+ *
+ * @param value
+ * The new value for this property.
+ * <br>Can be <jk>null</jk>
+ * @return This object (for method chaining).
+ */
+ public Builder req(RestCallLoggingDetail value) {
+ this.req = value;
+ return this;
+ }
+
+ /**
+ * The level of detail to log on a response.
+ *
+ * <p>
+ * The default value is {@link RestCallLoggingDetail#SHORT}.
+ *
+ * @param value
+ * The new value for this property.
+ * <br>Can be <jk>null</jk>
+ * @return This object (for method chaining).
+ */
+ public Builder res(RestCallLoggingDetail value) {
+ this.res = value;
+ return this;
+ }
+
+ /**
+ * The logging level to use for logging the request/response.
+ *
+ * <p>
+ * The default value is {@link Level#INFO}.
+ *
+ * @param value
+ * The new value for this property.
+ * <br>Can be <jk>null</jk>
+ * @return This object (for method chaining).
+ */
+ public Builder level(Level value) {
+ this.level = value;
+ return this;
+ }
+
+ /**
+ * Instantiates a new {@link RestCallLoggerRule} object using the settings in this builder.
+ *
+ * @return A new {@link RestCallLoggerRule} object.
+ */
+ public RestCallLoggerRule build() {
+ return new RestCallLoggerRule(this);
+ }
+ }
+
+ /**
+ * Returns <jk>true</jk> if this rule matches the specified parameters.
+ *
+ * @param statusCode The HTTP response status code.
+ * @param debug Whether debug is enabled on the request.
+ * @param e Exception thrown while handling the request.
+ * @return <jk>true</jk> if this rule matches the specified parameters.
+ */
+ public boolean matches(int statusCode, boolean debug, Throwable e) {
+ if (debugOnly && ! debug)
+ return false;
+ if (matchAll)
+ return true;
+ if (codeMatcher != null && codeMatcher.matches(null, statusCode))
+ return true;
+ if (exceptionMatcher != null && e != null)
+ if (exceptionMatcher.matches(null, e.getClass().getName()) || exceptionMatcher.matches(null, e.getClass().getSimpleName()))
+ return true;
+ return false;
+ }
+
+ /**
+ * Returns the detail level for HTTP requests.
+ *
+ * @return the detail level for HTTP requests.
+ */
+ public RestCallLoggingDetail getReqDetail() {
+ return req;
+ }
+
+ /**
+ * Returns the detail level for HTTP responses.
+ *
+ * @return the detail level for HTTP responses.
+ */
+ public RestCallLoggingDetail getResDetail() {
+ return res;
+ }
+
+ /**
+ * Returns the log level.
+ *
+ * @return the log level.
+ */
+ public Level getLevel() {
+ return level;
+ }
+
+ @Override /* Object */
+ public String toString() {
+ return SimpleJsonSerializer.DEFAULT.toString(
+ new DefaultFilteringObjectMap()
+ .append("codes", codeMatcher)
+ .append("exceptions", exceptionMatcher)
+ .append("debugOnly", debugOnly)
+ .append("matchAll", matchAll)
+ .append("level", level)
+ .append("req", req)
+ .append("res", res)
+ );
+ }
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallLoggingDetail.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallLoggingDetail.java
new file mode 100644
index 0000000..803c8df
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestCallLoggingDetail.java
@@ -0,0 +1,41 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.rest;
+
+/**
+ * Represents the amount of detail to include in a log entry for HTTP requests and responses.
+ */
+public enum RestCallLoggingDetail {
+
+ /**
+ * Log only the request and response status lines.
+ */
+ SHORT,
+
+ /**
+ * Log status lines and also headers.
+ */
+ MEDIUM,
+
+ /**
+ * Log status lines, headers, and bodies if available.
+ */
+ LONG;
+
+ boolean isOneOf(RestCallLoggingDetail...values) {
+ for (RestCallLoggingDetail v : values)
+ if (v == this)
+ return true;
+ return false;
+ }
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index 6c28622..a1a7776 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -26,7 +26,6 @@
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
-import java.util.logging.*;
import javax.activation.*;
import javax.servlet.*;
@@ -1578,6 +1577,62 @@
public static final String REST_logger = PREFIX + ".logger.o";
/**
+ * Configuration property: Logging rules.
+ *
+ * <h5 class='section'>Property:</h5>
+ * <ul>
+ * <li><b>Name:</b> <js>"RestContext.logRules.lo"</js>
+ * <li><b>Data type:</b> <c>List<{@link RestCallLoggerRule}></c>
+ * <li><b>Default:</b> empty list
+ * <li><b>Session property:</b> <jk>false</jk>
+ * <li><b>Annotations:</b>
+ * <ul>
+ * <li class='ja'>{@link RestResource#logRules()}
+ * </ul>
+ * <li><b>Methods:</b>
+ * <ul>
+ * <li class='jm'>{@link RestContextBuilder#logRules(RestCallLoggerRule...)}
+ * </ul>
+ * </ul>
+ *
+ * <h5 class='section'>Description:</h5>
+ * <p>
+ * Specifies rules on how to handle logging of HTTP requests/responses.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode w800'>
+ * <jc>// Option #1 - Registered via annotation.</jc>
+ * <ja>@RestResource</ja>(
+ * logRules={
+ * <ja>@LogRule</ja>(codes=<js>"400-499"</js>, level=<js>"WARNING"</js>, req=<js>"SHORT"</js>, res=<js>"MEDIUM"</js>),
+ * <ja>@LogRule</ja>(codes=<js>">=500"</js>, level=<js>"SEVERE"</js>, req=<js>"LONG"</js>, res=<js>"LONG"</js>)
+ * }
+ * )
+ * <jk>public class</jk> MyResource {
+ *
+ * <jc>// Option #2 - Registered via builder passed in through resource constructor.</jc>
+ * <jk>public</jk> MyResource(RestContextBuilder builder) <jk>throws</jk> Exception {
+ *
+ * <jc>// Using method on builder.</jc>
+ * builder.logRules(
+ * LoggingRule.<jsm>create</jsm>().codes(<js>"400-499"</js>).level(<jsf>WARNING</jsf>).req(<jsf>SHORT</jsf>).res(<jsf>MEDIUM</jsf>).build(),
+ * LoggingRule.<jsm>create</jsm>().codes(<js>">=500"</js>).level(<jsf>SEVERE</jsf>).req(<jsf>LONG</jsf>).res(<jsf>LONG</jsf>).build()
+ * );
+ *
+ * <jc>// Same, but using property with JSON value.</jc>
+ * builder.set(<jsf>REST_logRules</jsf>, <js>"[{codes:'400-499',level:'WARNING',...},...]"</js>);
+ * }
+ * }
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul>
+ * <li class='link'>{@doc juneau-rest-server.LoggingAndErrorHandling}
+ * </ul>
+ */
+ public static final String REST_logRules = PREFIX + ".logRules.lo";
+
+ /**
* Configuration property: The maximum allowed input size (in bytes) on HTTP requests.
*
* <h5 class='section'>Property:</h5>
@@ -3465,6 +3520,7 @@
private final Map<String,RestMethodContext> callMethods;
private final Map<String,RestContext> childResources;
private final RestLogger logger;
+ private final RestCallLoggerConfig loggingConfig;
private final RestCallHandler callHandler;
private final RestInfoProvider infoProvider;
private final RestException initException;
@@ -3605,8 +3661,8 @@
staticFileResponseHeaders = getMapProperty(REST_staticFileResponseHeaders, Object.class);
logger = getInstanceProperty(REST_logger, resource, RestLogger.class, NoOpRestLogger.class, resourceResolver, this);
- if (debug)
- logger.setLevel(Level.FINE);
+
+ loggingConfig = RestCallLoggerConfig.create().rules(getInstanceArrayProperty(REST_logRules, resource, RestCallLoggerRule.class, new RestCallLoggerRule[0], resourceResolver, resource, this)).build();
properties = builder.properties;
serializers =
@@ -4374,6 +4430,22 @@
}
/**
+ * Returns the logger to use for this resource.
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul>
+ * <li class='jf'>{@link #REST_logger}
+ * </ul>
+ *
+ * @return
+ * The logger to use for this resource.
+ * <br>Never <jk>null</jk>.
+ */
+ RestCallLoggerConfig getLoggingConfig() {
+ return loggingConfig;
+ }
+
+ /**
* Returns the resource bundle used by this resource.
*
* <h5 class='section'>See Also:</h5>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java
index 08f0601..77656f2 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java
@@ -1156,6 +1156,30 @@
}
/**
+ * Configuration property: Logging rules.
+ *
+ * <p>
+ * Specifies rules on how to handle logging of HTTP requests/responses.
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul>
+ * <li class='jf'>{@link RestContext#REST_logRules}
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul>
+ * <li class='link'>{@doc juneau-rest-server.LoggingAndErrorHandling}
+ * </ul>
+ *
+ * @param value
+ * The new value for this setting.
+ * @return This object (for method chaining).
+ */
+ public RestContextBuilder logRules(RestCallLoggerRule...value) {
+ return set(REST_logRules, value);
+ }
+
+ /**
* Configuration property: The maximum allowed input size (in bytes) on HTTP requests.
*
* <p>
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
index 07c6047..e8e52d4 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestMethodContext.java
@@ -434,6 +434,32 @@
public static final String RESTMETHOD_httpMethod = PREFIX + ".httpMethod.s";
/**
+ * Configuration property: Logging rules.
+ *
+ * <h5 class='section'>Property:</h5>
+ * <ul>
+ * <li><b>Name:</b> <js>"RestContext.logRules.lo"</js>
+ * <li><b>Data type:</b> <c>List<{@link RestCallLoggerRule}></c>
+ * <li><b>Default:</b> empty list
+ * <li><b>Session property:</b> <jk>false</jk>
+ * <li><b>Annotations:</b>
+ * <ul>
+ * <li class='ja'>{@link RestMethod#logRules()}
+ * </ul>
+ * </ul>
+ *
+ * <h5 class='section'>Description:</h5>
+ * <p>
+ * Specifies rules on how to handle logging of HTTP requests/responses.
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul>
+ * <li class='link'>{@doc juneau-rest-server.LoggingAndErrorHandling}
+ * </ul>
+ */
+ public static final String RESTMETHOD_logRules = PREFIX + ".logRules.lo";
+
+ /**
* Configuration property: Method-level matchers.
*
* <h5 class='section'>Property:</h5>
@@ -560,6 +586,7 @@
final List<MediaType>
supportedAcceptTypes,
supportedContentTypes;
+ final RestCallLoggerConfig loggingConfig;
final Map<Class<?>,ResponseBeanMeta> responseBeanMetas = new ConcurrentHashMap<>();
final Map<Class<?>,ResponsePartMeta> headerPartMetas = new ConcurrentHashMap<>();
@@ -726,6 +753,7 @@
this.debug = getBooleanProperty(RESTMETHOD_debug, false);
this.debugHeader = getStringProperty(RESTMETHOD_debugHeader, null);
this.debugParam = getStringProperty(RESTMETHOD_debugParam, null);
+ this.loggingConfig = RestCallLoggerConfig.create().rules(getInstanceArrayProperty(REST_logRules, method, RestCallLoggerRule.class, new RestCallLoggerRule[0], rr, r, this)).parent(context.getLoggingConfig()).build();
}
ResponseBeanMeta getResponseBeanMeta(Object o) {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/LogRule.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/LogRule.java
new file mode 100644
index 0000000..31d4512
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/LogRule.java
@@ -0,0 +1,102 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.rest.annotation;
+
+import org.apache.juneau.rest.*;
+
+/**
+ * Represents a single logging rule for how to handle logging of HTTP requests/responses.
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul>
+ * <li class='jf'>{@link RestContext#REST_logRules}
+ * <li class='jf'>{@link RestMethodContext#RESTMETHOD_logRules}
+ * </ul>
+ */
+@SuppressWarnings("javadoc")
+public @interface LogRule {
+
+ /**
+ * Sets the bean filters for the serializers and parsers defined on this method.
+ *
+ * <p>
+ * If no value is specified, the bean filters are inherited from the class.
+ * <br>Otherwise, this value overrides the bean filters defined on the class.
+ *
+ * <p>
+ * Use {@link Inherit} to inherit bean filters defined on the class.
+ *
+ * <p>
+ * Use {@link None} to suppress inheriting bean filters defined on the class.
+ */
+ public String codes() default "";
+ public String exceptions() default "";
+ public String debugOnly() default "false";
+ public String logLevel() default "INFO";
+
+ public String req() default "SHORT";
+ public String res() default "SHORT";
+ public String verbose() default "false";
+//
+//
+// sb.append("\n=== HTTP Request (incoming) ====================================================");
+// sb.append("\n").append(method).append(" ").append(req.getRequestURI()).append((qs == null ? "" : "?" + qs));
+// sb.append("\n\tResponse code: ").append(res.getStatus());
+// if (execTime != null)
+// sb.append("\n\tExec time: ").append(res.getStatus()).append("ms");
+// if (reqBody != null)
+// sb.append("\n\tReq body: ").append(reqBody.length).append(" bytes");
+// if (resBody != null)
+// sb.append("\n\tRes body: ").append(resBody.length).append(" bytes");
+// sb.append("\n---Request Headers---");
+// for (Enumeration<String> hh = req.getHeaderNames(); hh.hasMoreElements();) {
+// String h = hh.nextElement();
+// sb.append("\n\t").append(h).append(": ").append(req.getHeader(h));
+// }
+// if (context != null && ! context.getDefaultRequestHeaders().isEmpty()) {
+// sb.append("\n---Default Servlet Headers---");
+// for (Map.Entry<String,Object> h : context.getDefaultRequestHeaders().entrySet()) {
+// sb.append("\n\t").append(h.getKey()).append(": ").append(h.getValue());
+// }
+// }
+// if (reqBody != null && reqBody.length > 0) {
+// try {
+// sb.append("\n---Request Body UTF-8---");
+// sb.append("\n").append(new String(reqBody, IOUtils.UTF8));
+// sb.append("\n---Request Body Hex---");
+// sb.append("\n").append(toSpacedHex(reqBody));
+// } catch (Exception e1) {
+// sb.append("\n").append(e1.getLocalizedMessage());
+// }
+// }
+// sb.append("\n---Response Headers---");
+// for (String h : res.getHeaderNames()) {
+// sb.append("\n\t").append(h).append(": ").append(res.getHeader(h));
+// }
+// if (resBody != null && resBody.length > 0) {
+// try {
+// sb.append("\n---Response Body UTF-8---");
+// sb.append("\n").append(new String(resBody, IOUtils.UTF8));
+// sb.append("\n---Response Body Hex---");
+// sb.append("\n").append(toSpacedHex(resBody));
+// } catch (Exception e1) {
+// sb.append(e1.getLocalizedMessage());
+// }
+// }
+// if (e != null) {
+// sb.append("\n---Exception---");
+// sb.append("\n").append(getStackTrace(e));
+// }
+// sb.append("\n=== END ========================================================================");
+
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Logging.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Logging.java
new file mode 100644
index 0000000..f92df40
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/Logging.java
@@ -0,0 +1,52 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
+// * with the License. You may obtain a copy of the License at *
+// * *
+// * http://www.apache.org/licenses/LICENSE-2.0 *
+// * *
+// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
+// * specific language governing permissions and limitations under the License. *
+// ***************************************************************************************************************************
+package org.apache.juneau.rest.annotation;
+
+import org.apache.juneau.rest.*;
+
+/**
+ * Represents a single logging rule for how to handle logging of HTTP requests/responses.
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul>
+ * <li class='jf'>{@link RestContext#REST_logRules}
+ * <li class='jf'>{@link RestMethodContext#RESTMETHOD_logRules}
+ * </ul>
+ */
+@SuppressWarnings("javadoc")
+public @interface Logging {
+
+ /**
+ * Sets the bean filters for the serializers and parsers defined on this method.
+ *
+ * <p>
+ * If no value is specified, the bean filters are inherited from the class.
+ * <br>Otherwise, this value overrides the bean filters defined on the class.
+ *
+ * <p>
+ * Use {@link Inherit} to inherit bean filters defined on the class.
+ *
+ * <p>
+ * Use {@link None} to suppress inheriting bean filters defined on the class.
+ */
+ public String stHashing() default ""; // false,true,time(ms)
+
+ public String debug() default ""; // false,true,header
+
+ public String noTrace() default ""; // false,true,header
+
+ public String level() default "";
+
+ public LogRule[] rules() default {};
+
+}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestMethod.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestMethod.java
index e3dee38..71921a6 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestMethod.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestMethod.java
@@ -1049,4 +1049,26 @@
* </ul>
*/
MethodSwagger swagger() default @MethodSwagger;
+
+ /**
+ * Configuration property: Logging rules.
+ *
+ * <p>
+ * Specifies rules on how to handle logging of HTTP requests/responses.
+ *
+ * <h5 class='section'>Notes:</h5>
+ * <ul class='spaced-list'>
+ * <li>
+ * Supports {@doc DefaultRestSvlVariables}
+ * (e.g. <js>"$L{my.localized.variable}"</js>).
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul>
+ * <li class='jf'>{@link RestContext#REST_logRules}
+ * <li class='jf'>{@link RestMethodContext#RESTMETHOD_logRules}
+ * <li class='link'>{@doc juneau-rest-server.LoggingAndErrorHandling}
+ * </ul>
+ */
+ LogRule[] logRules() default {};
}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestMethodConfigApply.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestMethodConfigApply.java
index 7cc50a1..dafbc34 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestMethodConfigApply.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestMethodConfigApply.java
@@ -213,6 +213,9 @@
if (! a.debugParam().isEmpty())
psb.set(RESTMETHOD_debugParam, a.debugParam());
+ if (a.logRules().length != 0)
+ psb.set(RESTMETHOD_logRules, parseRules(a.logRules()));
+
HtmlDoc hd = a.htmldoc();
new HtmlDocBuilder(psb).process(hd);
for (Class<? extends Widget> wc : hd.widgets()) {
@@ -222,4 +225,20 @@
psb.addTo(HTMLDOC_script, "$W{"+w.getName()+".style}");
}
}
+
+ private List<ObjectMap> parseRules(LogRule[] rules) {
+ List<ObjectMap> l = new ArrayList<>(rules.length);
+ for (LogRule r : rules) {
+ l.add(
+ new DefaultFilteringObjectMap()
+ .append("codes", string(r.codes()))
+ .append("exceptions", string(r.exceptions()))
+ .append("debugOnly", bool(r.debugOnly()))
+ .append("logLevel", string(r.logLevel()))
+ .append("req", string(r.req()))
+ .append("res", string(r.res()))
+ );
+ }
+ return l;
+ }
}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java
index 6ec4c0a..9db3ab3 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java
@@ -1403,4 +1403,51 @@
* </ul>
*/
String debugParam() default "";
+
+ /**
+ * Configuration property: Logging rules.
+ *
+ * <p>
+ * Specifies rules on how to handle logging of HTTP requests/responses.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode w800'>
+ * <jc>// Option #1 - Registered via annotation.</jc>
+ * <ja>@RestResource</ja>(
+ * logRules={
+ * <ja>@LogRule</ja>(codes=<js>"400-499"</js>, level=<js>"WARNING"</js>, req=<js>"SHORT"</js>, res=<js>"MEDIUM"</js>),
+ * <ja>@LogRule</ja>(codes=<js>">=500"</js>, level=<js>"SEVERE"</js>, req=<js>"LONG"</js>, res=<js>"LONG"</js>)
+ * }
+ * )
+ * <jk>public class</jk> MyResource {
+ *
+ * <jc>// Option #2 - Registered via builder passed in through resource constructor.</jc>
+ * <jk>public</jk> MyResource(RestContextBuilder builder) <jk>throws</jk> Exception {
+ *
+ * <jc>// Using method on builder.</jc>
+ * builder.logRules(
+ * LoggingRule.<jsm>create</jsm>().codes(<js>"400-499"</js>).level(<jsf>WARNING</jsf>).req(<jsf>SHORT</jsf>).res(<jsf>MEDIUM</jsf>).build(),
+ * LoggingRule.<jsm>create</jsm>().codes(<js>">=500"</js>).level(<jsf>SEVERE</jsf>).req(<jsf>LONG</jsf>).res(<jsf>LONG</jsf>).build()
+ * );
+ *
+ * <jc>// Same, but using property with JSON value.</jc>
+ * builder.set(<jsf>REST_logRules</jsf>, <js>"[{codes:'400-499',level:'WARNING',...},...]"</js>);
+ * }
+ * }
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5>
+ * <ul class='spaced-list'>
+ * <li>
+ * Supports {@doc DefaultRestSvlVariables}
+ * (e.g. <js>"$L{my.localized.variable}"</js>).
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5>
+ * <ul>
+ * <li class='jf'>{@link RestContext#REST_logRules}
+ * <li class='link'>{@doc juneau-rest-server.LoggingAndErrorHandling}
+ * </ul>
+ */
+ LogRule[] logRules() default {};
}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResourceConfigApply.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResourceConfigApply.java
index 8c6a9a0..b696a99 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResourceConfigApply.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResourceConfigApply.java
@@ -14,6 +14,9 @@
import static org.apache.juneau.rest.RestContext.*;
import static org.apache.juneau.rest.util.RestUtils.*;
+
+import java.util.*;
+
import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.html.HtmlDocSerializer.*;
import static org.apache.juneau.internal.ArrayUtils.*;
@@ -242,6 +245,9 @@
if (! a.roleGuard().isEmpty())
psb.addTo(REST_roleGuard, string(a.roleGuard()));
+ if (a.logRules().length != 0)
+ psb.set(REST_logRules, parseRules(a.logRules()));
+
HtmlDoc hd = a.htmldoc();
new HtmlDocBuilder(psb).process(hd);
for (Class<? extends Widget> wc : hd.widgets()) {
@@ -257,4 +263,20 @@
return value.substring(1);
return value;
}
+
+ private List<ObjectMap> parseRules(LogRule[] rules) {
+ List<ObjectMap> l = new ArrayList<>(rules.length);
+ for (LogRule r : rules) {
+ l.add(
+ new DefaultFilteringObjectMap()
+ .append("codes", string(r.codes()))
+ .append("exceptions", string(r.exceptions()))
+ .append("debugOnly", bool(r.debugOnly()))
+ .append("logLevel", string(r.logLevel()))
+ .append("req", string(r.req()))
+ .append("res", string(r.res()))
+ );
+ }
+ return l;
+ }
}