Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .agents/skills/exceptionless-java/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
name: exceptionless-java
description: Use this skill when a developer wants to install, configure, troubleshoot, or integrate the Exceptionless Java client in plain Java, Spring Boot (MVC or WebFlux), Jakarta Servlet containers, Logback/SLF4J, Log4j 2, or java.util.logging apps. Use it for API keys, startup, self-hosting, sending errors/logs/feature usage/404/custom events, custom properties, sessions, heartbeats, user identity, PII/data exclusions, plugins, project client configuration values (@@log, @@error, @@DataExclusions), queues, persistence, and shutdown behavior, even if they only ask "how do I wire up Exceptionless in Java?"
---

# Exceptionless Java Client

Produce source-accurate setup code and guidance for the Exceptionless Java client (`com.exceptionless:*`,
version 0.9 and later, Java 17+). Keep answers compact and copyable, with imports.

## Pick the module

| App | Dependency | Setup |
| --- | --- | --- |
| Spring Boot 3 or 4 | `exceptionless-spring-boot-starter` | `exceptionless.api-key` property; inject `ExceptionlessClient` |
| Servlet container (Tomcat, Jetty...) | `exceptionless-servlet` | `ExceptionlessFilter` + `Exceptionless.startup(...)` (the filter passes requests through until the default client has started) |
| Plain Java / CLI / desktop | `exceptionless-core` | `Exceptionless.startup("KEY")` |
| Logback / SLF4J | `exceptionless-logback` | `com.exceptionless.logback.ExceptionlessAppender` |
| Log4j 2 | `exceptionless-log4j2` | `<Exceptionless name="..." />` appender |
| java.util.logging | `exceptionless-jul` | `com.exceptionless.jul.ExceptionlessHandler` |

Always recommend importing `com.exceptionless:exceptionless-bom` to align versions.

## Rules

- Start once: `Exceptionless.startup("KEY")` or `Exceptionless.startup(c -> c.apiKey(...)...)`. With no
arguments it reads `exceptionless.*` system properties, `EXCEPTIONLESS_*` environment variables
(`EXCEPTIONLESS_API_KEY`, `EXCEPTIONLESS_SERVER_URL`) and `exceptionless.properties`.
- In Spring Boot, never call `Exceptionless.startup`; the starter creates the client bean and makes it
the default. Customize with an `ExceptionlessConfigurationCustomizer` bean.
`exceptionless.logging.minimum-level` is a `LogLevel` (`trace` ... `off`, any case; aliases such as
`warning` are rejected) and `exceptionless.storage-path` a directory path.
- The starter only wires Logback. For Log4j 2, add `exceptionless-log4j2` and the `<Exceptionless />`
appender to `log4j2.xml`; without `apiKey` it uses the starter's client.
- `serverUrl(...)` also sets the config and heartbeat URLs; override those after it.
- The API key, server URLs, storage path, proxy and TLS settings are locked once the client starts.
- `submitException` / `createException` take any `Throwable`; causes and suppressed exceptions are included.
- Builders end with `.submit()`. Submitting never blocks on the network and never throws.
- Short-lived jobs: queued events are sent on JVM shutdown automatically; call
`Exceptionless.flush(Duration.ofSeconds(10))` when the process may be killed abruptly.
- Logging appenders send `warn` and above by default (`minimumLevel`); the project's `@@log:<logger>`
settings override that in both directions. Appenders without `apiKey` use the default client once it
has started; messages logged earlier are not sent.
- Reference ids must be 8-100 characters of letters, digits and dashes; error events get one automatically.
- Use lowercase log levels in settings: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `off`.
- Don't put application loggers under the `com.exceptionless` package; the integrations treat those as
the client's own diagnostics and skip them.
- For another logging framework, build the appender on `com.exceptionless.logging.LogEventSupport`
(client selection, owned client lifecycle, recursion guard, level checks, event creation).

## Privacy

Private information (machine name, IPs, headers, cookies, query strings, form data, user names) is on by
default. In production examples that collect request or user data, show `includePrivateInformation(false)`
or the specific toggles, and `addDataExclusions("*password*", "*secret*", "*token*")`. Exclusions are
wildcards, case-insensitive, applied at every depth. `Authorization`, `Cookie` and similar credential
headers are never sent.

## Project settings (client configuration values)

| Key | Example | Effect |
| --- | --- | --- |
| `@@log:<logger>` | `@@log:com.example.*` = `info` | Minimum log level; most specific wildcard wins |
| `@@error:<exception type>` | `@@error:java.io.IOException` = `false` | Discard errors with that type in the cause chain |
| `@@<type>:<source>` | `@@404:/favicon.ico` = `false` | Discard usage/404/custom events by source |
| `@@DataExclusions` | `*password*,ssn` | Extra exclusions |
| `@@IncludePrivateInformation` | `false` | Turn private info off |
| `@@UserAgentBotPatterns` | `*bot*` | Ignore bot requests |

## Troubleshooting

- Nothing arrives: check `Exceptionless.getConfiguration().validate()` (empty when valid) and
`isEnabled()`. A missing/invalid key disables the client with a warning instead of throwing.
- Enable client diagnostics: the client logs through `System.Logger` under `com.exceptionless`
(routed to SLF4J/Log4j/JUL by whatever bridge the app uses). Set that logger to `DEBUG`.
- Self-hosted behind a redirect (http → https): the client reports redirects instead of following them;
use the final URL.
- Events dropped with HTTP 402: the organization is over its plan limit; the client pauses for an hour.

## Source anchors

Verify behavior in:

- `exceptionless-core/src/main/java/com/exceptionless/Exceptionless.java` (static default client)
- `exceptionless-core/src/main/java/com/exceptionless/ExceptionlessClient.java`
- `exceptionless-core/src/main/java/com/exceptionless/ExceptionlessConfiguration.java`
- `exceptionless-core/src/main/java/com/exceptionless/EventBuilder.java`
- `exceptionless-core/src/main/java/com/exceptionless/plugins/` (pipeline, exclusions, privacy)
- `exceptionless-core/src/main/java/com/exceptionless/queue/DefaultEventQueue.java` (status handling)
- `exceptionless-spring-boot-starter/src/main/java/com/exceptionless/spring/boot/`
- `exceptionless-servlet/src/main/java/com/exceptionless/servlet/ExceptionlessFilter.java`
- `exceptionless-core/src/main/java/com/exceptionless/logging/LogEventSupport.java` (shared appender logic)
- The appenders in `exceptionless-logback`, `exceptionless-log4j2`, `exceptionless-jul`
- `README.md` and `samples/`
4 changes: 4 additions & 0 deletions .agents/skills/exceptionless-java/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Exceptionless Java"
short_description: "Configure the Exceptionless Java client"
default_prompt: "Use $exceptionless-java to configure, document, or troubleshoot the Exceptionless Java client."
23 changes: 23 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
version: 2
updates:
- package-ecosystem: maven
directory: /
schedule:
interval: weekly
groups:
test-dependencies:
patterns: ["org.junit*", "org.assertj*", "org.awaitility*", "org.testcontainers*", "org.eclipse.jetty*"]
maven-plugins:
patterns: ["org.apache.maven.plugins*", "org.codehaus.mojo*", "org.jacoco*", "org.sonatype.central*", "com.diffplug.spotless*"]
# Integration targets (Logback, Log4j, the Servlet API, Spring Boot) are provided dependencies:
# applications bring their own version, so these only set what the integrations are compiled
# and tested against, and are bumped deliberately (e.g. alongside the -Pspring-boot-4 profile).
ignore:
- dependency-name: "ch.qos.logback:*"
- dependency-name: "org.apache.logging.log4j:*"
- dependency-name: "jakarta.servlet:*"
- dependency-name: "org.springframework.boot:*"
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
227 changes: 227 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
name: Build

# Versions come from git tags (MinVer), like the other Exceptionless builds:
# - A v* tag (v0.9.0-beta.1, v1.0.0) builds that version and publishes it to Maven Central.
# - Other pushes build a pre-release version, such as 0.9.0-alpha.0.12 on main or
# 0.9.0-alpha.my-branch.0.12 on a branch, and publish it to GitHub Packages.
# Pre-release labels must be alpha, beta or rc: Maven orders unknown labels such as "preview"
# after the release (1.0.0-preview.1 > 1.0.0).
# - Pull requests build and test only.
#
# Required secrets for releases: CENTRAL_USERNAME and CENTRAL_PASSWORD (a Central Portal user
# token), GPG_PRIVATE_KEY and GPG_PASSPHRASE.
on:
push:
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Never cancel a tag build; it may be publishing a release.
cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}

defaults:
run:
shell: bash

env:
MAVEN_ARGS: --batch-mode --no-transfer-progress

jobs:
version:
name: Version
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: Set up .NET for MinVer
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.0.x

- name: Compute version
id: version
run: |
# Pre-release versions from branches other than main, maintenance branches (1.x, 1.2),
# tags and pull requests carry the branch name.
branch="${GITHUB_REF##*/}"
if [[ "$GITHUB_REF" != refs/heads/* || "$branch" == main || "$branch" =~ ^[0-9]+\.(x|[0-9]+)$ ]]; then
branch=""
fi
branch="$(tr -c 'A-Za-z0-9-' '-' <<< "$branch" | sed -E 's/-+$//')"
suffix="${branch:+$branch.}"

dotnet tool install --global minver-cli --version 8.0.0
version="$(minver --tag-prefix v --default-pre-release-identifiers "alpha.${suffix}0" --minimum-major-minor 0.9)"

# After a pre-release tag (v1.1.0-beta.1) MinVer appends only the height; add the branch.
if [[ -n "$branch" && "$version" != *"$branch"* ]]; then
version="$(sed -E "s/\.([0-9]+)$/.${suffix}\1/" <<< "$version")"
fi

echo "version=$version" >> "$GITHUB_OUTPUT"
echo "### Version: $version" >> "$GITHUB_STEP_SUMMARY"
echo "Version: $version (ref: $GITHUB_REF, event: $GITHUB_EVENT_NAME)"

build:
name: JDK ${{ matrix.java }} on ${{ matrix.os }}
needs: version
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
java: [17, 21, 25]
include:
- os: windows-latest
java: 21
- os: macos-latest
java: 21
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: temurin
java-version: ${{ matrix.java }}
cache: maven

- name: Build and test
run: ./mvnw verify -Drevision="$VERSION"
env:
VERSION: ${{ needs.version.outputs.version }}

- name: Upload test reports
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: test-reports-${{ matrix.os }}-jdk${{ matrix.java }}
path: '**/target/surefire-reports/'

spring-boot-4:
name: Spring Boot 4 compatibility
needs: version
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up JDK
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: temurin
java-version: 21
cache: maven

- name: Test the starter against Spring Boot 4
run: ./mvnw -pl exceptionless-spring-boot-starter -am verify -Pspring-boot-4 -Drevision="$VERSION"
env:
VERSION: ${{ needs.version.outputs.version }}

e2e:
name: End-to-end (real Exceptionless server)
needs: version
runs-on: ubuntu-latest
timeout-minutes: 45
# Every push (so every published build) and pull requests labeled e2e.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'e2e')
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up JDK
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: temurin
java-version: 21
cache: maven

- name: Run end-to-end tests
run: ./mvnw -pl exceptionless-e2e-tests -am verify -De2e=true -Drevision="$VERSION"
env:
VERSION: ${{ needs.version.outputs.version }}

publish-ci:
name: Publish CI packages
needs: [version, build, spring-boot-4, e2e]
if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/heads/') && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up JDK
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: temurin
java-version: 21
cache: maven
server-id: github
server-username: GITHUB_ACTOR
server-password: GITHUB_TOKEN

- name: Publish to GitHub Packages
# A CI package is a convenience; failing to publish one doesn't fail the build.
continue-on-error: true
run: >-
./mvnw deploy -DskipTests -Drevision="$VERSION"
-DaltDeploymentRepository="github::https://maven.pkg.github.com/$GITHUB_REPOSITORY"
env:
VERSION: ${{ needs.version.outputs.version }}
GITHUB_TOKEN: ${{ github.token }}

release:
name: Publish to Maven Central
needs: [version, build, spring-boot-4, e2e]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Check the version matches the tag
run: |
if [[ "$VERSION" != "${GITHUB_REF_NAME#v}" ]]; then
echo "::error::Tag $GITHUB_REF_NAME is not a version tag (computed version: $VERSION)."
exit 1
fi
env:
VERSION: ${{ needs.version.outputs.version }}

- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up JDK
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: temurin
java-version: 21
cache: maven
server-id: central
server-username: CENTRAL_USERNAME
server-password: CENTRAL_PASSWORD
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
gpg-passphrase: GPG_PASSPHRASE

- name: Publish to Maven Central
run: ./mvnw deploy -Prelease -DskipTests -Drevision="$VERSION"
env:
VERSION: ${{ needs.version.outputs.version }}
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
13 changes: 0 additions & 13 deletions .github/workflows/greetings.yml

This file was deleted.

31 changes: 0 additions & 31 deletions .github/workflows/maven-publish.yml

This file was deleted.

Loading
Loading