join JNA-based log4j appender into this bridj-based consumer implementation, update to Gradle 6.4.1, Log4j 2.13.3

This commit is contained in:
Jörg Prante 2020-06-08 11:14:54 +02:00
parent 422641ab1d
commit 6927d31b70
30 changed files with 975 additions and 159 deletions

157
README.md Normal file
View file

@ -0,0 +1,157 @@
# Systemd journal for Java
[![Build Status](https://travis-ci.org/jprante/systemd-journal-appender.png?branch=master)](https://travis-ci.org/jprante/systemd-journal)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.xbib/log4j-systemd-journal/badge.svg)](http://maven-badges.herokuapp.com/maven-central/org.xbib/log4j-systemd-journal)
[![Apache License](https://img.shields.io/github/license/jprante/log4j-systemd-journal.svg)](https://opensource.org/licenses/Apache-2.0)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/JoergPrante)
## Reading systemd-journal from Java
Please see the junit test file to find out how to consume systemd journal from Java.
The implementation use bridj.
## Log4j2 systemd-journal appender
This [Log4j][log4j] appender logs event meta data such as timestamp, logger name, exception stacktrace,
[ThreadContext (MDC)][thread-context] or the thread name to [fields][systemd-journal-fields]
in [systemd journal][systemd-journal].
Learn more about systemd-journal at Lennart Poettering's site [systemd for Developers III][systemd-for-developers]
or at the manual page [systemd journal][systemd-journal].
## Usage
Add the following Maven dependency to your project:
Gradle
```
dependency {
runtime "org.xbib:log4j-systemd-journal:2.13.3.0"
}
```
### Runtime dependencies ###
- Java 11+
- Linux with systemd library installed (/usr/lib64/libsystemd.so)
- Log4j 2.12.0+
**Note:**
JNA requires execute permissions in `java.io.tmpdir` (which defaults to `/tmp`).
For example, if the folder is mounted with "`noexec`" for security reasons, you need to define a different temporary directory for JNA:
-Djna.tmpdir=/tmp-folder/with/exec/permissions
## Configuration
The appender can be configured with the following properties
Property name | Default | Type | Description
--------------------- | ----------------- | ------- | -----------
`logSource` | false | boolean | Determines whether the log locations are logged. Note that there is a performance overhead when switched on. The data is logged in standard systemd journal fields `CODE_FILE`, `CODE_LINE` and `CODE_FUNC`.
`logStacktrace` | true | boolean | Determines whether the full exception stack trace is logged. This data is logged in the user field `STACKTRACE`.
`logThreadName` | true | boolean | Determines whether the thread name is logged. This data is logged in the user field `THREAD_NAME`.
`logLoggerName` | true | boolean | Determines whether the logger name is logged. This data is logged in the user field `LOG4J_LOGGER`.
`logAppenderName` | true | boolean | Determines whether the appender name is logged. This data is logged in the user field `LOG4J_APPENDER`.
`logThreadContext` | true | boolean | Determines whether the [thread context][thread-context] is logged. Each key/value pair is logged as user field with the `threadContextPrefix` prefix.
`threadContextPrefix` | `THREAD_CONTEXT_` | String | Determines how [thread context][thread-context] keys should be prefixed when `logThreadContext` is set to true. Note that keys need to match the regex pattern `[A-Z0-9_]+` and are normalized otherwise.
`syslogIdentifier` | null | String | This data is logged in the user field `SYSLOG_IDENTIFIER`. If this is not set, the underlying system will use the command name (usually `java`) instead.
## Example ##
### `log4j2.xml`
```xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO" packages="org.xbib.log4j.systemd">
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</Console>
<SystemdJournalAppender name="journal" logStacktrace="true" logSource="false" />
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="console" />
<AppenderRef ref="journal" />
</Root>
</Loggers>
</Configuration>
```
This will tell Log4j to log to [systemd journal][systemd-journal] as well as to stdout (console).
Note that a layout is optional for `SystemdJournal`.
This is because meta data of a log event such as the timestamp, the logger name or the Java thread name are mapped to [systemd-journal fields][systemd-journal-fields] and need not be rendered into a string that loses all the semantic information.
### `YourExample.java`
```java
import org.apache.logging.log4j.*;
class YourExample {
private static Logger logger = LogManager.getLogger(YourExample.class);
public static void main(String[] args) {
ThreadContext.put("MY_KEY", "some value");
logger.info("this is an example");
}
}
```
Running this sample class will log a message to journald:
### Systemd Journal
```
# journalctl -n
Okt 13 21:26:00 myhost java[2370]: this is an example
```
Use `journalctl -o verbose` to show all fields:
```
# journalctl -o verbose -n
Di 2015-09-29 21:07:05.850017 CEST [s=45e0…;i=984;b=c257…;m=1833…;t=520e…;x=3e1e…]
PRIORITY=6
_TRANSPORT=journal
_UID=1000
_GID=1000
_CAP_EFFECTIVE=0
_SYSTEMD_OWNER_UID=1000
_SYSTEMD_SLICE=user-1000.slice
_MACHINE_ID=4abc6d…
_HOSTNAME=myhost
_SYSTEMD_CGROUP=/user.slice/user-1000.slice/session-2.scope
_SYSTEMD_SESSION=2
_SYSTEMD_UNIT=session-2.scope
_BOOT_ID=c257f8…
THREAD_NAME=main
LOG4J_LOGGER=org.xbib.log4j.systemd.SystemdJournalAppenderIntegrationTest
_COMM=java
_EXE=/usr/bin/java
MESSAGE=this is a test message with a MDC
CODE_FILE=SystemdJournalAppenderIntegrationTest.java
CODE_FUNC=testMessageWithMDC
CODE_LINE=36
THREAD_CONTEXT_MY_KEY=some value
SYSLOG_IDENTIFIER=log4j2-test
LOG4J_APPENDER=Journal
_PID=8224
_CMDLINE=/usr/bin/java …
_SOURCE_REALTIME_TIMESTAMP=1443553625850017
```
Note that the [ThreadContext][thread-context] key-value pair `{"MY_KEY": "some value"}` is automatically added as field with prefix `THREAD_CONTEXT`.
You can use the power of [systemd journal][systemd-journal] to filter for interesting messages. Example:
`journalctl CODE_FUNC=testMessageWithMDC THREAD_NAME=main` will only show messages that are logged from the Java main thread in a method called `testMessageWithMDC`.
## Bridj or JNA
As you noted, I use both bridj and JNA. But only one is necessary. The only reason for this is that it works.
bridj looks easier and more powerful, but is getting old. I am considering a fork of bridj and implement a log4j2 appender for bridj, or porting the API methods `sd_journal_open`, `sd_journal_add_match`, etc. to JNA.
Feel free to submit patches.

View file

@ -1,122 +1,34 @@
plugins { plugins {
id "org.sonarqube" version "2.6.1" id "de.marcphilipp.nexus-publish" version "0.4.0"
id "io.codearte.nexus-staging" version "0.11.0" id "io.codearte.nexus-staging" version "0.21.1"
} }
apply plugin: 'java' wrapper {
apply plugin: 'maven' gradleVersion = "${project.property('gradle.wrapper.version')}"
distributionType = Wrapper.DistributionType.ALL
dependencies {
implementation "com.nativelibs4java:bridj:${project.property('bridj.version')}"
testImplementation "org.junit.jupiter:junit-jupiter-api:${project.property('junit.version')}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${project.property('junit.version')}"
testImplementation "org.mockito:mockito-junit-jupiter:${project.property('mockito.version')}"
}
compileJava {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
compileTestJava {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
test {
enabled = true
useJUnitPlatform()
systemProperty 'jna.debug', 'true'
testLogging {
events 'PASSED', 'FAILED', 'SKIPPED'
}
afterSuite { desc, result ->
if (!desc.parent) {
println "\nTest result: ${result.resultType}"
println "Test summary: ${result.testCount} test, " +
"${result.successfulTestCount} succeeded, " +
"${result.failedTestCount} failed " +
"${result.skippedTestCount} skipped "
}
}
}
task javadocJar(type: Jar, dependsOn: classes) {
from javadoc
into "build/tmp"
classifier 'javadoc'
}
task sourcesJar(type: Jar, dependsOn: classes) {
from sourceSets.main.allSource
into "build/tmp"
classifier 'sources'
}
artifacts {
archives javadocJar, sourcesJar
} }
ext { ext {
user = 'jprante' user = 'jprante'
projectDescription = 'Systemd journal bindings' name = 'systemd-journal'
scmUrl = 'https://github.com/jprante/systemd-journal' description = 'Systemd journal bindings and logging adapters for Java'
scmConnection = 'scm:git:git://github.com/jprante/systemd-journal.git' inceptionYear = '2018'
scmDeveloperConnection = 'scm:git:git://github.com/jprante/systemd-journal.git' url = 'https://github.com/' + user + '/' + name
scmUrl = 'https://github.com/' + user + '/' + name
scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git'
scmDeveloperConnection = 'scm:git:ssh://git@github.com:' + user + '/' + name + '.git'
issueManagementSystem = 'Github'
issueManagementUrl = ext.scmUrl + '/issues'
licenseName = 'The Apache License, Version 2.0'
licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
} }
task sonaTypeUpload(type: Upload) { subprojects {
group = 'publish' apply plugin: 'java-library'
configuration = configurations.archives apply from: rootProject.file('gradle/ide/idea.gradle')
uploadDescriptor = true apply from: rootProject.file('gradle/compile/java.gradle')
repositories { apply from: rootProject.file('gradle/test/junit5.gradle')
if (project.hasProperty('ossrhUsername')) { apply from: rootProject.file('gradle/publishing/publication.gradle')
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots') {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
pom.project {
groupId project.group
artifactId project.name
version project.version
name project.name
description description
packaging 'jar'
inceptionYear '2018'
url scmUrl
organization {
name 'xbib'
url 'http://xbib.org'
}
developers {
developer {
id user
name 'Jörg Prante'
email 'joergprante@gmail.com'
url 'https://github.com/jprante'
}
}
scm {
url scmUrl
connection scmConnection
developerConnection scmDeveloperConnection
}
licenses {
license {
name 'The Apache License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
}
}
}
}
} }
nexusStaging { apply from: rootProject.file('gradle/publishing/sonatype.gradle')
packageGroup = "org.xbib"
}

View file

@ -2,7 +2,8 @@ group = org.xbib
name = systemd-journal name = systemd-journal
version = 1.0.0 version = 1.0.0
gradle.wrapper.version = 6.4.1
bridj.version = 0.7.0 bridj.version = 0.7.0
jna.version = 5.5.0
junit.version = 5.4.2 log4j.version = 2.13.3
mockito.version = 2.27.0 mockito.version = 3.3.3

View file

@ -0,0 +1,43 @@
apply plugin: 'java-library'
java {
modularity.inferModulePath.set(true)
}
compileJava {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
compileTestJava {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
jar {
manifest {
attributes('Implementation-Version': project.version)
}
}
task sourcesJar(type: Jar, dependsOn: classes) {
classifier 'sources'
from sourceSets.main.allSource
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier 'javadoc'
}
artifacts {
archives sourcesJar, javadocJar
}
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:all,-fallthrough'
}
javadoc {
options.addStringOption('Xdoclint:none', '-quiet')
}

View file

@ -0,0 +1,55 @@
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
configurations {
asciidoclet
}
dependencies {
asciidoclet "org.asciidoctor:asciidoclet:${project.property('asciidoclet.version')}"
}
asciidoctor {
backends 'html5'
outputDir = file("${rootProject.projectDir}/docs")
separateOutputDirs = false
attributes 'source-highlighter': 'coderay',
idprefix: '',
idseparator: '-',
toc: 'left',
doctype: 'book',
icons: 'font',
encoding: 'utf-8',
sectlink: true,
sectanchors: true,
linkattrs: true,
imagesdir: 'img',
stylesheet: "${projectDir}/src/docs/asciidoc/css/foundation.css"
}
/*javadoc {
options.docletpath = configurations.asciidoclet.files.asType(List)
options.doclet = 'org.asciidoctor.Asciidoclet'
//options.overview = "src/docs/asciidoclet/overview.adoc"
options.addStringOption "-base-dir", "${projectDir}"
options.addStringOption "-attribute",
"name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}"
configure(options) {
noTimestamp = true
}
}*/
/*javadoc {
options.docletpath = configurations.asciidoclet.files.asType(List)
options.doclet = 'org.asciidoctor.Asciidoclet'
options.overview = "${rootProject.projectDir}/src/docs/asciidoclet/overview.adoc"
options.addStringOption "-base-dir", "${projectDir}"
options.addStringOption "-attribute",
"name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}"
options.destinationDirectory(file("${projectDir}/docs/javadoc"))
configure(options) {
noTimestamp = true
}
}*/

13
gradle/ide/idea.gradle Normal file
View file

@ -0,0 +1,13 @@
apply plugin: 'idea'
idea {
module {
outputDir file('build/classes/java/main')
testOutputDir file('build/classes/java/test')
}
}
if (project.convention.findPlugin(JavaPluginConvention)) {
//sourceSets.main.output.classesDirs = file("build/classes/java/main")
//sourceSets.test.output.classesDirs = file("build/classes/java/test")
}

View file

@ -0,0 +1,64 @@
apply plugin: "de.marcphilipp.nexus-publish"
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
artifact javadocJar
pom {
name = project.name
description = rootProject.ext.description
url = rootProject.ext.url
inceptionYear = rootProject.ext.inceptionYear
packaging = 'jar'
organization {
name = 'xbib'
url = 'https://xbib.org'
}
developers {
developer {
id = 'jprante'
name = 'Jörg Prante'
email = 'joergprante@gmail.com'
url = 'https://github.com/jprante'
}
}
scm {
url = rootProject.ext.scmUrl
connection = rootProject.ext.scmConnection
developerConnection = rootProject.ext.scmDeveloperConnection
}
issueManagement {
system = rootProject.ext.issueManagementSystem
url = rootProject.ext.issueManagementUrl
}
licenses {
license {
name = rootProject.ext.licenseName
url = rootProject.ext.licenseUrl
distribution = 'repo'
}
}
}
}
}
}
if (project.hasProperty("signing.keyId")) {
apply plugin: 'signing'
signing {
sign publishing.publications.mavenJava
}
}
nexusPublishing {
repositories {
sonatype {
username = project.property('ossrhUsername')
password = project.property('ossrhPassword')
packageGroup = "org.xbib"
}
}
}

View file

@ -0,0 +1,11 @@
if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) {
apply plugin: 'io.codearte.nexus-staging'
nexusStaging {
username = project.property('ossrhUsername')
password = project.property('ossrhPassword')
packageGroup = "org.xbib"
}
}

28
gradle/test/junit5.gradle Normal file
View file

@ -0,0 +1,28 @@
def junitVersion = project.hasProperty('junit.version')?project.property('junit.version'):'5.6.2'
def hamcrestVersion = project.hasProperty('hamcrest.version')?project.property('hamcrest.version'):'2.2'
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}"
testImplementation "org.hamcrest:hamcrest-library:${hamcrestVersion}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
}
test {
useJUnitPlatform()
systemProperty 'jna.debug_load', 'true'
failFast = true
testLogging {
events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED'
}
afterSuite { desc, result ->
if (!desc.parent) {
println "\nTest result: ${result.resultType}"
println "Test summary: ${result.testCount} tests, " +
"${result.successfulTestCount} succeeded, " +
"${result.failedTestCount} failed, " +
"${result.skippedTestCount} skipped"
}
}
}

View file

@ -1,6 +1,5 @@
#Thu Mar 19 17:07:57 CET 2020
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

31
gradlew vendored
View file

@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"` APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"` JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath # We build the pattern for arguments to be converted via cygpath
@ -154,19 +156,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
else else
eval `echo args$i`="\"$arg\"" eval `echo args$i`="\"$arg\""
fi fi
i=$((i+1)) i=`expr $i + 1`
done done
case $i in case $i in
(0) set -- ;; 0) set -- ;;
(1) set -- "$args0" ;; 1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;; 2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;; 3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac esac
fi fi
@ -175,14 +177,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " " echo " "
} }
APP_ARGS=$(save "$@") APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules # Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

4
gradlew.bat vendored
View file

@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@ -81,6 +84,7 @@ set CMD_LINE_ARGS=%*
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

View file

@ -0,0 +1,8 @@
version = "${project.property('log4j.version')}.0"
dependencies {
implementation "net.java.dev.jna:jna:${project.property('jna.version')}"
implementation "org.apache.logging.log4j:log4j-core:${project.property('log4j.version')}"
testImplementation "org.mockito:mockito-junit-jupiter:${project.property('mockito.version')}"
}

View file

@ -0,0 +1,59 @@
package org.xbib.log4j.systemd;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* Format java exception messages and stack traces.
*/
public final class ExceptionFormatter {
private ExceptionFormatter() {
}
/**
* Format exception with stack trace.
*
* @param t the thrown object
* @return the formatted exception
*/
public static String format(Throwable t) {
StringBuilder sb = new StringBuilder();
append(sb, t, 0, true);
return sb.toString();
}
/**
* Append Exception to string builder.
* @param sb string builder
* @param t the exception
* @param level exception nested level
* @param details details
*
*/
private static void append(StringBuilder sb, Throwable t, int level, boolean details) {
if (((t != null) && (t.getMessage() != null)) && (!t.getMessage().isEmpty())) {
if (details && (level > 0)) {
sb.append("\n\nCaused by\n");
}
sb.append(t.getMessage());
}
if (details) {
if (t != null) {
if ((t.getMessage() != null) && (t.getMessage().isEmpty())) {
sb.append("\n\nCaused by ");
} else {
sb.append("\n\n");
}
}
StringWriter sw = new StringWriter();
if (t != null) {
t.printStackTrace(new PrintWriter(sw));
}
sb.append(sw.toString());
}
if (t != null && t.getCause() != null) {
append(sb, t.getCause(), level + 1, details);
}
}
}

View file

@ -0,0 +1,183 @@
package org.xbib.log4j.systemd;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Core;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.util.Booleans;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
/**
* Log4j appender for systemd journal.
*/
@Plugin(name = "SystemdJournalAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE, printObject = true)
public class SystemdJournalAppender extends AbstractAppender {
private final SystemdLibraryAPI systemdLibraryAPI;
private final boolean logSource;
private final boolean logStacktrace;
private final boolean logThreadName;
private final boolean logLoggerName;
private final boolean logAppenderName;
private final boolean logThreadContext;
private final String threadContextPrefix;
private final String syslogIdentifier;
public SystemdJournalAppender(String name, Filter filter, Layout<?> layout, boolean ignoreExceptions,
SystemdLibraryAPI systemdLibraryAPI,
boolean logSource, boolean logStacktrace, boolean logThreadName,
boolean logLoggerName, boolean logAppenderName, boolean logThreadContext,
String threadContextPrefix, String syslogIdentifier) {
super(name, filter, layout, ignoreExceptions, Property.EMPTY_ARRAY);
this.systemdLibraryAPI = systemdLibraryAPI != null ? systemdLibraryAPI : SystemdLibraryAPI.getInstance();
this.logSource = logSource;
this.logStacktrace = logStacktrace;
this.logThreadName = logThreadName;
this.logLoggerName = logLoggerName;
this.logAppenderName = logAppenderName;
this.logThreadContext = logThreadContext;
if (threadContextPrefix == null) {
this.threadContextPrefix = "THREAD_CONTEXT_";
} else {
this.threadContextPrefix = normalizeKey(threadContextPrefix);
}
this.syslogIdentifier = syslogIdentifier;
}
@PluginFactory
public static SystemdJournalAppender createAppender(@PluginAttribute("name") final String name,
@PluginAttribute("ignoreExceptions") final String ignoreExceptionsString,
@PluginAttribute("logSource") final String logSourceString,
@PluginAttribute("logStacktrace") final String logStacktraceString,
@PluginAttribute("logLoggerName") final String logLoggerNameString,
@PluginAttribute("logAppenderName") final String logAppenderNameString,
@PluginAttribute("logThreadName") final String logThreadNameString,
@PluginAttribute("logThreadContext") final String logThreadContextString,
@PluginAttribute("threadContextPrefix") final String threadContextPrefix,
@PluginAttribute("syslogIdentifier") final String syslogIdentifier,
@PluginElement("Layout") final Layout<?> layout,
@PluginElement("Filter") final Filter filter,
@PluginConfiguration final Configuration config) {
boolean ignoreExceptions = Booleans.parseBoolean(ignoreExceptionsString, true);
boolean logSource = Booleans.parseBoolean(logSourceString, false);
boolean logStacktrace = Booleans.parseBoolean(logStacktraceString, true);
boolean logThreadName = Booleans.parseBoolean(logThreadNameString, true);
boolean logLoggerName = Booleans.parseBoolean(logLoggerNameString, true);
boolean logAppenderName = Booleans.parseBoolean(logAppenderNameString, true);
boolean logThreadContext = Booleans.parseBoolean(logThreadContextString, true);
if (name == null) {
LOGGER.error("No name provided for SystemdJournalAppender");
return null;
}
SystemdLibraryAPI systemdLibraryAPI = SystemdLibraryAPI.getInstance();
return new SystemdJournalAppender(name, filter, layout, ignoreExceptions,
systemdLibraryAPI,
logSource, logStacktrace, logThreadName, logLoggerName, logAppenderName,
logThreadContext, threadContextPrefix, syslogIdentifier);
}
private int log4jLevelToJournalPriority(Level level) {
switch (level.getStandardLevel()) {
case FATAL:
return 2;
case ERROR:
return 3;
case WARN:
return 4;
case INFO:
return 6;
case DEBUG:
case TRACE:
return 7;
default:
throw new IllegalArgumentException("unable to map log level: " + level);
}
}
@Override
public void append(LogEvent event) {
List<Object> args = new ArrayList<>();
args.add(buildFormattedMessage(event));
args.add("PRIORITY=%d");
args.add(log4jLevelToJournalPriority(event.getLevel()));
if (logThreadName) {
args.add("THREAD_NAME=%s");
args.add(event.getThreadName());
}
if (logLoggerName) {
args.add("LOG4J_LOGGER=%s");
args.add(event.getLoggerName());
}
if (logAppenderName) {
args.add("LOG4J_APPENDER=%s");
args.add(getName());
}
if (logStacktrace && event.getThrown() != null) {
args.add("STACKTRACE=%s");
args.add(ExceptionFormatter.format(event.getThrown()));
}
if (logSource && event.getSource() != null) {
String fileName = event.getSource().getFileName();
args.add("CODE_FILE=%s");
args.add(fileName);
String methodName = event.getSource().getMethodName();
args.add("CODE_FUNC=%s");
args.add(methodName);
int lineNumber = event.getSource().getLineNumber();
args.add("CODE_LINE=%d");
args.add(lineNumber);
}
if (logThreadContext) {
ReadOnlyStringMap context = event.getContextData();
if (context != null) {
for (Entry<String, String> entry : context.toMap().entrySet()) {
String key = entry.getKey();
args.add(threadContextPrefix + normalizeKey(key) + "=%s");
args.add(entry.getValue());
}
}
}
if (syslogIdentifier != null && !syslogIdentifier.isEmpty()) {
args.add("SYSLOG_IDENTIFIER=%s");
args.add(syslogIdentifier);
}
args.add(null);
int rc = systemdLibraryAPI.journal_send("MESSAGE=%s", args);
if (rc != 0) {
LOGGER.error("sd_journal_send failed: " + rc);
}
}
private String buildFormattedMessage(LogEvent event) {
if (getLayout() != null) {
return new String(getLayout().toByteArray(event), StandardCharsets.UTF_8);
}
return event.getMessage().getFormattedMessage();
}
private static String normalizeKey(String key) {
return key.toUpperCase().replaceAll("[^_A-Z0-9]", "_");
}
}

View file

@ -0,0 +1,8 @@
package org.xbib.log4j.systemd;
import com.sun.jna.Library;
public interface SystemdLibrary extends Library {
int sd_journal_send(String format, Object... args);
}

View file

@ -0,0 +1,43 @@
package org.xbib.log4j.systemd;
import com.sun.jna.Native;
import java.util.List;
/**
* The systemd library API, loaded by Java Native Access (JNA).
*
* The native library is loaded only once, so this class is a singleton.
*/
public class SystemdLibraryAPI {
private static final SystemdLibraryAPI instance = new SystemdLibraryAPI();
private final SystemdLibrary systemdLibrary;
private SystemdLibraryAPI() {
this.systemdLibrary = loadLibrary();
}
public static SystemdLibraryAPI getInstance() {
return instance;
}
public int journal_send(String format, Object... args) {
return systemdLibrary.sd_journal_send(format, args);
}
public int journal_send(String format, List<Object> args) {
return systemdLibrary.sd_journal_send(format, args.toArray());
}
private static SystemdLibrary loadLibrary() {
try {
return Native.load("systemd", SystemdLibrary.class);
} catch (UnsatisfiedLinkError e) {
throw new RuntimeException("Failed to load systemd library." +
" Please note that JNA requires an executable temporary folder." +
" It can be explicitly defined with -Djna.tmpdir", e);
}
}
}

View file

@ -0,0 +1,54 @@
package org.xbib.log4j.systemd;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
@EnabledOnOs({OS.LINUX})
class SystemdJournalAppenderIntegrationTest {
private static final Logger logger = LogManager.getLogger(SystemdJournalAppenderIntegrationTest.class.getName());
@BeforeEach
void clearMdc() {
ThreadContext.clearAll();
}
@Test
void testMessages() {
logger.trace("this is a test message with level TRACE");
logger.debug("this is a test message with level DEBUG");
logger.info("this is a test message with level INFO");
logger.warn("this is a test message with level WARN");
logger.error("this is a test message with level ERROR");
}
@Test
void testMessageWithUnicode() {
logger.info("this is a test message with unicode: →←üöß");
}
@Test
void testMessageWithMDC() {
ThreadContext.put("some key1", "some value %d");
ThreadContext.put("some key2", "some other value with unicode: →←üöß");
logger.info("this is a test message with a MDC");
}
@Test
void testMessageWithPlaceholders() {
ThreadContext.put("some key1%s", "%1$");
ThreadContext.put("%1$", "%1$");
logger.info("this is a test message with special placeholder characters: %1$");
}
@Test
void testMessageWithStacktrace() {
logger.info("this is a test message with an exception", new RuntimeException("some exception"));
}
}

View file

@ -0,0 +1,141 @@
package org.xbib.log4j.systemd;
import java.util.ArrayList;
import java.util.List;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.impl.Log4jLogEvent;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.spi.DefaultThreadContextMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@EnabledOnOs({OS.LINUX})
@ExtendWith(MockitoExtension.class)
class SystemdJournalAppenderTest {
@Mock
private Message message;
@Mock
private SystemdLibraryAPI api;
@BeforeEach
void prepare() {
ThreadContext.clearAll();
}
@Test
void testSimple() {
SystemdLibraryAPI api = mock(SystemdLibraryAPI.class);
Message message = mock(Message.class);
SystemdJournalAppender journalAppender =
new SystemdJournalAppender("Journal", null, null, false, api,
false, false, false, false, false, false, null, null);
when(message.getFormattedMessage()).thenReturn("some message");
LogEvent event = new Log4jLogEvent.Builder().setMessage(message).setLevel(Level.INFO).build();
journalAppender.append(event);
List<Object> expectedArgs = new ArrayList<>();
expectedArgs.add("some message");
expectedArgs.add("PRIORITY=%d");
expectedArgs.add(6);
expectedArgs.add(null);
verify(api).journal_send("MESSAGE=%s", expectedArgs);
}
@Test
void testLogSource() {
SystemdLibraryAPI api = mock(SystemdLibraryAPI.class);
Message message = mock(Message.class);
SystemdJournalAppender journalAppender =
new SystemdJournalAppender("Journal", null, null, false, api,
true, false, false, false, false, false, null, null);
when(message.getFormattedMessage()).thenReturn("some message");
LogEvent event = new Log4jLogEvent.Builder() //
.setMessage(message)//
.setLoggerFqcn(journalAppender.getClass().getName())//
.setLevel(Level.INFO).build();
event.setIncludeLocation(true);
journalAppender.append(event);
List<Object> expectedArgs = new ArrayList<>();
expectedArgs.add("some message");
expectedArgs.add("PRIORITY=%d");
expectedArgs.add(6);
expectedArgs.add("CODE_FILE=%s");
expectedArgs.add("SystemdJournalAppenderTest.java");
expectedArgs.add("CODE_FUNC=%s");
expectedArgs.add("testLogSource");
expectedArgs.add("CODE_LINE=%d");
expectedArgs.add(69);
expectedArgs.add(null);
verify(api).journal_send("MESSAGE=%s", expectedArgs);
}
@Test
void testDoNotLogException() {
SystemdLibraryAPI api = mock(SystemdLibraryAPI.class);
Message message = mock(Message.class);
SystemdJournalAppender journalAppender =
new SystemdJournalAppender("Journal", null, null, false, api,
false, false, false, false, false, false, null, null);
when(message.getFormattedMessage()).thenReturn("some message");
LogEvent event = new Log4jLogEvent.Builder()
.setMessage(message)
.setLoggerFqcn(journalAppender.getClass().getName())
.setThrown(new Throwable())
.setLevel(Level.INFO).build();
event.setIncludeLocation(true);
journalAppender.append(event);
List<Object> expectedArgs = new ArrayList<>();
expectedArgs.add("some message");
expectedArgs.add("PRIORITY=%d");
expectedArgs.add(6);
expectedArgs.add(null);
verify(api).journal_send("MESSAGE=%s", expectedArgs);
}
@Test
void testThreadAndContext() {
SystemdLibraryAPI api = mock(SystemdLibraryAPI.class);
Message message = mock(Message.class);
SystemdJournalAppender journalAppender =
new SystemdJournalAppender("Journal", null, null, false, api,
false, false, true, true, true, true, null, "some-identifier");
when(message.getFormattedMessage()).thenReturn("some message");
DefaultThreadContextMap contextMap = new DefaultThreadContextMap();
LogEvent event = mock(LogEvent.class);
when(event.getMessage()).thenReturn(message);
when(event.getLoggerName()).thenReturn("some logger");
when(event.getLevel()).thenReturn(Level.INFO);
when(event.getThreadName()).thenReturn("the thread");
when(event.getContextData()).thenReturn(contextMap);
contextMap.put("foo%s$1%d", "bar");
journalAppender.append(event);
List<Object> expectedArgs = new ArrayList<>();
expectedArgs.add("some message");
expectedArgs.add("PRIORITY=%d");
expectedArgs.add(6);
expectedArgs.add("THREAD_NAME=%s");
expectedArgs.add("the thread");
expectedArgs.add("LOG4J_LOGGER=%s");
expectedArgs.add("some logger");
expectedArgs.add("LOG4J_APPENDER=%s");
expectedArgs.add("Journal");
expectedArgs.add("THREAD_CONTEXT_FOO_S_1_D=%s");
expectedArgs.add("bar");
expectedArgs.add("SYSLOG_IDENTIFIER=%s");
expectedArgs.add("some-identifier");
expectedArgs.add(null);
verify(api).journal_send("MESSAGE=%s", expectedArgs);
}
}

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO" packages="org.xbib.log4j.systemd">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</Console>
<SystemdJournalAppender name="JournalWithLayout"
logStacktrace="true"
logThreadName="true"
logSource="true"
logLoggerName="true"
logAppenderName="true"
logThreadContext="true"
threadContextPrefix="THREAD_CONTEXT_"
syslogIdentifier="log4j2-test-with-layout">
<PatternLayout pattern="[%t] %-5level - %msg%n" />
</SystemdJournalAppender>
<SystemdJournalAppender name="JournalWithoutLayout"
logStacktrace="true"
logThreadName="true"
logSource="true"
logLoggerName="true"
logAppenderName="true"
logThreadContext="true"
threadContextPrefix="THREAD_CONTEXT_"
syslogIdentifier="log4j2-test-no-layout"
/>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="Console" />
<AppenderRef ref="JournalWithLayout" />
<AppenderRef ref="JournalWithoutLayout" />
</Root>
</Loggers>
</Configuration>

2
settings.gradle Normal file
View file

@ -0,0 +1,2 @@
include 'systemd-journal'
include 'log4j-systemd-journal'

View file

@ -0,0 +1,5 @@
dependencies {
implementation "com.nativelibs4java:bridj:${project.property('bridj.version')}"
testImplementation "org.mockito:mockito-junit-jupiter:${project.property('mockito.version')}"
}

View file

@ -1,4 +1,4 @@
package org.xbib.systemd; package org.xbib.systemd.journal;
public class DefaultJournalEntry implements JournalEntry { public class DefaultJournalEntry implements JournalEntry {

View file

@ -1,4 +1,4 @@
package org.xbib.systemd; package org.xbib.systemd.journal;
public interface JournalEntry { public interface JournalEntry {

View file

@ -1,4 +1,4 @@
package org.xbib.systemd; package org.xbib.systemd.journal;
public interface Syslog { public interface Syslog {
int LOG_EMERG = 0; int LOG_EMERG = 0;

View file

@ -1,4 +1,4 @@
package org.xbib.systemd; package org.xbib.systemd.journal;
import org.bridj.Pointer; import org.bridj.Pointer;
import org.bridj.SizeT; import org.bridj.SizeT;

View file

@ -1,9 +1,6 @@
package org.xbib.systemd; package org.xbib.systemd.journal;
import org.bridj.BridJ; import org.bridj.*;
import org.bridj.CRuntime;
import org.bridj.Pointer;
import org.bridj.SizeT;
import org.bridj.ann.Library; import org.bridj.ann.Library;
import org.bridj.ann.Ptr; import org.bridj.ann.Ptr;
import org.bridj.ann.Runtime; import org.bridj.ann.Runtime;
@ -32,10 +29,6 @@ public class SystemdJournalLibrary {
public static final String SD_ID128_FORMAT_STR = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"; public static final String SD_ID128_FORMAT_STR = "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x";
public static Pointer<Byte> sd_id128_to_string(sd_id128 id, Pointer<Byte> s) {
return Pointer.pointerToAddress(sd_id128_to_string(id, Pointer.getPeer(s)), Byte.class);
}
@Ptr @Ptr
protected native static long sd_id128_to_string(sd_id128 id, @Ptr long s); protected native static long sd_id128_to_string(sd_id128 id, @Ptr long s);
@ -81,7 +74,7 @@ public class SystemdJournalLibrary {
protected native static int sd_journal_send(@Ptr long format, Object... varArgs1); protected native static int sd_journal_send(@Ptr long format, Object... varArgs1);
public static int sd_journal_sendv(Pointer iov, int n) { public static int sd_journal_sendv(Pointer<? extends StructObject> iov, int n) {
return sd_journal_sendv(Pointer.getPeer(iov), n); return sd_journal_sendv(Pointer.getPeer(iov), n);
} }
@ -121,7 +114,7 @@ public class SystemdJournalLibrary {
@Ptr long format, Object... varArgs1); @Ptr long format, Object... varArgs1);
public static int sd_journal_sendv_with_location(Pointer<Byte> file, Pointer<Byte> line, Pointer<Byte> func, public static int sd_journal_sendv_with_location(Pointer<Byte> file, Pointer<Byte> line, Pointer<Byte> func,
Pointer iov, int n) { Pointer<? extends StructObject> iov, int n) {
return sd_journal_sendv_with_location(Pointer.getPeer(file), Pointer.getPeer(line), Pointer.getPeer(func), return sd_journal_sendv_with_location(Pointer.getPeer(file), Pointer.getPeer(line), Pointer.getPeer(func),
Pointer.getPeer(iov), n); Pointer.getPeer(iov), n);
} }

View file

@ -1,4 +1,4 @@
package org.xbib.systemd; package org.xbib.systemd.journal;
import java.io.IOException; import java.io.IOException;

View file

@ -1,4 +1,4 @@
package org.xbib.systemd; package org.xbib.systemd.journal;
import org.bridj.BridJ; import org.bridj.BridJ;
import org.bridj.Pointer; import org.bridj.Pointer;
@ -18,13 +18,13 @@ public class sd_id128 extends StructObject {
@Array({16}) @Array({16})
@Field(0) @Field(0)
public Pointer<Byte > bytes() { public Pointer<Byte> bytes() {
return io.getPointerField(this, 0); return io.getPointerField(this, 0);
} }
@Array({2}) @Array({2})
@Field(1) @Field(1)
public Pointer<Long > qwords() { public Pointer<Long> qwords() {
return io.getPointerField(this, 1); return io.getPointerField(this, 1);
} }
@ -32,7 +32,7 @@ public class sd_id128 extends StructObject {
super(); super();
} }
public sd_id128(Pointer pointer) { public sd_id128(Pointer<? extends StructObject> pointer) {
super(pointer); super(pointer);
} }
} }

View file

@ -1,14 +1,14 @@
package org.xbib.systemd; package org.xbib.systemd.journal;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.condition.OS;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@DisabledOnOs({OS.MAC, OS.WINDOWS}) @EnabledOnOs({OS.LINUX})
class SystemdJournalReaderTest { class SystemdJournalReaderTest {
private static final Logger logger = Logger.getLogger(SystemdJournalReaderTest.class.getName()); private static final Logger logger = Logger.getLogger(SystemdJournalReaderTest.class.getName());
@ -18,7 +18,7 @@ class SystemdJournalReaderTest {
SystemdJournalConsumer consumer = new SystemdJournalConsumer("SYSLOG_IDENTIFIER=su", SystemdJournalConsumer consumer = new SystemdJournalConsumer("SYSLOG_IDENTIFIER=su",
entry -> logger.log(Level.INFO, entry.toString())); entry -> logger.log(Level.INFO, entry.toString()));
Executors.newSingleThreadExecutor().submit(consumer); Executors.newSingleThreadExecutor().submit(consumer);
// exit after 1 minute // consuming for some seconds
Thread.sleep(60000L); Thread.sleep(10000L);
} }
} }