Skip to content

Commit 76046b4

Browse files
authored
feature: add minimum breadcrumb level filtering (#211)
1 parent d7bd4f1 commit 76046b4

6 files changed

Lines changed: 539 additions & 32 deletions

File tree

backtrace-library/src/androidTest/java/backtraceio/library/breadcrumbs/BacktraceBreadcrumbsTest.java

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import android.content.Context;
1010
import androidx.test.ext.junit.runners.AndroidJUnit4;
1111
import androidx.test.platform.app.InstrumentationRegistry;
12+
import backtraceio.library.enums.BacktraceBreadcrumbLevel;
1213
import backtraceio.library.enums.BacktraceBreadcrumbType;
1314
import java.io.File;
1415
import java.io.IOException;
@@ -145,6 +146,203 @@ public void testAddBreadcrumbRejectsDisabledType() {
145146
}
146147
}
147148

149+
@Test
150+
public void testAddBreadcrumbRejectsLevelBelowMinimum() {
151+
try {
152+
cleanUp();
153+
154+
backtraceBreadcrumbs =
155+
new BacktraceBreadcrumbs(context.getFilesDir().getAbsolutePath());
156+
backtraceBreadcrumbs.enableBreadcrumbs(
157+
context,
158+
BacktraceBreadcrumbType.ALL,
159+
BacktraceBreadcrumbs.DEFAULT_MAX_LOG_SIZE_BYTES,
160+
BacktraceBreadcrumbLevel.WARNING);
161+
162+
// Below threshold, dropped
163+
assertFalse(backtraceBreadcrumbs.addBreadcrumb(
164+
"test-debug", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.DEBUG));
165+
assertFalse(backtraceBreadcrumbs.addBreadcrumb(
166+
"test-info", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.INFO));
167+
168+
List<String> breadcrumbLogFileData = BreadcrumbsReader.readBreadcrumbLogFile(
169+
context.getFilesDir().getAbsolutePath());
170+
171+
// Only the configuration breadcrumb should be present.
172+
assertEquals(1, breadcrumbLogFileData.size());
173+
} catch (Exception ex) {
174+
fail(ex.getMessage());
175+
}
176+
}
177+
178+
@Test
179+
public void testAddBreadcrumbAcceptsLevelAtOrAboveMinimum() {
180+
try {
181+
cleanUp();
182+
183+
backtraceBreadcrumbs =
184+
new BacktraceBreadcrumbs(context.getFilesDir().getAbsolutePath());
185+
backtraceBreadcrumbs.enableBreadcrumbs(
186+
context,
187+
BacktraceBreadcrumbType.ALL,
188+
BacktraceBreadcrumbs.DEFAULT_MAX_LOG_SIZE_BYTES,
189+
BacktraceBreadcrumbLevel.WARNING);
190+
191+
// At threshold, retain
192+
assertTrue(backtraceBreadcrumbs.addBreadcrumb(
193+
"test-warning", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.WARNING));
194+
// Above threshold, retain
195+
assertTrue(backtraceBreadcrumbs.addBreadcrumb(
196+
"test-error", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.ERROR));
197+
assertTrue(backtraceBreadcrumbs.addBreadcrumb(
198+
"test-fatal", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.FATAL));
199+
200+
List<String> breadcrumbLogFileData = BreadcrumbsReader.readBreadcrumbLogFile(
201+
context.getFilesDir().getAbsolutePath());
202+
203+
// Configuration breadcrumb + three added.
204+
assertEquals(4, breadcrumbLogFileData.size());
205+
206+
assertEquals("test-warning", new JSONObject(breadcrumbLogFileData.get(1)).get("message"));
207+
assertEquals("test-error", new JSONObject(breadcrumbLogFileData.get(2)).get("message"));
208+
assertEquals("test-fatal", new JSONObject(breadcrumbLogFileData.get(3)).get("message"));
209+
} catch (Exception ex) {
210+
fail(ex.getMessage());
211+
}
212+
}
213+
214+
@Test
215+
public void testAddBreadcrumbDefaultMinLevelAcceptsAllLevels() {
216+
try {
217+
// default min level should be DEBUG on setUp()
218+
assertEquals(BacktraceBreadcrumbLevel.DEBUG, backtraceBreadcrumbs.getMinBreadcrumbLevel());
219+
220+
assertTrue(backtraceBreadcrumbs.addBreadcrumb(
221+
"test-debug", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.DEBUG));
222+
assertTrue(backtraceBreadcrumbs.addBreadcrumb(
223+
"test-info", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.INFO));
224+
assertTrue(backtraceBreadcrumbs.addBreadcrumb(
225+
"test-warning", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.WARNING));
226+
assertTrue(backtraceBreadcrumbs.addBreadcrumb(
227+
"test-error", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.ERROR));
228+
assertTrue(backtraceBreadcrumbs.addBreadcrumb(
229+
"test-fatal", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.FATAL));
230+
231+
List<String> breadcrumbLogFileData = BreadcrumbsReader.readBreadcrumbLogFile(
232+
context.getFilesDir().getAbsolutePath());
233+
234+
// Configuration breadcrumb + five added.
235+
assertEquals(6, breadcrumbLogFileData.size());
236+
} catch (Exception ex) {
237+
fail(ex.getMessage());
238+
}
239+
}
240+
241+
@Test
242+
public void testReEnableBreadcrumbsUpdatesMinLevel() {
243+
try {
244+
cleanUp();
245+
246+
backtraceBreadcrumbs =
247+
new BacktraceBreadcrumbs(context.getFilesDir().getAbsolutePath());
248+
backtraceBreadcrumbs.enableBreadcrumbs(
249+
context,
250+
BacktraceBreadcrumbType.ALL,
251+
BacktraceBreadcrumbs.DEFAULT_MAX_LOG_SIZE_BYTES,
252+
BacktraceBreadcrumbLevel.DEBUG);
253+
assertEquals(BacktraceBreadcrumbLevel.DEBUG, backtraceBreadcrumbs.getMinBreadcrumbLevel());
254+
255+
// Initial threshold lets DEBUG through.
256+
assertTrue(backtraceBreadcrumbs.addBreadcrumb(
257+
"pre-change", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.DEBUG));
258+
259+
// Tighten threshold by re-enabling.
260+
backtraceBreadcrumbs.enableBreadcrumbs(
261+
context,
262+
BacktraceBreadcrumbType.ALL,
263+
BacktraceBreadcrumbs.DEFAULT_MAX_LOG_SIZE_BYTES,
264+
BacktraceBreadcrumbLevel.ERROR);
265+
assertEquals(BacktraceBreadcrumbLevel.ERROR, backtraceBreadcrumbs.getMinBreadcrumbLevel());
266+
267+
// Now DEBUG is dropped; ERROR still goes through.
268+
assertFalse(backtraceBreadcrumbs.addBreadcrumb(
269+
"post-change-debug", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.DEBUG));
270+
assertTrue(backtraceBreadcrumbs.addBreadcrumb(
271+
"post-change-error", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.ERROR));
272+
} catch (Exception ex) {
273+
fail(ex.getMessage());
274+
}
275+
}
276+
277+
@Test
278+
public void testNullMinBreadcrumbLevelDefaultsToDebug() {
279+
try {
280+
cleanUp();
281+
282+
backtraceBreadcrumbs =
283+
new BacktraceBreadcrumbs(context.getFilesDir().getAbsolutePath());
284+
285+
assertTrue(backtraceBreadcrumbs.enableBreadcrumbs(context, (BacktraceBreadcrumbLevel) null));
286+
assertEquals(BacktraceBreadcrumbLevel.DEBUG, backtraceBreadcrumbs.getMinBreadcrumbLevel());
287+
288+
assertTrue(backtraceBreadcrumbs.addBreadcrumb(
289+
"debug-after-null-min-level", BacktraceBreadcrumbType.MANUAL, BacktraceBreadcrumbLevel.DEBUG));
290+
291+
List<String> breadcrumbLogFileData = BreadcrumbsReader.readBreadcrumbLogFile(
292+
context.getFilesDir().getAbsolutePath());
293+
294+
JSONObject configBreadcrumb = new JSONObject(breadcrumbLogFileData.get(0));
295+
assertEquals("Breadcrumbs configuration", configBreadcrumb.get("message"));
296+
assertEquals("debug", configBreadcrumb.getJSONObject("attributes").get("breadcrumb.level"));
297+
298+
JSONObject debugBreadcrumb = new JSONObject(breadcrumbLogFileData.get(1));
299+
assertEquals("debug-after-null-min-level", debugBreadcrumb.get("message"));
300+
assertEquals("debug", debugBreadcrumb.get("level"));
301+
} catch (Exception ex) {
302+
fail(ex.getMessage());
303+
}
304+
}
305+
306+
@Test
307+
public void testNullBreadcrumbLevelDefaultsToInfo() {
308+
try {
309+
assertTrue(backtraceBreadcrumbs.addBreadcrumb("null-level", BacktraceBreadcrumbType.MANUAL, null));
310+
311+
List<String> breadcrumbLogFileData = BreadcrumbsReader.readBreadcrumbLogFile(
312+
context.getFilesDir().getAbsolutePath());
313+
314+
JSONObject parsedBreadcrumb = new JSONObject(breadcrumbLogFileData.get(1));
315+
assertEquals("null-level", parsedBreadcrumb.get("message"));
316+
assertEquals("info", parsedBreadcrumb.get("level"));
317+
} catch (Exception ex) {
318+
fail(ex.getMessage());
319+
}
320+
}
321+
322+
@Test
323+
public void testConfigurationBreadcrumbIncludesMinLevel() {
324+
try {
325+
cleanUp();
326+
327+
backtraceBreadcrumbs =
328+
new BacktraceBreadcrumbs(context.getFilesDir().getAbsolutePath());
329+
backtraceBreadcrumbs.enableBreadcrumbs(
330+
context,
331+
BacktraceBreadcrumbType.ALL,
332+
BacktraceBreadcrumbs.DEFAULT_MAX_LOG_SIZE_BYTES,
333+
BacktraceBreadcrumbLevel.WARNING);
334+
335+
List<String> breadcrumbLogFileData = BreadcrumbsReader.readBreadcrumbLogFile(
336+
context.getFilesDir().getAbsolutePath());
337+
338+
JSONObject configBreadcrumb = new JSONObject(breadcrumbLogFileData.get(0));
339+
assertEquals("Breadcrumbs configuration", configBreadcrumb.get("message"));
340+
assertEquals("warning", configBreadcrumb.getJSONObject("attributes").get("breadcrumb.level"));
341+
} catch (Exception ex) {
342+
fail(ex.getMessage());
343+
}
344+
}
345+
148346
@Test
149347
public void testAddBreadcrumbWithAttributes() {
150348
try {

backtrace-library/src/androidTest/java/backtraceio/library/breadcrumbs/BreadcrumbsReader.java

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,67 @@
55
import java.io.IOException;
66
import java.util.ArrayList;
77
import java.util.List;
8+
import org.json.JSONException;
9+
import org.json.JSONObject;
810

11+
/**
12+
* Test helper that extracts breadcrumb JSON records from the on-disk QueueFile log.
13+
*/
914
public class BreadcrumbsReader {
1015

16+
/**
17+
* Reads the breadcrumb log file and returns each breadcrumb as a JSON string.
18+
*
19+
* <p>Lines are trimmed to their first {@code '{'}} to strip QueueFile framing bytes,
20+
* then kept only if they parse as JSON and contain a {@code timestamp} field. This
21+
* filters out framing noise and orphaned sub-objects exposed by rollover.</p>
22+
*
23+
* @param path directory containing the breadcrumb log
24+
* @return breadcrumb records in file order
25+
*/
1126
public static List<String> readBreadcrumbLogFile(String path) throws IOException {
1227
BacktraceBreadcrumbs breadcrumbs = new BacktraceBreadcrumbs(path);
1328
File breadcrumbLogFile = new File(breadcrumbs.getBreadcrumbLogPath());
1429

1530
List<String> breadcrumbLogFileData = new ArrayList<String>();
16-
FileInputStream inputStream = new FileInputStream(breadcrumbLogFile.getAbsolutePath());
17-
18-
// The encoding contains headers for the encoded data
19-
// We just throw away lines that don't start with "timestamp
20-
StringBuilder stringBuilder = new StringBuilder();
21-
while (inputStream.available() > 0) {
22-
char c = (char) inputStream.read();
23-
if (c == '\n') {
24-
String line = stringBuilder.toString();
25-
if (line.matches(".*timestamp.*")) {
26-
breadcrumbLogFileData.add(line);
31+
32+
try (FileInputStream inputStream = new FileInputStream(breadcrumbLogFile.getAbsolutePath())) {
33+
StringBuilder stringBuilder = new StringBuilder();
34+
int readByte;
35+
36+
while ((readByte = inputStream.read()) != -1) {
37+
char c = (char) readByte;
38+
if (c == '\n') {
39+
addBreadcrumbJsonRecordIfValid(breadcrumbLogFileData, stringBuilder.toString());
40+
stringBuilder.setLength(0);
41+
continue;
2742
}
28-
stringBuilder = new StringBuilder();
29-
continue;
43+
44+
stringBuilder.append(c);
45+
}
46+
47+
if (stringBuilder.length() > 0) {
48+
addBreadcrumbJsonRecordIfValid(breadcrumbLogFileData, stringBuilder.toString());
3049
}
31-
stringBuilder.append(c);
3250
}
3351

3452
return breadcrumbLogFileData;
3553
}
54+
55+
private static void addBreadcrumbJsonRecordIfValid(List<String> breadcrumbLogFileData, String line) {
56+
int braceStart = line.indexOf('{');
57+
if (braceStart < 0) {
58+
return;
59+
}
60+
61+
String candidate = line.substring(braceStart);
62+
try {
63+
JSONObject parsed = new JSONObject(candidate);
64+
if (parsed.has("timestamp")) {
65+
breadcrumbLogFileData.add(candidate);
66+
}
67+
} catch (JSONException ignored) {
68+
// Ignore QueueFile framing bytes and partial rollover fragments.
69+
}
70+
}
3671
}

0 commit comments

Comments
 (0)