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&lt;{@link RestCallLoggerRule}&gt;</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&lt;{@link RestCallLoggerRule}&gt;</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;
+	}
 }