All configuration for a plain Java setup goes through the fluent builder returned by Observarium.builder(). Each method returns the builder for chaining. Call .build() to obtain the immutable Observarium instance.
| Method | Type | Default | Description |
|---|---|---|---|
scrubLevel(ScrubLevel) |
ScrubLevel |
BASIC |
Controls which PII patterns are active. See Scrub Levels below. |
addScrubPattern(Pattern) |
java.util.regex.Pattern |
— | Adds a single compiled regex to the active set. Each match is replaced with [REDACTED]. Can be called multiple times. |
fingerprinter(ExceptionFingerprinter) |
ExceptionFingerprinter |
DefaultExceptionFingerprinter |
Replaces the built-in fingerprinter. See Custom Fingerprinter. |
scrubber(DataScrubber) |
DataScrubber |
DefaultDataScrubber |
Replaces the built-in scrubber entirely. When set, scrubLevel and addScrubPattern are ignored. See Custom Scrubber. |
traceContextProvider(TraceContextProvider) |
TraceContextProvider |
MdcTraceContextProvider |
Replaces the MDC-based trace reader. See Custom TraceContextProvider. |
addPostingService(PostingService) |
PostingService |
— | Appends a posting service to the list. Can be called multiple times. |
postingServices(List<PostingService>) |
List<PostingService> |
— | Replaces the entire posting service list at once. |
listener(ObservariumListener) |
ObservariumListener |
no-op | Registers a lifecycle listener that receives callbacks for exception captures, drops, and posting outcomes. Used by observarium-micrometer to bridge events to Micrometer meters. See ObservariumListener. |
queueCapacity(int) |
int |
256 |
Capacity of the bounded ArrayBlockingQueue that backs the single background worker thread. When the queue is full, new events are dropped and a warning is logged. |
maxDuplicateComments(int) |
int |
5 |
Maximum number of duplicate comments posted on a single existing issue before further recurrences are dropped silently. Use -1 for unlimited. See Duplicate Comment Limit. |
Minimum working example:
Observarium obs = Observarium.builder()
.addPostingService(new GitHubPostingService(
GitHubConfig.of("ghp_token", "owner", "repo")))
.build();All properties are under the observarium prefix. Use either application.yml or application.properties.
| Property | Type | Default | Description |
|---|---|---|---|
observarium.scrub-level |
NONE | BASIC | STRICT |
BASIC |
PII scrub level applied to messages and stack traces. |
observarium.github.enabled |
boolean |
false |
Enable the GitHub posting service. |
observarium.github.token |
String |
— | GitHub personal access token or fine-grained token. |
observarium.github.owner |
String |
— | GitHub repository owner (organization name or user login). |
observarium.github.repo |
String |
— | GitHub repository name. |
observarium.github.label-prefix |
String |
observarium |
Label applied to all issues created by Observarium. |
observarium.jira.enabled |
boolean |
false |
Enable the Jira posting service. |
observarium.jira.base-url |
String |
— | Jira instance URL, e.g. https://myorg.atlassian.net. |
observarium.jira.username |
String |
— | Jira account username (email address for Jira Cloud). |
observarium.jira.api-token |
String |
— | Jira API token. |
observarium.jira.project-key |
String |
— | Jira project key, e.g. OPS. |
observarium.jira.issue-type |
String |
Bug |
Jira issue type name for created issues. |
observarium.gitlab.enabled |
boolean |
false |
Enable the GitLab posting service. |
observarium.gitlab.base-url |
String |
— | GitLab instance URL, e.g. https://gitlab.com. |
observarium.gitlab.private-token |
String |
— | GitLab personal access token or project access token. |
observarium.gitlab.project-id |
String |
— | GitLab numeric project ID or namespace/project path. |
observarium.email.enabled |
boolean |
false |
Enable the Email posting service. |
observarium.email.smtp-host |
String |
— | SMTP server hostname. |
observarium.email.smtp-port |
int |
587 |
SMTP server port. |
observarium.email.from |
String |
— | Sender address. |
observarium.email.to |
String |
— | Recipient address. |
observarium.email.username |
String |
— | SMTP authentication username. |
observarium.email.password |
String |
— | SMTP authentication password. |
observarium.email.auth |
boolean |
true |
Enable SMTP authentication. |
observarium.email.start-tls |
boolean |
true |
Enable STARTTLS. |
observarium.max-duplicate-comments |
int |
5 |
Maximum number of duplicate comments posted on a single existing issue. Use -1 for unlimited. See Duplicate Comment Limit. |
Example application.yml:
observarium:
scrub-level: STRICT
github:
owner: acme
repo: backend
token: ${GITHUB_TOKEN}
jira:
base-url: https://acme.atlassian.net
username: ${JIRA_USERNAME}
api-token: ${JIRA_TOKEN}
project-key: OPSIdentical keys to Spring Boot; use application.properties or application.yaml.
The Quarkus module uses the same property names as the Spring Boot module, including observarium.max-duplicate-comments. Refer to the Spring Boot table above for the complete list.
Example application.properties:
observarium.scrub-level=STRICT
observarium.github.owner=acme
observarium.github.repo=backend
observarium.github.token=${GITHUB_TOKEN}The ScrubLevel enum controls which regular expressions DefaultDataScrubber applies to exception messages and full stack trace text. Every match is replaced with the literal string [REDACTED].
No patterns are applied. The raw exception message and stack trace are sent to the posting service unchanged.
Use this only in development environments where the data contains no production PII.
Applies patterns that target credentials and tokens likely to appear in exception messages:
| Pattern | Example match |
|---|---|
| Key-value credentials | password=hunter2, token: abc123, api_key=xyz |
| Bearer tokens | Bearer eyJhbGciO... |
// Input: "Connection failed: password=supersecret host=db.internal"
// Output: "Connection failed: [REDACTED] host=db.internal"
Applies all BASIC patterns plus patterns for personal data:
| Pattern | Example match |
|---|---|
| Email addresses | user@example.com |
| IPv4 addresses | 192.168.1.42 |
| Phone numbers (US format) | 555-867-5309, 5558675309 |
// Input: "User alice@example.com from 10.0.0.5 called support at 555-123-4567"
// Output: "User [REDACTED] from [REDACTED] called support at [REDACTED]"
Additional patterns are applied after all built-in patterns at the active level. The replacement is always [REDACTED].
import java.util.regex.Pattern;
Observarium obs = Observarium.builder()
// Redact internal order IDs: ORD-followed by digits
.addScrubPattern(Pattern.compile("ORD-\\d+"))
// Redact UUIDs
.addScrubPattern(Pattern.compile(
"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
Pattern.CASE_INSENSITIVE))
.addPostingService(...)
.build();Custom patterns are additive: they do not replace the built-in level's patterns.
To bypass DefaultDataScrubber entirely, implement DataScrubber and pass it to the builder with .scrubber(). When a custom scrubber is provided, scrubLevel and addScrubPattern are ignored.
import io.hephaistos.observarium.scrub.DataScrubber;
public class MyDataScrubber implements DataScrubber {
@Override
public String scrub(String text) {
if (text == null) {
return null;
}
// Replace all digits with *
return text.replaceAll("\\d", "*");
}
}
Observarium obs = Observarium.builder()
.scrubber(new MyDataScrubber())
.addPostingService(...)
.build();DefaultExceptionFingerprinter computes a SHA-256 hash over the exception class name, every frame's className#methodName, and the class names of the full cause chain. Line numbers are excluded so the fingerprint is stable across minor refactors.
To override, implement ExceptionFingerprinter:
import io.hephaistos.observarium.fingerprint.ExceptionFingerprinter;
public class TopFrameFingerprinter implements ExceptionFingerprinter {
@Override
public String fingerprint(Throwable throwable) {
// Group by exception type and top frame only
StackTraceElement top = throwable.getStackTrace().length > 0
? throwable.getStackTrace()[0]
: null;
String key = throwable.getClass().getName()
+ (top != null ? "#" + top.getClassName() + "." + top.getMethodName() : "");
return Integer.toHexString(key.hashCode());
}
}
Observarium obs = Observarium.builder()
.fingerprinter(new TopFrameFingerprinter())
.addPostingService(...)
.build();MdcTraceContextProvider reads trace_id and span_id from SLF4J MDC. The default key names match what the OpenTelemetry Java Agent and most tracing bridges write to MDC.
Override the keys when your tracing library uses different names:
import io.hephaistos.observarium.trace.MdcTraceContextProvider;
// Brave / Spring Cloud Sleuth uses "traceId" and "spanId"
Observarium obs = Observarium.builder()
.traceContextProvider(new MdcTraceContextProvider("traceId", "spanId"))
.addPostingService(...)
.build();Implement TraceContextProvider from scratch when MDC is not the right source:
import io.hephaistos.observarium.trace.TraceContextProvider;
import io.opentelemetry.api.trace.Span;
public class OtelApiTraceContextProvider implements TraceContextProvider {
@Override
public String getTraceId() {
Span span = Span.current();
return span.getSpanContext().isValid()
? span.getSpanContext().getTraceId()
: null;
}
@Override
public String getSpanId() {
Span span = Span.current();
return span.getSpanContext().isValid()
? span.getSpanContext().getSpanId()
: null;
}
}
Observarium obs = Observarium.builder()
.traceContextProvider(new OtelApiTraceContextProvider())
.addPostingService(...)
.build();Observarium.captureException() returns immediately with a CompletableFuture<List<PostingResult>>. The actual work (fingerprinting, scrubbing, HTTP calls to the issue tracker) executes on a single daemon background thread backed by an ArrayBlockingQueue.
Key properties:
- Single worker thread — events are processed in submission order, no concurrency within Observarium itself.
- Bounded queue — when the queue reaches
queueCapacity(default 256), new events are dropped silently except for aWARNlog line:"Observarium queue full, dropping exception report". This protects the application from backpressure caused by a slow issue tracker. - Shutdown — both the JVM shutdown hook and
obs.shutdown()wait up to 10 seconds for in-flight work to complete, then force shutdown only if the drain times out, and then close all posting services.obs.shutdown()blocks for the duration of this sequence. Call it explicitly when you need to stop processing before JVM exit, for example in a@PreDestroymethod. - Failure isolation — if a posting service throws an unchecked exception,
ExceptionProcessorcatches it, logs it atERROR, and returns aPostingResult.failure(...). The application thread that calledcaptureExceptionis never affected.
// Inspect results if you need to know the outcome
CompletableFuture<List<PostingResult>> future =
obs.captureException(e, Severity.ERROR);
future.thenAccept(results ->
results.forEach(r -> {
if (r.success()) {
System.out.println("Issue: " + r.url());
} else {
System.err.println("Failed: " + r.errorMessage());
}
})
);ObservariumListener is a callback interface in observarium-core that lets you observe the internal lifecycle of the processing pipeline without modifying core logic. All methods have no-op defaults; implement only the events you care about.
| Method | Called when | Thread |
|---|---|---|
onExceptionCaptured(Severity) |
An exception is successfully enqueued | Caller's thread |
onExceptionDropped() |
An exception is dropped because the queue is full | Caller's thread |
onPostingCompleted(serviceName, duplicate, success, durationNanos) |
A posting service finishes processing | Background worker thread |
onQueueSizeAvailable(Supplier<Integer>) |
The Observarium instance is constructed; provides a live queue-depth supplier |
Construction thread |
Implementations must be thread-safe and must not throw. Any exception thrown from a callback is caught and logged but otherwise ignored.
Register a listener via the builder:
Observarium obs = Observarium.builder()
.listener(new MyObservariumListener())
.addPostingService(...)
.build();The primary built-in use of this interface is ObservariumMeterBinder from observarium-micrometer, which bridges these callbacks to Micrometer meters. See Micrometer Integration for setup details.
When an exception recurs frequently, Observarium caps the number of duplicate comments posted on an existing issue to prevent issue tracker noise.
For each duplicate occurrence, ExceptionProcessor retrieves the current comment count from DuplicateSearchResult and compares it against maxDuplicateComments:
| Condition | Action |
|---|---|
commentCount < maxDuplicateComments |
Normal commentOnIssue call — the recurrence is appended to the issue. |
commentCount == maxDuplicateComments |
postCommentLimitNotice is called once — a final "Comment Limit Reached" notice is posted on the issue. |
commentCount > maxDuplicateComments |
The occurrence is dropped silently. observarium.comments.dropped counter is incremented. ObservariumListener.onCommentDropped(serviceName) is called. |
Note: The notice itself is an additional comment, so the total number of comments Observarium may post is
maxDuplicateComments + 1(N regular comments plus the final notice). For example, withmaxDuplicateComments=5, up to 6 comments may appear on the issue: 5 duplicate occurrence comments and 1 limit notice.
The comment count is read from the tracker API during findDuplicate():
| Backend | API field |
|---|---|
| GitHub | comments field on the issue JSON |
| GitLab | user_notes_count field on the issue JSON |
| Jira | fields.comment.total from the issue response |
The count reflects all comments on the issue, not just those posted by Observarium. This means comments left by human users, bots, or other integrations also count toward the limit. This is intentional: if an issue already has significant discussion, additional automated noise is unwanted regardless of who posted the existing comments.
GitLab caveat: GitLab's
user_notes_countincludes system-generated notes (label changes, milestone updates, etc.) in addition to user comments. This means the limit may trigger earlier than expected on issues with frequent label or milestone activity.
DuplicateSearchResult.found(id, url) (the 2-argument form) returns COMMENT_COUNT_UNKNOWN = -1 for the comment count. When ExceptionProcessor sees -1, it treats the count as below the limit and always allows the comment through. This means custom PostingService implementations that have not been updated to return a comment count continue to work without restriction. See Custom Posting Service for how to supply the count.
Plain Java builder:
Observarium obs = Observarium.builder()
.maxDuplicateComments(10) // cap at 10 comments per issue
// .maxDuplicateComments(-1) // unlimited
.addPostingService(new GitHubPostingService(GitHubConfig.of(token, "owner", "repo")))
.build();Spring Boot / Quarkus property:
observarium.max-duplicate-comments=10