diff --git a/AGENTS.md b/AGENTS.md index 361c4604..a3971a53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,9 +10,10 @@ LLM-based agents can accelerate development only if they respect our house rules | Requirement | Rationale | |--------------|-----------| -| **British English** spelling (`organisation`, `licence`, *not* `organization`, `license`) except technical US spellings like `synchronized` | Keeps wording consistent with Chronicle's London HQ and existing docs. See the University of Oxford style guide for reference. | -| **ASCII-7 only** (code-points 0-127). Avoid smart quotes, non-breaking spaces and accented characters. | ASCII-7 survives every toolchain Chronicle uses, incl. low-latency binary wire formats that expect the 8th bit to be 0. | -| If a symbol is not available in ASCII-7, use a textual form such as `micro-second`, `>=`, `:alpha:`, `:yes:`. This is the preferred approach and Unicode must not be inserted. | Extended or '8-bit ASCII' variants are *not* portable and are therefore disallowed. | +| **British English** spelling (`organisation`, `licence`, *not* `organization`, `license`) except technical US spellings like `synchronized` | Keeps wording consistent with Chronicle's London HQ and existing docs. See the [University of Oxford style guide](https://www.ox.ac.uk/public-affairs/style-guide) for reference. | +| **ISO-8859-1** (code-points 0-255). Avoid smart quotes, non-breaking spaces and accented characters. | ISO-8859-1 survives every toolchain Chronicle uses. | +| If a symbol is not available in ISO-8859-1, use a textual form such as `>=`, `:alpha:`, `:yes:`. This is the preferred approach and Unicode must not be inserted. | Non-ISO-8859-1 encodings (including other '8-bit ASCII' variants) are *not* portable and are therefore disallowed. | +| Tools to check ISO-8859-1 compliance include `iconv -f ISO-8859-1 -t ISO-8859-1` and IDE settings that flag non-ISO-8859-1 characters. | These help catch stray Unicode or mis-encoded characters before code review. | ## Javadoc guidelines @@ -26,25 +27,44 @@ noise and slows readers down. | Prefer `@param` for *constraints* and `@throws` for *conditions*, following Oracle's style guide. | Pad comments to reach a line-length target. | | Remove or rewrite autogenerated Javadoc for trivial getters/setters. | Leave stale comments that now contradict the code. | -The principle that Javadoc should only explain what is *not* manifest from the signature is well-established in the -wider Java community. +The principle that Javadoc should only explain what is *not* manifest from the +signature is well-established in the wider Java community. -## Build & test commands +Inline comments should also avoid noise. The following example shows the +difference: -Agents must verify that the project still compiles and all unit tests pass before opening a PR: +```java +// BAD: adds no value +int count; // the count + +// GOOD: explains a subtlety +// count of messages pending flush +int count; +``` + +## Build, test, and quality commands + +Agents must verify that the project still compiles and all unit tests pass before opening a PR. Running from a clean checkout avoids stale artefacts: ```bash # From repo root -mvn -q verify +mvn -q clean verify ``` ## Commit-message & PR etiquette -1. **Subject line <= 72 chars**, imperative mood: "Fix roll-cycle offset in `ExcerptAppender`". +1. **Subject line <= 72 chars**, imperative mood: Fix roll-cycle offset in `ExcerptAppender`. 2. Reference the JIRA/GitHub issue if it exists. 3. In *body*: *root cause -> fix -> measurable impact* (latency, allocation, etc.). Use ASCII bullet points. 4. **Run `mvn verify`** again after rebasing. +### When to open a PR + +* Open a pull request once your branch builds and tests pass with `mvn -q clean verify`. +* Link the PR to the relevant issue or decision record. +* Keep PRs focused: avoid bundling unrelated refactoring with new features. +* Re-run the build after addressing review comments to ensure nothing broke. + ## What to ask the reviewers * *Is this AsciiDoc documentation precise enough for a clean-room re-implementation?* @@ -53,6 +73,14 @@ mvn -q verify * Does the commit point back to the relevant requirement or decision tag? * Would an example or small diagram help future maintainers? +### Security checklist (review **after every change**) + +**Run a security review on *every* PR**: Walk through the diff looking for input validation, authentication, authorisation, encoding/escaping, overflow, resource exhaustion and timing-attack issues. + +**Never commit secrets or credentials**: tokens, passwords, private keys, TLS materials, internal hostnames, Use environment variables, HashiCorp Vault, AWS/GCP Secret Manager, etc. + +**Document security trade-offs**: Chronicle prioritises low-latency systems; sometimes we relax safety checks for specific reasons. Future maintainers must find these hot-spots quickly, In Javadoc and `.adoc` files call out *why* e.g. "Unchecked cast for performance - assumes trusted input". + ## Project requirements See the [Decision Log](adoc/decision-log.adoc) for the latest project decisions. @@ -79,12 +107,13 @@ This tight loop informs the AI accurately and creates immediate clarity for all * **Doc-First for New Work**: For *new* features or requirements, aim to update documentation first, then use AI to help produce or refine corresponding code and tests. For refactoring or initial bootstrapping, updates might flow from code/tests back to documentation, which should then be reviewed and finalised. * **Small Commits**: Each commit should ideally relate to a single requirement or coherent change, making reviews easier for humans and AI analysis tools. - **Team Buy-In**: Encourage everyone to review AI outputs critically and contribute to maintaining the synchronicity of all artefacts. +* SLF4J 2.x is the default API and provider via `logger-slf4j-2`; keep `logger-slf4j` for SLF4J 1.7.x only and use explicit bridges (for example `slf4j-reload4j`) when preserving old call sites. ## AI Agent Guidelines When using AI agents to assist with development, please adhere to the following guidelines: -* **Respect the Language & Character-set Policy**: Ensure all AI-generated content follows the British English and ASCII-7 guidelines outlined above. +* **Respect the Language & Character-set Policy**: Ensure all AI-generated content follows the British English and ISO-8859-1 guidelines outlined above. Focus on Clarity: AI-generated documentation should be clear and concise and add value beyond what is already present in the code or existing documentation. * **Avoid Redundancy**: Do not generate content that duplicates existing documentation or code comments unless it provides additional context or clarification. * **Review AI Outputs**: Always review AI-generated content for accuracy, relevance, and adherence to the project's documentation standards before committing it to the repository. @@ -122,8 +151,7 @@ Date:: YYYY-MM-DD Context:: * What is the issue that this decision addresses? * What are the driving forces, constraints, and requirements? -Decision Statement:: -* What is the change that is being proposed or was decided? +Decision Statement :: What is the change that is being proposed or was decided? Alternatives Considered:: * [Alternative 1 Name/Type]: ** *Description:* Brief description of the alternative. @@ -159,3 +187,23 @@ section:: Top Level Section ### Emphasis and Bold Text In AsciiDoc, an underscore `_` is _emphasis_; `*text*` is *bold*. + +### Section Numbering + +Use automatic section numbering for all `.adoc` files. + +* Add `:sectnums:` to the document header. +* Do not prefix section titles with manual numbers to avoid duplication. + +```asciidoc += Document Title +Chronicle Software +:toc: +:sectnums: +:lang: en-GB +:source-highlighter: rouge + +The document overview goes here. + +== Section 1 Title +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..507a64a8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,208 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Overview + +Chronicle Logger is a high-performance logging library built on Chronicle Queue. It provides adapters for major Java logging frameworks (SLF4J, Logback, Log4J 1/2, JUL, JCL) that write to binary Chronicle Queues rather than text files. This approach minimises logging overhead and enables sub-microsecond logging latency. + +**Key architectural principle:** Chronicle Logger writes synchronously to off-heap queues, ensuring the last log message is always visible before application termination whilst maintaining low latency through zero-allocation design. + +## Module Structure + +This is a multi-module Maven project with the following modules: + +* `logger-core` - Core API (`ChronicleLogWriter`, `ChronicleLogManager`) and configuration +* `logger-slf4j` - SLF4J 1.x binding +* `logger-slf4j-2` - SLF4J 2.x binding +* `logger-logback` - Logback appender +* `logger-log4j-1` - Log4J 1.2 appender +* `logger-log4j-2` - Log4J 2.x appender +* `logger-jul` - Java Util Logging handler +* `logger-jcl` - Apache Commons Logging binding +* `logger-tools` - CLI tools (`ChroniCat`, `ChroniTail`) and `ChronicleLogReader` API +* `benchmark` - JMH performance benchmarks + +All binding modules depend on `logger-core` and translate framework-specific APIs into Chronicle Queue writes. + +## Build Commands + +### Full Build + +```bash +# Clean build with tests +mvn clean verify + +# Build without tests (faster) +mvn clean install -DskipTests + +# Quiet build with tests +mvn -q clean verify +``` + +### Module-Specific Builds + +```bash +# Build single module with dependencies +mvn -pl logger-slf4j -am clean install + +# Build single module without tests +mvn -pl logger-core -am -DskipTests install +``` + +### Running Tests + +```bash +# Run all tests in a module +mvn -pl logger-slf4j test + +# Run single test class +mvn -pl logger-core -Dtest=DefaultChronicleLogWriterTest test + +# Run specific test method +mvn -pl logger-core -Dtest=DefaultChronicleLogWriterTest#testWrite test +``` + +### Running Benchmarks + +```bash +cd benchmark +mvn clean install +# Run JMH benchmarks +java -jar target/benchmarks.jar + +# Run specific benchmark +java -jar target/benchmarks.jar + +# Run with custom parameters +java -jar target/benchmarks.jar -f 1 -wi 3 -i 5 +``` + +### Code Quality + +```bash +# Run Checkstyle (inherited from java-parent-pom) +mvn checkstyle:check + +# Run SpotBugs +mvn spotbugs:check + +# Run all quality checks with tests +mvn clean verify +``` + +## Architecture Overview + +### Write Path + +1. Logging framework (e.g., SLF4J) receives a log call +2. Binding checks log level against configured minimum +3. If enabled, binding forwards event to `ChronicleLogWriter` +4. `ChronicleLogWriter` serialises data into Chronicle Queue using configured wire type +5. Write is synchronous but zero-allocation on hot path for minimal overhead + +### Configuration + +* Properties-based bindings (SLF4J, JCL): Use `chronicle.logger.properties` (classpath or path specified via `-Dchronicle.logger.properties`) +* Native framework bindings (Logback, Log4J): Use XML configuration with `ChronicleAppender`/`ChronicleHandler` +* `ChronicleLogManager` caches writers per queue path +* Each logger requires distinct queue path; sharing paths between loggers is unsupported + +### Reading Logs + +Binary logs require tools to read: + +* `ChroniCat` - Dump log contents to STDOUT +* `ChroniTail` - Stream log contents like Unix `tail -f` +* `ChronicleLogReader` - Programmatic API for custom log processing + +Example: +```bash +mvn exec:java -Dexec.mainClass="net.openhft.chronicle.logger.tools.ChroniCat" \ + -Dexec.args="/tmp/chronicle-logs/my-logger" +``` + +## Dependencies + +Chronicle Logger depends on: + +* `chronicle-core` - Low-level utilities +* `chronicle-bytes` - Off-heap memory access +* `chronicle-queue` - Persisted message queue + +Versions are managed through `chronicle-bom` imported in the parent POM. + +## Documentation Structure + +Documentation follows Chronicle standards: + +* Location: `src/main/docs/` (AsciiDoc format) +* Key files: + - `architecture-overview.adoc` - High-level architecture + - `project-requirements.adoc` - Requirements catalogue with Nine-Box tags (e.g., `CLG-FN-001`) + - `functional-requirements.adoc` - Functional requirements summary + - `decision-log.adoc` - Architecture Decision Records + - `testing-strategy.adoc` - Testing approach and traceability + - `code-review-playbook.adoc` - Code review guidelines +* Format: British English, ISO-8859-1 character set +* Requirements use Nine-Box taxonomy: `FN` (Functional), `NF-P` (Performance), `NF-S` (Security), `NF-O` (Operability), etc. + +See `AGENTS.md` for detailed documentation standards and contribution guidelines. + +## Code Conventions + +* Follow Chronicle coding standards in `AGENTS.md` +* British English spelling (except technical US terms like `synchronized`) +* ISO-8859-1 character set only +* Javadoc should explain _why_ and _how_, not restate obvious signatures +* Preserve Nine-Box requirement tags in comments (e.g., `// CLG-NF-P-001: Zero-allocation write path`) + +## Common Development Tasks + +### Adding Support for New Logging Framework + +1. Create new module `logger-` following existing patterns +2. Implement framework-specific adapter/appender/handler +3. Delegate to `ChronicleLogWriter` from `logger-core` +4. Add configuration support (properties or framework-native) +5. Write tests extending appropriate base test class +6. Add module to parent `pom.xml` `` section + +### Modifying Log Format + +1. Changes to `ChronicleLogWriter` interface affect all bindings +2. Update serialisation in `DefaultChronicleLogWriter` +3. Update deserialisation in `ChronicleLogReader` (logger-tools) +4. Update CLI tools if record schema changes +5. Update relevant tests across modules + +### Performance Testing + +Benchmark changes that affect write path: + +```bash +cd benchmark +mvn clean install +java -jar target/benchmarks.jar -f 1 +``` + +Compare results with baseline in README.adoc. + +## Important Notes + +1. **Log4Shell Immunity**: `logger-log4j-2` does not suffer from log4shell vulnerability as it does not reuse Log4J2 message formatting machinery + +2. **Queue Path Uniqueness**: Each logger must have a distinct Chronicle Queue path. Two loggers sharing the same path is unsupported and will cause errors + +3. **Non-Hierarchical Loggers**: Logger names like `my.domain.package` and `my.domain` are distinct; no hierarchical grouping is applied + +4. **Synchronous Writes**: All writes are synchronous to ensure last messages are visible before application termination. This differs from async appenders but maintains sub-microsecond latency through zero-allocation design + +5. **Binary Format**: Logs are binary (not text). Use provided tools (`ChroniCat`, `ChroniTail`) or `ChronicleLogReader` API to read them + +## Related Documentation + +* `AGENTS.md` - Detailed contribution guidelines, build commands, documentation standards +* `README.adoc` - Project overview, configuration examples, performance metrics +* `src/main/docs/architecture-overview.adoc` - Detailed architecture documentation +* `src/main/docs/project-requirements.adoc` - Complete requirements catalogue diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 00000000..522363fd --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,202 @@ +# GEMINI.md + +This file provides guidance to Gemini when working with code in this repository. + +## Repository Overview + +Chronicle Logger is a high-performance logging library built on Chronicle Queue. It provides adapters for major Java logging frameworks (SLF4J, Logback, Log4J 1/2, JUL, JCL) that write to binary Chronicle Queues rather than text files. This approach minimises logging overhead and enables sub-microsecond logging latency. + +**Key architectural principle:** Chronicle Logger writes synchronously to off-heap queues, ensuring the last log message is always visible before application termination whilst maintaining low latency through zero-allocation design. + +## Module Structure + +This is a multi-module Maven project with the following modules: + +* `logger-core` - Core API (`ChronicleLogWriter`, `ChronicleLogManager`) and configuration +* `logger-slf4j` - SLF4J 1.x binding +* `logger-slf4j-2` - SLF4J 2.x binding +* `logger-logback` - Logback appender +* `logger-log4j-1` - Log4J 1.2 appender +* `logger-log4j-2` - Log4J 2.x appender +* `logger-jul` - Java Util Logging handler +* `logger-jcl` - Apache Commons Logging binding +* `logger-tools` - CLI tools (`ChroniCat`, `ChroniTail`) and `ChronicleLogReader` API +* `benchmark` - JMH performance benchmarks + +All binding modules depend on `logger-core` and translate framework-specific APIs into Chronicle Queue writes. + +## Build Commands + +### Full Build + +```bash +# Clean build with tests +mvn clean verify + +# Build without tests (faster) +mvn clean install -DskipTests + +# Quiet build with tests +mvn -q clean verify +``` + +### Module-Specific Builds + +```bash +# Build single module with dependencies +mvn -pl logger-slf4j -am clean install + +# Build single module without tests +mvn -pl logger-core -am -DskipTests install +``` + +### Running Tests + +```bash +# Run all tests in a module +mvn -pl logger-slf4j test + +# Run single test class +mvn -pl logger-core -Dtest=DefaultChronicleLogWriterTest test + +# Run specific test method +mvn -pl logger-core -Dtest=DefaultChronicleLogWriterTest#testWrite test +``` + +### Running Benchmarks + +```bash +cd benchmark +mvn clean install +# Run JMH benchmarks +java -jar target/benchmarks.jar + +# Run specific benchmark +java -jar target/benchmarks.jar + +# Run with custom parameters +java -jar target/benchmarks.jar -f 1 -wi 3 -i 5 +``` + +### Code Quality + +```bash +# Run Checkstyle (inherited from java-parent-pom) +mvn checkstyle:check + +# Run SpotBugs +mvn spotbugs:check + +# Run all quality checks with tests +mvn clean verify +``` + +## Architecture Overview + +### Write Path + +1. Logging framework (e.g., SLF4J) receives a log call +2. Binding checks log level against configured minimum +3. If enabled, binding forwards event to `ChronicleLogWriter` +4. `ChronicleLogWriter` serialises data into Chronicle Queue using configured wire type +5. Write is synchronous but zero-allocation on hot path for minimal overhead + +### Configuration + +* Properties-based bindings (SLF4J, JCL): Use `chronicle.logger.properties` (classpath or path specified via `-Dchronicle.logger.properties`) +* Native framework bindings (Logback, Log4J): Use XML configuration with `ChronicleAppender`/`ChronicleHandler` +* `ChronicleLogManager` caches writers per queue path +* Each logger requires distinct queue path; sharing paths between loggers is unsupported + +### Reading Logs + +Binary logs require tools to read: + +* `ChroniCat` - Dump log contents to STDOUT +* `ChroniTail` - Stream log contents like Unix `tail -f` +* `ChronicleLogReader` - Programmatic API for custom log processing + +Example: + +```bash +mvn exec:java -Dexec.mainClass="net.openhft.chronicle.logger.tools.ChroniCat" \ + -Dexec.args="/tmp/chronicle-logs/my-logger" +``` + +## Dependencies + +Chronicle Logger depends on: + +* `chronicle-core` - Low-level utilities +* `chronicle-bytes` - Off-heap memory access +* `chronicle-queue` - Persisted message queue + +Versions are managed through `chronicle-bom` imported in the parent POM. + +## Documentation Structure + +Documentation follows Chronicle standards: + +* Location: `src/main/docs/` (AsciiDoc format) +* Key files: + * `architecture-overview.adoc` - High-level architecture + * `project-requirements.adoc` - Requirements catalogue with Nine-Box tags (e.g., `CLG-FN-001`) + * `functional-requirements.adoc` - Functional requirements summary + * `decision-log.adoc` - Architecture Decision Records + * `testing-strategy.adoc` - Testing approach and traceability + * `code-review-playbook.adoc` - Code review guidelines +* Format: British English, ISO-8859-1 character set +* Requirements use Nine-Box taxonomy: `FN` (Functional), `NF-P` (Performance), `NF-S` (Security), `NF-O` (Operability), etc. + +See `AGENTS.md` for detailed documentation standards and contribution guidelines. + +## Code Conventions + +* Follow Chronicle coding standards in `AGENTS.md` +* British English spelling (except technical US terms like `synchronized`) +* ISO-8859-1 character set only +* Javadoc should explain _why_ and _how_, not restate obvious signatures +* Preserve Nine-Box requirement tags in comments (e.g., `// CLG-NF-P-001: Zero-allocation write path`) + +## Common Development Tasks + +### Adding Support for New Logging Framework + +1. Create new module `logger-` following existing patterns +2. Implement framework-specific adapter/appender/handler +3. Delegate to `ChronicleLogWriter` from `logger-core` +4. Add configuration support (properties or framework-native) +5. Write tests extending appropriate base test class +6. Add module to parent `pom.xml` `` section + +### Modifying Log Format + +1. Changes to `ChronicleLogWriter` interface affect all bindings +2. Update serialisation in `DefaultChronicleLogWriter` +3. Update deserialisation in `ChronicleLogReader` (logger-tools) +4. Update CLI tools if record schema changes +5. Update relevant tests across modules + +### Performance Testing + +Benchmark changes that affect write path: + +```bash +cd benchmark +mvn clean install +java -jar target/benchmarks.jar -f 1 +``` + +Compare results with baseline in README.adoc. + +## Important Notes + +1. **Log4Shell Immunity**: `logger-log4j-2` does not suffer from log4shell vulnerability as it does not reuse Log4J2 message formatting machinery + +2. **Queue Path Uniqueness**: Each logger must have a distinct Chronicle Queue path. Two loggers sharing the same path is unsupported and will cause errors + +3. **Non-Hierarchical Loggers**: Logger names like `my.domain.package` and `my.domain` are distinct; no hierarchical grouping is applied + +4. **Synchronous Writes**: All writes are synchronous to ensure last messages are visible before application termination. This differs from async appenders but maintains sub-microsecond latency through zero-allocation design + +5. **Binary Format**: Logs are binary (not text). Use provided tools (`ChroniCat`, `ChroniTail`) or `ChronicleLogReader` API to read them diff --git a/README.adoc b/README.adoc index 079da1b7..1adeb8ac 100644 --- a/README.adoc +++ b/README.adoc @@ -1,13 +1,18 @@ = Chronicle Logger +:toc: +:lang: en-GB +:source-highlighter: rouge Chronicle Software -:css-signature: demo -:toc: macro -:toclevels: 2 +:toc: :icons: font +:lang: en-GB +:toclevels: 2 +:css-signature: demo +:source-highlighter: rouge image:https://maven-badges.herokuapp.com/maven-central/net.openhft/chronicle-logger/badge.svg[caption="",link=https://maven-badges.herokuapp.com/maven-central/net.openhft/chronicle-logger] image:https://javadoc.io/badge2/net.openhft/chronicle-logger/javadoc.svg[link="https://www.javadoc.io/doc/net.openhft/chronicle-logger/latest/index.html"] -//image:https://javadoc-badge.appspot.com/net.openhft/chronicle-wire.svg?label=javadoc[JavaDoc, link=https://www.javadoc.io/doc/net.openhft/chronicle-logger] +// image:https://javadoc-badge.appspot.com/net.openhft/chronicle-wire.svg?label=javadoc[JavaDoc, link=https://www.javadoc.io/doc/net.openhft/chronicle-logger] image:https://img.shields.io/github/license/OpenHFT/Chronicle-Logger[GitHub] image:https://img.shields.io/badge/release%20notes-subscribe-brightgreen[link="https://chronicle.software/release-notes/"] image:https://sonarcloud.io/api/project_badges/measure?project=OpenHFT_Chronicle-Logger&metric=alert_status[link="https://sonarcloud.io/dashboard?id=OpenHFT_Chronicle-Logger"] @@ -21,16 +26,16 @@ toc::[] === Version [#image-maven] -[caption="", link=https://maven-badges.herokuapp.com/maven-central/net.openhft/chronicle-logger] +[caption="",link=https://maven-badges.herokuapp.com/maven-central/net.openhft/chronicle-logger] image::https://maven-badges.herokuapp.com/maven-central/net.openhft/chronicle-logger/badge.svg[] == Overview -Today most programs require the logging of large amounts of data, especially in trading systems where this is a -regulatory requirement. Loggers can affect your system performance, therefore logging is sometimes kept to a minimum. -With Chronicle Logger we aim to minimize logging overhead, freeing your system to focus on the business logic. +Today most programs require the logging of large amounts of data, especially in trading systems where this is a regulatory requirement. +Loggers can affect your system performance, therefore logging is sometimes kept to a minimum. +With Chronicle Logger we aim to minimise logging overhead, freeing your system to focus on the business logic. -Chronicle logger supports most of the standard logging API’s including: +Chronicle logger supports most of the standard logging APIs including: * <> * <> @@ -39,24 +44,24 @@ Chronicle logger supports most of the standard logging API’s including: * <> * <> -Chronicle Logger is able to aggregate all your logs to a central store. It has built-in resilience, so you will never -lose messages. +Chronicle Logger is able to aggregate all your logs to a central store. +It has built-in resilience, so you will never lose messages. -At the moment, Chronicle Logger only supports binary logs, which is beneficial for write speed but requires extra tools -to read them. We provide some basic #tools[tools] for that and an API to develop your own. +At the moment, Chronicle Logger only supports binary logs, which is beneficial for write speed but requires extra tools to read them. +We provide some basic #tools[tools] for that and an API to develop your own. NOTE: `chronicle-logger-log4j-2` does not suffer from the log4shell vulnerability as it does not reuse the Log4J2 message formatting machinery == How it works -Chronicle Logger is built on top of Chronicle Queue. It provides multiple logging frameworks' adapters and is a low latency, -high throughput synchronous writer. Unlike asynchronous writers, you will always see the last message before -the application dies, as usually it is the last message that is the most valuable. +Chronicle Logger is built on top of Chronicle Queue. +It provides multiple logging frameworks' adapters and is a low latency, high throughput synchronous writer. +Unlike asynchronous writers, you will always see the last message before the application dies, as usually it is the last message that is the most valuable. == Performance -We have run a benchmark to compare Chronicle Logger with normal file appender of Log4J2 (the quickest of mainstream -logging frameworks). Results below: +We have run a benchmark to compare Chronicle Logger with normal file appender of Log4J2 (the quickest of mainstream logging frameworks). +Results below: |=== |*Benchmark* |*Mode*|*Samples*|*Score*|*Score error*|*Units* @@ -68,6 +73,7 @@ logging frameworks). Results below: |=== Test Hardware: + [source] ---- Intel Core i7-6700K @@ -77,10 +83,9 @@ Intel Core i7-6700K == Bindings -All config files for bindings support limited variable interpolation where the variables are replaced with the -corresponding values from the same configuration file or the system properties. We have one predefined variable, `pid`, -so `${pid}` will replaced by current process id. System properties have the precedence in placeholder replacement -so they can be overriden. +All config files for bindings support limited variable interpolation where the variables are replaced with the corresponding values from the same configuration file or the system properties. +We have one predefined variable, `pid`, so `${pid}` will replaced by current process id. +System properties have the precedence in placeholder replacement so they can be overridden. The following can be configured for each logger: @@ -97,34 +102,45 @@ Additionally, underlying Chronicle Queue can be tweaked by providing the followi * `blockSize` * `rollCycle` -If set, these will override the default Chronicle Queue configuration. _Use with caution!_ +If set, these will override the default Chronicle Queue configuration. +_Use with caution!_ *Please Note* - * Loggers are not hierarchically grouped so `my.domain.package.MyClass1` and `my.domain` are two distinct entities. - * The `path` is used to track the underlying Chronicle Queue so having two loggers configured with the same `path` is unsupported +* Loggers are not hierarchically grouped so `my.domain.package.MyClass1` and `my.domain` are two distinct entities. +* The `path` is used to track the underlying Chronicle Queue so having two loggers configured with the same `path` is unsupported === chronicle-logger-slf4j The chronicle-logger-slf4j is an implementation of SLF4J API > 1.7.x. -To configure this sl4j binding you need to specify the location of a properties files (file-system or classpath) -via system properties: +==== SLF4J compatibility + +|=== +|SLF4J API choice|Logback choice|Chronicle binding|Notes + +|1.7.x|1.2.x|`chronicle-logger-slf4j`|Legacy StaticLoggerBinder binding for SLF4J 1.x +|2.0.x+|1.3.x+|`chronicle-logger-slf4j-2`|Default for new builds; ServiceLoader provider for SLF4J 2.x +|2.0.x with 1.2.x or 1.7.x with 1.3.x|n/a|(not supported)|Mixing SLF4J/Logback versions is rejected by SLF4J and will leave you without a provider +|=== + +For SLF4J 2.x projects use `chronicle-logger-slf4j-2`; keep `chronicle-logger-slf4j` only when you deliberately stay on SLF4J 1.7.x. Legacy SLF4J call sites can be bridged without code changes by adding `slf4j-reload4j` (routes SLF4J to Log4J 1.x) or the Log4J 2 SLF4J binding (routes SLF4J to Log4J 2); ensure exactly one provider is on the classpath to avoid NOP fallbacks. + +To configure this sl4j binding you need to specify the location of a properties files (file-system or classpath) via system properties: [source] ---- -Dchronicle.logger.properties=${pathToYourPropertiesFile} ---- -Alternatively, you could use one of the default locations: `chronicle-logger.properties` +Alternatively, you could use one of the default locations: `chronicle-logger.properties` or `config/chronicle-logger.properties` located in the classpath. -The default configuration is build using properties with `chronicle.logger.root` as prefix but you can also set -per-logger settings i.e. `chronicle.logger.L1.*` +The default configuration is build using properties with `chronicle.logger.root` as prefix but you can also set per-logger settings i.e. `chronicle.logger.L1.*` ==== Config Example -[source, properties] +[source,properties] ---- # shared properties chronicle.base = ${java.io.tmpdir}/chronicle-logs/${pid} @@ -148,7 +164,7 @@ The chronicle-logger-logback module provides appender for Logback: `net.openhft. ==== Config Example -[source, xml] +[source,xml] ---- @@ -171,7 +187,7 @@ We provide log4j1 appender `net.openhft.chronicle.logger.log4j1.ChronicleAppende ==== Config Example -[source, xml] +[source,xml] ---- @@ -228,12 +244,12 @@ We provide log4j1 appender `net.openhft.chronicle.logger.log4j1.ChronicleAppende === chronicle-logger-log4j-2 -Use `` element in `` to create Chronicle appender. Optional `` element can be -used to tweak underlying Chronicle Queue. +Use `` element in `` to create Chronicle appender. +Optional `` element can be used to tweak underlying Chronicle Queue. ==== Config Example -[source, xml] +[source,xml] ---- @@ -289,7 +305,7 @@ Use `net.openhft.chronicle.logger.jul.ChronicleHandler` as a handler ==== Config Example -[source, properties] +[source,properties] ---- handlers=java.util.logging.ConsoleHandler, net.openhft.chronicle.logger.jul.ChronicleHandler @@ -311,19 +327,19 @@ chronicle.useParentHandlers=false === chronicle-logger-jcl -Similar to slf4j, to configure this binding you need to specify the location of a properties files (file-system or classpath) -via system properties: +Similar to slf4j, to configure this binding you need to specify the location of a properties files (file-system or classpath) via system properties: + [source] ---- -Dchronicle.logger.properties=${pathToYourPropertiesFile} ---- -Alternatively, you could use one of the default locations: `chronicle-logger.properties` +Alternatively, you could use one of the default locations: `chronicle-logger.properties` or `config/chronicle-logger.properties` located in the classpath. ==== Config Example -[source, properties] +[source,properties] ---- chronicle.logger.base = ${java.io.tmpdir}/chronicle-jcl chronicle.logger.root.path = ${chronicle.logger.base}/root @@ -337,14 +353,15 @@ chronicle.logger.logger_1.level = info === Tools * `net.openhft.chronicle.logger.tools.ChroniCat` - tool to dump log contents to STDOUT + [source] ---- +---- ChroniCat [-w ] - wire format, default BINARY_LIGHT - base path of Chronicle Logs storage mvn exec:java -Dexec.mainClass="net.openhft.chronicle.logger.tools.ChroniCat" -Dexec.args="..." ---- +---- * `net.openhft.chronicle.logger.tools.ChroniTail` - same as ChroniCat but waits for more data, similar to *nix `tail` utility @@ -357,6 +374,12 @@ ChroniTail [-w ] mvn exec:java -Dexec.mainClass="net.openhft.chronicle.logger.tools.ChroniTail" -Dexec.args="..." ---- -* We also provide generic interface to interact with logs, `net.openhft.chronicle.logger.tools.ChronicleLogReader`, -allowing arbitrary operations with decoded log lines. Please refer to javadocs. +* We also provide generic interface to interact with logs, `net.openhft.chronicle.logger.tools.ChronicleLogReader`, allowing arbitrary operations with decoded log lines. +Please refer to javadocs. + +== Further reading +* link:src/main/docs/architecture-overview.adoc[Architecture overview] +* link:src/main/docs/project-requirements.adoc[Requirements catalogue] +* link:src/main/docs/decision-log.adoc[Decision log] +* link:src/main/docs/code-review-playbook.adoc[Code review playbook] diff --git a/adoc/project-requirements.adoc b/adoc/project-requirements.adoc index c39ca503..d3152d52 100644 --- a/adoc/project-requirements.adoc +++ b/adoc/project-requirements.adoc @@ -1,6 +1,7 @@ = Functional Requirements for Chronicle Logger :toc: :lang: en-GB +:source-highlighter: rouge == Introduction Chronicle Logger is a multi-module Java project providing adapters for various logging frameworks on top of Chronicle Queue. @@ -8,62 +9,63 @@ This document outlines the functional requirements for all modules within this r == General Requirements -- Provide high performance log persistence via Chronicle Queue. -- Support storing log messages in binary format to maximize throughput. -- Allow configuration of log path, log level and Chronicle Queue settings via properties or XML. -- Ensure consistent API and configuration approach across all bindings. -- Include tools for reading log messages from Chronicle Queue. +* Provide high performance log persistence via Chronicle Queue. +* Support storing log messages in binary format to maximize throughput. +* Allow configuration of log path, log level and Chronicle Queue settings via properties or XML. +* Ensure consistent API and configuration approach across all bindings. +* Include tools for reading log messages from Chronicle Queue. == Modules === logger-core -- Contains core interfaces and implementations used by all other modules. -- Provides `ChronicleLogWriter`, `ChronicleLogManager` and related configuration classes. -- Must expose a simple builder-based API to create loggers programmatically. -- Supplies a default implementation for writing log events to Chronicle Queue. +* Contains core interfaces and implementations used by all other modules. +* Provides `ChronicleLogWriter`, `ChronicleLogManager` and related configuration classes. +* Must expose a simple builder-based API to create loggers programmatically. +* Supplies a default implementation for writing log events to Chronicle Queue. === logger-jul -- Implements a `java.util.logging.Handler` writing to Chronicle Queue. -- Must integrate with standard JUL configuration files. -- Provides a factory to create handlers based on properties. +* Implements a `java.util.logging.Handler` writing to Chronicle Queue. +* Must integrate with standard JUL configuration files. +* Provides a factory to create handlers based on properties. === logger-jcl -- Provides a bridge for Apache Commons Logging. -- Supports configuration via properties file specified through the `chronicle.logger.properties` system property or default locations. +* Provides a bridge for Apache Commons Logging. +* Supports configuration via properties file specified through the `chronicle.logger.properties` system property or default locations. === logger-slf4j -- Implements the SLF4J 1.x binding. -- Exposes `ChronicleLoggerFactory` to create loggers. -- Must respect standard SLF4J configuration via properties files. +* Implements the SLF4J 1.x binding. +* Exposes `ChronicleLoggerFactory` to create loggers. +* Must respect standard SLF4J configuration via properties files. === logger-slf4j-2 -- Adds SLF4J 2.x API support. -- Follows the same configuration strategy as `logger-slf4j`. +* Adds SLF4J 2.x API support. +* Follows the same configuration strategy as `logger-slf4j`. === logger-logback -- Supplies a Logback `Appender` that writes events to Chronicle Queue. -- Accepts an optional `` section to tune queue parameters. +* Supplies a Logback `Appender` that writes events to Chronicle Queue. +* Accepts an optional `` section to tune queue parameters. === logger-log4j-1 -- Implements a Log4J 1.2 `Appender`. -- Configuration is performed via XML as part of the log4j configuration file. +* Implements a Log4J 1.2 `Appender`. +* Configuration is performed via XML as part of the log4j configuration file. === logger-log4j-2 -- Provides an `Appender` for Log4J 2. -- Configuration is done using the `` XML element, optionally extended with ``. +* Provides an `Appender` for Log4J 2. +* Configuration is done using the `` XML element, optionally extended with ``. === logger-tools -- Contains command line utilities to inspect and tail Chronicle log files. -- Provides the `ChroniCat` and `ChroniTail` tools as well as a generic `ChronicleLogReader` API. +* Contains command line utilities to inspect and tail Chronicle log files. +* Provides the `ChroniCat` and `ChroniTail` tools as well as a generic `ChronicleLogReader` API. === benchmark -- Includes micro benchmarks to assess logging performance. -- Uses JMH and example configuration files to measure throughput under different scenarios. +* Includes micro benchmarks to assess logging performance. +* Uses JMH and example configuration files to measure throughput under different scenarios. == Non-functional Requirements -- All modules must compile on Java 8 or later. -- Logging operations should minimize latency and avoid unnecessary allocations. -- Binary log format must remain compatible across modules. +* All modules must compile on Java 8 or later. +* Logging operations should minimize latency and avoid unnecessary allocations. +* Binary log format must remain compatible across modules. == Conclusion -The Chronicle Logger repository delivers unified high performance logging for multiple logging frameworks. Each module must conform to the requirements listed above to provide a consistent and reliable logging solution. +The Chronicle Logger repository delivers unified high performance logging for multiple logging frameworks. +Each module must conform to the requirements listed above to provide a consistent and reliable logging solution. diff --git a/benchmark/src/main/java/net/openhft/Lo4J2PerfTest.java b/benchmark/src/main/java/net/openhft/Lo4J2PerfTest.java index 2d520078..9169b635 100644 --- a/benchmark/src/main/java/net/openhft/Lo4J2PerfTest.java +++ b/benchmark/src/main/java/net/openhft/Lo4J2PerfTest.java @@ -12,6 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.file.FileSystems; import java.util.concurrent.TimeUnit; /** @@ -48,7 +49,7 @@ private static String rootPath() { // Be careful if using /tmp - quite often it's mounted as TMPFS, e.g. as RAM disk. // This will nullify the benefits of Chronicle Logger String path = System.getenv("HOME"); - String sep = System.getProperty("file.separator"); + String sep = FileSystems.getDefault().getSeparator(); if (!path.endsWith(sep)) { path += sep; diff --git a/benchmark/src/main/java/net/openhft/package-info.java b/benchmark/src/main/java/net/openhft/package-info.java new file mode 100644 index 00000000..00b389bc --- /dev/null +++ b/benchmark/src/main/java/net/openhft/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +/** + * Benchmark programmes for Chronicle Logger. + * + *

Code in this source root exercises Chronicle logging appenders + * and configurations under different workloads to characterise + * throughput and latency. + * + *

These classes are not part of the public API and are used only + * for performance testing. + */ +package net.openhft; diff --git a/logger-core/src/main/java/net/openhft/chronicle/logger/ChronicleLogConfig.java b/logger-core/src/main/java/net/openhft/chronicle/logger/ChronicleLogConfig.java index a352d873..656dd5d2 100644 --- a/logger-core/src/main/java/net/openhft/chronicle/logger/ChronicleLogConfig.java +++ b/logger-core/src/main/java/net/openhft/chronicle/logger/ChronicleLogConfig.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.io.InputStream; import java.lang.management.ManagementFactory; +import java.nio.file.Files; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -15,24 +16,24 @@ /** * Reads logger settings from a properties file. - * + *

* The loader checks the system property {@code chronicle.logger.properties} and * then the files {@code chronicle-logger.properties} and * {@code config/chronicle-logger.properties} on the class path. Each value may * contain {@code ${name}} placeholders that reference other keys or system * properties. The token {@code ${pid}} expands to the process id. - * + *

* Configuration example: - * + *

* # default * chronicle.logger.base = ${java.io.tmpdir}/chronicle/${pid} - * + *

* # logger : root * chronicle.logger.root.path = ${chronicle.logger.base}/root * chronicle.logger.root.level = debug * chronicle.logger.root.shortName = false * chronicle.logger.root.append = false - * + *

* # logger : Logger1 * chronicle.logger.Logger1.path = ${chronicle.logger.base}/logger_1 * chronicle.logger.Logger1.level = info @@ -105,10 +106,12 @@ private static ChronicleLogConfig load(InputStream in) { if (in != null) { Properties properties = new Properties(); - try { - properties.load(in); - in.close(); - } catch (IOException ignored) { + try (InputStream input = in) { + properties.load(input); + } catch (IOException e) { + System.err.printf("Failed to load Chronicle logger configuration: %s%n", + e.getMessage()); + return null; } return load(interpolate(properties)); @@ -117,7 +120,7 @@ private static ChronicleLogConfig load(InputStream in) { System.err.printf( "Unable to configure chronicle-logger:" + " configuration file not found in default locations (%s)" - + " or System property (%s) is not defined \n", + + " or System property (%s) is not defined%n", DEFAULT_CFG_LOCATIONS.toString(), KEY_PROPERTIES_FILE); } @@ -165,7 +168,7 @@ private static InputStream getConfigurationStream(String cfgPath) throws IOExcep return Thread.currentThread().getContextClassLoader().getResourceAsStream(cfgPath); } else if (cfgFile.canRead()) { - return new FileInputStream(cfgFile); + return Files.newInputStream(cfgFile.toPath()); } } @@ -243,7 +246,8 @@ public String getString(final String loggerName, final String shortName) { } public Boolean getBoolean(final String shortName) { - return getBoolean(shortName, null); + String prop = getString(shortName); + return "true".equalsIgnoreCase(prop); } public Boolean getBoolean(final String shortName, boolean defval) { @@ -253,7 +257,7 @@ public Boolean getBoolean(final String shortName, boolean defval) { public Boolean getBoolean(final String loggerName, final String shortName) { String prop = getString(loggerName, shortName); - return (prop != null) ? "true".equalsIgnoreCase(prop) : null; + return "true".equalsIgnoreCase(prop); } public Boolean getBoolean(final String loggerName, final String shortName, boolean defval) { diff --git a/logger-core/src/main/java/net/openhft/chronicle/logger/ChronicleLogLevel.java b/logger-core/src/main/java/net/openhft/chronicle/logger/ChronicleLogLevel.java index c65d2909..bf92b7e6 100644 --- a/logger-core/src/main/java/net/openhft/chronicle/logger/ChronicleLogLevel.java +++ b/logger-core/src/main/java/net/openhft/chronicle/logger/ChronicleLogLevel.java @@ -18,7 +18,6 @@ * * A higher value represents a more severe event. */ - public enum ChronicleLogLevel { ERROR(50, "ERROR"), WARN(40, "WARN"), diff --git a/logger-core/src/main/java/net/openhft/chronicle/logger/ChronicleLogManager.java b/logger-core/src/main/java/net/openhft/chronicle/logger/ChronicleLogManager.java index a432708f..4c0de399 100644 --- a/logger-core/src/main/java/net/openhft/chronicle/logger/ChronicleLogManager.java +++ b/logger-core/src/main/java/net/openhft/chronicle/logger/ChronicleLogManager.java @@ -50,6 +50,8 @@ public void clear() { try { writer.close(); } catch (IOException e) { + System.err.printf("Unable to close ChronicleLogWriter %s: %s%n", + writer, e.getMessage()); } } @@ -94,7 +96,7 @@ private ChronicleQueue newChronicle(String path, String name) { ChronicleQueue cq = this.cfg.getAppenderConfig().build(path, wireType); if (!cfg.getBoolean(name, ChronicleLogConfig.KEY_APPEND, true)) { // TODO re-enable when it's implemented. ATM it throws UnsupportedOperationException... - //cq.clear(); + System.err.printf("Clearing Chronicle logger queue for '%s' is not supported yet.%n", name); } return cq; } diff --git a/logger-core/src/main/java/net/openhft/chronicle/logger/DefaultChronicleLogWriter.java b/logger-core/src/main/java/net/openhft/chronicle/logger/DefaultChronicleLogWriter.java index 1446b81d..7f7cbe73 100644 --- a/logger-core/src/main/java/net/openhft/chronicle/logger/DefaultChronicleLogWriter.java +++ b/logger-core/src/main/java/net/openhft/chronicle/logger/DefaultChronicleLogWriter.java @@ -92,7 +92,7 @@ public void write( threadName, loggerName, message, - throwable.toString()); + throwable); } return; } diff --git a/logger-core/src/main/java/net/openhft/chronicle/logger/LogAppenderConfig.java b/logger-core/src/main/java/net/openhft/chronicle/logger/LogAppenderConfig.java index 7f71acb3..7c6f1ab6 100644 --- a/logger-core/src/main/java/net/openhft/chronicle/logger/LogAppenderConfig.java +++ b/logger-core/src/main/java/net/openhft/chronicle/logger/LogAppenderConfig.java @@ -135,6 +135,8 @@ public void setProperty(@NotNull final String propName, final String propValue) method.invoke(this, propValue); } } catch (Exception e) { + System.err.printf("Unable to set property '%s' to '%s': %s%n", + propName, propValue, e.getMessage()); } } } diff --git a/logger-core/src/main/java/net/openhft/chronicle/logger/package-info.java b/logger-core/src/main/java/net/openhft/chronicle/logger/package-info.java new file mode 100644 index 00000000..532c43a6 --- /dev/null +++ b/logger-core/src/main/java/net/openhft/chronicle/logger/package-info.java @@ -0,0 +1,15 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +/** + * Core Chronicle logger abstractions and configuration. + * + *

This package defines the main Chronicle logging interfaces, + * configuration objects, and log writers that other logger modules + * and applications build on. + * + *

The module provides a Chronicle Queue backed implementation of + * familiar logging concepts while keeping the core surface small and + * stable. + */ +package net.openhft.chronicle.logger; diff --git a/logger-core/src/test/java/net/openhft/chronicle/logger/DefaultChronicleLogWriterTest.java b/logger-core/src/test/java/net/openhft/chronicle/logger/DefaultChronicleLogWriterTest.java index a0250a88..73844a92 100644 --- a/logger-core/src/test/java/net/openhft/chronicle/logger/DefaultChronicleLogWriterTest.java +++ b/logger-core/src/test/java/net/openhft/chronicle/logger/DefaultChronicleLogWriterTest.java @@ -82,7 +82,7 @@ public void testWrite() { l.add(vi.object(Object.class)); } }); - assertArrayEquals(new Object[]{10, 12.1}, args.toArray(new Object[args.size()])); + assertArrayEquals(new Object[]{10, 12.1}, args.toArray(new Object[0])); } try (DocumentContext dc = tailer.readingDocument()) { diff --git a/logger-jcl/src/main/java/net/openhft/chronicle/logger/jcl/ChronicleLoggerFactory.java b/logger-jcl/src/main/java/net/openhft/chronicle/logger/jcl/ChronicleLoggerFactory.java index 32147c9c..5cd636b7 100644 --- a/logger-jcl/src/main/java/net/openhft/chronicle/logger/jcl/ChronicleLoggerFactory.java +++ b/logger-jcl/src/main/java/net/openhft/chronicle/logger/jcl/ChronicleLoggerFactory.java @@ -99,7 +99,7 @@ public Log getInstance(String name) throws LogConfigurationException { * Create or return a cached logger for the given name. The method is * synchronised so that only one instance per name is created. */ - private synchronized Log getLogger(String name) throws IOException { + private synchronized Log getLogger(String name) { ChronicleLogger logger = loggers.get(name); if (logger == null) { loggers.put( diff --git a/logger-jcl/src/main/java/net/openhft/chronicle/logger/jcl/package-info.java b/logger-jcl/src/main/java/net/openhft/chronicle/logger/jcl/package-info.java new file mode 100644 index 00000000..83ab5a1f --- /dev/null +++ b/logger-jcl/src/main/java/net/openhft/chronicle/logger/jcl/package-info.java @@ -0,0 +1,11 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +/** + * Apache Commons Logging bridge for Chronicle Logger. + * + *

Types in this package adapt Commons Logging APIs onto Chronicle + * Logger so existing JCL based applications can emit logs via + * Chronicle appenders. + */ +package net.openhft.chronicle.logger.jcl; diff --git a/logger-jcl/src/test/java/net/openhft/chronicle/logger/jcl/JclChronicleLoggerTest.java b/logger-jcl/src/test/java/net/openhft/chronicle/logger/jcl/JclChronicleLoggerTest.java index 74c0b192..9544eab7 100644 --- a/logger-jcl/src/test/java/net/openhft/chronicle/logger/jcl/JclChronicleLoggerTest.java +++ b/logger-jcl/src/test/java/net/openhft/chronicle/logger/jcl/JclChronicleLoggerTest.java @@ -58,7 +58,6 @@ public void testLogger() { Log l1 = LogFactory.getLog("jcl-chronicle"); Log l2 = LogFactory.getLog("jcl-chronicle"); Log l3 = LogFactory.getLog("logger_1"); - Log l4 = LogFactory.getLog("readwrite"); assertNotNull(l1); assertEquals(ChronicleLogger.class, l1.getClass()); @@ -69,6 +68,8 @@ public void testLogger() { assertNotNull(l3); assertEquals(ChronicleLogger.class, l3.getClass()); + Log l4 = LogFactory.getLog("readwrite"); + assertNotNull(l4); assertEquals(ChronicleLogger.class, l4.getClass()); @@ -79,26 +80,26 @@ public void testLogger() { ChronicleLogger cl1 = (ChronicleLogger) l1; - assertEquals(cl1.level(), ChronicleLogLevel.DEBUG); - assertEquals(cl1.name(), "jcl-chronicle"); + assertEquals(ChronicleLogLevel.DEBUG, cl1.level()); + assertEquals("jcl-chronicle", cl1.name()); assertTrue(cl1.writer() instanceof DefaultChronicleLogWriter); assertEquals(WireType.BINARY_LIGHT, ((DefaultChronicleLogWriter) cl1.writer()).getWireType()); ChronicleLogger cl2 = (ChronicleLogger) l2; - assertEquals(cl2.level(), ChronicleLogLevel.DEBUG); - assertEquals(cl2.name(), "jcl-chronicle"); + assertEquals(ChronicleLogLevel.DEBUG, cl2.level()); + assertEquals("jcl-chronicle", cl2.name()); assertTrue(cl2.writer() instanceof DefaultChronicleLogWriter); assertEquals(WireType.BINARY_LIGHT, ((DefaultChronicleLogWriter) cl2.writer()).getWireType()); ChronicleLogger cl3 = (ChronicleLogger) l3; - assertEquals(cl3.level(), ChronicleLogLevel.INFO); - assertEquals(cl3.name(), "logger_1"); + assertEquals(ChronicleLogLevel.INFO, cl3.level()); + assertEquals("logger_1", cl3.name()); assertTrue(cl3.writer() instanceof DefaultChronicleLogWriter); assertEquals(WireType.JSON, ((DefaultChronicleLogWriter) cl3.writer()).getWireType()); ChronicleLogger cl4 = (ChronicleLogger) l4; - assertEquals(cl4.level(), ChronicleLogLevel.DEBUG); - assertEquals(cl4.name(), "readwrite"); + assertEquals(ChronicleLogLevel.DEBUG, cl4.level()); + assertEquals("readwrite", cl4.name()); assertTrue(cl4.writer() instanceof DefaultChronicleLogWriter); assertEquals(WireType.BINARY_LIGHT, ((DefaultChronicleLogWriter) cl4.writer()).getWireType()); } @@ -114,7 +115,7 @@ public void testLogging() throws IOException { Thread.currentThread().setName(threadId); for (ChronicleLogLevel level : LOG_LEVELS) { - log(logger, level, "level is " + level.toString()); + log(logger, level, "level is " + level); } try (final ChronicleQueue cq = getChronicleQueue(testId)) { @@ -129,7 +130,7 @@ public void testLogging() throws IOException { assertEquals(level, wire.read("level").asEnum(ChronicleLogLevel.class)); assertEquals(threadId, wire.read("threadName").text()); assertEquals(testId, wire.read("loggerName").text()); - assertEquals("level is " + level.toString(), wire.read("message").text()); + assertEquals("level is " + level, wire.read("message").text()); assertFalse(wire.hasMore()); } } diff --git a/logger-jcl/src/test/java/net/openhft/chronicle/logger/jcl/JclTestBase.java b/logger-jcl/src/test/java/net/openhft/chronicle/logger/jcl/JclTestBase.java index 6f17a162..ce923104 100644 --- a/logger-jcl/src/test/java/net/openhft/chronicle/logger/jcl/JclTestBase.java +++ b/logger-jcl/src/test/java/net/openhft/chronicle/logger/jcl/JclTestBase.java @@ -6,13 +6,15 @@ import net.openhft.chronicle.logger.ChronicleLogLevel; import org.apache.commons.logging.Log; +import java.nio.file.FileSystems; + class JclTestBase { static final ChronicleLogLevel[] LOG_LEVELS = ChronicleLogLevel.values(); static String basePath() { String path = System.getProperty("java.io.tmpdir"); - String sep = System.getProperty("file.separator"); + String sep = FileSystems.getDefault().getSeparator(); if (!path.endsWith(sep)) { path += sep; @@ -22,7 +24,7 @@ static String basePath() { } static String basePath(String loggerName) { - return basePath() + System.getProperty("file.separator") + loggerName; + return basePath() + FileSystems.getDefault().getSeparator() + loggerName; } static void log(Log logger, ChronicleLogLevel level, String message) { diff --git a/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/ChronicleHandler.java b/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/ChronicleHandler.java index 9386a574..2cc97549 100644 --- a/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/ChronicleHandler.java +++ b/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/ChronicleHandler.java @@ -31,10 +31,9 @@ public class ChronicleHandler extends AbstractChronicleHandler { /** * Create the handler and initialise the writer from the LogManager. * - * @throws IOException if the Chronicle queue cannot be opened */ @SuppressWarnings("this-escape") - public ChronicleHandler() throws IOException { + public ChronicleHandler() { ChronicleHandlerConfig handlerCfg = new ChronicleHandlerConfig(getClass()); String appenderPath = handlerCfg.getString("path", null); diff --git a/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/ChronicleHandlerConfig.java b/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/ChronicleHandlerConfig.java index c57fa79e..93985954 100644 --- a/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/ChronicleHandlerConfig.java +++ b/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/ChronicleHandlerConfig.java @@ -151,8 +151,7 @@ Level getLevelProperty(String name, Level defaultValue) { if (val == null) { return defaultValue; } - Level l = Level.parse(val.trim()); - return l != null ? l : defaultValue; + return Level.parse(val.trim()); } /** diff --git a/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/ChronicleLoggerManager.java b/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/ChronicleLoggerManager.java index 2770fdd7..0ace0fec 100644 --- a/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/ChronicleLoggerManager.java +++ b/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/ChronicleLoggerManager.java @@ -75,7 +75,7 @@ public void reset() throws SecurityException { * name. The method is synchronised to avoid duplicate creation when many * threads request the same logger concurrently. */ - private synchronized Logger doGetLogger(String name) throws IOException { + private synchronized Logger doGetLogger(String name) { Logger logger = loggers.get(name); if (logger == null) { final ChronicleLogWriter writer = manager.getWriter(name); diff --git a/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/package-info.java b/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/package-info.java new file mode 100644 index 00000000..8879c2ad --- /dev/null +++ b/logger-jul/src/main/java/net/openhft/chronicle/logger/jul/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +/** + * {@code java.util.logging} bridge for Chronicle Logger. + * + *

This package implements JUL handlers and factories that route log + * records into Chronicle Logger. + */ +package net.openhft.chronicle.logger.jul; diff --git a/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulHandlerChronicleTest.java b/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulHandlerChronicleTest.java index 039d39ca..9f2c8f4f 100644 --- a/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulHandlerChronicleTest.java +++ b/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulHandlerChronicleTest.java @@ -26,12 +26,11 @@ /** * Tests that {@link ChronicleHandler} writes JUL events to a Chronicle Queue. - * + *

* The LogManager loads a properties file to register the handler. The global * {@link DiskSpaceMonitor} is closed before the tests run and the temporary * queue directory is removed after each test. */ - public class JulHandlerChronicleTest extends JulHandlerTestBase { @NotNull diff --git a/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulHandlerTestBase.java b/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulHandlerTestBase.java index 2fd6c44e..4ea95496 100644 --- a/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulHandlerTestBase.java +++ b/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulHandlerTestBase.java @@ -6,6 +6,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.nio.file.FileSystems; import java.util.logging.LogManager; import static org.junit.Assert.assertNotNull; @@ -15,7 +16,7 @@ public class JulHandlerTestBase extends JulTestBase { protected static String rootPath() { String path = System.getProperty("java.io.tmpdir"); - String sep = System.getProperty("file.separator"); + String sep = FileSystems.getDefault().getSeparator(); if (!path.endsWith(sep)) { path += sep; @@ -26,7 +27,7 @@ protected static String rootPath() { protected static String basePath(String type) { return rootPath() - + System.getProperty("file.separator") + + FileSystems.getDefault().getSeparator() + type; } @@ -43,6 +44,8 @@ protected void setupLogManager(String id) throws IOException { LogManager manager = LogManager.getLogManager(); manager.reset(); - manager.readConfiguration(new FileInputStream(cfgFile)); + try (FileInputStream in = new FileInputStream(cfgFile)) { + manager.readConfiguration(in); + } } } diff --git a/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulLoggerTestBase.java b/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulLoggerTestBase.java index 3a8e7d4a..70bc996f 100644 --- a/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulLoggerTestBase.java +++ b/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulLoggerTestBase.java @@ -3,13 +3,14 @@ */ package net.openhft.chronicle.logger.jul; +import java.nio.file.FileSystems; import java.util.logging.LogManager; class JulLoggerTestBase extends JulTestBase { static String basePath() { String path = System.getProperty("java.io.tmpdir"); - String sep = System.getProperty("file.separator"); + String sep = FileSystems.getDefault().getSeparator(); if (!path.endsWith(sep)) { path += sep; @@ -20,7 +21,7 @@ static String basePath() { static String basePath(String loggerName) { return basePath() - + System.getProperty("file.separator") + + FileSystems.getDefault().getSeparator() + loggerName; } diff --git a/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulTestBase.java b/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulTestBase.java index 163cb29a..ffb5e992 100644 --- a/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulTestBase.java +++ b/logger-jul/src/test/java/net/openhft/chronicle/logger/jul/JulTestBase.java @@ -10,7 +10,7 @@ public class JulTestBase { - protected static final ChronicleLogLevel[] LOG_LEVELS = ChronicleLogLevel.values(); + static final ChronicleLogLevel[] LOG_LEVELS = ChronicleLogLevel.values(); protected static void log(Logger logger, ChronicleLogLevel level, String fmt, Object... args) { switch (level) { diff --git a/logger-log4j-1/src/main/java/net/openhft/chronicle/logger/log4j1/AbstractChronicleAppender.java b/logger-log4j-1/src/main/java/net/openhft/chronicle/logger/log4j1/AbstractChronicleAppender.java index 91b2ac5f..967afde7 100644 --- a/logger-log4j-1/src/main/java/net/openhft/chronicle/logger/log4j1/AbstractChronicleAppender.java +++ b/logger-log4j-1/src/main/java/net/openhft/chronicle/logger/log4j1/AbstractChronicleAppender.java @@ -16,7 +16,7 @@ /** * Base Log4j 1.x appender for Chronicle. - * + *

* The class manages filter handling and delegates the actual write * operation to a {@link ChronicleLogWriter} created by * {@link #createWriter()}. @@ -62,11 +62,7 @@ public static ChronicleLogLevel toChronicleLogLevel(final Level level) { @Override public void activateOptions() { if (path != null) { - try { - this.writer = createWriter(); - } catch (IOException e) { - LogLog.warn("Exception [" + name + "].", e); - } + this.writer = createWriter(); } else { LogLog.warn("path option not set for appender [" + name + "]."); } @@ -173,6 +169,8 @@ public void doAppend(LoggingEvent event) { case Filter.ACCEPT: f = null; break; + default: + break; } } @@ -200,9 +198,8 @@ public void doAppend(LoggingEvent event) { * Creates the {@link ChronicleLogWriter} used by this appender. * * @return the writer instance - * @throws IOException if the queue cannot be opened */ - protected abstract ChronicleLogWriter createWriter() throws IOException; + protected abstract ChronicleLogWriter createWriter(); /** * Closes the writer when the appender is stopped. diff --git a/logger-log4j-1/src/main/java/net/openhft/chronicle/logger/log4j1/ChronicleAppender.java b/logger-log4j-1/src/main/java/net/openhft/chronicle/logger/log4j1/ChronicleAppender.java index 481c5a38..be6aa55d 100644 --- a/logger-log4j-1/src/main/java/net/openhft/chronicle/logger/log4j1/ChronicleAppender.java +++ b/logger-log4j-1/src/main/java/net/openhft/chronicle/logger/log4j1/ChronicleAppender.java @@ -60,7 +60,7 @@ public void rollCycle(String rollCycle) { } @Override - protected ChronicleLogWriter createWriter() throws IOException { + protected ChronicleLogWriter createWriter() { return new DefaultChronicleLogWriter(this.config.build(this.getPath(), this.getWireType())); } diff --git a/logger-log4j-1/src/main/java/net/openhft/chronicle/logger/log4j1/package-info.java b/logger-log4j-1/src/main/java/net/openhft/chronicle/logger/log4j1/package-info.java new file mode 100644 index 00000000..8fb6458d --- /dev/null +++ b/logger-log4j-1/src/main/java/net/openhft/chronicle/logger/log4j1/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +/** + * Log4j 1.x appenders for Chronicle Logger. + * + *

Classes in this package integrate Chronicle logging back ends + * with legacy Log4j 1.x based applications. + */ +package net.openhft.chronicle.logger.log4j1; diff --git a/logger-log4j-1/src/test/java/net/openhft/chronicle/logger/log4j1/Log4j1TestBase.java b/logger-log4j-1/src/test/java/net/openhft/chronicle/logger/log4j1/Log4j1TestBase.java index 6bfbf8bb..fb4aae4a 100644 --- a/logger-log4j-1/src/test/java/net/openhft/chronicle/logger/log4j1/Log4j1TestBase.java +++ b/logger-log4j-1/src/test/java/net/openhft/chronicle/logger/log4j1/Log4j1TestBase.java @@ -8,6 +8,7 @@ import org.apache.log4j.Logger; import java.io.File; +import java.nio.file.FileSystems; class Log4j1TestBase { @@ -15,7 +16,7 @@ class Log4j1TestBase { static String rootPath() { String path = System.getProperty("java.io.tmpdir"); - String sep = System.getProperty("file.separator"); + String sep = FileSystems.getDefault().getSeparator(); if (!path.endsWith(sep)) { path += sep; diff --git a/logger-log4j-2/pom.xml b/logger-log4j-2/pom.xml index 3a35bdf9..ffc9b990 100644 --- a/logger-log4j-2/pom.xml +++ b/logger-log4j-2/pom.xml @@ -48,6 +48,13 @@ org.apache.logging.log4j log4j-api + + + + org.slf4j + slf4j-simple + test + diff --git a/logger-log4j-2/src/main/java/net/openhft/chronicle/logger/log4j2/AbstractChronicleAppender.java b/logger-log4j-2/src/main/java/net/openhft/chronicle/logger/log4j2/AbstractChronicleAppender.java index fa7f7002..44b81635 100644 --- a/logger-log4j-2/src/main/java/net/openhft/chronicle/logger/log4j2/AbstractChronicleAppender.java +++ b/logger-log4j-2/src/main/java/net/openhft/chronicle/logger/log4j2/AbstractChronicleAppender.java @@ -89,24 +89,18 @@ public void setWireType(String wireType) { * Builds the {@link ChronicleLogWriter} for this appender. * * @return writer bound to the configured path and wire type - * @throws IOException if the Chronicle queue cannot be opened */ - protected abstract ChronicleLogWriter createWriter() throws IOException; + protected abstract ChronicleLogWriter createWriter(); protected abstract void doAppend(@NotNull final LogEvent event, @NotNull final ChronicleLogWriter writer); @Override public void start() { if (getPath() == null) { - LOGGER.error("Appender " + getName() + " has configuration errors and is not started!"); + LOGGER.error("Appender {} has configuration errors and is not started!", getName()); } else { - try { - this.writer = createWriter(); - } catch (IOException e) { - this.writer = null; - LOGGER.error("Appender " + getName() + " " + e.getMessage()); - } + this.writer = createWriter(); super.start(); } @@ -118,7 +112,7 @@ public void stop() { try { this.writer.close(); } catch (IOException e) { - LOGGER.error("Appender " + getName() + " " + e.getMessage()); + LOGGER.error("Appender {} {}", getName(), e.getMessage()); } } diff --git a/logger-log4j-2/src/main/java/net/openhft/chronicle/logger/log4j2/ChronicleAppender.java b/logger-log4j-2/src/main/java/net/openhft/chronicle/logger/log4j2/ChronicleAppender.java index 4aee77cc..fd07dff0 100644 --- a/logger-log4j-2/src/main/java/net/openhft/chronicle/logger/log4j2/ChronicleAppender.java +++ b/logger-log4j-2/src/main/java/net/openhft/chronicle/logger/log4j2/ChronicleAppender.java @@ -24,7 +24,6 @@ * {@code chronicleCfg} element allows Chronicle Queue settings such as block * size and roll cycle to be supplied.

*/ - @Plugin( name = "Chronicle", category = "Core", @@ -86,7 +85,7 @@ public void doAppend(@NotNull final LogEvent event, @NotNull final ChronicleLogW } @Override - protected ChronicleLogWriter createWriter() throws IOException { + protected ChronicleLogWriter createWriter() { return new DefaultChronicleLogWriter(config.build(getPath(), getWireType())); } diff --git a/logger-log4j-2/src/main/java/net/openhft/chronicle/logger/log4j2/package-info.java b/logger-log4j-2/src/main/java/net/openhft/chronicle/logger/log4j2/package-info.java new file mode 100644 index 00000000..3463630b --- /dev/null +++ b/logger-log4j-2/src/main/java/net/openhft/chronicle/logger/log4j2/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +/** + * Log4j 2.x appenders for Chronicle Logger. + * + *

This package provides Log4j 2.x plugins that write log events to + * Chronicle backed appenders, enabling low latency logging pipelines. + */ +package net.openhft.chronicle.logger.log4j2; diff --git a/logger-log4j-2/src/test/java/net/openhft/chronicle/logger/log4j2/Log4j2TestBase.java b/logger-log4j-2/src/test/java/net/openhft/chronicle/logger/log4j2/Log4j2TestBase.java index 3ebddae4..0156609c 100644 --- a/logger-log4j-2/src/test/java/net/openhft/chronicle/logger/log4j2/Log4j2TestBase.java +++ b/logger-log4j-2/src/test/java/net/openhft/chronicle/logger/log4j2/Log4j2TestBase.java @@ -8,6 +8,7 @@ import org.apache.logging.log4j.LogManager; import java.io.File; +import java.nio.file.FileSystems; public class Log4j2TestBase { @@ -15,7 +16,7 @@ public class Log4j2TestBase { static String rootPath() { String path = System.getProperty("java.io.tmpdir"); - String sep = System.getProperty("file.separator"); + String sep = FileSystems.getDefault().getSeparator(); if (!path.endsWith(sep)) { path += sep; diff --git a/logger-logback/src/main/java/net/openhft/chronicle/logger/logback/AbstractChronicleAppender.java b/logger-logback/src/main/java/net/openhft/chronicle/logger/logback/AbstractChronicleAppender.java index 2242e8e4..976327b5 100644 --- a/logger-logback/src/main/java/net/openhft/chronicle/logger/logback/AbstractChronicleAppender.java +++ b/logger-logback/src/main/java/net/openhft/chronicle/logger/logback/AbstractChronicleAppender.java @@ -84,9 +84,8 @@ public void setWireType(String wireType) { * Creates the Chronicle writer used to store events. * * @return a log writer bound to the configured queue - * @throws IOException if the writer cannot be created */ - protected abstract ChronicleLogWriter createWriter() throws IOException; + protected abstract ChronicleLogWriter createWriter(); /** * Logs a single event using the supplied writer. @@ -137,13 +136,8 @@ public void start() { addError("Appender " + getName() + " has configuration errors and is not started!"); } else { - try { - this.writer = createWriter(); - this.started = true; - } catch (IOException e) { - this.writer = null; - addError("Appender " + getName() + " " + e.getMessage()); - } + this.writer = createWriter(); + this.started = true; } } diff --git a/logger-logback/src/main/java/net/openhft/chronicle/logger/logback/ChronicleAppender.java b/logger-logback/src/main/java/net/openhft/chronicle/logger/logback/ChronicleAppender.java index 6f044945..757e82f2 100644 --- a/logger-logback/src/main/java/net/openhft/chronicle/logger/logback/ChronicleAppender.java +++ b/logger-logback/src/main/java/net/openhft/chronicle/logger/logback/ChronicleAppender.java @@ -51,7 +51,7 @@ public void setChronicleConfig(final LogAppenderConfig config) { } @Override - protected ChronicleLogWriter createWriter() throws IOException { + protected ChronicleLogWriter createWriter() { return new DefaultChronicleLogWriter(this.config.build(this.getPath(), getWireType())); } diff --git a/logger-logback/src/main/java/net/openhft/chronicle/logger/logback/package-info.java b/logger-logback/src/main/java/net/openhft/chronicle/logger/logback/package-info.java new file mode 100644 index 00000000..d5381e53 --- /dev/null +++ b/logger-logback/src/main/java/net/openhft/chronicle/logger/logback/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +/** + * Logback appenders that write events to Chronicle logs. + *

+ * The classes in this package bridge Logback's logging model to Chronicle + * storage, typically backed by Chronicle Queue. They translate + * {@code ILoggingEvent} instances into Chronicle messages, preserving + * structured data such as mapped diagnostic context (MDC), caller data and + * stack traces when configured to do so. + *

+ * These appenders are intended to be configured via standard Logback XML + * or Groovy configuration. They form part of the supported Chronicle + * logging surface but their internal helper classes may change between + * releases. + */ +package net.openhft.chronicle.logger.logback; diff --git a/logger-logback/src/test/java/net/openhft/chronicle/logger/logback/LogbackIndexedChronicleConfigTest.java b/logger-logback/src/test/java/net/openhft/chronicle/logger/logback/LogbackIndexedChronicleConfigTest.java index 8ed23324..70f4c9e1 100644 --- a/logger-logback/src/test/java/net/openhft/chronicle/logger/logback/LogbackIndexedChronicleConfigTest.java +++ b/logger-logback/src/test/java/net/openhft/chronicle/logger/logback/LogbackIndexedChronicleConfigTest.java @@ -22,7 +22,7 @@ public void setup() { } @Test - public void testBinaryIndexedChronicleAppenderConfig() throws IOException { + public void testBinaryIndexedChronicleAppenderConfig() { final String loggerName = "config-binary-chronicle"; final String appenderName = "CONFIG-BINARY-CHRONICLE"; diff --git a/logger-logback/src/test/java/net/openhft/chronicle/logger/logback/LogbackTestBase.java b/logger-logback/src/test/java/net/openhft/chronicle/logger/logback/LogbackTestBase.java index 36bcfd27..8dbf9b2d 100644 --- a/logger-logback/src/test/java/net/openhft/chronicle/logger/logback/LogbackTestBase.java +++ b/logger-logback/src/test/java/net/openhft/chronicle/logger/logback/LogbackTestBase.java @@ -8,13 +8,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.file.FileSystems; + public class LogbackTestBase { static final ChronicleLogLevel[] LOG_LEVELS = ChronicleLogLevel.values(); static String rootPath() { String path = System.getProperty("java.io.tmpdir"); - String sep = System.getProperty("file.separator"); + String sep = FileSystems.getDefault().getSeparator(); if (!path.endsWith(sep)) { path += sep; @@ -25,7 +27,7 @@ static String rootPath() { static String basePath(String type) { return rootPath() - + System.getProperty("file.separator") + + FileSystems.getDefault().getSeparator() + type; } diff --git a/logger-slf4j-2/src/main/java/net/openhft/chronicle/logger/slf4j2/ChronicleLoggerFactory.java b/logger-slf4j-2/src/main/java/net/openhft/chronicle/logger/slf4j2/ChronicleLoggerFactory.java index e4eef6a3..ad5664a1 100644 --- a/logger-slf4j-2/src/main/java/net/openhft/chronicle/logger/slf4j2/ChronicleLoggerFactory.java +++ b/logger-slf4j-2/src/main/java/net/openhft/chronicle/logger/slf4j2/ChronicleLoggerFactory.java @@ -5,6 +5,7 @@ import net.openhft.chronicle.logger.ChronicleLogManager; import net.openhft.chronicle.logger.ChronicleLogWriter; +import net.openhft.chronicle.logger.ChronicleLoggerFactoryControl; import org.slf4j.ILoggerFactory; import org.slf4j.Logger; import org.slf4j.helpers.NOPLogger; @@ -35,7 +36,7 @@ * log.info("Started"); * */ -public class ChronicleLoggerFactory implements ILoggerFactory { +public class ChronicleLoggerFactory implements ILoggerFactory, ChronicleLoggerFactoryControl { private final Map loggers; private final ChronicleLogManager manager; @@ -70,9 +71,11 @@ public Logger getLogger(String name) { /** * Clear cached loggers and reload the manager configuration. *

- * Primarily used by tests when the properties file has changed. + * Primarily used by tests when the properties file has changed via + * {@link net.openhft.chronicle.logger.ChronicleLoggerFactoryControl}. */ - synchronized void reload() { + @Override + public synchronized void reload() { this.loggers.clear(); this.manager.reload(); } diff --git a/logger-slf4j-2/src/main/java/net/openhft/chronicle/logger/slf4j2/package-info.java b/logger-slf4j-2/src/main/java/net/openhft/chronicle/logger/slf4j2/package-info.java new file mode 100644 index 00000000..570d89a7 --- /dev/null +++ b/logger-slf4j-2/src/main/java/net/openhft/chronicle/logger/slf4j2/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +/** + * SLF4J 2.x binding backed by Chronicle logging. + *

+ * This package provides the {@code ChronicleLoggerFactory} and related + * infrastructure used when Chronicle is selected as the SLF4J 2.x + * provider. It connects the SLF4J API to Chronicle log writers so that + * application code using {@code org.slf4j.Logger} can emit events into + * Chronicle-backed queues without being aware of the underlying storage. + *

+ * Configuration is typically supplied via properties (for example + * {@code chronicle.logger.root.path}) rather than by using these classes + * directly. The package is part of the public integration surface for + * Chronicle logging. + */ +package net.openhft.chronicle.logger.slf4j2; diff --git a/logger-slf4j-2/src/main/java/org/slf4j/impl/ChronicleServiceProvider.java b/logger-slf4j-2/src/main/java/org/slf4j/impl/ChronicleServiceProvider.java index 7f4840d4..b5b62bfc 100644 --- a/logger-slf4j-2/src/main/java/org/slf4j/impl/ChronicleServiceProvider.java +++ b/logger-slf4j-2/src/main/java/org/slf4j/impl/ChronicleServiceProvider.java @@ -28,7 +28,7 @@ public class ChronicleServiceProvider implements SLF4JServiceProvider { * The value of this field is modified with each major release. */ // to avoid constant folding by the compiler, this field must *not* be final - public static String REQUESTED_API_VERSION = "2.0.99"; // !final + public static final String REQUESTED_API_VERSION = "2.0.99"; // !final public ChronicleServiceProvider() {} /** diff --git a/logger-slf4j-2/src/main/java/org/slf4j/impl/OutputChoice.java b/logger-slf4j-2/src/main/java/org/slf4j/impl/OutputChoice.java index 0b66b5df..a6a1d994 100644 --- a/logger-slf4j-2/src/main/java/org/slf4j/impl/OutputChoice.java +++ b/logger-slf4j-2/src/main/java/org/slf4j/impl/OutputChoice.java @@ -22,7 +22,7 @@ class OutputChoice { enum OutputChoiceType { - SYS_OUT, CACHED_SYS_OUT, SYS_ERR, CACHED_SYS_ERR, FILE; + SYS_OUT, CACHED_SYS_OUT, SYS_ERR, CACHED_SYS_ERR, FILE } final OutputChoiceType outputChoiceType; diff --git a/logger-slf4j-2/src/main/java/org/slf4j/impl/SimpleLogger.java b/logger-slf4j-2/src/main/java/org/slf4j/impl/SimpleLogger.java index 8cac2795..3e6f1a93 100644 --- a/logger-slf4j-2/src/main/java/org/slf4j/impl/SimpleLogger.java +++ b/logger-slf4j-2/src/main/java/org/slf4j/impl/SimpleLogger.java @@ -170,7 +170,7 @@ public class SimpleLogger extends LegacyAbstractLogger { protected static final int LOG_LEVEL_WARN = LocationAwareLogger.WARN_INT; protected static final int LOG_LEVEL_ERROR = LocationAwareLogger.ERROR_INT; - static char SP = ' '; + static final char SP = ' '; static final String TID_PREFIX = "tid="; // The OFF level can only be used in configuration files to disable logging. @@ -249,7 +249,7 @@ String recursivelyComputeLevelString() { while ((levelString == null) && (indexOfLastDot > -1)) { tempName = tempName.substring(0, indexOfLastDot); levelString = CONFIG_PARAMS.getStringProperty(SimpleLogger.LOG_KEY_PREFIX + tempName, null); - indexOfLastDot = String.valueOf(tempName).lastIndexOf("."); + indexOfLastDot = tempName.lastIndexOf("."); } return levelString; } @@ -423,9 +423,9 @@ private void innerHandleNormalizedLoggingCall(Level level, List markers, if (CONFIG_PARAMS.showShortLogName) { if (shortLogName == null) shortLogName = computeShortName(); - buf.append(String.valueOf(shortLogName)).append(" - "); + buf.append(shortLogName).append(" - "); } else if (CONFIG_PARAMS.showLogName) { - buf.append(String.valueOf(name)).append(" - "); + buf.append(name).append(" - "); } if (markers != null) { diff --git a/logger-slf4j-2/src/main/java/org/slf4j/impl/SimpleLoggerConfiguration.java b/logger-slf4j-2/src/main/java/org/slf4j/impl/SimpleLoggerConfiguration.java index 8b65d927..4b30783f 100644 --- a/logger-slf4j-2/src/main/java/org/slf4j/impl/SimpleLoggerConfiguration.java +++ b/logger-slf4j-2/src/main/java/org/slf4j/impl/SimpleLoggerConfiguration.java @@ -34,14 +34,13 @@ public class SimpleLoggerConfiguration { private static final String CONFIGURATION_FILE = "simplelogger.properties"; - static int DEFAULT_LOG_LEVEL_DEFAULT = SimpleLogger.LOG_LEVEL_INFO; + static final int DEFAULT_LOG_LEVEL_DEFAULT = SimpleLogger.LOG_LEVEL_INFO; int defaultLogLevel = DEFAULT_LOG_LEVEL_DEFAULT; private static final boolean SHOW_DATE_TIME_DEFAULT = false; boolean showDateTime = SHOW_DATE_TIME_DEFAULT; private static final String DATE_TIME_FORMAT_STR_DEFAULT = null; - private static String dateTimeFormatStr = DATE_TIME_FORMAT_STR_DEFAULT; DateFormat dateFormatter = null; @@ -50,12 +49,13 @@ public class SimpleLoggerConfiguration { /** * See https://jira.qos.ch/browse/SLF4J-499 + * * @since 1.7.33 and 2.0.0-alpha6 */ private static final boolean SHOW_THREAD_ID_DEFAULT = false; boolean showThreadId = SHOW_THREAD_ID_DEFAULT; - final static boolean SHOW_LOG_NAME_DEFAULT = true; + static final boolean SHOW_LOG_NAME_DEFAULT = true; boolean showLogName = SHOW_LOG_NAME_DEFAULT; private static final boolean SHOW_SHORT_LOG_NAME_DEFAULT = false; @@ -69,13 +69,13 @@ public class SimpleLoggerConfiguration { OutputChoice outputChoice = null; private static final boolean CACHE_OUTPUT_STREAM_DEFAULT = false; - private boolean cacheOutputStream = CACHE_OUTPUT_STREAM_DEFAULT; private static final String WARN_LEVELS_STRING_DEFAULT = "WARN"; String warnLevelString = WARN_LEVELS_STRING_DEFAULT; private final Properties properties = new Properties(); + @SuppressWarnings("deprecation") // Util.report() deprecated in SLF4J 2.0.x but still functional void init() { loadProperties(); @@ -88,13 +88,13 @@ void init() { showDateTime = getBooleanProperty(SimpleLogger.SHOW_DATE_TIME_KEY, SHOW_DATE_TIME_DEFAULT); showThreadName = getBooleanProperty(SimpleLogger.SHOW_THREAD_NAME_KEY, SHOW_THREAD_NAME_DEFAULT); showThreadId = getBooleanProperty(SimpleLogger.SHOW_THREAD_ID_KEY, SHOW_THREAD_ID_DEFAULT); - dateTimeFormatStr = getStringProperty(SimpleLogger.DATE_TIME_FORMAT_KEY, DATE_TIME_FORMAT_STR_DEFAULT); + String dateTimeFormatStr = getStringProperty(SimpleLogger.DATE_TIME_FORMAT_KEY, DATE_TIME_FORMAT_STR_DEFAULT); levelInBrackets = getBooleanProperty(SimpleLogger.LEVEL_IN_BRACKETS_KEY, LEVEL_IN_BRACKETS_DEFAULT); warnLevelString = getStringProperty(SimpleLogger.WARN_LEVEL_STRING_KEY, WARN_LEVELS_STRING_DEFAULT); logFile = getStringProperty(SimpleLogger.LOG_FILE_KEY, logFile); - cacheOutputStream = getBooleanProperty(SimpleLogger.CACHE_OUTPUT_STREAM_STRING_KEY, CACHE_OUTPUT_STREAM_DEFAULT); + boolean cacheOutputStream = getBooleanProperty(SimpleLogger.CACHE_OUTPUT_STREAM_STRING_KEY, CACHE_OUTPUT_STREAM_DEFAULT); outputChoice = computeOutputChoice(logFile, cacheOutputStream); if (dateTimeFormatStr != null) { @@ -111,7 +111,6 @@ void init() { * class loader is used if no resource is found. Missing files or * errors result in an empty property set. */ - private void loadProperties() { // Add props from the resource simplelogger.properties @SuppressWarnings({"deprecation", "removal"}) @@ -176,6 +175,7 @@ static int stringToLevel(String levelStr) { return SimpleLogger.LOG_LEVEL_INFO; } + @SuppressWarnings("deprecation") // Util.report() deprecated in SLF4J 2.0.x but still functional private static OutputChoice computeOutputChoice(String logFile, boolean cacheOutputStream) { if ("System.err".equalsIgnoreCase(logFile)) if (cacheOutputStream) diff --git a/logger-slf4j-2/src/main/java/org/slf4j/impl/package-info.java b/logger-slf4j-2/src/main/java/org/slf4j/impl/package-info.java new file mode 100644 index 00000000..7fc88a86 --- /dev/null +++ b/logger-slf4j-2/src/main/java/org/slf4j/impl/package-info.java @@ -0,0 +1,15 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +/** + * SLF4J 2.x service-provider implementation for Chronicle logging. + *

+ * Types in this package implement the SLF4J service-provider interface and + * wire the SLF4J API to the Chronicle logger factory. They are discovered + * via Java's service-loader mechanism and are not intended to be referenced + * directly by application code. + *

+ * Behaviour follows the contracts defined by the SLF4J API. The set of + * implementation classes may change between releases without prior notice. + */ +package org.slf4j.impl; diff --git a/logger-slf4j-2/src/test/java/net/openhft/chronicle/logger/slf4j2/Slf4jChronicleLoggerPerfTest.java b/logger-slf4j-2/src/test/java/net/openhft/chronicle/logger/slf4j2/Slf4jChronicleLoggerPerfTest.java index a44e9e6c..c7bbb177 100644 --- a/logger-slf4j-2/src/test/java/net/openhft/chronicle/logger/slf4j2/Slf4jChronicleLoggerPerfTest.java +++ b/logger-slf4j-2/src/test/java/net/openhft/chronicle/logger/slf4j2/Slf4jChronicleLoggerPerfTest.java @@ -26,7 +26,7 @@ *

  • multi-thread throughput across ten threads.
  • * */ -@Ignore +@Ignore(/*Long running performance test*/) public class Slf4jChronicleLoggerPerfTest extends Slf4jTestBase { @Before diff --git a/logger-slf4j-2/src/test/java/net/openhft/chronicle/logger/slf4j2/Slf4jChronicleLoggerTest.java b/logger-slf4j-2/src/test/java/net/openhft/chronicle/logger/slf4j2/Slf4jChronicleLoggerTest.java index 8bfcaa95..da3205a6 100644 --- a/logger-slf4j-2/src/test/java/net/openhft/chronicle/logger/slf4j2/Slf4jChronicleLoggerTest.java +++ b/logger-slf4j-2/src/test/java/net/openhft/chronicle/logger/slf4j2/Slf4jChronicleLoggerTest.java @@ -51,8 +51,8 @@ public void tearDown() { @Test public void testLoggerFactory() { assertEquals( - getChronicleLoggerFactory().getClass(), - ChronicleLoggerFactory.class); + ChronicleLoggerFactory.class, + getChronicleLoggerFactory().getClass()); } @Test @@ -60,7 +60,6 @@ public void testLogger() { Logger l1 = LoggerFactory.getLogger("slf4j-chronicle"); Logger l2 = LoggerFactory.getLogger("slf4j-chronicle"); Logger l3 = LoggerFactory.getLogger("logger_1"); - Logger l4 = LoggerFactory.getLogger("readwrite"); assertNotNull(l1); assertTrue(l1 instanceof ChronicleLogger); @@ -71,6 +70,8 @@ public void testLogger() { assertNotNull(l3); assertTrue(l3 instanceof ChronicleLogger); + Logger l4 = LoggerFactory.getLogger("readwrite"); + assertNotNull(l4); assertTrue(l4 instanceof ChronicleLogger); @@ -81,24 +82,24 @@ public void testLogger() { ChronicleLogger cl1 = (ChronicleLogger) l1; - assertEquals(cl1.getLevel(), ChronicleLogLevel.DEBUG); - assertEquals(cl1.getName(), "slf4j-chronicle"); + assertEquals(ChronicleLogLevel.DEBUG, cl1.getLevel()); + assertEquals("slf4j-chronicle", cl1.getName()); assertTrue(cl1.getWriter() instanceof DefaultChronicleLogWriter); ChronicleLogger cl2 = (ChronicleLogger) l2; - assertEquals(cl2.getLevel(), ChronicleLogLevel.DEBUG); - assertEquals(cl2.getName(), "slf4j-chronicle"); + assertEquals(ChronicleLogLevel.DEBUG, cl2.getLevel()); + assertEquals("slf4j-chronicle", cl2.getName()); assertTrue(cl2.getWriter() instanceof DefaultChronicleLogWriter); ChronicleLogger cl3 = (ChronicleLogger) l3; - assertEquals(cl3.getLevel(), ChronicleLogLevel.INFO); + assertEquals(ChronicleLogLevel.INFO, cl3.getLevel()); assertTrue(cl3.getWriter() instanceof DefaultChronicleLogWriter); - assertEquals(cl3.getName(), "logger_1"); + assertEquals("logger_1", cl3.getName()); ChronicleLogger cl4 = (ChronicleLogger) l4; - assertEquals(cl4.getLevel(), ChronicleLogLevel.DEBUG); + assertEquals(ChronicleLogLevel.DEBUG, cl4.getLevel()); assertTrue(cl4.getWriter() instanceof DefaultChronicleLogWriter); - assertEquals(cl4.getName(), "readwrite"); + assertEquals("readwrite", cl4.getName()); } @Test @@ -183,6 +184,7 @@ public void testLogging() throws IOException { assertNull(wire); } + //noinspection LoggingPlaceholderCountMatchesArgumentCount logger.warn("Test object", new Object()); try (DocumentContext dc = tailer.readingDocument()) { diff --git a/logger-slf4j-2/src/test/java/net/openhft/chronicle/logger/slf4j2/Slf4jTestBase.java b/logger-slf4j-2/src/test/java/net/openhft/chronicle/logger/slf4j2/Slf4jTestBase.java index 77e0f496..3e9653eb 100644 --- a/logger-slf4j-2/src/test/java/net/openhft/chronicle/logger/slf4j2/Slf4jTestBase.java +++ b/logger-slf4j-2/src/test/java/net/openhft/chronicle/logger/slf4j2/Slf4jTestBase.java @@ -10,13 +10,15 @@ import org.slf4j.LoggerFactory; import org.slf4j.impl.ChronicleServiceProvider; +import java.nio.file.FileSystems; + class Slf4jTestBase { static final ChronicleLogLevel[] LOG_LEVELS = ChronicleLogLevel.values(); static String basePath() { String path = System.getProperty("java.io.tmpdir"); - String sep = System.getProperty("file.separator"); + String sep = FileSystems.getDefault().getSeparator(); if (!path.endsWith(sep)) { path += sep; @@ -27,7 +29,7 @@ static String basePath() { static String basePath(String loggerName) { return basePath() - + System.getProperty("file.separator") + + FileSystems.getDefault().getSeparator() + loggerName; } @@ -72,15 +74,15 @@ ChronicleLoggerFactory getChronicleLoggerFactory() { return (ChronicleLoggerFactory) provider.getLoggerFactory(); } - protected final class RunnableLogger implements Runnable { + protected static final class RunnableLogger implements Runnable { private final Logger logger; private final int runs; private final String fmt; - private final String fmtBase = " > val1={}, val2={}, val3={}"; public RunnableLogger(int runs, int pad, String loggerName) { this.logger = LoggerFactory.getLogger(loggerName); this.runs = runs; + String fmtBase = " > val1={}, val2={}, val3={}"; this.fmt = StringUtils.rightPad(fmtBase, pad + fmtBase.length() - (4 + 8 + 8), "X"); } diff --git a/logger-slf4j/src/main/java/net/openhft/chronicle/logger/slf4j/ChronicleLogger.java b/logger-slf4j/src/main/java/net/openhft/chronicle/logger/slf4j/ChronicleLogger.java index 3440aa3f..c964cb66 100644 --- a/logger-slf4j/src/main/java/net/openhft/chronicle/logger/slf4j/ChronicleLogger.java +++ b/logger-slf4j/src/main/java/net/openhft/chronicle/logger/slf4j/ChronicleLogger.java @@ -14,6 +14,9 @@ * messages are forwarded when the requested {@link ChronicleLogLevel} * is enabled.

    */ +// SLF4J 1.x base retained for backwards compatibility with existing deployments. +// The type is deprecated in SLF4J 2.x but still supported, so we suppress the +// deprecation warning here to keep the binder usable without breaking callers. @SuppressWarnings({"serial", "deprecation"}) public final class ChronicleLogger extends org.slf4j.helpers.MarkerIgnoringBase { diff --git a/logger-slf4j/src/main/java/net/openhft/chronicle/logger/slf4j/package-info.java b/logger-slf4j/src/main/java/net/openhft/chronicle/logger/slf4j/package-info.java new file mode 100644 index 00000000..8ff0b3ea --- /dev/null +++ b/logger-slf4j/src/main/java/net/openhft/chronicle/logger/slf4j/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +/** + * SLF4J 1.7.x binding backed by Chronicle logging. + *

    + * The classes in this package provide an {@code ILoggerFactory} and + * supporting infrastructure that route SLF4J 1.x log events to Chronicle + * writers. This allows existing code using the classic SLF4J API to log to + * Chronicle without code changes. + *

    + * Configuration is driven by properties (for example + * {@code chronicle.logger.root.path}) and by SLF4J conventions rather than + * by direct use of these types. Where practical the behaviour mirrors that + * of the SLF4J 2.x binding to ease migration. + */ +package net.openhft.chronicle.logger.slf4j; diff --git a/logger-slf4j/src/main/java/org/slf4j/impl/OutputChoice.java b/logger-slf4j/src/main/java/org/slf4j/impl/OutputChoice.java index 2799d30b..5edcbc10 100644 --- a/logger-slf4j/src/main/java/org/slf4j/impl/OutputChoice.java +++ b/logger-slf4j/src/main/java/org/slf4j/impl/OutputChoice.java @@ -23,7 +23,7 @@ class OutputChoice { enum OutputChoiceType { - SYS_OUT, CACHED_SYS_OUT, SYS_ERR, CACHED_SYS_ERR, FILE; + SYS_OUT, CACHED_SYS_OUT, SYS_ERR, CACHED_SYS_ERR, FILE } final OutputChoiceType outputChoiceType; diff --git a/logger-slf4j/src/main/java/org/slf4j/impl/SimpleLogger.java b/logger-slf4j/src/main/java/org/slf4j/impl/SimpleLogger.java index 8c9aaf75..2402ab5f 100644 --- a/logger-slf4j/src/main/java/org/slf4j/impl/SimpleLogger.java +++ b/logger-slf4j/src/main/java/org/slf4j/impl/SimpleLogger.java @@ -27,15 +27,15 @@ */ package org.slf4j.impl; -import java.io.PrintStream; -import java.util.Date; - import org.slf4j.Logger; import org.slf4j.event.LoggingEvent; import org.slf4j.helpers.FormattingTuple; import org.slf4j.helpers.MessageFormatter; import org.slf4j.spi.LocationAwareLogger; +import java.io.PrintStream; +import java.util.Date; + /** * Lightweight {@link Logger} writing to a single {@link PrintStream}. The * logger initialises its configuration once, then each call checks the level, @@ -70,12 +70,16 @@ * @author Robert Burrell Donkin * @author Cédrik LIME */ +// Retains the SLF4J 1.x style base class for compatibility with existing +// configuration and behaviour. MarkerIgnoringBase is deprecated in SLF4J 2.x +// but still functions, so we suppress the deprecation warning rather than +// risk diverging from the upstream SimpleLogger semantics. @SuppressWarnings("deprecation") public class SimpleLogger extends org.slf4j.helpers.MarkerIgnoringBase { private static final long serialVersionUID = -632788891211436180L; - private static long START_TIME = System.currentTimeMillis(); + private static final long START_TIME = System.currentTimeMillis(); protected static final int LOG_LEVEL_TRACE = LocationAwareLogger.TRACE_INT; protected static final int LOG_LEVEL_DEBUG = LocationAwareLogger.DEBUG_INT; @@ -105,9 +109,13 @@ static void init() { CONFIG_PARAMS.init(); } - /** The current log level */ + /** + * The current log level + */ protected int currentLogLevel = LOG_LEVEL_INFO; - /** The short name of this simple log instance */ + /** + * The short name of this simple log instance + */ private transient String shortLogName = null; /** @@ -157,7 +165,7 @@ String recursivelyComputeLevelString() { while ((levelString == null) && (indexOfLastDot > -1)) { tempName = tempName.substring(0, indexOfLastDot); levelString = CONFIG_PARAMS.getStringProperty(SimpleLogger.LOG_KEY_PREFIX + tempName, null); - indexOfLastDot = String.valueOf(tempName).lastIndexOf("."); + indexOfLastDot = tempName.lastIndexOf("."); } return levelString; } @@ -166,12 +174,9 @@ String recursivelyComputeLevelString() { * This is our internal implementation for logging regular * (non-parameterized) log messages. * - * @param level - * One of the LOG_LEVEL_XXX constants defining the log level - * @param message - * The message itself - * @param t - * The exception whose stack trace should be logged + * @param level One of the LOG_LEVEL_XXX constants defining the log level + * @param message The message itself + * @param t The exception whose stack trace should be logged */ private void log(int level, String message, Throwable t) { if (!isLevelEnabled(level)) { @@ -212,9 +217,9 @@ private void log(int level, String message, Throwable t) { if (CONFIG_PARAMS.showShortLogName) { if (shortLogName == null) shortLogName = computeShortName(); - buf.append(String.valueOf(shortLogName)).append(" - "); + buf.append(shortLogName).append(" - "); } else if (CONFIG_PARAMS.showLogName) { - buf.append(String.valueOf(name)).append(" - "); + buf.append(name).append(" - "); } // Append the message @@ -226,18 +231,19 @@ private void log(int level, String message, Throwable t) { protected String renderLevel(int level) { switch (level) { - case LOG_LEVEL_TRACE: - return "TRACE"; - case LOG_LEVEL_DEBUG: - return ("DEBUG"); - case LOG_LEVEL_INFO: - return "INFO"; - case LOG_LEVEL_WARN: - return CONFIG_PARAMS.warnLevelString; - case LOG_LEVEL_ERROR: - return "ERROR"; + case LOG_LEVEL_TRACE: + return "TRACE"; + case LOG_LEVEL_DEBUG: + return "DEBUG"; + case LOG_LEVEL_INFO: + return "INFO"; + case LOG_LEVEL_WARN: + return CONFIG_PARAMS.warnLevelString; + case LOG_LEVEL_ERROR: + return "ERROR"; + default: + throw new IllegalStateException("Unrecognized level [" + level + "]"); } - throw new IllegalStateException("Unrecognized level [" + level + "]"); } void write(StringBuilder buf, Throwable t) { @@ -292,8 +298,7 @@ private void formatAndLog(int level, String format, Object... arguments) { /** * Is the given log level currently enabled? * - * @param logLevel - * is this level enabled? + * @param logLevel is this level enabled? */ protected boolean isLevelEnabled(int logLevel) { // log level are numerically ordered so can use simple numeric @@ -301,7 +306,9 @@ protected boolean isLevelEnabled(int logLevel) { return (logLevel >= currentLogLevel); } - /** Are {@code trace} messages currently enabled? */ + /** + * Are {@code trace} messages currently enabled? + */ public boolean isTraceEnabled() { return isLevelEnabled(LOG_LEVEL_TRACE); } @@ -338,12 +345,16 @@ public void trace(String format, Object... argArray) { formatAndLog(LOG_LEVEL_TRACE, format, argArray); } - /** Log a message of level TRACE, including an exception. */ + /** + * Log a message of level TRACE, including an exception. + */ public void trace(String msg, Throwable t) { log(LOG_LEVEL_TRACE, msg, t); } - /** Are {@code debug} messages currently enabled? */ + /** + * Are {@code debug} messages currently enabled? + */ public boolean isDebugEnabled() { return isLevelEnabled(LOG_LEVEL_DEBUG); } @@ -380,12 +391,16 @@ public void debug(String format, Object... argArray) { formatAndLog(LOG_LEVEL_DEBUG, format, argArray); } - /** Log a message of level DEBUG, including an exception. */ + /** + * Log a message of level DEBUG, including an exception. + */ public void debug(String msg, Throwable t) { log(LOG_LEVEL_DEBUG, msg, t); } - /** Are {@code info} messages currently enabled? */ + /** + * Are {@code info} messages currently enabled? + */ public boolean isInfoEnabled() { return isLevelEnabled(LOG_LEVEL_INFO); } @@ -422,12 +437,16 @@ public void info(String format, Object... argArray) { formatAndLog(LOG_LEVEL_INFO, format, argArray); } - /** Log a message of level INFO, including an exception. */ + /** + * Log a message of level INFO, including an exception. + */ public void info(String msg, Throwable t) { log(LOG_LEVEL_INFO, msg, t); } - /** Are {@code warn} messages currently enabled? */ + /** + * Are {@code warn} messages currently enabled? + */ public boolean isWarnEnabled() { return isLevelEnabled(LOG_LEVEL_WARN); } @@ -464,12 +483,16 @@ public void warn(String format, Object... argArray) { formatAndLog(LOG_LEVEL_WARN, format, argArray); } - /** Log a message of level WARN, including an exception. */ + /** + * Log a message of level WARN, including an exception. + */ public void warn(String msg, Throwable t) { log(LOG_LEVEL_WARN, msg, t); } - /** Are {@code error} messages currently enabled? */ + /** + * Are {@code error} messages currently enabled? + */ public boolean isErrorEnabled() { return isLevelEnabled(LOG_LEVEL_ERROR); } @@ -506,7 +529,9 @@ public void error(String format, Object... argArray) { formatAndLog(LOG_LEVEL_ERROR, format, argArray); } - /** Log a message of level ERROR, including an exception. */ + /** + * Log a message of level ERROR, including an exception. + */ public void error(String msg, Throwable t) { log(LOG_LEVEL_ERROR, msg, t); } diff --git a/logger-slf4j/src/main/java/org/slf4j/impl/SimpleLoggerConfiguration.java b/logger-slf4j/src/main/java/org/slf4j/impl/SimpleLoggerConfiguration.java index 35f12dca..447d0dbd 100644 --- a/logger-slf4j/src/main/java/org/slf4j/impl/SimpleLoggerConfiguration.java +++ b/logger-slf4j/src/main/java/org/slf4j/impl/SimpleLoggerConfiguration.java @@ -12,8 +12,6 @@ import java.text.SimpleDateFormat; import java.util.Properties; -import org.slf4j.helpers.Reporter; -import org.slf4j.helpers.Util; import org.slf4j.impl.OutputChoice.OutputChoiceType; /** @@ -36,7 +34,7 @@ public class SimpleLoggerConfiguration { private static final String CONFIGURATION_FILE = "simplelogger.properties"; /** Default level used when no property is set (INFO). */ - static int DEFAULT_LOG_LEVEL_DEFAULT = SimpleLogger.LOG_LEVEL_INFO; + static final int DEFAULT_LOG_LEVEL_DEFAULT = SimpleLogger.LOG_LEVEL_INFO; /** * Runtime default log level as configured by * {@code org.slf4j.simpleLogger.defaultLogLevel}. @@ -62,7 +60,7 @@ public class SimpleLoggerConfiguration { boolean showThreadName = SHOW_THREAD_NAME_DEFAULT; /** Default for {@code org.slf4j.simpleLogger.showLogName} (true). */ - final static boolean SHOW_LOG_NAME_DEFAULT = true; + static final boolean SHOW_LOG_NAME_DEFAULT = true; /** Whether the logger name should appear in output. */ boolean showLogName = SHOW_LOG_NAME_DEFAULT; @@ -77,7 +75,7 @@ public class SimpleLoggerConfiguration { boolean levelInBrackets = LEVEL_IN_BRACKETS_DEFAULT; /** Default for {@code org.slf4j.simpleLogger.logFile} (System.err). */ - private static String LOG_FILE_DEFAULT = "System.err"; + private static final String LOG_FILE_DEFAULT = "System.err"; /** Destination for log output. */ private String logFile = LOG_FILE_DEFAULT; /** Actual output target decided after initialisation. */ @@ -85,11 +83,6 @@ public class SimpleLoggerConfiguration { /** Default for {@code org.slf4j.simpleLogger.cacheOutputStream} (false). */ private static final boolean CACHE_OUTPUT_STREAM_DEFAULT = false; - /** - * When true the {@code System.out/err} stream is cached on start up rather - * than looked up for every log entry. - */ - private boolean cacheOutputStream = CACHE_OUTPUT_STREAM_DEFAULT; /** Default text used for the WARN level. */ private static final String WARN_LEVELS_STRING_DEFAULT = "WARN"; @@ -116,14 +109,19 @@ void init() { logFile = getStringProperty(SimpleLogger.LOG_FILE_KEY, logFile); - cacheOutputStream = getBooleanProperty(SimpleLogger.CACHE_OUTPUT_STREAM_STRING_KEY, CACHE_OUTPUT_STREAM_DEFAULT); + /* + * When true the {@code System.out/err} stream is cached on start up rather + * than looked up for every log entry. + */ + boolean cacheOutputStream = getBooleanProperty(SimpleLogger.CACHE_OUTPUT_STREAM_STRING_KEY, CACHE_OUTPUT_STREAM_DEFAULT); outputChoice = computeOutputChoice(logFile, cacheOutputStream); if (dateTimeFormatStr != null) { try { dateFormatter = new SimpleDateFormat(dateTimeFormatStr); } catch (IllegalArgumentException e) { - Reporter.error("Bad date format in " + CONFIGURATION_FILE + "; will output relative time", e); + System.err.println("Bad date format in " + CONFIGURATION_FILE + "; will output relative time"); + e.printStackTrace(); } } } @@ -169,7 +167,7 @@ String getStringProperty(String name) { try { prop = System.getProperty(name); } catch (SecurityException e) { - ; // none // Ignore + // none // Ignore } return (prop == null) ? properties.getProperty(name) : prop; } @@ -209,7 +207,8 @@ else if ("System.out".equalsIgnoreCase(logFile)) { PrintStream printStream = new PrintStream(fos); return new OutputChoice(printStream); } catch (FileNotFoundException e) { - Reporter.error("Could not open [" + logFile + "]. Defaulting to System.err", e); + System.err.println("Could not open [" + logFile + "]. Defaulting to System.err"); + e.printStackTrace(); return new OutputChoice(OutputChoiceType.SYS_ERR); } } diff --git a/logger-slf4j/src/main/java/org/slf4j/impl/StaticLoggerBinder.java b/logger-slf4j/src/main/java/org/slf4j/impl/StaticLoggerBinder.java index 825fa7ac..43b8e156 100644 --- a/logger-slf4j/src/main/java/org/slf4j/impl/StaticLoggerBinder.java +++ b/logger-slf4j/src/main/java/org/slf4j/impl/StaticLoggerBinder.java @@ -44,7 +44,7 @@ private StaticLoggerBinder() { * * @return the StaticLoggerBinder singleton */ - public static final StaticLoggerBinder getSingleton() { + public static StaticLoggerBinder getSingleton() { return SINGLETON; } diff --git a/logger-slf4j/src/main/java/org/slf4j/impl/package-info.java b/logger-slf4j/src/main/java/org/slf4j/impl/package-info.java new file mode 100644 index 00000000..c65bc2fa --- /dev/null +++ b/logger-slf4j/src/main/java/org/slf4j/impl/package-info.java @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +/** + * SLF4J 1.7.x SPI implementation for Chronicle logging. + *

    + * This package hosts the {@code StaticLoggerBinder} and related classes + * required by the SLF4J 1.x service-provider contract. They install the + * Chronicle logger factory as the active {@code ILoggerFactory} so that + * calls via the classic {@code LoggerFactory} API are backed by Chronicle. + *

    + * These classes are loaded reflectively by SLF4J and should not normally be + * referenced directly. Their names and presence follow SLF4J expectations + * but internal details may evolve over time. + */ +package org.slf4j.impl; diff --git a/logger-slf4j/src/test/java/net/openhft/chronicle/logger/slf4j/Slf4jChronicleLoggerPerfTest.java b/logger-slf4j/src/test/java/net/openhft/chronicle/logger/slf4j/Slf4jChronicleLoggerPerfTest.java index ad66ef0c..29e2120e 100644 --- a/logger-slf4j/src/test/java/net/openhft/chronicle/logger/slf4j/Slf4jChronicleLoggerPerfTest.java +++ b/logger-slf4j/src/test/java/net/openhft/chronicle/logger/slf4j/Slf4jChronicleLoggerPerfTest.java @@ -16,7 +16,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -@Ignore +@Ignore(/*Long running performance test*/) public class Slf4jChronicleLoggerPerfTest extends Slf4jTestBase { @Before @@ -25,7 +25,7 @@ public void setUp() { "chronicle.logger.properties", "chronicle.logger.perf.properties"); - getChronicleLoggerFactory().reload(); + reloadChronicleLoggerFactory(); } @After diff --git a/logger-slf4j/src/test/java/net/openhft/chronicle/logger/slf4j/Slf4jChronicleLoggerTest.java b/logger-slf4j/src/test/java/net/openhft/chronicle/logger/slf4j/Slf4jChronicleLoggerTest.java index f8f7b389..f1f68eff 100644 --- a/logger-slf4j/src/test/java/net/openhft/chronicle/logger/slf4j/Slf4jChronicleLoggerTest.java +++ b/logger-slf4j/src/test/java/net/openhft/chronicle/logger/slf4j/Slf4jChronicleLoggerTest.java @@ -5,7 +5,6 @@ import net.openhft.chronicle.core.io.IOTools; import net.openhft.chronicle.logger.ChronicleLogLevel; -import net.openhft.chronicle.logger.DefaultChronicleLogWriter; import net.openhft.chronicle.queue.ChronicleQueue; import net.openhft.chronicle.wire.DocumentContext; import net.openhft.chronicle.wire.Wire; @@ -15,7 +14,6 @@ import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.slf4j.impl.StaticLoggerBinder; import java.io.IOException; import java.nio.file.Files; @@ -40,7 +38,7 @@ public void setUp() { "chronicle.logger.properties" ); - getChronicleLoggerFactory().reload(); + reloadChronicleLoggerFactory(); } @After @@ -181,6 +179,7 @@ public void testLogging() throws IOException { assertNull(wire); } + //noinspection LoggingPlaceholderCountMatchesArgumentCount logger.warn("Test object", new Object()); try (DocumentContext dc = tailer.readingDocument()) { diff --git a/logger-slf4j/src/test/java/net/openhft/chronicle/logger/slf4j/Slf4jTestBase.java b/logger-slf4j/src/test/java/net/openhft/chronicle/logger/slf4j/Slf4jTestBase.java index 5b385d33..852618ff 100644 --- a/logger-slf4j/src/test/java/net/openhft/chronicle/logger/slf4j/Slf4jTestBase.java +++ b/logger-slf4j/src/test/java/net/openhft/chronicle/logger/slf4j/Slf4jTestBase.java @@ -3,13 +3,13 @@ */ package net.openhft.chronicle.logger.slf4j; -import net.openhft.chronicle.core.OS; -import net.openhft.chronicle.core.util.Time; import net.openhft.chronicle.logger.ChronicleLogLevel; +import net.openhft.chronicle.logger.ChronicleLoggerFactoryControl; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.slf4j.impl.StaticLoggerBinder; + +import java.nio.file.FileSystems; class Slf4jTestBase { @@ -17,7 +17,7 @@ class Slf4jTestBase { static String basePath() { String path = System.getProperty("java.io.tmpdir"); - String sep = System.getProperty("file.separator"); + String sep = FileSystems.getDefault().getSeparator(); if (!path.endsWith(sep)) { path += sep; @@ -28,7 +28,7 @@ static String basePath() { static String basePath(String loggerName) { return basePath() - + System.getProperty("file.separator") + + FileSystems.getDefault().getSeparator() + loggerName; } @@ -64,22 +64,47 @@ static void warmup(Logger logger) { } } + void reloadChronicleLoggerFactory() { + Object factory = getChronicleLoggerFactory(); + if (factory instanceof ChronicleLoggerFactoryControl) { + ((ChronicleLoggerFactoryControl) factory).reload(); + return; + } + throw new IllegalStateException("Unsupported ChronicleLoggerFactory: " + factory.getClass()); + } + /** * @return the ChronicleLoggerFactory singleton + * Works with both SLF4J 1.x (StaticLoggerBinder) and 2.x (ServiceProvider) */ - ChronicleLoggerFactory getChronicleLoggerFactory() { - return (ChronicleLoggerFactory) StaticLoggerBinder.getSingleton().getLoggerFactory(); + Object getChronicleLoggerFactory() { + try { + // Try SLF4J 2.x approach first (ServiceProvider) + Class providerClass = Class.forName("org.slf4j.impl.ChronicleServiceProvider"); + Object provider = providerClass.getDeclaredConstructor().newInstance(); + providerClass.getMethod("initialize").invoke(provider); + return providerClass.getMethod("getLoggerFactory").invoke(provider); + } catch (Exception e) { + // Fall back to SLF4J 1.x approach (StaticLoggerBinder) + try { + Class binderClass = Class.forName("org.slf4j.impl.StaticLoggerBinder"); + Object binder = binderClass.getMethod("getSingleton").invoke(null); + return binderClass.getMethod("getLoggerFactory").invoke(binder); + } catch (Exception ex) { + throw new RuntimeException("Unable to get ChronicleLoggerFactory via SLF4J 1.x or 2.x", ex); + } + } } - protected final class RunnableLogger implements Runnable { + protected static final class RunnableLogger implements Runnable { private final Logger logger; private final int runs; private final String fmt; - private final String fmtBase = " > val1={}, val2={}, val3={}"; public RunnableLogger(int runs, int pad, String loggerName) { this.logger = LoggerFactory.getLogger(loggerName); this.runs = runs; + String fmtBase = " > val1={}, val2={}, val3={}"; this.fmt = StringUtils.rightPad(fmtBase, pad + fmtBase.length() - (4 + 8 + 8), "X"); } diff --git a/logger-tools/src/main/java/net/openhft/chronicle/logger/tools/ChronicleLogReader.java b/logger-tools/src/main/java/net/openhft/chronicle/logger/tools/ChronicleLogReader.java index 8aeece64..ec37d8fd 100644 --- a/logger-tools/src/main/java/net/openhft/chronicle/logger/tools/ChronicleLogReader.java +++ b/logger-tools/src/main/java/net/openhft/chronicle/logger/tools/ChronicleLogReader.java @@ -78,7 +78,7 @@ public static void printf( threadName, loggerName, message, - throwable.toString()); + throwable); } } @@ -97,8 +97,9 @@ public void processLogs(@NotNull ChronicleLogProcessor processor, boolean waitFo if (waitForIt) { try { Thread.sleep(50L); - } catch (InterruptedException ignored) { - + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; } continue; } else { @@ -127,7 +128,7 @@ public void processLogs(@NotNull ChronicleLogProcessor processor, boolean waitFo valueIn.skipValue(); } } - Object[] args = argsL.toArray(new Object[argsL.size()]); + Object[] args = argsL.toArray(new Object[0]); processor.process(timestamp, level, threadName, loggerName, message, th, args); } } diff --git a/logger-tools/src/main/java/net/openhft/chronicle/logger/tools/package-info.java b/logger-tools/src/main/java/net/openhft/chronicle/logger/tools/package-info.java new file mode 100644 index 00000000..19c6beaf --- /dev/null +++ b/logger-tools/src/main/java/net/openhft/chronicle/logger/tools/package-info.java @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +/** + * Command-line tools for working with Chronicle logs. + *

    + * Utilities in this package provide simple inspection and processing of + * Chronicle log directories, including tailing, printing and transforming + * stored log events. They are intended for operational use and ad-hoc + * diagnostics rather than as embedded libraries. + *

    + * Tools rely on Chronicle Queue and Chronicle Wire for storage and + * serialisation. They assume exclusive access to the target directory when + * performing destructive operations. + */ +package net.openhft.chronicle.logger.tools; diff --git a/pom.xml b/pom.xml index 86d68543..e91642df 100644 --- a/pom.xml +++ b/pom.xml @@ -109,7 +109,7 @@ logger-core - logger-jul + logger-jcl logger-slf4j logger-slf4j-2 diff --git a/src/main/docs/architecture-overview.adoc b/src/main/docs/architecture-overview.adoc index d3709ce1..872c38dd 100644 --- a/src/main/docs/architecture-overview.adoc +++ b/src/main/docs/architecture-overview.adoc @@ -1,6 +1,7 @@ = Chronicle Logger Architecture Overview :toc: :lang: en-GB +:source-highlighter: rouge == Purpose @@ -13,7 +14,7 @@ The repository is split into focused modules that all depend on `logger-core`: * `logger-core` supplies `ChronicleLogWriter`, `ChronicleLogManager`, and configuration helpers (see link:project-requirements.adoc#clg-fn-001[CLG-FN-001]). -* Binding modules (`logger-slf4j`, `logger-slf4j-2`, `logger-logback`, +* Binding modules (`logger-slf4j` for SLF4J 1.7.x, `logger-slf4j-2` for SLF4J 2.x ServiceProvider, `logger-logback`, `logger-log4j-1`, `logger-log4j-2`, `logger-jcl`, `logger-jul`) translate framework-specific APIs into Chronicle writes (link:project-requirements.adoc#clg-fn-003[CLG-FN-003]). * `logger-tools` exposes reusable readers and CLI utilities (link:project-requirements.adoc#clg-fn-004[CLG-FN-004]). * `benchmark` packages JMH scenarios that observe the performance envelopes (link:project-requirements.adoc#clg-fn-005[CLG-FN-005]). @@ -32,11 +33,14 @@ link:decision-log.adoc#clg-ops-002[CLG-OPS-002]). == Chronicle Queue Write Path -1. Framework binding receives a log call and checks the requested level against its cached minimum. -2. When enabled, the binding forwards the event to `ChronicleLogWriter`, supplying timestamp, thread name, logger name, message pattern, optional throwable, and argument array (link:project-requirements.adoc#clg-fn-001[CLG-FN-001]). -3. `ChronicleLogWriter` serialises the data into a Chronicle Queue using the configured wire type, avoiding heap allocation on the hot path (link:project-requirements.adoc#clg-nf-p-001[CLG-NF-P-001]). +. Framework binding receives a log call and checks the requested level against its cached minimum. +. For SLF4J 2.x, discovery is via `SLF4JServiceProvider`; SLF4J 1.x bindings expose `StaticLoggerBinder`. +Legacy SLF4J callers can be bridged to Log4j1 via `slf4j-reload4j` and to Log4j2 via `log4j-slf4j2-impl`, keeping old code paths functional while defaulting new usage to `logger-slf4j-2`. +. When enabled, the binding forwards the event to `ChronicleLogWriter`, supplying timestamp, thread name, logger name, message pattern, optional throwable, and argument array (link:project-requirements.adoc#clg-fn-001[CLG-FN-001]). +. `ChronicleLogWriter` serialises the data into a Chronicle Queue using the configured wire type, avoiding heap allocation on the hot path (link:project-requirements.adoc#clg-nf-p-001[CLG-NF-P-001]). -Tailers and readers consume the queue either within the same JVM or from external processes. `ChronicleLogReader` provides a stable record schema and zero-garbage consumption (link:project-requirements.adoc#clg-nf-p-003[CLG-NF-P-003]). +Tailers and readers consume the queue either within the same JVM or from external processes. +`ChronicleLogReader` provides a stable record schema and zero-garbage consumption (link:project-requirements.adoc#clg-nf-p-003[CLG-NF-P-003]). == Tooling and Observability diff --git a/src/main/docs/code-review-playbook.adoc b/src/main/docs/code-review-playbook.adoc index 82a54c67..3357a4f6 100644 --- a/src/main/docs/code-review-playbook.adoc +++ b/src/main/docs/code-review-playbook.adoc @@ -1,10 +1,12 @@ = Chronicle Logger Code Review Playbook +:toc: :lang: en-GB +:source-highlighter: rouge == Purpose This playbook lists the static-analysis and coverage checks executed by the -`code-review` Maven profile so reviewers can reproduce the pipeline locally on the project’s supported JDKs. +`code-review` Maven profile so reviewers can reproduce the pipeline locally on the project's supported JDKs. == Supported JDKs diff --git a/src/main/docs/decision-log.adoc b/src/main/docs/decision-log.adoc index f91ae71f..c0f2a28c 100644 --- a/src/main/docs/decision-log.adoc +++ b/src/main/docs/decision-log.adoc @@ -1,4 +1,21 @@ -=== CLG-DOC-001 Adopt Nine-Box Requirements Catalogue += Chronicle Logger - Decision Log +:toc: +:sectnums: +:lang: en-GB +:source-highlighter: rouge + +This file captures component-specific architectural and operational decisions for the Chronicle Logger module. +Identifiers follow the `--NNN` pattern with scope `CLG` (Chronicle Logger). + +== Decision Index + +* link:#CLG-DOC-001[CLG-DOC-001 Adopt Nine-Box Requirements Catalogue] +* link:#CLG-OPS-002[CLG-OPS-002 Enforce Explicit Queue Path Configuration] +* link:#CLG-NF-P-003[CLG-NF-P-003 Chronicle Log Reader Allocation Budget] +* link:#CLG-FN-006[CLG-FN-006 Default to SLF4J 2.x with Legacy Bridges] + +[[CLG-DOC-001]] +== CLG-DOC-001 Adopt Nine-Box Requirements Catalogue Date:: 2025-10-17 Context:: @@ -7,8 +24,8 @@ Context:: Decision Statement:: * Chronicle Logger adopts the Nine-Box schema (FN, NF-P, NF-S, NF-O, DOC, OPS, TEST, RISK) with scope prefix `CLG` for all requirements and decisions. Alternatives Considered:: -* Free-form numbering – low effort, but tooling and audits cannot map entries consistently. -* Per-module numbering – splits visibility across bindings and complicates shared obligations. +* Free-form numbering - low effort, but tooling and audits cannot map entries consistently. +* Per-module numbering - splits visibility across bindings and complicates shared obligations. Rationale for Decision:: * Aligns Chronicle Logger with company-wide guidance and enables cross-linking with test and benchmark artefacts. * Simplifies audit checks for requirement coverage. @@ -17,7 +34,8 @@ Impact & Consequences:: Notes/Links:: * link:project-requirements.adoc[Requirements Catalogue] -=== CLG-OPS-002 Enforce Explicit Queue Path Configuration +[[CLG-OPS-002]] +== CLG-OPS-002 Enforce Explicit Queue Path Configuration Date:: 2025-10-17 Context:: @@ -26,8 +44,8 @@ Context:: Decision Statement:: * All bindings must require an explicit Chronicle Queue path from configuration and fail fast with a warning if the directory is absent or unwritable. Alternatives Considered:: -* Auto-create directories – masks configuration errors and may violate deployment policies. -* Fallback to in-memory logging – deviates from Chronicle Logger’s persistence contract. +* Auto-create directories - masks configuration errors and may violate deployment policies. +* Fallback to in-memory logging - deviates from Chronicle Logger's persistence contract. Rationale for Decision:: * Prevents silent data loss, aligns with operational expectations in regulated environments. Impact & Consequences:: @@ -36,7 +54,8 @@ Impact & Consequences:: Notes/Links:: * link:project-requirements.adoc#clg-nf-o-002[CLG-NF-O-002] -=== CLG-NF-P-003 Chronicle Log Reader Allocation Budget +[[CLG-NF-P-003]] +== CLG-NF-P-003 Chronicle Log Reader Allocation Budget Date:: 2025-10-17 Context:: @@ -44,10 +63,34 @@ Context:: Decision Statement:: * `ChronicleLogReader` must remain allocation-neutral during steady-state reads and expose benchmarks that enforce the constraint. Alternatives Considered:: -* Accept sporadic allocations – risks latency regressions and contradicts Chronicle low-GC ethos. +* Accept sporadic allocations - risks latency regressions and contradicts Chronicle low-GC ethos. Rationale for Decision:: * Sustains deterministic behaviour for monitoring tools and aligns with performance requirements. Impact & Consequences:: * Reader implementation audited; benchmarks now gate memory usage. Notes/Links:: * link:project-requirements.adoc#clg-nf-p-003[CLG-NF-P-003] + +[[CLG-FN-006]] +== CLG-FN-006 Default to SLF4J 2.x with Legacy Bridges + +Date:: 2025-11-20 +Context:: +* SLF4J 2.x replaces `StaticLoggerBinder` discovery with `SLF4JServiceProvider`. +* Chronicle Logger ships both `logger-slf4j` (SLF4J 1.x) and `logger-slf4j-2` (SLF4J 2.x) along with Log4j 1/2 bindings. +* Existing applications and tests call SLF4J APIs and expect Log4j appenders; migrating code immediately is not always feasible. +Decision Statement:: +* Make SLF4J 2.x the default API and provider via `logger-slf4j-2`. +* Preserve SLF4J 1.x support via the legacy binding and documented bridges (`slf4j-reload4j` for Log4J 1.x, Log4J 2 SLF4J binding for Log4j2), so unchanged SLF4J callers continue to reach Chronicle appenders. +Alternatives Considered:: +* Drop SLF4J 1.x outright - simplest surface area but breaks existing deployments. +* Maintain a dual-purpose `logger-slf4j` with both binder and provider - increases user confusion and classpath conflicts. +Rationale for Decision:: +* Aligns with upstream SLF4J direction while offering a migration path for legacy code. +* Reduces classpath conflict risk by keeping bindings separate and bridges explicit. +Impact & Consequences:: +* Tests now include SLF4J provider health checks and bridge coverage. +* Documentation must warn against mixing API/provider versions and point to the correct module per SLF4J generation. +Notes/Links:: +* link:project-requirements.adoc#clg-fn-003[CLG-FN-003] +* link:architecture-overview.adoc[Architecture Overview] diff --git a/src/main/docs/functional-requirements.adoc b/src/main/docs/functional-requirements.adoc new file mode 100644 index 00000000..b3dd89c2 --- /dev/null +++ b/src/main/docs/functional-requirements.adoc @@ -0,0 +1,36 @@ += Chronicle Logger - Functional Requirements +:toc: +:lang: en-GB +:source-highlighter: rouge + +== Purpose + +This document summarises the core functional behaviours for Chronicle Logger and its bindings. +It organises the `CLG-FN-*` entries from `project-requirements.adoc` into domains and links them to verification and supporting documentation. +The full catalogue, including performance, security, test, documentation and operational requirements, remains in `project-requirements.adoc`. + +== Domain Overview + +[cols="1,3,3",options="header"] +|=== +| Tag range | Domain | Notes +| CLG-FN-001 .. CLG-FN-005 | Core logging API, bindings, tools and benchmarks | Chronicle Queue-backed logging, framework integration and measurement. +|=== + +== Core Logging and Binding Behaviour (CLG-FN-001 .. CLG-FN-005) + +[cols="1,4,3",options="header"] +|=== +| ID | Requirement (summary) | Verification and references +| CLG-FN-001 | `ChronicleLogWriter` shall persist log events with timestamp, level, thread name, logger name, message pattern, throwable and arguments without allocating on the steady-state hot path. | Core writer behaviour is exercised by `logger-core` tests such as `DefaultChronicleLogWriterTest` together with binding tests (for example `ChronicleLoggerBehaviourTest`, `Slf4jChronicleLoggerTest` and `Slf4jChronicleLoggerPerfTest`) that round-trip events via `ChronicleLogReader`; allocation characteristics are checked via benchmarks in the `benchmark` module as described in `project-requirements.adoc`. +| CLG-FN-002 | `ChronicleLogManager` shall load configuration from the `chronicle.logger.properties` system property or class-path defaults, supporting per-logger overrides for path, level, wire type and append mode. | Configuration flow is covered by tests such as `ChronicleLoggingConfigTest` (SLF4J bindings), `ChronicleLoggingConfigTest` under `logger-slf4j-2`, and the various `*ConfigurationTest` classes, which supply temporary properties files or framework-specific configuration and assert resolved settings; the overall configuration path is documented in `architecture-overview.adoc`. +| CLG-FN-003 | Each binding (SLF4J 1.x, SLF4J 2.x, JUL, JCL, Logback, Log4J 1 and Log4J 2) shall honour its native configuration entry points while delegating writes through `ChronicleLogWriter`. | Framework-specific tests such as `Slf4jChronicleLoggerTest`, `JulLoggerChronicleTest`, `JclChronicleLoggerTest`, `LogbackChronicleBinaryAppenderTest`, `LogbackChronicleProgrammaticConfigTest`, `Log4j1ChronicleLogTest` and `Log4j2BinaryTest` configure Chronicle appenders using each technology's standard mechanisms and verify Chronicle Queue output; binding responsibilities are summarised in `architecture-overview.adoc`. +| CLG-FN-004 | Logger tools (`ChroniCat`, `ChroniTail`, `ChronicleLogReader`) shall read binary Chronicle logs, display tail output and stream entries to custom processors without data loss. | `logger-tools` tests such as `ChronicleLogReaderTest` and `ChronicleCliToolsTest` replay fixture queues, assert correct decoding of structured events and verify that processor callbacks receive ordered entries; tool behaviour is described in `project-requirements.adoc` and user-facing documentation. +| CLG-FN-005 | The benchmark module shall provide repeatable JMH suites covering append throughput and tail latency for key bindings with configurable message sizes. | Performance behaviour is validated by JMH benchmarks in the `benchmark` module together with binding-specific performance tests such as `Slf4jChronicleLoggerPerfTest` and its SLF4J 2.x equivalent; these scenarios are used to enforce `CLG-NF-P-*` targets from `project-requirements.adoc`. +|=== + +== Traceability + +* `src/main/docs/project-requirements.adoc` remains the authoritative source for all `CLG-*` requirements, including non-functional and operational entries. +* Design decisions in `src/main/docs/decision-log.adoc` (for example `CLG-DOC-001`, `CLG-OPS-002`, `CLG-NF-P-003`) explain the rationale behind the functional contracts listed here. +* Tests and benchmarks in the logger modules should reference the appropriate `CLG-FN-*` identifiers to make coverage and performance obligations easy to audit; the test classes named in the table above serve as canonical examples for each requirement. diff --git a/src/main/docs/project-requirements.adoc b/src/main/docs/project-requirements.adoc index 5ef6d7e0..957b4b09 100644 --- a/src/main/docs/project-requirements.adoc +++ b/src/main/docs/project-requirements.adoc @@ -1,20 +1,23 @@ = Chronicle Logger Requirements Catalogue :toc: :lang: en-GB +:source-highlighter: rouge // ------------------------------------------------------------------- // Legend // ------------------------------------------------------------------- // Requirement identifiers follow the Nine-Box taxonomy: -// --NNN, where Scope = CLG (Chronicle Logger) -// Tag = FN | NF-P | NF-S | NF-O | DOC | OPS | TEST | RISK -// NNN = zero-padded sequence per tag. +// --NNN, where Scope = CLG (Chronicle Logger) +// Tag = FN | NF-P | NF-S | NF-O | DOC | OPS | TEST | RISK +// NNN = zero-padded sequence per tag. // Text must remain ASCII per AGENTS.md guidance. // ------------------------------------------------------------------- == Scope Chronicle Logger provides Chronicle Queue backed appenders, bridges, and tools for popular Java logging frameworks (SLF4J 1.x and 2.x, Logback, Log4J 1/2, Commons Logging, JUL). +SLF4J 2.x is the default API; SLF4J 1.x remains supported via the legacy binding and explicit bridges (for example `slf4j-reload4j` for Log4J 1.x). +Mixing API/provider versions is unsupported and must be prevented in tests and documentation. The requirements below define the contracts, quality targets, and operational obligations that all modules must meet. == Functional Requirements @@ -32,8 +35,8 @@ supporting per-logger overrides for path, level, wire type, and append mode. |In tests supply temp properties files and validate resolved settings per logger. |CLG-FN-003 |Each binding (SLF4J 1.x, SLF4J 2.x, JUL, JCL, Logback, Log4J 1, Log4J 2) shall honour its native configuration entry points while delegating writes -through `ChronicleLogWriter`. |Framework-specific tests assert that configuration -files enable Chronicle appenders and produce Chronicle Queue output. +through `ChronicleLogWriter`. Legacy SLF4J call sites shall be serviceable via documented bridges while `logger-slf4j-2` remains the default provider. |Framework-specific tests assert that configuration +files enable Chronicle appenders and produce Chronicle Queue output; bridge tests assert SLF4J callers still reach Chronicle appenders. |CLG-FN-004 |Logger tools (`ChroniCat`, `ChroniTail`, `ChronicleLogReader`) shall read binary Chronicle logs, display tail output, and stream entries to custom processors without data loss. |CLI smoke tests replay fixture queues; unit tests @@ -44,24 +47,24 @@ benchmark job executes weekly and publishes trend reports; spot checks confirm n regressions before releases. |=== -== Non-Functional Requirements – Performance +== Non-Functional Requirements - Performance [cols="1,5,4",options="header"] |=== |ID |Requirement |Benchmark Target |CLG-NF-P-001 |Steady-state append of INFO level messages through SLF4J 1.x binding -shall achieve ≥ 1.2 million events per second on a 3.2 GHz x86-64 host with +shall achieve >= 1.2 million events per second on a 3.2 GHz x86-64 host with Chronicle Queue BINARY_LIGHT. |JMH `Throughput` benchmark in `benchmark` module; flagged if throughput drops by > 10 percent relative to baseline. |CLG-NF-P-002 |Tail latency (99.9 percentile) for append operations with async -queue roll shall remain ≤ 750 microseconds under mixed INFO/WARN traffic. |Benchmark +queue roll shall remain <= 750 µs under mixed INFO/WARN traffic. |Benchmark module scenario combining timed roll cycle and mixed levels. -|CLG-NF-P-003 |`ChronicleLogReader` shall consume existing queues at ≥ 500k events +|link:decision-log.adoc#CLG-NF-P-003[CLG-NF-P-003] |`ChronicleLogReader` shall consume existing queues at >= 500k events per second while remaining allocation-neutral. |Micro benchmark comparing reader performance to baseline sample queues. |=== -== Non-Functional Requirements – Security and Operability +== Non-Functional Requirements - Security and Operability [cols="1,5,4",options="header"] |=== @@ -73,7 +76,7 @@ message content is not evaluated. |CLG-NF-O-001 |All modules must support JDK 8 through the current LTS release, with automated builds running `mvn -q verify` on each supported JDK. |CI matrix build status; release checklist captures JDK versions. -|CLG-NF-O-002 |Components shall emit operational warnings via standard logging +|link:decision-log.adoc#CLG-OPS-002[CLG-NF-O-002] |Components shall emit operational warnings via standard logging channels when Chronicle Queue paths are missing or unwritable, and fail fast rather than silently downgrading. |Negative-path tests create read-only directories and assert warning plus exception semantics. @@ -88,7 +91,7 @@ exercise concurrent reloads with leak detectors enabled. [cols="1,5",options="header"] |=== |ID |Requirement -|CLG-TEST-001 |Maintain ≥ 80 percent line coverage for `logger-core` and each +|CLG-TEST-001 |Maintain >= 80 percent line coverage for `logger-core` and each binding package, excluding generated or vendor code. Jacoco gate enforced in CI. |CLG-TEST-002 |Provide regression fixtures per binding that append structured @@ -103,7 +106,7 @@ failing the pipeline if throughput or latency deviates beyond documented thresho [cols="1,5",options="header"] |=== |ID |Requirement -|CLG-DOC-001 |Keep `architecture-overview.adoc` aligned with current module +|link:decision-log.adoc#CLG-DOC-001[CLG-DOC-001] |Keep `architecture-overview.adoc` aligned with current module structure, configuration flow, and decision references. |CLG-DOC-002 |Update this requirements catalogue and cross-link new entries when decisions in `decision-log.adoc` change behaviour or support matrices. diff --git a/src/main/docs/testing-strategy.adoc b/src/main/docs/testing-strategy.adoc new file mode 100644 index 00000000..0d3f5445 --- /dev/null +++ b/src/main/docs/testing-strategy.adoc @@ -0,0 +1,59 @@ += Chronicle Logger Testing Strategy +:toc: +:sectnums: +:lang: en-GB +:source-highlighter: rouge + +This document summarises how Chronicle Logger and its bindings are tested. +It complements the `CLG-*` requirements in `project-requirements.adoc` and the behavioural summary in `functional-requirements.adoc`. + +== Objectives + +Testing for Chronicle Logger aims to: + +* verify the functional requirements (`CLG-FN-*`) for logger-core, bindings and tools; +* exercise non-functional requirements (`CLG-NF-P-*`, `CLG-NF-O-*`) around throughput, latency and fail-fast behaviour; +* ensure integrations with Chronicle Queue remain stable across supported JDKs and dependency upgrades. + +== Unit and Integration Tests + +Unit and integration tests live in the individual module subprojects: + +* `logger-core` – writer and manager behaviour (for example `DefaultChronicleLogWriterTest`); +* `logger-slf4j` and `logger-slf4j-2` – SLF4J bindings (for example `ChronicleLoggerBehaviourTest`, `ChronicleLoggingConfigTest`, `Slf4jChronicleLoggerTest`), plus SLF4J provider health checks to fail fast on missing or conflicting providers; +* `logger-logback`, `logger-log4j-1`, `logger-log4j-2` and `logger-jul`, `logger-jcl` – framework-specific bindings and configuration tests (for example `LogbackChronicleBinaryAppenderTest`, `LogbackChronicleProgrammaticConfigTest`, `Log4j1ChronicleLogTest`, `Slf4jBridgeChronicleLogTest`, `Log4j2BinaryTest`, `JulHandlerChronicleTest`); +* `logger-tools` – CLI tools tests (for example `ChronicleLogReaderTest`, `ChronicleCliToolsTest`). + +These tests cover: + +* round-tripping structured log events through Chronicle Queue; +* configuration resolution from system properties and framework configuration files; +* basic error handling when queue paths are missing or unwritable. + +== Benchmark and Performance Tests + +Performance and latency requirements (`CLG-NF-P-*`) are exercised primarily via the `benchmark` module and binding-specific performance tests such as: + +* `Slf4jChronicleLoggerPerfTest` and its SLF4J 2.x equivalent; +* any JMH-based benchmarks defined under the `benchmark` module. + +These benchmarks measure: + +* steady-state append throughput for representative bindings; +* tail latency under mixed log levels and roll-cycle configurations. + +Results are used to detect regressions before releases and to validate targets documented in `project-requirements.adoc`. + +== Traceability + +`project-requirements.adoc` and `functional-requirements.adoc` describe the `CLG-*` requirements and reference test and benchmark classes. +When new behaviours are added or existing ones change, keep the following in sync: + +* add or update `CLG-*` entries in `project-requirements.adoc`; +* extend `functional-requirements.adoc` with the relevant test classes; +* add or update unit, integration or benchmark tests in the appropriate submodule. + +Legacy SLF4J call sites are exercised via bridges (`slf4j-reload4j` for SLF4J→Log4j1, and Log4j2’s SLF4J binding for SLF4J→Log4j2) to keep unchanged code paths working while SLF4J 2.x is the default. +Document the active bridge in each suite to avoid provider ambiguity. + +All tests should pass under the shared quality profiles (`mvn -P quality clean verify`) on the supported JDK matrix before shipping changes. diff --git a/system.properties b/system.properties index 507a5aa9..edbd4ba5 100644 --- a/system.properties +++ b/system.properties @@ -1 +1,3 @@ jvm.resource.tracing=true + +chronicle.disk.monitor.disable=true