diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c4f0783
--- /dev/null
+++ b/README.md
@@ -0,0 +1,157 @@
+# Systemd journal for Java
+
+[](https://travis-ci.org/jprante/systemd-journal)
+[](http://maven-badges.herokuapp.com/maven-central/org.xbib/log4j-systemd-journal)
+[](https://opensource.org/licenses/Apache-2.0)
+[](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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+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.
+
diff --git a/build.gradle b/build.gradle
index 55f6ff7..6fe61a0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,122 +1,34 @@
plugins {
- id "org.sonarqube" version "2.6.1"
- id "io.codearte.nexus-staging" version "0.11.0"
+ id "de.marcphilipp.nexus-publish" version "0.4.0"
+ id "io.codearte.nexus-staging" version "0.21.1"
}
-apply plugin: 'java'
-apply plugin: 'maven'
-
-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
+wrapper {
+ gradleVersion = "${project.property('gradle.wrapper.version')}"
+ distributionType = Wrapper.DistributionType.ALL
}
ext {
user = 'jprante'
- projectDescription = 'Systemd journal bindings'
- scmUrl = 'https://github.com/jprante/systemd-journal'
- scmConnection = 'scm:git:git://github.com/jprante/systemd-journal.git'
- scmDeveloperConnection = 'scm:git:git://github.com/jprante/systemd-journal.git'
+ name = 'systemd-journal'
+ description = 'Systemd journal bindings and logging adapters for Java'
+ inceptionYear = '2018'
+ 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) {
- group = 'publish'
- configuration = configurations.archives
- uploadDescriptor = true
- repositories {
- if (project.hasProperty('ossrhUsername')) {
- 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'
- }
- }
- }
- }
- }
- }
+subprojects {
+ apply plugin: 'java-library'
+ apply from: rootProject.file('gradle/ide/idea.gradle')
+ apply from: rootProject.file('gradle/compile/java.gradle')
+ apply from: rootProject.file('gradle/test/junit5.gradle')
+ apply from: rootProject.file('gradle/publishing/publication.gradle')
}
-nexusStaging {
- packageGroup = "org.xbib"
-}
+apply from: rootProject.file('gradle/publishing/sonatype.gradle')
diff --git a/gradle.properties b/gradle.properties
index 43c961c..1d9ee76 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,7 +2,8 @@ group = org.xbib
name = systemd-journal
version = 1.0.0
+gradle.wrapper.version = 6.4.1
bridj.version = 0.7.0
-
-junit.version = 5.4.2
-mockito.version = 2.27.0
+jna.version = 5.5.0
+log4j.version = 2.13.3
+mockito.version = 3.3.3
diff --git a/gradle/compile/java.gradle b/gradle/compile/java.gradle
new file mode 100644
index 0000000..c9bba7f
--- /dev/null
+++ b/gradle/compile/java.gradle
@@ -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')
+}
diff --git a/gradle/documentation/asciidoc.gradle b/gradle/documentation/asciidoc.gradle
new file mode 100644
index 0000000..87ba22e
--- /dev/null
+++ b/gradle/documentation/asciidoc.gradle
@@ -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
+ }
+}*/
diff --git a/gradle/ide/idea.gradle b/gradle/ide/idea.gradle
new file mode 100644
index 0000000..64e2167
--- /dev/null
+++ b/gradle/ide/idea.gradle
@@ -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")
+}
diff --git a/gradle/publishing/publication.gradle b/gradle/publishing/publication.gradle
new file mode 100644
index 0000000..c35fcb9
--- /dev/null
+++ b/gradle/publishing/publication.gradle
@@ -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"
+ }
+ }
+}
diff --git a/gradle/publishing/sonatype.gradle b/gradle/publishing/sonatype.gradle
new file mode 100644
index 0000000..e1813f3
--- /dev/null
+++ b/gradle/publishing/sonatype.gradle
@@ -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"
+ }
+}
diff --git a/gradle/test/junit5.gradle b/gradle/test/junit5.gradle
new file mode 100644
index 0000000..f994dba
--- /dev/null
+++ b/gradle/test/junit5.gradle
@@ -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"
+ }
+ }
+}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 58879c5..21e622d 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -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
distributionPath=wrapper/dists
-zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip
zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 83f2acf..fbd7c51 100755
--- a/gradlew
+++ b/gradlew
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
@@ -154,19 +156,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
else
eval `echo args$i`="\"$arg\""
fi
- i=$((i+1))
+ i=`expr $i + 1`
done
case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@@ -175,14 +177,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
-APP_ARGS=$(save "$@")
+APP_ARGS=`save "$@"`
# 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"
-# 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" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 24467a1..a9f778a 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
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.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@@ -81,6 +84,7 @@ set CMD_LINE_ARGS=%*
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
@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%
diff --git a/log4j-systemd-journal/build.gradle b/log4j-systemd-journal/build.gradle
new file mode 100644
index 0000000..7d5dd46
--- /dev/null
+++ b/log4j-systemd-journal/build.gradle
@@ -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')}"
+}
diff --git a/log4j-systemd-journal/src/main/java/org/xbib/log4j/systemd/ExceptionFormatter.java b/log4j-systemd-journal/src/main/java/org/xbib/log4j/systemd/ExceptionFormatter.java
new file mode 100644
index 0000000..049d1d8
--- /dev/null
+++ b/log4j-systemd-journal/src/main/java/org/xbib/log4j/systemd/ExceptionFormatter.java
@@ -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);
+ }
+ }
+}
diff --git a/log4j-systemd-journal/src/main/java/org/xbib/log4j/systemd/SystemdJournalAppender.java b/log4j-systemd-journal/src/main/java/org/xbib/log4j/systemd/SystemdJournalAppender.java
new file mode 100644
index 0000000..1ca1978
--- /dev/null
+++ b/log4j-systemd-journal/src/main/java/org/xbib/log4j/systemd/SystemdJournalAppender.java
@@ -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