SLING-5505 - Allow recording of caller stacktrace with the logs
Caller stack trace would be included if `caller` is set to true. One can specify the list packages which should be excluded with `callerPrefixFilter`
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1729769 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/main/java/org/apache/sling/tracer/internal/CallerFilter.java b/src/main/java/org/apache/sling/tracer/internal/CallerFilter.java
new file mode 100644
index 0000000..56d8ca7
--- /dev/null
+++ b/src/main/java/org/apache/sling/tracer/internal/CallerFilter.java
@@ -0,0 +1,31 @@
+/*
+ * 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.sling.tracer.internal;
+
+interface CallerFilter {
+ CallerFilter ALL = new CallerFilter() {
+ @Override
+ public boolean include(StackTraceElement ste) {
+ return true;
+ }
+ };
+
+ boolean include(StackTraceElement ste);
+}
diff --git a/src/main/java/org/apache/sling/tracer/internal/CallerStackReporter.java b/src/main/java/org/apache/sling/tracer/internal/CallerStackReporter.java
new file mode 100644
index 0000000..59006bb
--- /dev/null
+++ b/src/main/java/org/apache/sling/tracer/internal/CallerStackReporter.java
@@ -0,0 +1,93 @@
+/*
+ * 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.sling.tracer.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.util.Arrays.asList;
+
+class CallerStackReporter {
+ /**
+ * Package names which should be excluded from the stack as they do not serve
+ * any purpose and bloat the size
+ */
+ private static final CallerFilter FWK_EXCLUDE_FILTER = new PrefixExcludeFilter(asList(
+ "java.lang.Thread",
+ "org.apache.sling.tracer.internal",
+ "ch.qos.logback.classic",
+ "sun.reflect",
+ "java.lang.reflect"
+ ));
+ private final CallerFilter callerFilter;
+ private final int start;
+ private final int depth;
+
+ public CallerStackReporter(int depth){
+ this(0, depth, CallerFilter.ALL);
+ }
+
+ public CallerStackReporter(int start, int depth, CallerFilter filter){
+ this.start = start;
+ this.depth = depth;
+ this.callerFilter = filter;
+ }
+
+ public List<StackTraceElement> report(){
+ return report(Thread.currentThread().getStackTrace());
+ }
+
+ public List<StackTraceElement> report(StackTraceElement[] stack){
+ List<StackTraceElement> filteredStack = fwkExcludedStack(stack);
+ List<StackTraceElement> result = new ArrayList<StackTraceElement>(filteredStack.size());
+
+ //Iterate over the filtered stack with limits applicable on that not on actual stack
+ for (int i = 0; i < filteredStack.size(); i++) {
+ StackTraceElement ste = filteredStack.get(i);
+ if (i >= start && i < depth
+ && callerFilter.include(ste)){
+ result.add(ste);
+ }
+ }
+ return result;
+ }
+
+ private List<StackTraceElement> fwkExcludedStack(StackTraceElement[] stack) {
+ List<StackTraceElement> filteredStack = new ArrayList<StackTraceElement>(stack.length);
+ for (StackTraceElement ste : stack) {
+ if (FWK_EXCLUDE_FILTER.include(ste)){
+ filteredStack.add(ste);
+ }
+ }
+ return filteredStack;
+ }
+
+ public CallerFilter getCallerFilter() {
+ return callerFilter;
+ }
+
+ public int getStart() {
+ return start;
+ }
+
+ public int getDepth() {
+ return depth;
+ }
+}
diff --git a/src/main/java/org/apache/sling/tracer/internal/JSONRecording.java b/src/main/java/org/apache/sling/tracer/internal/JSONRecording.java
index f508e9e..a319a45 100644
--- a/src/main/java/org/apache/sling/tracer/internal/JSONRecording.java
+++ b/src/main/java/org/apache/sling/tracer/internal/JSONRecording.java
@@ -32,6 +32,7 @@
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.zip.GZIPInputStream;
@@ -129,7 +130,7 @@
if (logger.startsWith(OAK_QUERY_PKG)) {
queryCollector.record(level, logger, tuple);
}
- logs.add(new LogEntry(level, logger, tuple));
+ logs.add(new LogEntry(tc, level, logger, tuple));
}
@Override
@@ -233,11 +234,20 @@
final String logger;
final FormattingTuple tuple;
final long timestamp = System.currentTimeMillis();
+ final List<StackTraceElement> caller;
- private LogEntry(Level level, String logger, FormattingTuple tuple) {
+ private LogEntry(TracerConfig tc, Level level, String logger, FormattingTuple tuple) {
this.level = level != null ? level : Level.INFO;
this.logger = logger;
this.tuple = tuple;
+ this.caller = getCallerData(tc);
+ }
+
+ private static List<StackTraceElement> getCallerData(TracerConfig tc) {
+ if (tc.isReportCallerStack()){
+ return tc.getCallerReporter().report();
+ }
+ return Collections.emptyList();
}
private static String toString(Object o) {
@@ -274,6 +284,15 @@
jw.key("exception").value(getStackTraceAsString(t));
}
+
+ if (!caller.isEmpty()){
+ jw.key("caller");
+ jw.array();
+ for (StackTraceElement o : caller) {
+ jw.value(o.toString());
+ }
+ jw.endArray();
+ }
}
}
diff --git a/src/main/java/org/apache/sling/tracer/internal/PrefixExcludeFilter.java b/src/main/java/org/apache/sling/tracer/internal/PrefixExcludeFilter.java
new file mode 100644
index 0000000..8199010
--- /dev/null
+++ b/src/main/java/org/apache/sling/tracer/internal/PrefixExcludeFilter.java
@@ -0,0 +1,57 @@
+/*
+ * 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.sling.tracer.internal;
+
+import java.util.List;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Filter which returns false if the package of stack trace element
+ * is part of exclude list of prefixes
+ */
+class PrefixExcludeFilter implements CallerFilter{
+ private final List<String> prefixesToExclude;
+
+ public PrefixExcludeFilter(List<String> prefixes) {
+ this.prefixesToExclude = ImmutableList.copyOf(prefixes);
+ }
+
+ public static PrefixExcludeFilter from(String filter){
+ List<String> prefixes = Splitter.on('|').omitEmptyStrings().trimResults().splitToList(filter);
+ return new PrefixExcludeFilter(prefixes);
+ }
+
+ @Override
+ public boolean include(StackTraceElement ste) {
+ String className = ste.getClassName();
+ for (String prefix : prefixesToExclude){
+ if (className.startsWith(prefix)){
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public List<String> getPrefixesToExclude() {
+ return prefixesToExclude;
+ }
+}
diff --git a/src/main/java/org/apache/sling/tracer/internal/TracerConfig.java b/src/main/java/org/apache/sling/tracer/internal/TracerConfig.java
index 8f7a65b..597d93c 100644
--- a/src/main/java/org/apache/sling/tracer/internal/TracerConfig.java
+++ b/src/main/java/org/apache/sling/tracer/internal/TracerConfig.java
@@ -19,6 +19,9 @@
package org.apache.sling.tracer.internal;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
import ch.qos.logback.classic.Level;
import ch.qos.logback.core.CoreConstants;
@@ -37,11 +40,17 @@
private final String loggerName;
private final Level level;
private final int depth;
+ private final CallerStackReporter callerReporter;
public TracerConfig(String loggerName, Level level) {
+ this(loggerName, level, null);
+ }
+
+ public TracerConfig(String loggerName, Level level,@Nullable CallerStackReporter reporter) {
this.loggerName = loggerName;
this.level = level;
this.depth = getDepth(loggerName);
+ this.callerReporter = reporter;
}
public boolean match(String loggerName) {
@@ -59,7 +68,7 @@
}
@Override
- public int compareTo(TracerConfig o) {
+ public int compareTo(@Nonnull TracerConfig o) {
int comp = depth > o.depth ? -1 : depth < o.depth ? 1 : 0;
if (comp == 0) {
comp = loggerName.compareTo(o.loggerName);
@@ -79,6 +88,14 @@
return level;
}
+ public boolean isReportCallerStack(){
+ return callerReporter != null;
+ }
+
+ public CallerStackReporter getCallerReporter() {
+ return callerReporter;
+ }
+
private static int getDepth(String loggerName) {
int depth = 0;
int fromIndex = 0;
diff --git a/src/main/java/org/apache/sling/tracer/internal/TracerSet.java b/src/main/java/org/apache/sling/tracer/internal/TracerSet.java
index c4bcdbb..176d57a 100644
--- a/src/main/java/org/apache/sling/tracer/internal/TracerSet.java
+++ b/src/main/java/org/apache/sling/tracer/internal/TracerSet.java
@@ -28,6 +28,9 @@
class TracerSet {
public static final String LEVEL = "level";
+ public static final String CALLER = "caller";
+ public static final String CALLER_PREFIX_FILTER = "callerPrefixFilter";
+
private final String name;
private final List<TracerConfig> configs;
@@ -72,8 +75,35 @@
//Defaults to Debug
Level level = Level.valueOf(e.getAttributeValue(LEVEL));
- result.add(new TracerConfig(category, level));
+ CallerStackReporter reporter = createReporter(e);
+ result.add(new TracerConfig(category, level, reporter));
}
return Collections.unmodifiableList(result);
}
+
+ static CallerStackReporter createReporter(ManifestHeader.Entry e) {
+ String caller = e.getAttributeValue(CALLER);
+ if (caller == null){
+ return null;
+ }
+
+ if ("true".equals(caller)){
+ return new CallerStackReporter(0, Integer.MAX_VALUE, CallerFilter.ALL);
+ }
+
+ CallerFilter filter = CallerFilter.ALL;
+ int depth;
+ try{
+ depth = Integer.parseInt(caller);
+ } catch (NumberFormatException ignore){
+ return null;
+ }
+
+ String filterValue = e.getAttributeValue(CALLER_PREFIX_FILTER);
+ if (filterValue != null){
+ filter = PrefixExcludeFilter.from(filterValue);
+ }
+
+ return new CallerStackReporter(0, depth, filter);
+ }
}
diff --git a/src/test/java/org/apache/sling/tracer/internal/CallerFinderTest.java b/src/test/java/org/apache/sling/tracer/internal/CallerFinderTest.java
index 29906ef..fc104f9 100644
--- a/src/test/java/org/apache/sling/tracer/internal/CallerFinderTest.java
+++ b/src/test/java/org/apache/sling/tracer/internal/CallerFinderTest.java
@@ -30,7 +30,7 @@
@Test
public void determineCallerSingle() throws Exception{
CallerFinder cf = new CallerFinder(new String[] {"o.a.s", "o.a.j.o"});
- StackTraceElement[] stack = createStack(
+ StackTraceElement[] stack = asStack(
"o.a.j.o.a",
"o.a.j.o.b",
"c.a.g.w",
@@ -47,7 +47,7 @@
@Test
public void determineCallerMultipleApi() throws Exception{
CallerFinder cf = new CallerFinder(new String[] {"o.a.s", "o.a.j.o"});
- StackTraceElement[] stack = createStack(
+ StackTraceElement[] stack = asStack(
"o.a.j.o.a",
"o.a.j.o.b",
"o.a.s.a",
@@ -62,7 +62,7 @@
assertNotNull(caller);
assertEquals("c.a.g.w", caller.getClassName());
- stack = createStack(
+ stack = asStack(
"o.a.j.o.a",
"o.a.j.o.b",
"o.a.s.a",
@@ -89,7 +89,7 @@
@Test
public void nullCaller() throws Exception{
CallerFinder cf = new CallerFinder(new String[] {"o.a1.s", "o.a1.j.o"});
- StackTraceElement[] stack = createStack(
+ StackTraceElement[] stack = asStack(
"o.a.j.o.a",
"o.a.j.o.b",
"o.a.s.a",
@@ -104,7 +104,7 @@
assertNull(caller);
}
- private static StackTraceElement[] createStack(String ... stack){
+ static StackTraceElement[] asStack(String ... stack){
StackTraceElement[] result = new StackTraceElement[stack.length];
for (int i = 0; i < stack.length; i++) {
result[i] = new StackTraceElement(stack[i], "foo", null, 0);
diff --git a/src/test/java/org/apache/sling/tracer/internal/CallerStackReporterTest.java b/src/test/java/org/apache/sling/tracer/internal/CallerStackReporterTest.java
new file mode 100644
index 0000000..8ae067e
--- /dev/null
+++ b/src/test/java/org/apache/sling/tracer/internal/CallerStackReporterTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.sling.tracer.internal;
+
+import java.util.List;
+
+import org.junit.Test;
+
+import static org.apache.sling.tracer.internal.CallerFinderTest.asStack;
+import static org.junit.Assert.assertArrayEquals;
+
+public class CallerStackReporterTest {
+
+ @Test
+ public void startAndStop() throws Exception {
+ StackTraceElement[] s = asStack("0", "1", "2", "3", "4", "5");
+ assertArrayEquals(new String[]{"0", "1", "2", "3"}, arr(new CallerStackReporter(4).report(s)));
+ assertArrayEquals(new String[]{"0"}, arr(new CallerStackReporter(1).report(s)));
+ assertArrayEquals(new String[]{"2", "3"}, arr(new CallerStackReporter(2, 4, CallerFilter.ALL).report(s)));
+ }
+
+ @Test
+ public void filter() throws Exception{
+ StackTraceElement[] s = asStack("0", "1", "2", "3", "4", "5");
+ CallerFilter f = new CallerFilter() {
+ @Override
+ public boolean include(StackTraceElement ste) {
+ String name = ste.getClassName();
+ return name.equals("1") || name.equals("2");
+ }
+ };
+
+ assertArrayEquals(new String[]{"1", "2"}, arr(new CallerStackReporter(0, 4, f).report(s)));
+ }
+
+ @Test
+ public void prefixFilter() throws Exception{
+ StackTraceElement[] s = asStack("a.b.c", "a.b.d", "f.g.h", "m.g.i", "4", "5");
+ assertArrayEquals(new String[]{"a.b.c", "a.b.d", "f.g.h", "m.g.i"},
+ arr(new CallerStackReporter(0, 4, CallerFilter.ALL).report(s)));
+
+ CallerFilter f = PrefixExcludeFilter.from("a.b|f.g");
+ assertArrayEquals(new String[]{"m.g.i"},
+ arr(new CallerStackReporter(0, 4, f).report(s)));
+ }
+
+ private static String[] arr(List<StackTraceElement> list) {
+ String[] result = new String[list.size()];
+ for (int i = 0; i < list.size(); i++) {
+ result[i] = list.get(i).getClassName();
+ }
+ return result;
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/tracer/internal/JSONRecordingTest.java b/src/test/java/org/apache/sling/tracer/internal/JSONRecordingTest.java
index 14ab480..8daf2b4 100644
--- a/src/test/java/org/apache/sling/tracer/internal/JSONRecordingTest.java
+++ b/src/test/java/org/apache/sling/tracer/internal/JSONRecordingTest.java
@@ -25,7 +25,6 @@
import ch.qos.logback.classic.Level;
import org.apache.sling.commons.json.JSONObject;
-import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.MDC;
import org.slf4j.helpers.FormattingTuple;
@@ -104,12 +103,31 @@
assertEquals(tp1.getMessage(), l1.getString("message"));
assertEquals(1, l1.getJSONArray("params").length());
assertFalse(l1.has("exception"));
+ assertFalse(l1.has("caller"));
assertTrue(l1.has("timestamp"));
JSONObject l3 = json.getJSONArray("logs").getJSONObject(2);
assertNotNull(l3.get("exception"));
}
+ @Test
+ public void logsWithCaller() throws Exception{
+ StringWriter sw = new StringWriter();
+ final JSONRecording r = new JSONRecording("abc", request, true);
+
+ TracerConfig config = new TracerConfig(TracerContext.QUERY_LOGGER,
+ Level.INFO, new CallerStackReporter(20));
+ r.log(config, Level.INFO, "foo", tuple("foo"));
+
+ r.done();
+ r.render(sw);
+
+ JSONObject json = new JSONObject(sw.toString());
+ JSONObject l1 = json.getJSONArray("logs").getJSONObject(0);
+ assertTrue(l1.has("caller"));
+ assertTrue(l1.getJSONArray("caller").length() > 0);
+ }
+
private static FormattingTuple tuple(String msg){
return MessageFormatter.format(msg, null);
}
diff --git a/src/test/java/org/apache/sling/tracer/internal/TracerSetTest.java b/src/test/java/org/apache/sling/tracer/internal/TracerSetTest.java
new file mode 100644
index 0000000..fda855c
--- /dev/null
+++ b/src/test/java/org/apache/sling/tracer/internal/TracerSetTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.sling.tracer.internal;
+
+import org.apache.sling.commons.osgi.ManifestHeader;
+import org.junit.Test;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+
+public class TracerSetTest {
+
+ @Test
+ public void nullReporter() throws Exception{
+ CallerStackReporter r = TracerSet.createReporter(createEntry("foo"));
+ assertNull(r);
+ }
+
+ @Test
+ public void completeStack() throws Exception{
+ CallerStackReporter r = TracerSet.createReporter(createEntry("foo;caller=true"));
+ assertNotNull(r);
+ assertEquals(Integer.MAX_VALUE, r.getDepth());
+ }
+
+ @Test
+ public void depthSpecified() throws Exception{
+ CallerStackReporter r = TracerSet.createReporter(createEntry("foo;caller=28"));
+ assertNotNull(r);
+ assertEquals(28, r.getDepth());
+ }
+
+ @Test
+ public void invalidDepth() throws Exception{
+ CallerStackReporter r = TracerSet.createReporter(createEntry("foo;caller=abc"));
+ assertNull(r);
+ }
+
+ @Test
+ public void prefixFilter() throws Exception{
+ CallerStackReporter r = TracerSet.createReporter(createEntry("foo;caller=28;callerPrefixFilter=\"a|b\""));
+ assertNotNull(r);
+ assertEquals(28, r.getDepth());
+ assertTrue(r.getCallerFilter() instanceof PrefixExcludeFilter);
+ PrefixExcludeFilter f = (PrefixExcludeFilter) r.getCallerFilter();
+ assertEquals(asList("a", "b"), f.getPrefixesToExclude());
+ }
+
+ private static ManifestHeader.Entry createEntry(String config){
+ ManifestHeader parsedConfig = ManifestHeader.parse(config);
+ return parsedConfig.getEntries()[0];
+ }
+}