Developer Guide
This section covers the data model, configuration reference, log capture pipeline, and extension points available to developers integrating Activity Log into an application.
Data Model
Activity Log persists six core entities. An activity log record holds a single captured log event. An extractor defines the rules that determine which log events are saved. A remover defines the rules that determine when old records are deleted. A log viewer defines a saved view configuration used to display logs. A logger filter and a timestamp filter are reusable filter components shared by both extractors and removers.
---
config:
layout: elk
---
erDiagram
al_activitylog {
integer id PK
timestamp timestamp
varchar logger
text detail
varchar level
text stacktrace
varchar session_id
}
al_extractor {
integer id PK
varchar name
boolean active
varchar levels
boolean save_stacktrace
boolean save_session_id
}
al_remover {
integer id PK
varchar name
varchar expiration_unit
integer expiration_value
boolean active
varchar levels
}
al_log_viewer {
integer id PK
varchar title
varchar levels_filter
varchar logger_filter
varchar detail_filter
varchar route
timestamp start_date_time_filter
timestamp end_date_time_filter
boolean logger_column_visible
boolean detail_column_visible
boolean level_column_visible
boolean timestamp_column_visible
boolean logger_filter_enabled
boolean detail_filter_enabled
boolean level_filter_enabled
boolean timestamp_filter_enabled
boolean session_filter_enabled
boolean live_enabled
}
al_logger_filter {
integer id PK
varchar type
varchar regex_pattern
}
al_timestamp_filter {
integer id PK
varchar filter_type
date since_date
date until_date
time from_time
time to_time
varchar days_of_week
varchar zone
}
al_extractor ||--o{ al_extractor_logger_filters : "logger filters"
al_extractor ||--o{ al_extractor_detail_filters : "detail filters"
al_extractor ||--o{ al_extractor_timestamp_filters : "timestamp filters"
al_remover ||--o{ al_remover_logger_filters : "logger filters"
al_remover ||--o{ al_remover_detail_filters : "detail filters"
al_remover ||--o{ al_remover_timestamp_filters : "timestamp filters"
al_extractor_logger_filters }o--|| al_logger_filter : ""
al_extractor_detail_filters }o--|| al_logger_filter : ""
al_remover_logger_filters }o--|| al_logger_filter : ""
al_remover_detail_filters }o--|| al_logger_filter : ""
al_extractor_timestamp_filters }o--|| al_timestamp_filter : ""
al_remover_timestamp_filters }o--|| al_timestamp_filter : ""
Database Schema
Activity Log manages twelve tables. The schema below represents the DDL generated by Hibernate for a standard relational database.
CREATE TABLE al_activitylog (
id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
timestamp TIMESTAMP(6),
logger VARCHAR(255),
detail VARCHAR(32600),
level VARCHAR(255),
stacktrace TEXT,
session_id VARCHAR(255),
CONSTRAINT pk_al_activitylog PRIMARY KEY (id)
);
CREATE SEQUENCE al_logger_filter_seq START WITH 1 INCREMENT BY 50;
CREATE TABLE al_logger_filter (
id INTEGER NOT NULL,
type VARCHAR(255),
regex_pattern VARCHAR(255),
CONSTRAINT pk_al_logger_filter PRIMARY KEY (id)
);
CREATE SEQUENCE al_timestamp_filter_seq START WITH 1 INCREMENT BY 50;
CREATE TABLE al_timestamp_filter (
id INTEGER NOT NULL,
filter_type VARCHAR(255),
since_date DATE,
until_date DATE,
from_time TIME,
to_time TIME,
days_of_week VARCHAR(255),
zone VARCHAR(255),
CONSTRAINT pk_al_timestamp_filter PRIMARY KEY (id)
);
CREATE SEQUENCE al_extractor_seq START WITH 1 INCREMENT BY 50;
CREATE TABLE al_extractor (
id INTEGER NOT NULL,
name VARCHAR(255) NOT NULL,
active BOOLEAN,
levels VARCHAR(255),
save_stacktrace BOOLEAN,
save_session_id BOOLEAN,
CONSTRAINT pk_al_extractor PRIMARY KEY (id),
CONSTRAINT uq_al_extractor_name UNIQUE (name)
);
CREATE SEQUENCE al_remover_seq START WITH 1 INCREMENT BY 50;
CREATE TABLE al_remover (
id INTEGER NOT NULL,
name VARCHAR(255) NOT NULL,
expiration_unit VARCHAR(255),
expiration_value INTEGER,
active BOOLEAN,
levels VARCHAR(255),
CONSTRAINT pk_al_remover PRIMARY KEY (id),
CONSTRAINT uq_al_remover_name UNIQUE (name)
);
CREATE SEQUENCE al_log_viewer_seq START WITH 1 INCREMENT BY 50;
CREATE TABLE al_log_viewer (
id INTEGER NOT NULL,
title VARCHAR(255),
levels_filter VARCHAR(255),
logger_filter VARCHAR(255),
detail_filter VARCHAR(255),
route VARCHAR(255),
start_date_time_filter TIMESTAMP(6),
end_date_time_filter TIMESTAMP(6),
logger_column_visible BOOLEAN NOT NULL DEFAULT FALSE,
detail_column_visible BOOLEAN NOT NULL DEFAULT FALSE,
level_column_visible BOOLEAN NOT NULL DEFAULT FALSE,
timestamp_column_visible BOOLEAN NOT NULL DEFAULT FALSE,
logger_filter_enabled BOOLEAN NOT NULL DEFAULT FALSE,
detail_filter_enabled BOOLEAN NOT NULL DEFAULT FALSE,
level_filter_enabled BOOLEAN NOT NULL DEFAULT FALSE,
timestamp_filter_enabled BOOLEAN NOT NULL DEFAULT FALSE,
session_filter_enabled BOOLEAN NOT NULL DEFAULT FALSE,
live_enabled BOOLEAN NOT NULL DEFAULT FALSE,
CONSTRAINT pk_al_log_viewer PRIMARY KEY (id)
);
CREATE TABLE al_extractor_logger_filters (
extractor_entity_id INTEGER NOT NULL,
logger_filters_id INTEGER NOT NULL,
CONSTRAINT fk_al_extractor_logger_filters_extractor FOREIGN KEY (extractor_entity_id) REFERENCES al_extractor (id),
CONSTRAINT fk_al_extractor_logger_filters_logger_filter FOREIGN KEY (logger_filters_id) REFERENCES al_logger_filter (id)
);
CREATE TABLE al_extractor_detail_filters (
extractor_entity_id INTEGER NOT NULL,
detail_filters_id INTEGER NOT NULL,
CONSTRAINT fk_al_extractor_detail_filters_extractor FOREIGN KEY (extractor_entity_id) REFERENCES al_extractor (id),
CONSTRAINT fk_al_extractor_detail_filters_logger_filter FOREIGN KEY (detail_filters_id) REFERENCES al_logger_filter (id)
);
CREATE TABLE al_extractor_timestamp_filters (
extractor_entity_id INTEGER NOT NULL,
timestamp_filters_id INTEGER NOT NULL,
CONSTRAINT fk_al_extractor_timestamp_filters_extractor FOREIGN KEY (extractor_entity_id) REFERENCES al_extractor (id),
CONSTRAINT fk_al_extractor_timestamp_filters_timestamp_filter FOREIGN KEY (timestamp_filters_id) REFERENCES al_timestamp_filter (id)
);
CREATE TABLE al_remover_logger_filters (
remover_entity_id INTEGER NOT NULL,
logger_filters_id INTEGER NOT NULL,
CONSTRAINT fk_al_remover_logger_filters_remover FOREIGN KEY (remover_entity_id) REFERENCES al_remover (id),
CONSTRAINT fk_al_remover_logger_filters_logger_filter FOREIGN KEY (logger_filters_id) REFERENCES al_logger_filter (id)
);
CREATE TABLE al_remover_detail_filters (
remover_entity_id INTEGER NOT NULL,
detail_filters_id INTEGER NOT NULL,
CONSTRAINT fk_al_remover_detail_filters_remover FOREIGN KEY (remover_entity_id) REFERENCES al_remover (id),
CONSTRAINT fk_al_remover_detail_filters_logger_filter FOREIGN KEY (detail_filters_id) REFERENCES al_logger_filter (id)
);
CREATE TABLE al_remover_timestamp_filters (
remover_entity_id INTEGER NOT NULL,
timestamp_filters_id INTEGER NOT NULL,
CONSTRAINT fk_al_remover_timestamp_filters_remover FOREIGN KEY (remover_entity_id) REFERENCES al_remover (id),
CONSTRAINT fk_al_remover_timestamp_filters_timestamp_filter FOREIGN KEY (timestamp_filters_id) REFERENCES al_timestamp_filter (id)
);
al_activitylog is the only table that uses GENERATED BY DEFAULT AS IDENTITY, reflecting the GenerationType.IDENTITY on ActivityLogEntity: log records are inserted one at a time by the capture pipeline, so there is nothing for a sequence allocation to amortise. Every other entity uses GenerationType.AUTO and therefore draws its identifiers from a Hibernate sequence.
The level column stores the string representation of the AuditLevel enum. The levels column in al_extractor and al_remover stores a comma-separated list of AuditLevel values. The expiration_unit column stores the string representation of a ChronoUnit value. The days_of_week column in al_timestamp_filter stores a comma-separated list of DayOfWeek values, and its zone column the identifier of the time zone the filter's wall-clock values were authored in — see Time Zones in Timestamp Filters. The detail column in al_activitylog is widened well past the 255-character default so that long messages are stored whole, and stacktrace is mapped as a large text column (unbounded length), so full stack traces are stored without truncation and without relying on database large-object handling.
Instant-based timestamps (al_activitylog.timestamp and the al_log_viewer date-time filter bounds) are persisted as absolute points in time (UTC) and rendered in the browser's time zone by the views. Storing instants rather than local date-time strings keeps ordering and range filtering correct regardless of the time zone of the server or of the user viewing the data.
Both al_logger_filter and al_timestamp_filter are referenced by extractor and remover join tables: each entity uses a dedicated set of join tables to avoid cross-contamination of filter associations. The al_logger_filter type covers both logger-name matching and detail-content matching, distinguished by the type column storing the LoggerFilterType enum value.
Module Overview
Activity Log is structured as six Maven modules following the AppJars layered architecture:
| Module | Artifact ID | Description |
|---|---|---|
| Model | appjars-activity-log-model |
DTOs, enums, auto-configuration |
| Business API | appjars-activity-log-business |
ActivityLogService, ExtractorService, RemoverService, LogViewerService interfaces |
| Business Impl | appjars-activity-log-business-impl |
Service implementations, ActivityLogAppender, ActivityLogAppendersConnector, ExpiredLogsPruner |
| Data API | appjars-activity-log-data |
ActivityLogDao, ExtractorDao, RemoverDao, LogViewerDao interfaces |
| Data Impl | appjars-activity-log-data-impl |
JPA entities and DAO implementations |
| Flow UI | appjars-activity-log-flow |
Vaadin views and route configuration |
A monolithic application includes the three implementation modules (-business-impl, -data-impl, -flow). The API modules (-business, -data) are pulled in transitively.
Spring Auto-Configuration
Activity Log registers itself through Spring Boot's auto-configuration mechanism. The entry point is ActivityLogAutoConfiguration, declared in:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
This class scans the com.appjars.activitylog package for all Spring components and JPA entities. On startup, ActivityLogAppendersConnector initialises a BlockingQueue with the configured capacity and starts a background consumer thread that drains the queue and persists log records via ActivityLogService. The Log4j2 custom appender (ActivityLogAppender) is registered through the standard Log4j2 plugin mechanism and begins forwarding events to the connector as soon as logging is active.
Log Capture Pipeline
Activity Log captures log events through a three-stage pipeline that sits outside the standard Spring request lifecycle:
flowchart TD
A[Application Code] -->|log statement| B[Log4j2]
B -->|plugin appender| C[ActivityLogAppender]
C -->|offer| D[BlockingQueue\ncapacity 1000]
D -->|consumer thread| E[ActivityLogAppendersConnector]
E -->|extractor matching| F{Active extractor\nmatches?}
F -->|yes| G[ActivityLogService.save]
F -->|no| H[discard]
G --> I[(al_activitylog)]
ActivityLogAppender is a Log4j2 plugin appender declared in log4j2.xml. It receives every log event that passes through the Log4j2 pipeline and offers it to the connector's queue. Events offered to a full queue are dropped rather than blocking the calling thread.
ActivityLogAppendersConnector maintains a BlockingQueue<LogEvent> and a single background consumer thread. The consumer takes events from the queue, evaluates each against all active extractors, and calls ActivityLogService.save() for every event that matches. If no active extractor matches the event, the record is not persisted.
Extractor matching checks whether the log event's level, logger name, message content, and timestamp fall within the rules defined by each active extractor's filter sets. An event must pass all filter sets on a given extractor to be saved by that extractor.
The queue capacity defaults to 1000 events. Under sustained high-throughput logging, events are dropped when the queue is full. This design prevents logging from blocking application threads, at the cost of potential loss of individual records during traffic spikes.
Startup Buffering
Log events emitted while the application is starting — before the Spring context is ready and the connector is available — would otherwise be lost. To avoid this, ActivityLogAppender holds early events in a bounded pre-context buffer and drains them into the connector once Activity Log becomes active. The buffer is configured with two optional attributes on the <ActivityLogAppender> element in log4j2.xml:
| Attribute | Default | Description |
|---|---|---|
bufferCapacity |
10000 |
Maximum number of startup events held before the oldest entries are evicted |
bufferLevel |
INFO |
Minimum severity level buffered during startup |
When the buffer fills, the oldest entries are dropped. If any events were evicted this way, a single summary record is persisted as an ERROR entry on activation, so the loss is visible in the log itself.
If the ActivityLogAppender is not declared in the logging configuration, the pre-context buffer is never created and startup events are not captured. Capture still works from activation on: Activity Log verifies the logging setup when the Spring context refreshes and repairs the configuration if needed — registering a fallback appender on the root logger, attaching a declared-but-unreferenced appender, or replacing an appender copy loaded by a different classloader. A setup no repair can save (wrong LogManager provider or wrong SLF4J binding) fails the startup instead. See Logging Requirements for the verification and repair rules and the properties that control them.
Logging Requirements
This section describes what the application's classpath must provide for capture to work, how those requirements are verified at startup, and how to diagnose a setup where logs are not being captured.
Supported logging backends
As of 2.0.0, Log4j2 is the only supported logging backend, and the requirements below are enforced at startup. Support for capturing through other backends (such as Logback, or directly at the SLF4J level) is under consideration for a future release.
The Two Requirements
For Activity Log to capture anything, two conditions must both hold at runtime:
-
log4j-core must be the active
LogManagerprovider. The appender is a Log4j2 plugin: if another provider wins (typicallylog4j-to-slf4j, which redirects the Log4j2 API to SLF4J), the Log4j2 configuration file is never read and the appender never receives a single event. -
log4j-slf4j2-impl must own the SLF4J binding. Most application logging — Spring, Hibernate, Vaadin, and most libraries — is emitted through the SLF4J API, not the Log4j2 API. If another binding owns SLF4J (typically
logback-classic), those events go to Logback and never reach Log4j2, even when requirement 1 holds. The appender then looks perfectly healthy while capturing almost nothing.
Both problems have the same root cause: spring-boot-starter-logging (Spring Boot's default) brings log4j-to-slf4j and logback-classic to the classpath.
Setting Up the Classpath
Add the Log4j2 starter and exclude the default logging starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
Every starter needs its own exclusion
spring-boot-starter-logging is a transitive dependency of every Spring Boot starter. The exclusion must be repeated on each starter the application declares — spring-boot-starter-web, spring-boot-starter-actuator, and so on. A single missed starter silently reintroduces the conflicting jars.
Do not place the exclusion on the spring-boot-dependencies BOM in <dependencyManagement>: Maven ignores <exclusions> on a dependency imported with <scope>import</scope>, so that exclusion has no effect at all while appearing to provide blanket coverage.
To verify the result, resolve the dependency tree and confirm that neither log4j-to-slf4j nor logback-classic appears:
mvn dependency:tree -Dincludes="*:log4j-to-slf4j,*:logback-classic"
To keep the classpath from regressing when new starters are added later, the ban can be enforced at build time:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.6.2</version>
<executions>
<execution>
<id>ban-spring-boot-starter-logging</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<excludes>
<exclude>org.springframework.boot:spring-boot-starter-logging</exclude>
</excludes>
</bannedDependencies>
</rules>
</configuration>
</execution>
</executions>
</plugin>
Startup Verification (Fail Fast)
Since 2.0.0, both requirements are verified when Activity Log activates (on the Spring context refresh). If either one fails, the application fails to start with a message naming the problem and the exact exclusion to add. A silently empty activity log is treated as worse than a loud boot error.
If you need the application to start anyway — accepting that activity logging will capture nothing or almost nothing — the failure can be downgraded to a warning:
com.appjars.activitylog.appender.strict=false
Why Activity Log does not force Log4j2 itself
A library could try to win the provider conflict programmatically (for example by shipping log4j2.component.properties or setting log4j.provider). This was evaluated and rejected: it restores logging emitted through the Log4j2 API, but the SLF4J binding — which carries nearly all real application logging — would still belong to Logback, so the logs would keep disappearing while the warning went silent. Fixing the classpath is the only correct solution, so that is what the startup error asks for.
Appender Registration and Self-Repair
The recommended setup declares the appender in log4j2.xml, as shown in Getting Started. Declaring it enables startup buffering: events emitted before the Spring context is ready are held in a bounded buffer and persisted once Activity Log activates.
On activation, Activity Log inspects the Log4j2 configuration and repairs common problems:
- Appender not present (not declared, or the plugin was not discovered — see IDE builds): a fallback appender is registered on the root logger, with a warning. Capture works from that point on, but startup events were not buffered.
- Appender declared but not referenced by any
<AppenderRef>: the declared appender is attached to the root logger, with a warning suggesting the missing reference. - Appender loaded by a different classloader (IDE hot-restart, classloader isolation): the unreachable copy is replaced by a reachable instance, preserving its name, filter, and logger attachments — including each
<AppenderRef>'slevelandfilter.
The first two repairs change what the configuration captures (they attach an unfiltered appender to the root logger). An application that prefers to keep its logging configuration untouched can decline them:
com.appjars.activitylog.appender.autoRegister=false
With auto-registration declined and no appender configured, activity logging stays inert and only the warning is emitted.
IDE Builds and Plugin Discovery
Log4j2 discovers plugins through a Log4j2Plugins.dat descriptor generated by an annotation processor during compilation. Maven builds run it automatically, but Eclipse JDT-based compilers (including the Eclipse IDE and VS Code Java tooling) have annotation processing disabled by default — which historically produced the confusing symptom of an application that captures logs when launched with mvn spring-boot:run but not from the IDE.
Two safeguards cover this case:
- The
packages="com.appjars.activitylog.business.service"attribute on<Configuration>(part of the recommendedlog4j2.xml) lets Log4j2 find the appender without the descriptor. - Even if discovery fails entirely, the fallback registration described above kicks in on activation.
Troubleshooting
| Startup message | Cause | Fix |
|---|---|---|
Log4j2 core is not the active logging implementation (LogManager resolved to ...) |
log4j-to-slf4j is on the classpath and outranks log4j-core as provider |
Exclude spring-boot-starter-logging from the starter that introduced it |
The SLF4J binding is ..., so logs emitted through the SLF4J API ... cannot be captured |
logback-classic (or another binding) owns the SLF4J binding instead of log4j-slf4j2-impl |
Exclude spring-boot-starter-logging from the starter that introduced it |
ActivityLogAppender was not found in the Log4j2 configuration |
The appender is not declared in log4j2.xml, or the plugin was not discovered (IDE build without annotation processing) |
Declare the appender and keep the packages attribute on <Configuration>; capture still works through the fallback, but without startup buffering |
ActivityLogAppender is declared in the Log4j2 configuration but no logger references it |
<ActivityLogAppender> exists under <Appenders> but no <AppenderRef> points to it |
Add <AppenderRef ref="ActivityLogAppender"/> to the intended <Root>/<Logger> |
The configured ActivityLogAppender was loaded by a different classloader |
IDE hot-restart or classloader isolation created an unreachable appender copy | Automatic — the copy is replaced; note the replaced copy's startup buffer is not recovered |
N activity log(s) were discarded during startup (persisted as an ERROR record) |
The startup buffer filled before the Spring context was ready | Increase bufferCapacity or raise bufferLevel on <ActivityLogAppender> |
Expired Log Pruning
ExpiredLogsPruner runs on a configurable schedule and deletes log records that have exceeded the expiration window defined by the active removers. It processes up to a configurable maximum number of records per round to bound the size of each delete operation, and it is scheduled by RemoverServiceImpl, which starts a pruning round only while at least one remover is active.
The pruner carries no removal criteria of its own: it calls ActivityLogService.deleteExpiredLogs(logsLimit), which reads the active removers itself. The criteria therefore always come from persisted, active configuration, and never from the caller.
The pruner is enabled by default and can be disabled by setting com.appjars.activitylog.logspruner.removalEnabled to false. When disabled, old records accumulate indefinitely unless deleted manually or through ActivityLogService.deleteExpiredLogs(logsLimit).
Time Zones in Timestamp Filters
The timestamp filters attached to extractors and removers are written in wall-clock terms — "1 to 31 July, 09:00 to 17:00", "weekdays, all day". A log record, by contrast, carries an absolute instant. Something has to decide which zone turns one into the other, and until 2.0.0 that was implicitly the zone of whichever JVM evaluated the filter.
Each al_timestamp_filter row now stores that zone explicitly, in its zone column, and evaluation projects the log's instant into it. A filter written by an administrator in Buenos Aires keeps meaning Buenos Aires office hours after the application moves to a server in another zone. The Time zone field in the time filter dialog is what sets the value; it defaults to the server zone. A row saved before the column existed has zone null and keeps being evaluated in the server zone, which is what it did when it was written.
The filter types differ in how their window repeats:
- Days of week and Days of week between two dates repeat their daily time range on every covered date. "Weekdays, 09:00 to 17:00" covers those hours on each weekday.
- Between two dates is a single continuous span: it runs from
since_dateatfrom_timestraight through tountil_dateatto_time, without returning to the start time each day. "1 July 09:00 to 31 July 17:00" therefore covers the whole of 15 July, including 03:00. A missing bound leaves that end of the span unbounded.
Because a span crosses midnight by definition, its end time does not have to fall later on the clock than its start time when it covers more than one date.
The from_time bound is inclusive and the to_time bound is exclusive; the dialog states this beside each field.
Configuration Properties
Log Capture
| Property | Default | Description |
|---|---|---|
com.appjars.activitylog.appendersconnector.queueCapacity |
1000 |
Maximum number of log events held in the capture queue before events are dropped |
com.appjars.activitylog.appender.strict |
true |
Fail the application startup when the logging setup cannot be captured — see Logging Requirements |
com.appjars.activitylog.appender.autoRegister |
true |
Allow Activity Log to register or re-attach its appender on activation — see Logging Requirements |
Log Pruning
| Property | Default | Description |
|---|---|---|
com.appjars.activitylog.logspruner.removalEnabled |
true |
Whether the expired logs pruner runs automatically |
com.appjars.activitylog.logspruner.pruningFrequency |
5 |
Interval in seconds between pruning rounds |
com.appjars.activitylog.logspruner.maxLogsPerRound |
500 |
Maximum number of records deleted in a single pruning round |
Display Formatting
| Property | Default | Description |
|---|---|---|
com.appjars.activitylog.dateformat |
dd-MM-yy |
Date format pattern used in log viewer columns |
com.appjars.activitylog.timeformat |
HH:mm |
Time format pattern used in log viewer columns |
com.appjars.activitylog.datetimeformat |
dd-MM-yyyy HH:mm:ss |
Combined date-time format pattern used in log viewer columns |
View URLs
| Property | Default | Description |
|---|---|---|
com.appjars.activitylog.url.activitylog |
al/activitylog |
URL path of the activity log view |
com.appjars.activitylog.url.extractors |
al/extractors |
URL path of the extractor list view |
com.appjars.activitylog.url.extractors-create |
al/extractors/create |
URL path of the extractor creation view |
com.appjars.activitylog.url.extractors-edit |
al/extractors/edit |
URL path of the extractor edit view |
com.appjars.activitylog.url.removers |
al/removers |
URL path of the remover list view |
com.appjars.activitylog.url.removers-create |
al/removers/create |
URL path of the remover creation view |
com.appjars.activitylog.url.removers-edit |
al/removers/edit |
URL path of the remover edit view |
com.appjars.activitylog.url.logviewer |
al/logviewer |
URL path of the log viewer list view |
com.appjars.activitylog.url.logviewer-create |
al/logviewer/create |
URL path of the log viewer creation view |
com.appjars.activitylog.url.logviewer-edit |
al/logviewer/edit |
URL path of the log viewer edit view |
Service API
ActivityLogService
ActivityLogService is the primary interface for accessing and managing log records. It extends CrudService and exposes the following methods specific to the module:
// Retrieve a paginated, filtered, and sorted stream of log records
Stream<ActivityLogDto> getLogs(int offset, int limit, ActivityLogFilter processFilter, List<ActivityLogSort> sortOrder);
// Count log records matching a filter, or all of them when the filter is null
Long countLogs(ActivityLogFilter processFilter);
// Record an entry and emit it through the application log at the same level,
// returning the id of the saved record
Integer log(String logger, String detail, AuditLevel level);
// Delete the records the currently active removers have expired
void deleteExpiredLogs(Integer logsLimit);
// Count records created today
int getTodayLogsCount();
log(logger, detail, level) is the entry point for recording application events deliberately, rather than by capture. The emitted event carries the level given as the argument, and capture is suspended for the calling thread while it is emitted, so exactly one record is persisted per call even when an active extractor would otherwise match it.
deleteExpiredLogs(logsLimit) is the method called by ExpiredLogsPruner on each scheduled round, and can also be called directly when a manual pruning pass is required. It reads the removal criteria from the currently active removers itself; the caller supplies only the maximum number of records to consider. There is no service method that deletes activity logs by arbitrary criteria — an audit trail should not offer one.
ExtractorService
ExtractorService manages extractor definitions. Key methods beyond standard CRUD:
// Toggle the active state of an extractor
void switchActiveExtractor(ExtractorDto extractor);
// Find an extractor by its unique name
Optional<ExtractorDto> findByName(String name);
Only active extractors participate in event matching. An application with no active extractor saves no log records regardless of how many events flow through the Log4j2 pipeline.
RemoverService
RemoverService manages remover definitions. Key methods beyond standard CRUD:
// Toggle the active state of a remover
void switchActiveRemover(RemoverDto remover);
// Find a remover by its unique name
Optional<RemoverDto> findByName(String name);
Only active removers are evaluated by ExpiredLogsPruner. A remover's expiration window is defined by combining expiration_value (a number) with expiration_unit (a ChronoUnit such as DAYS or HOURS).
LogViewerService
LogViewerService manages saved log viewer configurations. Key methods beyond standard CRUD:
// Find a log viewer by its route path
Optional<LogViewerDto> findByRoute(String route);
// Find a log viewer by its display title
Optional<LogViewerDto> findByTitle(String title);
Each log viewer record stores both the filter state and the column visibility configuration for a specific view route, allowing multiple independently configured log displays to coexist in the same application.
ActivityLogBroadcaster
ActivityLogBroadcaster is the module's event bus for newly captured records. It is what drives the Live toggle in the log views, and an application can use it to react to activity of its own.
// Publish an event to every current subscriber
void publish(ActivityLogEvent event);
// Subscribe a plain listener; the returned Registration removes it
<T extends ActivityLogEvent> Registration subscribe(Class<T> eventType, Consumer<T> listener);
// Subscribe a Vaadin component; the listener runs inside the component's UI
// and is removed automatically when the component detaches
<T extends ActivityLogEvent> Registration subscribe(Component caller, Class<T> eventType, Consumer<T> listener);
Publishing is unrestricted. Receiving is a full-licence feature: both subscribe overloads throw FreeLimitReachedException when no full licence is present. The component overload is the one to prefer in Vaadin code, because it dispatches through UI.access and unsubscribes on detach; the plain overload leaves both to the caller.
Recording Events Without Capturing Them
Activity Log suppresses capture around its own database writes, so the module never audits itself: neither the log views' own queries nor the pruner's report of what it deleted produce activity log records. Without that suppression each of them would close a loop — a captured entry causing a query that captures another entry — and the table would grow with no application activity behind it.
The same suppression is applied inside ActivityLogService.log, which is why that method persists exactly one record per call. The switch itself is internal to the module and is not part of the public API: an audit product that shipped a documented "stop auditing this thread" control would be undermining its own guarantees.
Customisation
Assigning a Router Layout
By default, Activity Log registers its views without a parent layout. To wrap them in the application's main layout, inject the RouteConfigurer bean using its qualifier and call setViewsRouterLayout in a @PostConstruct method:
@Autowired
@Qualifier("ActivityLogRouteConfigurer")
private RouteConfigurer routeConfigurer;
@PostConstruct
public void configure() {
routeConfigurer.setViewsRouterLayout(MainLayout.class);
}
The @Qualifier is required because multiple AppJars may contribute a bean named RouteConfigurer.
For an application whose shell is built from nested layouts, setViewsLayoutChain takes the whole chain, outermost layout last:
routeConfigurer.setViewsLayoutChain(List.of(SectionLayout.class, MainLayout.class));
Both setters must be called before the Vaadin service starts, which is what the @PostConstruct above guarantees: the routes are registered in serviceInit, and nothing reads the layout configuration afterwards.
Each view's resolved route is available from its own getter — getActivityLogUrl(), getExtractorsUrl(), getLogViewerUrl(), and so on — which is how an application builds a link to a view whose URL it has overridden. The getters are read-only: since 2.0.0 the matching setters are gone, because assigning to them after registration had no effect on anything.
Customising View URLs
The default URL paths can be overridden in application.properties:
com.appjars.activitylog.url.activitylog=myapp/activity
com.appjars.activitylog.url.extractors=myapp/activity/extractors
com.appjars.activitylog.url.removers=myapp/activity/removers
com.appjars.activitylog.url.logviewer=myapp/activity/viewers
The full list of configurable properties and their defaults is in Configuring View Route Paths.
Tuning the Capture Queue
For applications that generate very high log volumes, the queue capacity and pruning frequency can be adjusted to balance memory use against record loss:
com.appjars.activitylog.appendersconnector.queueCapacity=5000
com.appjars.activitylog.logspruner.pruningFrequency=60
com.appjars.activitylog.logspruner.maxLogsPerRound=2000
Increasing the queue capacity reduces the probability of event loss under burst load. Lengthening the pruning interval (in seconds) makes the background pruner run less often, which lowers database write pressure during periods of heavy activity, at the cost of expired records living a little longer.