diff --git a/Jenkinsfile b/Jenkinsfile index ea2d75d..7f442a8 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,186 +1,99 @@ -#!groovy - -// Required Jenkins plugins: -// https://wiki.jenkins-ci.org/display/JENKINS/Timestamper -// https://wiki.jenkins-ci.org/display/JENKINS/Static+Code+Analysis+Plug-ins -// https://wiki.jenkins-ci.org/display/JENKINS/Checkstyle+Plugin ? -// https://wiki.jenkins-ci.org/display/JENKINS/FindBugs+Plugin -// https://wiki.jenkins-ci.org/display/JENKINS/PMD+Plugin ? -// https://wiki.jenkins-ci.org/display/JENKINS/DRY+Plugin ? -// https://wiki.jenkins-ci.org/display/JENKINS/Task+Scanner+Plugin -// https://wiki.jenkins-ci.org/display/JENKINS/Javadoc+Plugin -// https://wiki.jenkins-ci.org/display/JENKINS/JaCoCo+Plugin ? - - -init() - -def branch_name -def branch_name_base -def build_number -def build_url -def git_commit -def job_name -def tag -def version -def build_type -def display_name - -def init() { - - // Keep the 5 most recent builds - properties([[$class: 'BuildDiscarderProperty', strategy: [$class: 'LogRotator', numToKeepStr: '5']]]) - - build_number = env.BUILD_NUMBER - build_url = env.BUILD_URL - job_name = "${env.JOB_NAME}" - branch_name = env.BRANCH_NAME - branch_name_docker = branch_name.replaceAll(/\//,'.') - persist = "/var/lib/jenkins/PERSIST/${branch_name_docker}" - - // execute the branch type specific pipeline code - try { - - if (branch_name.indexOf('release/')==0) build_type='release' - if (branch_name.indexOf('feature/')==0) build_type='feature' - if (branch_name.indexOf('develop')==0) build_type='develop' - if (branch_name.indexOf('hotfix')==0) build_type='hotfix' - if (branch_name.indexOf('bugfix')==0) build_type='bugfix' - if (branch_name.indexOf('master')==0) build_type='master' - - // common pipeline elements - node() { - Initialize() - SetVersion(build_type) - print_vars() // after SetVersion - all variables now defined - set_result('INPROGRESS') - Build() // builds database via flyway migration - } - - if (branch_name.indexOf('develop')==0) { - node() { - Deploy(); - } - } else if (branch_name.indexOf('release/')==0) { - node() { - Deploy(); - } - } - - node() { - set_result('SUCCESS') - } - - } catch (err) { - node() { - set_result('FAILURE') - } - throw err - } -} - -def Build() { - stage ('build') { - mvn "install -DskipTests=true -Dbuild.revision=${git_commit}" - step([$class: 'ArtifactArchiver', artifacts: '**/target/*.jar', fingerprint: true]) - } -} - -def Initialize() { - stage ('initialize') { - - // get new code - checkout scm - - git_commit = getSha1() - } -} - -def Deploy() { - stage ('deploy') { - mvn "deploy -DskipTests=true -Dbuild.number=${build_number} -Dbuild.revision=${git_commit}" - } -} - -def getSha1() { - sha1 = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() - echo "sha1 is ${sha1}" - return sha1 -} - -def mvn(args) { - withMaven( - maven: 'maven-3.6.1', - globalMavenSettingsConfig: 'locusworks-settings' - ) { - - sh "mvn ${args}" - - } -} - -def mvn_initial(args) { - mvn(args) -} - -def set_result(status) { - if ( status == 'SUCCESS' ) { - currentBuild.result = status - notify_bitbucket('SUCCESSFUL') - } else if ( status == 'FAILURE' ) { - currentBuild.result = status - notify_bitbucket('FAILED') - } else if ( status == 'INPROGRESS' ) { - notify_bitbucket('INPROGRESS') - } else { - error ("unknown status") - } - - // save in persistence file for access the status page - // make sure the directory exists first - sh "mkdir -p $persist && echo $status > $persist/build.result" -} - -def notify_bitbucket(state) { -} - -def print_vars() { - echo "build_number = ${build_number}" - echo "build_url = ${build_url}" - echo "job_name = ${job_name}" - echo "branch_name = ${branch_name}" - echo "branch_name_base = ${branch_name_base}" - echo "build_type = ${build_type}" - echo "display_name = ${currentBuild.displayName}" - echo "version = ${version}" - echo "git_commit = ${git_commit}" - -} - -def SetVersion( v ) { - stage ('set version') { - echo "set version ${v}" - branch_name_base = (branch_name =~ /([^\/]+$)/)[0][0] - if ( v == 'release' ) { - // for release branches, where the branch is named "release/1.2.3", - // derive the version and display name derive from the numeric suffix and append the build number - // 3.2.1.100 - version = branch_name_base + "." + build_number + "-RELEASE"; - //version = branch_name.substring('release/'.length()) + "." + build_number - currentBuild.displayName = version - } else if (v == 'develop') { - version = branch_name_base + "." + build_number + "-SNAPSHOT"; - currentBuild.displayName = version - } else { - // for all other branches the version number is 0 with an appended build number - // and for the display name use the jenkins default #n and add the branch name - // #101 - feature/user/foo - //version = '0.' + build_number - version = branch_name_base + "." + build_number - currentBuild.displayName = "#" + build_number + " - " + branch_name_base - } - display_name = currentBuild.displayName - mvn_initial "versions:set -DnewVersion=${version}" - } -} - -return this +pipeline { + agent any + + options { + buildDiscarder(logRotator(numToKeepStr: '5')) + disableConcurrentBuilds() + skipDefaultCheckout(true) + timestamps() + } + + stages { + stage('Checkout') { + steps { + checkout scm + } + } + + stage('Set version') { + steps { + script { + String branch = env.BRANCH_NAME + if (!branch) { + error('BRANCH_NAME is unavailable; this pipeline must run as a multibranch job') + } + + env.DEPLOY_BUILD = 'false' + + def releaseMatch = branch =~ /^release\/(\d+\.\d+\.\d+)$/ + if (releaseMatch.matches()) { + env.BUILD_VERSION = "${releaseMatch[0][1]}.${env.BUILD_NUMBER}-RELEASE" + env.DEPLOY_BUILD = 'true' + } else if (branch == 'develop') { + env.BUILD_VERSION = "develop.${env.BUILD_NUMBER}-SNAPSHOT" + env.DEPLOY_BUILD = 'true' + } else { + String safeBranch = branch.replaceAll(/[^A-Za-z0-9._-]/, '-') + env.BUILD_VERSION = "0.0.${env.BUILD_NUMBER}-${safeBranch}-SNAPSHOT" + } + currentBuild.displayName = env.BUILD_VERSION + } + + withMaven(maven: 'maven-3.9.16', globalMavenSettingsConfig: 'locusworks-settings') { + sh 'mvn -B org.codehaus.mojo:versions-maven-plugin:2.21.0:set -DgenerateBackupPoms=false -DnewVersion="$BUILD_VERSION"' + } + } + } + + stage('Prepare OWASP cache') { + steps { + lock(resource: 'owasp-nvd-cache') { + withMaven(maven: 'maven-3.9.16', globalMavenSettingsConfig: 'locusworks-settings') { + sh 'mkdir -p "$HOME/.cache/dependency-check"' + sh 'mvn -B org.owasp:dependency-check-maven:13.0.0:update-only -DdataDirectory="$HOME/.cache/dependency-check"' + } + } + } + } + + stage('Build and test') { + steps { + withMaven(maven: 'maven-3.9.16', globalMavenSettingsConfig: 'locusworks-settings') { + sh 'mvn -B clean verify -DdataDirectory="$HOME/.cache/dependency-check" -DautoUpdate=false -Dbuild.revision="$GIT_COMMIT"' + } + } + post { + always { + junit allowEmptyResults: true, testResults: '**/target/surefire-reports/*.xml' + } + success { + archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true + archiveArtifacts artifacts: '**/target/site/jacoco/**', fingerprint: true + } + } + } + + stage('Deploy') { + when { + expression { env.DEPLOY_BUILD == 'true' } + } + steps { + withMaven(maven: 'maven-3.9.16', globalMavenSettingsConfig: 'locusworks-settings') { + sh 'mvn -B deploy -DskipTests -Ddependency-check.skip=true -Dbuild.number="$BUILD_NUMBER" -Dbuild.revision="$GIT_COMMIT"' + } + } + } + } + + post { + success { + echo "Published build ${env.BUILD_VERSION} successfully" + } + failure { + echo "Build ${env.BUILD_VERSION ?: env.BUILD_NUMBER} failed" + } + cleanup { + deleteDir() + } + } +} diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml deleted file mode 100644 index 8a373e0..0000000 --- a/bitbucket-pipelines.yml +++ /dev/null @@ -1,14 +0,0 @@ -# This is a sample build configuration for Java (Maven). -# Check our guides at https://confluence.atlassian.com/x/zd-5Mw for more examples. -# Only use spaces to indent your .yml configuration. -# ----- -# You can specify a custom docker image from Docker Hub as your build environment. -image: maven:3.3.9 - -pipelines: - default: - - step: - caches: - - maven - script: # Modify the commands below to build your repository. - - mvn -B verify # -B batch mode makes Maven less verbose \ No newline at end of file diff --git a/pom.xml b/pom.xml index 795a5e6..c418f14 100644 --- a/pom.xml +++ b/pom.xml @@ -1,156 +1,197 @@ - - 4.0.0 - net.locusworks - applogger - 0.0.1-SNAPSHOT - Application Logger - Logger library for applications - - - ssh://gitea@gitea.locusworks.net:7999/locusworks/app-logger.git - - - locusworks - www.locusworks.net - - - - ${project.basedir} - 1.8 - 1.8 - 2.19.0 - 2.0.1 - https://nexus.locusworks.net - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M7 - - always - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.10.1 - - ${maven.compiler.source} - ${maven.compiler.target} - true - - -Xlint:all - - - - - org.owasp - dependency-check-maven - 7.2.0 - - - - check - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.1.0 - - - enforce - - - - - - - enforce - - - - - - - - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - - - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.apache.logging.log4j - log4j-slf4j-impl - ${log4j.version} - - - org.slf4j - slf4j-api - - - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - jcl-over-slf4j - ${slf4j.version} - - - - - - nexus-snapshot - ${nexus.repo}/repository/locusworks-snapshot/ - - - nexus-release - ${nexus.repo}/repository/locusworks-release/ - - - - - - locusworks-public - locusworks-public - ${nexus.repo}/repository/locusworks-public/ - - true - - - true - - - - - - - locusworks-public - ${nexus.repo}/repository/locusworks-public/ - - true - - - true - - - - - + + 4.0.0 + net.locusworks + applogger + 0.0.1-SNAPSHOT + Application Logger + Logger library for applications + + + ssh://gitea@gitea.locusworks.net:7999/locusworks/app-logger.git + + + locusworks + www.locusworks.net + + + + 2.0.18 + 5.14.4 + 5.23.0 + 0.8.15 + 21 + 21 + UTF-8 + https://nexus.locusworks.net + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + + prepare-agent + + + + report + verify + + report + + + + coverage-check + verify + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + 0.75 + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.6 + + @{argLine} -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + ${maven.compiler.source} + ${maven.compiler.target} + true + + -Xlint:all + + + + + org.owasp + dependency-check-maven + 13.0.0 + + true + true + true + true + e2eb1036-9b5b-4df9-95e7-0888be947011 + + + + + check + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.6.3 + + + enforce + + + + + + + enforce + + + + + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + + + + nexus-snapshot + ${nexus.repo}/repository/locusworks-snapshot/ + + + nexus-release + ${nexus.repo}/repository/locusworks-release/ + + + + + + locusworks-public + locusworks-public + ${nexus.repo}/repository/locusworks-public/ + + true + + + true + + + + + + + locusworks-public + ${nexus.repo}/repository/locusworks-public/ + + true + + + true + + + + + diff --git a/src/main/java/net/locusworks/logger/ApplicationLogger.java b/src/main/java/net/locusworks/logger/ApplicationLogger.java index a6f2b52..a0c8027 100644 --- a/src/main/java/net/locusworks/logger/ApplicationLogger.java +++ b/src/main/java/net/locusworks/logger/ApplicationLogger.java @@ -4,7 +4,9 @@ import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.slf4j.Marker; +import org.slf4j.Marker; +import org.slf4j.helpers.FormattingTuple; +import org.slf4j.helpers.MessageFormatter; /** * Logger implementation to use across applications. Extends slf4j logger to work with other logging applications @@ -13,9 +15,9 @@ import org.slf4j.Marker; */ public class ApplicationLogger implements org.slf4j.Logger { - protected static final String DEFAULT_FORMAT = "%s"; - - private static final Logger logger = LoggerFactory.getLogger(ApplicationLogger.class); + protected static final String DEFAULT_FORMAT = "{}"; + + private final Logger logger; private final String name; @@ -41,10 +43,15 @@ public class ApplicationLogger implements org.slf4j.Logger { * @param name name of the logger * @param level level of logging to set the logger to */ - protected ApplicationLogger(String name, LogLevel level) { - this.name = name; - this.level = level; - } + protected ApplicationLogger(String name, LogLevel level) { + this(name, level, LoggerFactory.getLogger(name)); + } + + ApplicationLogger(String name, LogLevel level, Logger logger) { + this.name = name; + this.level = level; + this.logger = logger; + } /** * Constructor @@ -104,8 +111,8 @@ public class ApplicationLogger implements org.slf4j.Logger { } @Override - public boolean isTraceEnabled() { - return this.level.log(LogLevel.TRACE); + public boolean isTraceEnabled() { + return enabled(LogLevel.TRACE) && logger.isTraceEnabled(); } @Override @@ -129,43 +136,43 @@ public class ApplicationLogger implements org.slf4j.Logger { } @Override - public void trace(String msg, Throwable t) { - trace("%s%n%s", msg, getStackTrace(t.getStackTrace())); + public void trace(String msg, Throwable t) { + log(LogLevel.TRACE, msg, t); } @Override - public boolean isTraceEnabled(Marker marker) { - return isTraceEnabled(); + public boolean isTraceEnabled(Marker marker) { + return enabled(LogLevel.TRACE) && logger.isTraceEnabled(marker); } @Override - public void trace(Marker marker, String msg) { - trace(msg); + public void trace(Marker marker, String msg) { + markerLog(LogLevel.TRACE, marker, msg); } @Override public void trace(Marker marker, String format, Object arg) { - trace(format, arg); + markerLog(LogLevel.TRACE, marker, format, arg); } @Override public void trace(Marker marker, String format, Object arg1, Object arg2) { - trace(format, arg1, arg2); + markerLog(LogLevel.TRACE, marker, format, arg1, arg2); } @Override public void trace(Marker marker, String format, Object... argArray) { - trace(format, argArray); + markerLog(LogLevel.TRACE, marker, format, argArray); } @Override public void trace(Marker marker, String msg, Throwable t) { - trace(msg, t); + markerLog(LogLevel.TRACE, marker, msg, t); } @Override - public boolean isDebugEnabled() { - return this.level.log(LogLevel.DEBUG); + public boolean isDebugEnabled() { + return enabled(LogLevel.DEBUG) && logger.isDebugEnabled(); } @Override @@ -189,43 +196,43 @@ public class ApplicationLogger implements org.slf4j.Logger { } @Override - public void debug(String msg, Throwable t) { - debug("%s%n%s", msg, getStackTrace(t.getStackTrace())); + public void debug(String msg, Throwable t) { + log(LogLevel.DEBUG, msg, t); } @Override - public boolean isDebugEnabled(Marker marker) { - return isDebugEnabled(); + public boolean isDebugEnabled(Marker marker) { + return enabled(LogLevel.DEBUG) && logger.isDebugEnabled(marker); } @Override - public void debug(Marker marker, String msg) { - debug(msg); + public void debug(Marker marker, String msg) { + markerLog(LogLevel.DEBUG, marker, msg); } @Override public void debug(Marker marker, String format, Object arg) { - debug(format, arg); + markerLog(LogLevel.DEBUG, marker, format, arg); } @Override public void debug(Marker marker, String format, Object arg1, Object arg2) { - debug(format, arg1, arg2); + markerLog(LogLevel.DEBUG, marker, format, arg1, arg2); } @Override public void debug(Marker marker, String format, Object... arguments) { - debug(format, arguments); + markerLog(LogLevel.DEBUG, marker, format, arguments); } @Override public void debug(Marker marker, String msg, Throwable t) { - debug(msg, t); + markerLog(LogLevel.DEBUG, marker, msg, t); } @Override - public boolean isInfoEnabled() { - return this.level.log(LogLevel.INFO); + public boolean isInfoEnabled() { + return enabled(LogLevel.INFO) && logger.isInfoEnabled(); } @Override @@ -249,8 +256,8 @@ public class ApplicationLogger implements org.slf4j.Logger { } @Override - public void info(String msg, Throwable t) { - info("%s%n%s", msg, getStackTrace(t.getStackTrace())); + public void info(String msg, Throwable t) { + log(LogLevel.INFO, msg, t); } /** @@ -282,38 +289,38 @@ public class ApplicationLogger implements org.slf4j.Logger { } @Override - public boolean isInfoEnabled(Marker marker) { - return isInfoEnabled(); + public boolean isInfoEnabled(Marker marker) { + return enabled(LogLevel.INFO) && logger.isInfoEnabled(marker); } @Override - public void info(Marker marker, String msg) { - info(msg); + public void info(Marker marker, String msg) { + markerLog(LogLevel.INFO, marker, msg); } @Override public void info(Marker marker, String format, Object arg) { - info(format, arg); + markerLog(LogLevel.INFO, marker, format, arg); } @Override public void info(Marker marker, String format, Object arg1, Object arg2) { - info(format, arg1, arg2); + markerLog(LogLevel.INFO, marker, format, arg1, arg2); } @Override public void info(Marker marker, String format, Object... arguments) { - info(format, arguments); + markerLog(LogLevel.INFO, marker, format, arguments); } @Override public void info(Marker marker, String msg, Throwable t) { - info(msg, t); + markerLog(LogLevel.INFO, marker, msg, t); } @Override - public boolean isWarnEnabled() { - return this.level.log(LogLevel.WARN); + public boolean isWarnEnabled() { + return enabled(LogLevel.WARN) && logger.isWarnEnabled(); } @Override @@ -337,38 +344,38 @@ public class ApplicationLogger implements org.slf4j.Logger { } @Override - public void warn(String msg, Throwable t) { - warn("%s%n%s", msg, getStackTrace(t.getStackTrace())); + public void warn(String msg, Throwable t) { + log(LogLevel.WARN, msg, t); } @Override - public boolean isWarnEnabled(Marker marker) { - return isWarnEnabled(); + public boolean isWarnEnabled(Marker marker) { + return enabled(LogLevel.WARN) && logger.isWarnEnabled(marker); } @Override - public void warn(Marker marker, String msg) { - warn(msg); + public void warn(Marker marker, String msg) { + markerLog(LogLevel.WARN, marker, msg); } @Override public void warn(Marker marker, String format, Object arg) { - warn(format, arg); + markerLog(LogLevel.WARN, marker, format, arg); } @Override public void warn(Marker marker, String format, Object arg1, Object arg2) { - warn(format, arg1, arg2); + markerLog(LogLevel.WARN, marker, format, arg1, arg2); } @Override public void warn(Marker marker, String format, Object... arguments) { - warn(format, arguments); + markerLog(LogLevel.WARN, marker, format, arguments); } @Override public void warn(Marker marker, String msg, Throwable t) { - warn(msg, t); + markerLog(LogLevel.WARN, marker, msg, t); } /** @@ -399,8 +406,8 @@ public class ApplicationLogger implements org.slf4j.Logger { } @Override - public boolean isErrorEnabled() { - return this.level.log(LogLevel.ERROR); + public boolean isErrorEnabled() { + return enabled(LogLevel.ERROR) && logger.isErrorEnabled(); } @Override @@ -428,8 +435,8 @@ public class ApplicationLogger implements org.slf4j.Logger { } @Override - public void error(String msg, Throwable t) { - error("%s%n%s", msg, getStackTrace(t.getStackTrace())); + public void error(String msg, Throwable t) { + log(LogLevel.ERROR, msg, t); } /** @@ -447,8 +454,8 @@ public class ApplicationLogger implements org.slf4j.Logger { * @param sb String builder to append message to * @param e throwable error to log */ - public void error(Map values, StringBuilder sb, Throwable e) { - error(values, null, sb); + public void error(Map values, StringBuilder sb, Throwable e) { + error(values, null, sb, e); } /** @@ -468,41 +475,38 @@ public class ApplicationLogger implements org.slf4j.Logger { * @param sb string builder to append messages to * @param e Throwable error to log */ - public void error(Map values, String valueFormat, StringBuilder sb, Throwable e) { - msgHelper(LogLevel.ERROR, values, valueFormat, sb); - if (e != null) { - msgHelper(LogLevel.ERROR,"", e); - } + public void error(Map values, String valueFormat, StringBuilder sb, Throwable e) { + log(LogLevel.ERROR, formatValues(values, valueFormat, sb), e); } @Override - public boolean isErrorEnabled(Marker marker) { - return isErrorEnabled(); + public boolean isErrorEnabled(Marker marker) { + return enabled(LogLevel.ERROR) && logger.isErrorEnabled(marker); } @Override - public void error(Marker marker, String msg) { - error(msg); + public void error(Marker marker, String msg) { + markerLog(LogLevel.ERROR, marker, msg); } @Override public void error(Marker marker, String format, Object arg) { - error(format, arg); + markerLog(LogLevel.ERROR, marker, format, arg); } @Override public void error(Marker marker, String format, Object arg1, Object arg2) { - error(format, arg1, arg2); + markerLog(LogLevel.ERROR, marker, format, arg1, arg2); } @Override public void error(Marker marker, String format, Object... arguments) { - error(format, arguments); + markerLog(LogLevel.ERROR, marker, format, arguments); } @Override public void error(Marker marker, String msg, Throwable t) { - error(msg, t); + markerLog(LogLevel.ERROR, marker, msg, t); } /** @@ -510,7 +514,7 @@ public class ApplicationLogger implements org.slf4j.Logger { * @param level log level to use * @param message message to log */ - protected void log(LogLevel level, String message) { + protected void log(LogLevel level, String message) { if (level == null ||this.level == null || level.toInt() < this.level.toInt()) return; switch (level) { @@ -535,7 +539,43 @@ public class ApplicationLogger implements org.slf4j.Logger { default: break; } - } + } + + private boolean enabled(LogLevel messageLevel) { + LogLevel current = this.level; + return current != null && current != LogLevel.OFF && messageLevel.toInt() >= current.toInt(); + } + + protected void log(LogLevel level, String message, Throwable throwable) { + if (level == null || this.level == null || level.toInt() < this.level.toInt()) return; + switch (level) { + case ALL: + case TRACE: logger.trace(message, throwable); break; + case DEBUG: logger.debug(message, throwable); break; + case INFO: logger.info(message, throwable); break; + case WARN: logger.warn(message, throwable); break; + case ERROR: + case FATAL: logger.error(message, throwable); break; + case OFF: + default: break; + } + } + + private void markerLog(LogLevel level, Marker marker, String message, Object... arguments) { + if (level == null || this.level == null || level.toInt() < this.level.toInt()) return; + FormattingTuple tuple = MessageFormatter.arrayFormat(message, arguments); + switch (level) { + case ALL: + case TRACE: logger.trace(marker, tuple.getMessage(), tuple.getThrowable()); break; + case DEBUG: logger.debug(marker, tuple.getMessage(), tuple.getThrowable()); break; + case INFO: logger.info(marker, tuple.getMessage(), tuple.getThrowable()); break; + case WARN: logger.warn(marker, tuple.getMessage(), tuple.getThrowable()); break; + case ERROR: + case FATAL: logger.error(marker, tuple.getMessage(), tuple.getThrowable()); break; + case OFF: + default: break; + } + } /** * Print the exception stack trace to a string @@ -566,11 +606,13 @@ public class ApplicationLogger implements org.slf4j.Logger { * @param valueFormat value format to map values as * @param sb String builder to append messages to */ - private void msgHelper(LogLevel level, Map values, String valueFormat, StringBuilder sb) { - - if (sb == null) { - sb = new StringBuilder(); - } + private void msgHelper(LogLevel level, Map values, String valueFormat, StringBuilder sb) { + log(level, formatValues(values, valueFormat, sb)); + } + + private String formatValues(Map values, String valueFormat, StringBuilder sb) { + if (values == null) throw new IllegalArgumentException("values must not be null"); + if (sb == null) sb = new StringBuilder(); if (valueFormat == null || valueFormat.trim().isEmpty()) { int longest = 0; @@ -590,8 +632,8 @@ public class ApplicationLogger implements org.slf4j.Logger { .map(entry -> String.format(format, entry.getKey(), entry.getValue())) .forEach(builder::append); - log(level, builder.toString()); - } + return builder.toString(); + } /** * Help format a message for logging @@ -599,8 +641,9 @@ public class ApplicationLogger implements org.slf4j.Logger { * @param format The string format to use * @param args Arguments to use in the string format */ - private void msgHelper(LogLevel level, String format, Object...args) { - log(level, String.format(format, args)); + private void msgHelper(LogLevel level, String format, Object...args) { + FormattingTuple tuple = MessageFormatter.arrayFormat(format, args); + log(level, tuple.getMessage(), tuple.getThrowable()); } /** @@ -619,7 +662,7 @@ public class ApplicationLogger implements org.slf4j.Logger { * @param message the message to log * @return the formatted string */ - private String format(String message, LogLevel level) { - return String.format("th%06d [%-5s] [%s] %s", Thread.currentThread().getId(), level, this.name, message); - } + private String format(String message, LogLevel level) { + return String.format("th%06d [%-5s] [%s] %s", Thread.currentThread().threadId(), level, this.name, message); + } } diff --git a/src/main/java/net/locusworks/logger/ApplicationLoggerFactory.java b/src/main/java/net/locusworks/logger/ApplicationLoggerFactory.java index d43d811..11729af 100644 --- a/src/main/java/net/locusworks/logger/ApplicationLoggerFactory.java +++ b/src/main/java/net/locusworks/logger/ApplicationLoggerFactory.java @@ -2,7 +2,7 @@ package net.locusworks.logger; import java.util.List; import java.util.Map; -import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; /*** @@ -12,9 +12,9 @@ import java.util.stream.Collectors; * @date 02/15/2018 */ public class ApplicationLoggerFactory { - private static Map loggers = new TreeMap<>(); - private static LogLevel DEFAULT_LEVEL = LogLevel.ALL; - private static ApplicationLoggerInitializer initializer; + private static final Map loggers = new ConcurrentHashMap<>(); + private static volatile LogLevel defaultLevel = LogLevel.ALL; + private static volatile ApplicationLoggerInitializer initializer; /** * Need to get the log level from the properties file. @@ -24,7 +24,8 @@ public class ApplicationLoggerFactory { */ public static void init(ApplicationLoggerInitializer init) { initializer = init; - DEFAULT_LEVEL = initializer == null ? LogLevel.INFO : initializer.initialize(); + LogLevel initializedLevel = initializer == null ? LogLevel.INFO : initializer.initialize(); + defaultLevel = initializedLevel == null ? LogLevel.INFO : initializedLevel; loadLoggers(); } @@ -60,7 +61,7 @@ public class ApplicationLoggerFactory { * @return */ private static LogLevel getDefaultLevel() { - return DEFAULT_LEVEL; + return defaultLevel; } /** @@ -70,10 +71,8 @@ public class ApplicationLoggerFactory { * @return the logger represented by the given name */ public static ApplicationLogger getLogger(String name, LogLevel level) { - if (!loggers.containsKey(name)) { - loggers.put(name, new ApplicationLogger(name, level)); - } - ApplicationLogger logger = loggers.get(name); + ApplicationLogger logger = loggers.computeIfAbsent(name, + loggerName -> new ApplicationLogger(loggerName, level == null ? getDefaultLevel() : level)); if (initializer != null) { initializer.postGetLogger(logger.getName(), logger.getLogLevel().toName()); } diff --git a/src/main/java/net/locusworks/logger/LineLoggingOutputStream.java b/src/main/java/net/locusworks/logger/LineLoggingOutputStream.java new file mode 100644 index 0000000..b0dd5a3 --- /dev/null +++ b/src/main/java/net/locusworks/logger/LineLoggingOutputStream.java @@ -0,0 +1,60 @@ +package net.locusworks.logger; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.function.Consumer; + +/** Buffers redirected output so each complete UTF-8 line becomes one log event. */ +final class LineLoggingOutputStream extends OutputStream { + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + private final Consumer sink; + + LineLoggingOutputStream(Consumer sink) { + this.sink = sink; + } + + @Override + public synchronized void write(int value) { + if (value == '\n') { + emit(); + } else { + buffer.write(value); + } + } + + @Override + public synchronized void write(byte[] bytes, int offset, int length) { + int end = offset + length; + int start = offset; + for (int index = offset; index < end; index++) { + if (bytes[index] == '\n') { + buffer.write(bytes, start, index - start); + emit(); + start = index + 1; + } + } + buffer.write(bytes, start, end - start); + } + + @Override + public synchronized void flush() { + if (buffer.size() > 0) emit(); + } + + @Override + public void close() throws IOException { + flush(); + super.close(); + } + + private void emit() { + byte[] bytes = buffer.toByteArray(); + buffer.reset(); + int length = bytes.length; + if (length > 0 && bytes[length - 1] == '\r') length--; + sink.accept(new String(bytes, 0, length, UTF_8)); + } +} diff --git a/src/main/java/net/locusworks/logger/LogLevel.java b/src/main/java/net/locusworks/logger/LogLevel.java index ad108c8..d11846a 100644 --- a/src/main/java/net/locusworks/logger/LogLevel.java +++ b/src/main/java/net/locusworks/logger/LogLevel.java @@ -16,10 +16,10 @@ public enum LogLevel { TRACE(1, "TRACE"), ALL(0, "ALL"); - private Integer intLevel; - private String levelName; + private final int intLevel; + private final String levelName; - private LogLevel(Integer intLevel, String levelName) { + LogLevel(int intLevel, String levelName) { this.intLevel = intLevel; this.levelName = levelName; } @@ -41,7 +41,7 @@ public enum LogLevel { * Get the integer representation of the log level * @return intLevel */ - public Integer toInt() { + public int toInt() { return this.intLevel; } @@ -64,7 +64,7 @@ public enum LogLevel { */ public static LogLevel getEnum(Integer level) { for (LogLevel l : values()) { - if (l.toInt().equals(level)) { + if (level != null && l.toInt() == level) { return l; } } @@ -77,6 +77,7 @@ public enum LogLevel { * @return the log level for the name or null if it cant be found */ public static LogLevel getEnum(String levelName) { + if (levelName == null) return null; for (LogLevel l : values()) { if (l.toName().equalsIgnoreCase(levelName)) { return l; @@ -84,4 +85,4 @@ public enum LogLevel { } return null; } -} \ No newline at end of file +} diff --git a/src/main/java/net/locusworks/logger/SystemDebugLogger.java b/src/main/java/net/locusworks/logger/SystemDebugLogger.java index 5a46181..942fd4d 100644 --- a/src/main/java/net/locusworks/logger/SystemDebugLogger.java +++ b/src/main/java/net/locusworks/logger/SystemDebugLogger.java @@ -1,36 +1,15 @@ package net.locusworks.logger; -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; +import java.io.PrintStream; @ApplicationLoggerInfo(name="System.Debug", defaultLevel=LogLevel.DEBUG) public class SystemDebugLogger extends PrintStream { - private ApplicationLogger logger; - public SystemDebugLogger() { this(ApplicationLoggerFactory.getLogger(SystemDebugLogger.class)); } - public SystemDebugLogger(ApplicationLogger logger) { - super(new ByteArrayOutputStream()); - this.logger = logger; - } - - @Override - public void write(byte[] b) { - logger.debug(new String(b, UTF_8)); - } - - @Override - public void write(byte[] b, int off, int len) { - logger.debug(new String(b, off, len, UTF_8)); - } - - @Override - public void write(int b) { - write(new byte[] { (byte)b }); - } -} + public SystemDebugLogger(ApplicationLogger logger) { + super(new LineLoggingOutputStream(logger::debug), true); + } +} diff --git a/src/main/java/net/locusworks/logger/SystemErrLogger.java b/src/main/java/net/locusworks/logger/SystemErrLogger.java index f7a9552..de0936e 100644 --- a/src/main/java/net/locusworks/logger/SystemErrLogger.java +++ b/src/main/java/net/locusworks/logger/SystemErrLogger.java @@ -1,36 +1,15 @@ package net.locusworks.logger; -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; +import java.io.PrintStream; @ApplicationLoggerInfo(name="System.Err", defaultLevel=LogLevel.ERROR) public class SystemErrLogger extends PrintStream { - private ApplicationLogger logger; - public SystemErrLogger() { this(ApplicationLoggerFactory.getLogger(SystemErrLogger.class)); } - public SystemErrLogger(ApplicationLogger logger) { - super(new ByteArrayOutputStream()); - this.logger = logger; - } - - @Override - public void write(byte[] b) { - logger.error(new String(b, UTF_8)); - } - - @Override - public void write(byte[] b, int off, int len) { - logger.error(new String(b, off, len, UTF_8)); - } - - @Override - public void write(int b) { - write(new byte[] { (byte)b }); - } -} + public SystemErrLogger(ApplicationLogger logger) { + super(new LineLoggingOutputStream(logger::error), true); + } +} diff --git a/src/main/java/net/locusworks/logger/SystemOutLogger.java b/src/main/java/net/locusworks/logger/SystemOutLogger.java index bf9e057..f67a136 100644 --- a/src/main/java/net/locusworks/logger/SystemOutLogger.java +++ b/src/main/java/net/locusworks/logger/SystemOutLogger.java @@ -1,36 +1,15 @@ package net.locusworks.logger; -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; +import java.io.PrintStream; @ApplicationLoggerInfo(name="System.Out", defaultLevel=LogLevel.OFF) public class SystemOutLogger extends PrintStream { - private ApplicationLogger logger; - public SystemOutLogger() { this(ApplicationLoggerFactory.getLogger(SystemOutLogger.class)); } - public SystemOutLogger(ApplicationLogger logger) { - super(new ByteArrayOutputStream()); - this.logger = logger; - } - - @Override - public void write(byte[] b) { - logger.info(new String(b, UTF_8)); - } - - @Override - public void write(byte[] b, int off, int len) { - logger.info(new String(b, off, len, UTF_8)); - } - - @Override - public void write(int b) { - write(new byte[] { (byte)b }); - } -} + public SystemOutLogger(ApplicationLogger logger) { + super(new LineLoggingOutputStream(logger::info), true); + } +} diff --git a/src/main/resources/log4j.xml b/src/main/resources/log4j.xml deleted file mode 100644 index f381635..0000000 --- a/src/main/resources/log4j.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - %d{dd-MMM-yyyy HH:mm:ss.SSS} %m%n - - - - - - - - - - - - - - diff --git a/src/test/java/net/locusworks/logger/ApplicationLoggerDelegationTest.java b/src/test/java/net/locusworks/logger/ApplicationLoggerDelegationTest.java new file mode 100644 index 0000000..e078842 --- /dev/null +++ b/src/test/java/net/locusworks/logger/ApplicationLoggerDelegationTest.java @@ -0,0 +1,101 @@ +package net.locusworks.logger; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.*; + +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.Marker; + +class ApplicationLoggerDelegationTest { + @Test + void delegatesFormattingMarkersAndThrowablesAtEveryLevel() { + Logger delegate = mock(Logger.class); + Marker marker = mock(Marker.class); + RuntimeException failure = new RuntimeException("boom"); + when(delegate.getName()).thenReturn("test"); + when(delegate.isTraceEnabled()).thenReturn(true); + when(delegate.isDebugEnabled()).thenReturn(true); + when(delegate.isInfoEnabled()).thenReturn(true); + when(delegate.isWarnEnabled()).thenReturn(true); + when(delegate.isErrorEnabled()).thenReturn(true); + when(delegate.isTraceEnabled(marker)).thenReturn(true); + when(delegate.isDebugEnabled(marker)).thenReturn(true); + when(delegate.isInfoEnabled(marker)).thenReturn(true); + when(delegate.isWarnEnabled(marker)).thenReturn(true); + when(delegate.isErrorEnabled(marker)).thenReturn(true); + + ApplicationLogger logger = new ApplicationLogger("test", LogLevel.ALL, delegate); + assertEquals("test", logger.getName()); + assertTrue(logger.isTraceEnabled()); + assertTrue(logger.isDebugEnabled(marker)); + assertTrue(logger.isInfoEnabled()); + assertTrue(logger.isWarnEnabled(marker)); + assertTrue(logger.isErrorEnabled()); + + logger.trace("trace {}", 1); + logger.trace("trace {} {}", 1, 2); + logger.trace("trace {}", new Object[] {1}); + logger.trace("trace failure", failure); + logger.debug("debug {}", 1); + logger.debug("debug {} {}", 1, 2); + logger.debug("debug {}", new Object[] {1}); + logger.debug("debug failure", failure); + logger.info("info {}", 1); + logger.info("info {} {}", 1, 2); + logger.info("info {}", new Object[] {1}); + logger.info("info failure", failure); + logger.warn("warn {}", 1); + logger.warn("warn {} {}", 1, 2); + logger.warn("warn {}", new Object[] {1}); + logger.warn("warn failure", failure); + logger.error("error {}", 1); + logger.error("error {} {}", 1, 2); + logger.error("error {}", new Object[] {1}); + logger.error("error failure", failure); + logger.error(failure); + + logger.trace(marker, "trace {}", 1); + logger.trace(marker, "trace {} {}", 1, 2); + logger.trace(marker, "trace", new Object[0]); + logger.trace(marker, "trace failure", failure); + logger.debug(marker, "debug {}", 1); + logger.debug(marker, "debug {} {}", 1, 2); + logger.debug(marker, "debug", new Object[0]); + logger.debug(marker, "debug failure", failure); + logger.info(marker, "info {}", 1); + logger.info(marker, "info {} {}", 1, 2); + logger.info(marker, "info", new Object[0]); + logger.info(marker, "info failure", failure); + logger.warn(marker, "warn {}", 1); + logger.warn(marker, "warn {} {}", 1, 2); + logger.warn(marker, "warn", new Object[0]); + logger.warn(marker, "warn failure", failure); + logger.error(marker, "error {}", 1); + logger.error(marker, "error {} {}", 1, 2); + logger.error(marker, "error", new Object[0]); + logger.error(marker, "error failure", failure); + + Map values = new LinkedHashMap<>(); + values.put("a", "one"); + values.put("long", "two"); + logger.info(values); + logger.warn(values, new StringBuilder("prefix")); + logger.error(values, "%s=%s", new StringBuilder(), failure); + + verify(delegate, atLeastOnce()).info(contains("info 1"), org.mockito.ArgumentMatchers.isNull(Throwable.class)); + verify(delegate).error("error failure", failure); + verify(delegate).error(marker, "error failure", failure); + verify(delegate).error(contains("a=one"), same(failure)); + + logger.setLogLevel(LogLevel.OFF); + assertEquals(LogLevel.OFF, logger.getLogLevel()); + assertFalse(logger.isErrorEnabled()); + logger.error("not logged"); + verify(delegate, never()).error(contains("not logged")); + } +} diff --git a/src/test/java/net/locusworks/logger/ApplicationLoggerFactoryTest.java b/src/test/java/net/locusworks/logger/ApplicationLoggerFactoryTest.java new file mode 100644 index 0000000..cb19805 --- /dev/null +++ b/src/test/java/net/locusworks/logger/ApplicationLoggerFactoryTest.java @@ -0,0 +1,51 @@ +package net.locusworks.logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class ApplicationLoggerFactoryTest { + @ApplicationLoggerInfo(name = "annotated-name", defaultLevel = LogLevel.WARN) + private static final class AnnotatedType { } + + @Test + void initializesLoadsConfiguresAndSavesLoggers() { + ApplicationLoggerInitializer initializer = mock(ApplicationLoggerInitializer.class); + when(initializer.initialize()).thenReturn(LogLevel.DEBUG); + ApplicationLoggerFactory.init(initializer); + verify(initializer).loadLoggers(); + + ApplicationLogger annotated = ApplicationLoggerFactory.getLogger(AnnotatedType.class); + assertEquals("annotated-name", annotated.getName()); + assertEquals(LogLevel.WARN, annotated.getLogLevel()); + verify(initializer).postGetLogger("annotated-name", "WARN"); + + ApplicationLoggerFactory.setLogLevels(List.of(new ApplicationLoggerFactory.LoggerInfo("annotated-name", "ERROR"))); + assertEquals(LogLevel.ERROR, annotated.getLogLevel()); + ApplicationLogger added = new ApplicationLogger("added-test", LogLevel.INFO); + ApplicationLoggerFactory.addLogger(added); + assertSame(added, ApplicationLoggerFactory.getLogger("added-test")); + ApplicationLoggerFactory.addLoggers(List.of(new ApplicationLoggerFactory.LoggerInfo("list-test", "TRACE"))); + assertEquals(LogLevel.TRACE, ApplicationLoggerFactory.getLogger("list-test").getLogLevel()); + + ApplicationLoggerFactory.saveLogLevels(); + verify(initializer).saveLogLevels(anyMap()); + ApplicationLoggerFactory.loadLoggers(null); + ApplicationLoggerFactory.saveLogLevels(null); + } + + @Test + void loggerInfoSupportsBeanAccess() { + ApplicationLoggerFactory.LoggerInfo info = new ApplicationLoggerFactory.LoggerInfo(); + info.setLogger("logger"); + info.setLevel("INFO"); + assertEquals("logger", info.getLogger()); + assertEquals("INFO", info.getLevel()); + } +} diff --git a/src/test/java/net/locusworks/logger/ApplicationLoggerTest.java b/src/test/java/net/locusworks/logger/ApplicationLoggerTest.java new file mode 100644 index 0000000..a439957 --- /dev/null +++ b/src/test/java/net/locusworks/logger/ApplicationLoggerTest.java @@ -0,0 +1,65 @@ +package net.locusworks.logger; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; + +class ApplicationLoggerTest { + @Test + void filtersMessagesBelowConfiguredLevel() { + ApplicationLogger logger = new ApplicationLogger("level-test", LogLevel.INFO); + assertTrue(logger.isInfoEnabled()); + assertFalse(logger.isDebugEnabled()); + } + + @Test + void factoryReturnsOneInstanceUnderConcurrency() { + List results = IntStream.range(0, 100).parallel() + .mapToObj(i -> ApplicationLoggerFactory.getLogger("concurrent-test")) + .toList(); + results.forEach(logger -> assertSame(results.get(0), logger)); + } + + @Test + void supportsSlf4jFormattingAndPreservesThrowable() throws Exception { + PrintStream original = System.err; + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (PrintStream capture = new PrintStream(output, true, UTF_8)) { + System.setErr(capture); + ApplicationLogger logger = new ApplicationLogger("format-test", LogLevel.INFO); + IllegalStateException failure = new IllegalStateException("failure-detail"); + logger.info("Hello {}", "world"); + logger.error("Operation failed", failure); + } finally { + System.setErr(original); + } + String logged = output.toString(UTF_8); + assertTrue(logged.contains("Hello world")); + assertTrue(logged.contains("IllegalStateException: failure-detail")); + } + + @Test + void systemStreamBuffersUtf8ByLine() { + CapturingLogger logger = new CapturingLogger(); + try (PrintStream stream = new SystemOutLogger(logger)) { + byte[] text = "héllo\nsecond\n".getBytes(UTF_8); + for (byte value : text) stream.write(value); + } + assertEquals(List.of("héllo", "second"), logger.messages); + } + + private static final class CapturingLogger extends ApplicationLogger { + private final List messages = new ArrayList<>(); + private CapturingLogger() { super("capture", LogLevel.ALL); } + @Override public void info(String message) { messages.add(message); } + } +} diff --git a/src/test/java/net/locusworks/logger/LogLevelTest.java b/src/test/java/net/locusworks/logger/LogLevelTest.java new file mode 100644 index 0000000..73892c2 --- /dev/null +++ b/src/test/java/net/locusworks/logger/LogLevelTest.java @@ -0,0 +1,26 @@ +package net.locusworks.logger; + +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + +class LogLevelTest { + @Test + void convertsNamesAndNumbers() { + for (LogLevel level : LogLevel.values()) { + assertEquals(level, LogLevel.getEnum(level.toInt())); + assertEquals(level, LogLevel.getEnum(level.toName().toLowerCase())); + assertEquals(level.toName(), level.toString()); + } + assertNull(LogLevel.getEnum(99)); + assertNull(LogLevel.getEnum((Integer) null)); + assertNull(LogLevel.getEnum("unknown")); + assertNull(LogLevel.getEnum((String) null)); + } + + @Test + void comparesSeverityAndHandlesNull() { + assertTrue(LogLevel.INFO.log(LogLevel.DEBUG)); + assertFalse(LogLevel.DEBUG.log(LogLevel.INFO)); + assertFalse(LogLevel.INFO.log(null)); + } +} diff --git a/src/test/java/net/locusworks/logger/SystemLoggerTest.java b/src/test/java/net/locusworks/logger/SystemLoggerTest.java new file mode 100644 index 0000000..689c7dc --- /dev/null +++ b/src/test/java/net/locusworks/logger/SystemLoggerTest.java @@ -0,0 +1,35 @@ +package net.locusworks.logger; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.io.PrintStream; +import org.junit.jupiter.api.Test; + +class SystemLoggerTest { + @Test + void routesCompleteLinesToTheirConfiguredLevels() { + ApplicationLogger logger = mock(ApplicationLogger.class); + try (PrintStream out = new SystemOutLogger(logger); + PrintStream err = new SystemErrLogger(logger); + PrintStream debug = new SystemDebugLogger(logger)) { + out.print("out\r\n"); + err.print("err\n"); + debug.print("debug"); + debug.flush(); + } + verify(logger).info("out"); + verify(logger).error("err"); + verify(logger).debug("debug"); + } + + @Test + void preservesUtf8SplitAcrossIndividualWrites() { + ApplicationLogger logger = mock(ApplicationLogger.class); + try (PrintStream out = new SystemOutLogger(logger)) { + for (byte value : "h\u00e9llo\n".getBytes(UTF_8)) out.write(value); + } + verify(logger).info("h\u00e9llo"); + } +}