Skip to content

Commit c705c20

Browse files
committed
Add StackHasher (fork/copy from Logstash)
1 parent a4730d0 commit c705c20

File tree

3 files changed

+578
-0
lines changed

3 files changed

+578
-0
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright 2013-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.avaje.logback.encoder;
17+
18+
import java.util.ArrayDeque;
19+
import java.util.Deque;
20+
21+
/**
22+
* Utility class that generates a hash from any Java {@link Throwable error}
23+
*
24+
* @author Pierre Smeyers
25+
*/
26+
public final class StackHasher {
27+
28+
private final StackElementFilter filter;
29+
30+
/**
31+
* Constructs a {@link StackHasher} with the given filter.
32+
*
33+
* @param filter filter
34+
*/
35+
public StackHasher(StackElementFilter filter) {
36+
this.filter = filter;
37+
}
38+
39+
/**
40+
* Constructs a {@link StackHasher} using {@link StackElementFilter#withSourceInfo()} filter
41+
*/
42+
public StackHasher() {
43+
this(StackElementFilter.withSourceInfo());
44+
}
45+
46+
/**
47+
* Generates a Hexadecimal hash for the given error stack.
48+
* <p>
49+
* Two errors with the same stack hash are most probably same errors.
50+
*
51+
* @param error the error to generate a hash from
52+
* @return the generated hexadecimal hash
53+
*/
54+
public String hexHash(Throwable error) {
55+
// compute topmost error hash, but don't queue the complete hashes chain
56+
return toHex(hash(error, null));
57+
}
58+
59+
/**
60+
* Generates and returns Hexadecimal hashes for the error stack and each ancestor {@link Throwable#getCause() cause}.
61+
* <p>
62+
* The first queue element is the stack hash for the topmost error, the next one (if any) is it's direct
63+
* {@link Throwable#getCause() cause} hash, and so on...
64+
*
65+
* @param error the error to generate a hash from
66+
* @return a Dequeue with hashes
67+
*/
68+
public Deque<String> hexHashes(Throwable error) {
69+
Deque<String> hexHashes = new ArrayDeque<String>();
70+
hash(error, hexHashes);
71+
return hexHashes;
72+
}
73+
74+
/**
75+
* Generates a hash (int) of the given error stack.
76+
* <p>
77+
* Two errors with the same stack hash are most probably same errors.
78+
*
79+
* @param error the error to generate a hash from
80+
* @param hexHashes
81+
* @return the generated hexadecimal hash
82+
*/
83+
int hash(Throwable error, Deque<String> hexHashes) {
84+
int hash = 0;
85+
86+
// compute parent error hash
87+
if (error.getCause() != null && error.getCause() != error) {
88+
// has parent error
89+
hash = hash(error.getCause(), hexHashes);
90+
}
91+
92+
// then this error hash
93+
// hash error classname
94+
hash = 31 * hash + error.getClass().getName().hashCode();
95+
// hash stacktrace
96+
for (StackTraceElement element : error.getStackTrace()) {
97+
if (filter.accept(element)) {
98+
hash = 31 * hash + hash(element);
99+
}
100+
}
101+
102+
// push hexadecimal representation of hash
103+
if (hexHashes != null) {
104+
hexHashes.push(toHex(hash));
105+
}
106+
107+
return hash;
108+
}
109+
110+
String toHex(int hash) {
111+
return String.format("%08x", hash);
112+
}
113+
114+
int hash(StackTraceElement element) {
115+
int result = element.getClassName().hashCode();
116+
result = 31 * result + element.getMethodName().hashCode();
117+
// let's assume filename is not necessary
118+
result = 31 * result + element.getLineNumber();
119+
return result;
120+
}
121+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright 2013-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.avaje.logback.encoder;
17+
18+
import org.junit.jupiter.api.Test;
19+
20+
import java.util.Deque;
21+
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
import static org.assertj.core.api.Assertions.fail;
24+
25+
public class StackHasherTest {
26+
27+
private static class StackTraceElementGenerator {
28+
public static void generateSingle() {
29+
oneSingle();
30+
}
31+
32+
public static void oneSingle() {
33+
twoSingle();
34+
}
35+
36+
private static void twoSingle() {
37+
threeSingle();
38+
}
39+
40+
private static void threeSingle() {
41+
four();
42+
}
43+
44+
private static void four() {
45+
five();
46+
}
47+
48+
private static void five() {
49+
six();
50+
}
51+
52+
private static void six() {
53+
seven();
54+
}
55+
56+
private static void seven() {
57+
eight();
58+
}
59+
60+
private static void eight() {
61+
throw new RuntimeException("message");
62+
}
63+
64+
public static void generateCausedBy() {
65+
oneCausedBy();
66+
}
67+
68+
private static void oneCausedBy() {
69+
twoCausedBy();
70+
}
71+
72+
private static void twoCausedBy() {
73+
try {
74+
threeSingle();
75+
} catch (RuntimeException e) {
76+
throw new RuntimeException("wrapper", e);
77+
}
78+
}
79+
}
80+
81+
@Test
82+
public void one_hash_should_be_generated() {
83+
try {
84+
StackTraceElementGenerator.generateSingle();
85+
fail("Exception must have been thrown");
86+
} catch (RuntimeException e) {
87+
// GIVEN
88+
StackHasher hasher = new StackHasher();
89+
90+
// WHEN
91+
Deque<String> hashes = hasher.hexHashes(e);
92+
93+
// THEN
94+
assertThat(hashes).hasSize(1);
95+
}
96+
}
97+
98+
@Test
99+
public void two_hashes_should_be_generated() {
100+
try {
101+
StackTraceElementGenerator.generateCausedBy();
102+
fail("Exception must have been thrown");
103+
} catch (RuntimeException e) {
104+
// GIVEN
105+
StackHasher hasher = new StackHasher();
106+
107+
// WHEN
108+
Deque<String> hashes = hasher.hexHashes(e);
109+
110+
// THEN
111+
assertThat(hashes).hasSize(2);
112+
}
113+
}
114+
private static class OnlyFromStackTraceElementGeneratorFilter implements StackElementFilter {
115+
@Override
116+
public boolean accept(StackTraceElement element) {
117+
return element.getClassName().equals(StackTraceElementGenerator.class.getName());
118+
}
119+
}
120+
121+
/**
122+
* Warning: computes expected hash based on StackTraceElementGenerator elements
123+
*
124+
* do not change methods name, line or it will break the test
125+
*/
126+
@Test
127+
public void expected_hash_should_be_generated() {
128+
try {
129+
StackTraceElementGenerator.generateSingle();
130+
fail("Exception must have been thrown");
131+
} catch (RuntimeException e) {
132+
// GIVEN
133+
StackHasher hasher = new StackHasher(new OnlyFromStackTraceElementGeneratorFilter());
134+
135+
// WHEN
136+
Deque<String> hashes = hasher.hexHashes(e);
137+
138+
// THEN
139+
assertThat(hashes)
140+
.hasSize(1)
141+
.first().isEqualTo("86983bb4");
142+
143+
String hash = hasher.hexHash(e);
144+
assertThat(hash).isEqualTo("86983bb4");
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)