Skip to content

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 : ""
Hold "Alt" / "Option" to enable pan & zoom

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)]
Hold "Alt" / "Option" to enable pan & zoom

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:

  1. log4j-core must be the active LogManager provider. The appender is a Log4j2 plugin: if another provider wins (typically log4j-to-slf4j, which redirects the Log4j2 API to SLF4J), the Log4j2 configuration file is never read and the appender never receives a single event.

  2. 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>'s level and filter.

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 recommended log4j2.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_date at from_time straight through to until_date at to_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.