├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bintray-support.gradle ├── build.gradle ├── dependencies.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ide-support.gradle ├── jacoco-support.gradle ├── opslogger-support ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── equalexperts │ │ └── logging │ │ ├── EnumContractRunner.java │ │ ├── GenerateLogMessageDocumentation.java │ │ ├── LogMessageContractTest.java │ │ └── OpsLoggerTestDouble.java │ └── test │ └── java │ └── com │ └── equalexperts │ └── logging │ └── OpsLoggerTestDoubleTest.java ├── opslogger ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── equalexperts │ │ └── logging │ │ ├── DiagnosticContextSupplier.java │ │ ├── LogMessage.java │ │ ├── OpsLogger.java │ │ ├── OpsLoggerFactory.java │ │ └── impl │ │ ├── ActiveRotationRegistry.java │ │ ├── ActiveRotationSupport.java │ │ ├── AsyncExecutor.java │ │ ├── AsyncOpsLogger.java │ │ ├── AsyncOpsLoggerFactory.java │ │ ├── BasicOpsLogger.java │ │ ├── BasicOpsLoggerFactory.java │ │ ├── Destination.java │ │ ├── DiagnosticContext.java │ │ ├── FileChannelProvider.java │ │ ├── FilesystemStackTraceProcessor.java │ │ ├── InfrastructureFactory.java │ │ ├── LogicalLogRecord.java │ │ ├── OutputStreamDestination.java │ │ ├── PathDestination.java │ │ ├── SimpleStackTraceProcessor.java │ │ ├── StackTraceProcessor.java │ │ └── ThrowableFingerprintCalculator.java │ └── test │ ├── java │ └── com │ │ └── equalexperts │ │ └── logging │ │ ├── OpsLoggerFactoryTest.java │ │ ├── OpsLoggerTest.java │ │ ├── RestoreActiveRotationRegistryFixture.java │ │ ├── RestoreSystemStreamsFixture.java │ │ ├── TempFileFixture.java │ │ └── impl │ │ ├── AbstractFileChannelProviderTest.java │ │ ├── ActiveRotationRegistryTest.java │ │ ├── AsyncExecutorTest.java │ │ ├── AsyncOpsLoggerFactoryTest.java │ │ ├── AsyncOpsLoggerTest.java │ │ ├── BasicOpsLoggerFactoryTest.java │ │ ├── BasicOpsLoggerTest.java │ │ ├── DiagnosticContextTest.java │ │ ├── FileChannelProviderTest.java │ │ ├── FilesystemStackTraceProcessorTest.java │ │ ├── InfrastructureFactoryTest.java │ │ ├── LogicalLogRecordTest.java │ │ ├── OutputStreamDestinationTest.java │ │ ├── PathDestinationTest.java │ │ ├── PrintStreamTestUtils.java │ │ ├── SimpleStackTraceProcessorTest.java │ │ ├── TestPrintStream.java │ │ └── ThrowableFingerprintCalculatorTest.java │ └── resources │ └── applicationContext.xml ├── sample-usage ├── build.gradle └── src │ ├── main │ ├── java │ │ └── uk │ │ │ └── gov │ │ │ └── gds │ │ │ └── performance │ │ │ └── collector │ │ │ ├── ClassThatLogs.java │ │ │ ├── CollectorLogMessages.java │ │ │ ├── Dagger1Main.java │ │ │ ├── Main.java │ │ │ └── SpringMain.java │ └── resources │ │ └── applicationContext.xml │ └── test │ └── java │ └── uk │ └── gov │ └── gds │ └── performance │ └── collector │ ├── ClassThatLogsTest.java │ └── CollectorLogMessagesTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | target 3 | out 4 | 5 | .svn 6 | .gradle 7 | .DS_Store 8 | 9 | *.ipr 10 | *.iml 11 | *.iws 12 | .idea 13 | 14 | .classpath 15 | .project 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | notifications: 5 | slack: 6 | secure: UyGM/gFksa2q28r0G98t9ubNLSdMXrYIK0QbKUX8XKUDry/VHXRBvurAjIAE63QwLISiSTDzl+Hx8opPbdwBmTywsqGIalqArZnIirDvflygQ6Wt2scVWDYiO46P90TngJLYaGZI+UcJKo5opa/RDOqKsAV6q0l52BvGPYM2xz0= 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Original Work Crown Copyright (c) 2014 4 | Portions Copyright (c) 2014 Equal Experts UK, Ltd. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this software except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/EqualExperts/opslogger.svg?branch=master)](https://travis-ci.org/EqualExperts/opslogger) 2 | [ ![Download](https://api.bintray.com/packages/equalexperts/open-source/opslogger/images/download.svg) ](https://bintray.com/equalexperts/open-source/opslogger/_latestVersion) 3 | 4 | ## How to get it 5 | This library is available as a gradle dependency via Jcenter: 6 | 7 | "com.equalexperts:opslogger:0.4.0" 8 | 9 | A support library is also available, which has extra features for testing your code that uses logging, and a tool 10 | to generate documentation. Production code should not depend on the support library. 11 | 12 | "com.equalexperts:opslogger-support:0.4.0" 13 | 14 | 15 | ### Using Maven? 16 | If you use maven, here are the dependency declarations you should use: 17 | 18 | ```xml 19 | 20 | com.equalexperts 21 | opslogger 22 | 0.4.0 23 | 24 | 25 | 26 | com.equalexperts 27 | opslogger-support 28 | 0.4.0 29 | 30 | ``` 31 | 32 | ### Not using Jcenter already? 33 | For maven, see this guide: https://bintray.com/bintray/jcenter# 34 | 35 | For gradle, just add this snippet to your build script: 36 | 37 | ```groovy 38 | repositories { 39 | jcenter() 40 | } 41 | ``` 42 | 43 | ## Documentation 44 | For now, the best documentation is the sample-usage project in this repository. More (and better!) documentation is coming soon. 45 | 46 | ## Getting started with the source code 47 | This project is built with [Gradle](http://www.gradle.org/). We use a feature called the "gradle wrapper" that will automatically install 48 | Gradle if you don't have it already. You can generate an IDE template by typing `./gradlew cleanIdea idea` from the command line. This 49 | will generate project files for [Intellij IDEA](http://www.jetbrains.com/idea/). Build the project and run all unit tests by typing 50 | `./gradlew ci`. You can get information on other tasks by typing `./gradlew tasks`. 51 | 52 | **Note that this software requires Java 8.** 53 | -------------------------------------------------------------------------------- /bintray-support.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'bintray' 2 | apply plugin: 'maven' 3 | 4 | task javadocJar(type: Jar, dependsOn: javadoc) { 5 | classifier = 'javadoc' 6 | from 'build/docs/javadoc' 7 | } 8 | 9 | task sourcesJar(type: Jar) { 10 | from sourceSets.main.allSource 11 | classifier = 'sources' 12 | } 13 | 14 | configurations { archives } 15 | 16 | artifacts { 17 | archives sourcesJar, javadocJar 18 | } 19 | 20 | task createPom << { 21 | pom { 22 | artifactId = project.archivesBaseName 23 | project { 24 | licenses { 25 | license { 26 | name 'The Apache Software License, Version 2.0' 27 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 28 | distribution 'repo' 29 | } 30 | } 31 | } 32 | }.writeTo("$buildDir/pom.xml") 33 | } 34 | 35 | bintray { 36 | if (project.hasProperty('bintrayUser')) { 37 | user = bintrayUser 38 | } 39 | 40 | if (project.hasProperty('bintrayKey')) { 41 | key = bintrayKey 42 | } 43 | configurations = ['archives'] 44 | pkg { 45 | repo = 'open-source' 46 | userOrg = 'equalexperts' 47 | name = getProject().archivesBaseName 48 | licenses = ['Apache-2.0'] 49 | } 50 | } 51 | 52 | bintrayUpload.dependsOn createPom 53 | 54 | install { 55 | description = 'Install artifacts into the local maven repository.' 56 | group = "Maven" 57 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply from: 'ide-support.gradle' 2 | apply from: 'dependencies.gradle' 3 | apply plugin: 'maven' 4 | 5 | task ci(type: GradleBuild) { 6 | description = 'Everything you need to run for continuous integration' 7 | group = "CI" 8 | tasks = ['clean', 'build', 'jacocoTestReport'] 9 | } 10 | 11 | version = "0.4.0" 12 | 13 | buildscript { 14 | repositories { 15 | jcenter() 16 | } 17 | dependencies { 18 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:0.3' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | jcenter() 4 | } 5 | } 6 | 7 | ext { 8 | ////////////////////////// 9 | // TEST DEPENDENCIES 10 | ////////////////////////// 11 | 12 | junit = "junit:junit:4.12" 13 | 14 | dagger = "com.squareup.dagger:dagger:1.2.2" 15 | daggerCompiler = "com.squareup.dagger:dagger-compiler:1.2.2" 16 | hamcrest = "org.hamcrest:hamcrest-all:1.3" 17 | mockito = "org.mockito:mockito-core:1.9.5" 18 | mutabilityDetector = "org.mutabilitydetector:MutabilityDetector:0.9.5" 19 | spring = "org.springframework:spring-context:4.0.4.RELEASE" 20 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EqualExperts/opslogger/a16b54e7050230ff4ebdf8f99e3c2dca4693963d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Sep 20 10:17:57 BST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 165 | if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then 166 | cd "$(dirname "$0")" 167 | fi 168 | 169 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 170 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /ide-support.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | apply plugin: 'idea' 3 | idea.ext.springFacets = [] 4 | ext.xml = { String rawXml -> new XmlParser().parseText(rawXml) } 5 | } 6 | 7 | //register spring facets for all projects with a springLocations property 8 | gradle.taskGraph.whenReady { 9 | allprojects { p -> 10 | if (p.plugins.hasPlugin('java') && p.idea.springFacets) { 11 | List springLocations = [p.idea.springFacets].flatten() 12 | 13 | idea { 14 | module { 15 | iml.withXml { provider -> 16 | Node facetComponent = p.xml '' 17 | 18 | springLocations.each { location -> 19 | def component = p.xml """ 20 | 21 | 22 | 23 | file://\$MODULE_DIR\$/${location} 24 | 25 | 26 | 27 | """ 28 | facetComponent.append(component) 29 | } 30 | 31 | provider.asNode().append(facetComponent) 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | cleanIdeaWorkspace.group = 'IDE' 40 | 41 | idea { 42 | project { 43 | languageLevel = '1.8' 44 | ipr.withXml { provider -> 45 | /* 46 | Automatically enable git or svn integration as appropriate — detects which VCS is used by running status commands 47 | */ 48 | def versionControlMappings = [svn: "svn info", Git: "git status"] 49 | 50 | 51 | def vcs = versionControlMappings.find { 52 | try { 53 | it.value.execute(null, rootProject.projectDir).waitFor() == 0 54 | } catch (Exception ignore) { 55 | return false 56 | } 57 | } 58 | 59 | if (vcs) { 60 | def vcsConfig = provider.asNode().component.find { it.'@name' == 'VcsDirectoryMappings' } 61 | vcsConfig.mapping[0].'@vcs' = vcs.key 62 | } 63 | } 64 | } 65 | 66 | workspace.iws.withXml { provider -> 67 | def runManagerComponent = provider.asNode().component.find { it.'@name' == 'RunManager'} 68 | def defaultJunitConfig = runManagerComponent.configuration.find {(it.'@default' == 'true') && (it.'@type' == 'JUnit')} 69 | 70 | defaultJunitConfig.option.find {it.'@name' == 'WORKING_DIRECTORY'}.'@value' = '$MODULE_DIR$' //working dir should match command line tests 71 | if (System.getenv('JAVA_OPTS')) { 72 | defaultJunitConfig.option.find {it.'@name' == 'VM_PARAMETERS'}.'@value' = System.getenv('JAVA_OPTS') //set java opts correctly for tests 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /jacoco-support.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'jacoco' 2 | 3 | jacoco { 4 | toolVersion = "0.7.5.201505241946" 5 | } -------------------------------------------------------------------------------- /opslogger-support/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | sourceCompatibility = "1.8" 4 | targetCompatibility = "1.8" 5 | 6 | dependencies { 7 | compile junit, mutabilityDetector 8 | compile project(":opslogger") 9 | testCompile mockito 10 | } 11 | 12 | group = 'com.equalexperts' 13 | archivesBaseName = "opslogger-support" 14 | version = rootProject.version 15 | 16 | apply from: rootProject.file("bintray-support.gradle") 17 | -------------------------------------------------------------------------------- /opslogger-support/src/main/java/com/equalexperts/logging/EnumContractRunner.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging; 2 | 3 | import org.junit.runner.Runner; 4 | import org.junit.runners.BlockJUnit4ClassRunner; 5 | import org.junit.runners.Suite; 6 | import org.junit.runners.model.FrameworkField; 7 | import org.junit.runners.model.FrameworkMethod; 8 | import org.junit.runners.model.InitializationError; 9 | 10 | import java.lang.annotation.ElementType; 11 | import java.lang.annotation.Retention; 12 | import java.lang.annotation.RetentionPolicy; 13 | import java.lang.annotation.Target; 14 | import java.util.ArrayList; 15 | import java.util.Collections; 16 | import java.util.List; 17 | 18 | public class EnumContractRunner extends Suite { 19 | 20 | private static final List NO_RUNNERS = Collections.emptyList(); 21 | 22 | private final List runners = new ArrayList<>(); 23 | 24 | public EnumContractRunner(Class klass) throws Throwable { 25 | super(klass, NO_RUNNERS); 26 | 27 | Class> enumClass = getEnumType(klass); 28 | for(Enum e : enumClass.getEnumConstants()) { 29 | runners.add(new TestClassRunnerForEnum(klass, e)); 30 | } 31 | } 32 | 33 | > Class getEnumType(Class klass) { 34 | EnumToTest annotation = klass.getAnnotation(EnumToTest.class); 35 | 36 | if (annotation == null) { 37 | throw new AssertionError("Test class must be annotated with @EnumToTest"); 38 | } 39 | Class potentialEnumClass = annotation.value(); 40 | if (!potentialEnumClass.isEnum()) { 41 | throw new AssertionError("@EnumToTest value() must be an Enum"); 42 | } 43 | 44 | @SuppressWarnings("unchecked") //checked above 45 | Class enumClass = (Class) potentialEnumClass; 46 | return enumClass; 47 | } 48 | 49 | @Override 50 | protected List getChildren() { 51 | return runners; 52 | } 53 | 54 | private class TestClassRunnerForEnum extends BlockJUnit4ClassRunner { 55 | private final Enum enumValue; 56 | 57 | public TestClassRunnerForEnum(Class klass, Enum enumValue) throws InitializationError { 58 | super(klass); 59 | this.enumValue = enumValue; 60 | } 61 | 62 | @Override 63 | protected Object createTest() throws Exception { 64 | Object test = getTestClass().getOnlyConstructor().newInstance(); 65 | getTestClass().getAnnotatedFields(EnumField.class).get(0).getField().set(test, enumValue); 66 | return test; 67 | } 68 | 69 | @Override 70 | protected String getName() { 71 | return enumValue.name(); 72 | } 73 | 74 | @Override 75 | protected String testName(FrameworkMethod method) { 76 | return getName() + "_" + method.getName(); 77 | } 78 | 79 | @Override 80 | protected void validateConstructor(List errors) { 81 | validateZeroArgConstructor(errors); 82 | } 83 | 84 | @Override 85 | protected void validateFields(List errors) { 86 | super.validateFields(errors); 87 | List fields = getTestClass().getAnnotatedFields(EnumField.class); 88 | if (fields.size() != 1) { 89 | errors.add(new Exception("Need exactly one field annotated with @EnumField")); 90 | } 91 | if (!fields.get(0).isPublic()) { 92 | fields.get(0).getField().setAccessible(true); 93 | } 94 | } 95 | } 96 | 97 | @Retention(RetentionPolicy.RUNTIME) 98 | @Target(ElementType.TYPE) 99 | public static @interface EnumToTest { 100 | public Class value(); 101 | } 102 | 103 | @Retention(RetentionPolicy.RUNTIME) 104 | @Target(ElementType.FIELD) 105 | public static @interface EnumField { 106 | 107 | } 108 | } -------------------------------------------------------------------------------- /opslogger-support/src/main/java/com/equalexperts/logging/GenerateLogMessageDocumentation.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.File; 5 | import java.io.FileWriter; 6 | import java.io.PrintWriter; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | 13 | public class GenerateLogMessageDocumentation { 14 | 15 | public static void main(String... args) throws Exception { 16 | String head = args[0]; 17 | String[] tail = Arrays.copyOfRange(args, 1, args.length); 18 | new GenerateLogMessageDocumentation(head, tail).generate(); 19 | } 20 | 21 | private final File outputFile; 22 | private final List classFoldersToDocument; 23 | 24 | GenerateLogMessageDocumentation(String outputFile, String... classFoldersToDocument) { 25 | this.outputFile = new File(outputFile); 26 | this.classFoldersToDocument = Arrays.asList(classFoldersToDocument); 27 | } 28 | 29 | public void generate() throws Exception { 30 | 31 | try(PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(outputFile)))) { 32 | for(String it : classFoldersToDocument) { 33 | Holder logMessageImplementationFound = new Holder<>(false); 34 | Path classFolder = Paths.get(it); 35 | Files.walk(classFolder) 36 | .filter(p -> !Files.isDirectory(p)) 37 | .filter(p -> p.toString().endsWith(".class")) 38 | .map(p -> loadClass(classFolder, p)) 39 | .filter(this::isValidClass) 40 | .peek(c -> logMessageImplementationFound.set(true)) 41 | .onClose(() -> { 42 | if (!logMessageImplementationFound.get()) { 43 | throw new RuntimeException("No LogMessage implementations found in " + classFolder.toString()); 44 | } 45 | }) 46 | .forEach(c -> documentLogMessageEnum(out, c)); 47 | } 48 | } 49 | } 50 | 51 | private Class loadClass(Path classFolder, Path classFile) { 52 | Path relativePath = classFolder.relativize(classFile); 53 | String className = toClassName(relativePath); 54 | 55 | try { 56 | return Class.forName(className); 57 | } catch (ClassNotFoundException e) { 58 | //this is an inner class, or not a class, etc 59 | return null; 60 | } 61 | } 62 | 63 | private String toClassName(Path relativePath) { 64 | String classNameWithDotClassExtension = relativePath.toString().replace('/', '.').replace('$', '.'); 65 | return classNameWithDotClassExtension.substring(0, (classNameWithDotClassExtension.length() - ".class".length())); 66 | } 67 | 68 | private void documentLogMessageEnum(PrintWriter out, Class clazz) { 69 | out.print(clazz.getName()); 70 | out.println(":"); 71 | out.println("Code\t\tMessage"); 72 | out.println("==========\t=========="); 73 | for(Object o : clazz.getEnumConstants()) { 74 | LogMessage message = (LogMessage) o; 75 | out.printf("%s\t%s\n", message.getMessageCode(), message.getMessagePattern()); 76 | } 77 | out.println(); 78 | } 79 | 80 | private boolean isValidClass(Class clazz) { 81 | return clazz != null && clazz.isEnum() && LogMessage.class.isAssignableFrom(clazz); 82 | } 83 | 84 | private static class Holder { 85 | private T instance; 86 | 87 | private Holder(T instance) { 88 | this.instance = instance; 89 | } 90 | 91 | public void set(T value) { 92 | instance = value; 93 | } 94 | 95 | public T get() { 96 | return instance; 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /opslogger-support/src/main/java/com/equalexperts/logging/LogMessageContractTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | 6 | import java.util.Comparator; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.concurrent.atomic.LongAdder; 11 | import java.util.function.Function; 12 | import java.util.stream.Collectors; 13 | 14 | import static com.equalexperts.logging.EnumContractRunner.EnumField; 15 | import static java.util.Arrays.stream; 16 | import static java.util.function.Function.identity; 17 | import static org.junit.Assert.assertEquals; 18 | import static org.junit.Assert.assertNotNull; 19 | import static org.junit.Assert.fail; 20 | import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; 21 | import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; 22 | 23 | @RunWith(EnumContractRunner.class) 24 | public abstract class LogMessageContractTest & LogMessage> { 25 | 26 | @SuppressWarnings("UnusedDeclaration") 27 | @EnumField 28 | private T enumValue; 29 | 30 | @Test 31 | public void getMessageCode_shouldReturnAUniqueValue() throws Exception { 32 | if (enumValue.getMessageCode() == null) { 33 | return; //don't test duplication for null values — too complicated anyway 34 | } 35 | if (enumValue.getMessageCode().equals("")) { 36 | return; //don't test duplication for empty string values — too complicated anyway 37 | } 38 | 39 | List otherLogMessagesWithThisCode = stream(enumValue.getDeclaringClass().getEnumConstants()) 40 | .filter(t -> t != enumValue) 41 | .filter(t -> enumValue.getMessageCode().equalsIgnoreCase(t.getMessageCode())) 42 | .map(this::formatForErrorMessage) 43 | .collect(Collectors.toList()); 44 | 45 | if (!otherLogMessagesWithThisCode.isEmpty()) { 46 | fail(enumValue.name() + " has the same code as " + String.join(",", otherLogMessagesWithThisCode)); 47 | } 48 | } 49 | 50 | @Test 51 | public void getMessageCode_shouldNotBeNull() throws Exception { 52 | assertNotNull(enumValue.getMessageCode()); 53 | } 54 | 55 | @Test 56 | public void getMessageCode_shouldNotBeEmptyString() throws Exception { 57 | if("".equals(enumValue.getMessageCode())) { 58 | fail("A proper message code is required"); 59 | } 60 | } 61 | 62 | @Test 63 | public void getMessagePattern_shouldNotBeNull() throws Exception { 64 | assertNotNull(enumValue.getMessagePattern()); 65 | } 66 | 67 | @Test 68 | public void getMessagePattern_shouldNotBeAnEmptyString() throws Exception { 69 | if("".equals(enumValue.getMessagePattern())) { 70 | fail("A proper message pattern is required"); 71 | } 72 | } 73 | 74 | @Test 75 | public void enumInstance_shouldBeImmutable() throws Exception { 76 | assertInstancesOf(enumValue.getClass(), areImmutable()); 77 | } 78 | 79 | @Test 80 | public void messageCodes_shouldAllBeTheSameLength() throws Exception { 81 | int mostCommonLength = getMostCommonLength(enumValue.getDeclaringClass().getEnumConstants()); 82 | assertEquals(enumValue.name() + " has a different length than " + mostCommonLength + ", the most common length", mostCommonLength, enumValue.getMessageCode().length()); 83 | } 84 | 85 | private int getMostCommonLength(T[] constants) { 86 | 87 | //calculate a histogram of messageCodeLengths 88 | Map codeLengthHistogram = stream(constants) 89 | .map(c -> c.getMessageCode().length()) 90 | .collect(Collectors.groupingBy(identity(), Collectors.counting())); 91 | 92 | //most common count 93 | return codeLengthHistogram.entrySet().stream() 94 | .max(Comparator.comparingInt(e -> e.getValue().intValue())) 95 | .map(Map.Entry::getKey) 96 | .get(); 97 | } 98 | 99 | private String formatForErrorMessage(T value) { 100 | return value.getClass().getSimpleName() + "." + value.name(); 101 | } 102 | } -------------------------------------------------------------------------------- /opslogger-support/src/main/java/com/equalexperts/logging/OpsLoggerTestDouble.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging; 2 | 3 | import org.hamcrest.CoreMatchers; 4 | import org.mutabilitydetector.AnalysisResult; 5 | import org.mutabilitydetector.Configurations; 6 | import org.mutabilitydetector.IsImmutable; 7 | import org.mutabilitydetector.locations.Dotted; 8 | 9 | import java.io.IOException; 10 | import java.util.*; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.function.Function; 13 | 14 | import static org.hamcrest.CoreMatchers.anyOf; 15 | import static org.junit.Assert.assertNotNull; 16 | import static org.junit.Assert.assertThat; 17 | import static org.mutabilitydetector.unittesting.MutabilityAssert.assertInstancesOf; 18 | import static org.mutabilitydetector.unittesting.MutabilityMatchers.areEffectivelyImmutable; 19 | import static org.mutabilitydetector.unittesting.MutabilityMatchers.areImmutable; 20 | 21 | /** 22 | * An OpsLogger implementation that validates arguments, but doesn't actually 23 | * log anything. Very useful for unit tests. 24 | */ 25 | public class OpsLoggerTestDouble & LogMessage> implements OpsLogger { 26 | private final Function, OpsLogger> nestedLoggerDecorator; 27 | private final Map, OpsLogger> nestedLoggers = new ConcurrentHashMap<>(); 28 | 29 | public OpsLoggerTestDouble() { 30 | this(Function.identity()); 31 | } 32 | 33 | OpsLoggerTestDouble(Function, OpsLogger> nestedLoggerDecorator) { 34 | this.nestedLoggerDecorator = nestedLoggerDecorator; 35 | } 36 | 37 | public static & LogMessage> OpsLogger withSpyFunction(Function, OpsLogger> spyFunction) { 38 | return spyFunction.apply(new OpsLoggerTestDouble<>(spyFunction)); 39 | } 40 | 41 | @Override 42 | public void log(T message, Object... details) { 43 | validate(message); 44 | ensureImmutableDetails(details); 45 | checkForTooManyFormatStringArguments(message.getMessagePattern(), details); 46 | validateFormatString(message.getMessagePattern(), details); 47 | } 48 | 49 | @Override 50 | public void logThrowable(T message, Throwable cause, Object... details) { 51 | validate(message); 52 | ensureImmutableDetails(details); 53 | assertNotNull("Throwable instance must be provided", cause); 54 | checkForTooManyFormatStringArguments(message.getMessagePattern(), details); 55 | validateFormatString(message.getMessagePattern(), details); 56 | } 57 | 58 | @Override 59 | public void close() throws IOException { 60 | throw new IllegalStateException("OpsLogger instances should not be closed by application code."); 61 | } 62 | 63 | @Override 64 | public OpsLogger with(DiagnosticContextSupplier contextSupplier) { 65 | return nestedLoggers.computeIfAbsent(contextSupplier.getMessageContext(), k -> createNestedLogger()); 66 | } 67 | 68 | Function, OpsLogger> getNestedLoggerDecorator() { 69 | return nestedLoggerDecorator; 70 | } 71 | 72 | private OpsLogger createNestedLogger() { 73 | return nestedLoggerDecorator.apply(new OpsLoggerTestDouble<>(nestedLoggerDecorator)); 74 | } 75 | 76 | private void validateFormatString(String pattern, Object... details) { 77 | //noinspection ResultOfMethodCallIgnored 78 | String.format(pattern, details); 79 | } 80 | 81 | private void checkForTooManyFormatStringArguments(String pattern, Object... details) { 82 | if (details.length > 1) { 83 | /* 84 | Check for too many arguments by removing one, and expecting "not enough arguments" to happen. 85 | */ 86 | try { 87 | //noinspection ResultOfMethodCallIgnored 88 | String.format(pattern, Arrays.copyOfRange(details, 0, details.length - 1)); 89 | throw new IllegalArgumentException("Too many format string arguments provided"); 90 | } catch (MissingFormatArgumentException expected) { 91 | //expected 92 | } 93 | } 94 | } 95 | 96 | private void validate(T message) { 97 | assertNotNull("LogMessage must be provided", message); 98 | assertNotNull("MessageCode must be provided", message.getMessageCode()); 99 | assertThat("MessageCode must be provided", message.getMessageCode(), CoreMatchers.not("")); 100 | assertNotNull("MessagePattern must be provided", message.getMessagePattern()); 101 | assertThat("MessagePattern must be provided", message.getMessagePattern(), CoreMatchers.not("")); 102 | } 103 | 104 | private void ensureImmutableDetails(Object... details) { 105 | for (Object o : details) { 106 | Class aClass = o.getClass(); 107 | if (!IMMUTABLE_CLASSES_FROM_THE_JDK.contains(aClass)) { 108 | assertInstancesOf(aClass, anyOf(areEffectivelyImmutable(), areImmutable())); 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * The immutability detector maintains this list, but itself only uses it on fields. 115 | * We need to pass these classes themselves. 116 | */ 117 | private static final Set IMMUTABLE_CLASSES_FROM_THE_JDK; 118 | 119 | static { 120 | try { 121 | Set temp = new HashSet<>(); 122 | for (Map.Entry entry : Configurations.JDK_CONFIGURATION.hardcodedResults().entrySet()) { 123 | if (entry.getValue().isImmutable == IsImmutable.IMMUTABLE) { 124 | temp.add(Class.forName(entry.getKey().toString())); 125 | } 126 | } 127 | IMMUTABLE_CLASSES_FROM_THE_JDK = Collections.unmodifiableSet(temp); 128 | } catch (ClassNotFoundException e) { 129 | throw new RuntimeException(e); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /opslogger-support/src/test/java/com/equalexperts/logging/OpsLoggerTestDoubleTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.mockito.ArgumentCaptor; 6 | import org.mockito.Captor; 7 | import org.mockito.Mock; 8 | import org.mockito.MockitoAnnotations; 9 | import org.mutabilitydetector.unittesting.MutabilityAssertionError; 10 | 11 | import java.util.*; 12 | import java.util.function.Function; 13 | 14 | import static org.hamcrest.core.StringContains.containsString; 15 | import static org.junit.Assert.*; 16 | import static org.mockito.Mockito.*; 17 | 18 | public class OpsLoggerTestDoubleTest { 19 | private final OpsLogger logger = new OpsLoggerTestDouble<>(); 20 | 21 | @Captor 22 | private ArgumentCaptor> captor; 23 | 24 | @Mock 25 | private Function, OpsLogger> mockSpyFunction; 26 | 27 | @Before 28 | public void setup() { 29 | MockitoAnnotations.initMocks(this); 30 | } 31 | 32 | //region tests for log 33 | 34 | @Test 35 | public void log_shouldAllowValidCalls() throws Exception { 36 | logger.log(TestMessages.Foo); 37 | } 38 | 39 | @Test 40 | public void log_shouldThrowAnException_givenAnInvalidFormatStringWithTheRightArguments() throws Exception { 41 | try { 42 | logger.log(TestMessages.BadFormatString, 42); 43 | fail("expected an exception"); 44 | } catch (IllegalFormatException e) { 45 | //this exception is expected 46 | } 47 | } 48 | 49 | @Test 50 | public void log_shouldThrowAnException_whenNotEnoughFormatStringArgumentsAreProvided() throws Exception { 51 | try { 52 | logger.log(TestMessages.Bar); 53 | fail("expected an exception"); 54 | } catch (IllegalFormatException e) { 55 | //this exception is expected 56 | } 57 | } 58 | 59 | @Test 60 | public void log_shouldThrowAnExceptionWhenTooManyFormatStringArgumentsAreProvided() throws Exception { 61 | try { 62 | logger.log(TestMessages.Bar, "Foo", "Bar"); 63 | fail("expected an exception"); 64 | } catch (IllegalArgumentException e) { 65 | //this exception is expected 66 | assertEquals("Too many format string arguments provided", e.getMessage()); 67 | } 68 | } 69 | 70 | @Test 71 | public void log_shouldCorrectlyAllowLogMessagesWithTwoOrMoreFormatStringArguments() throws Exception { 72 | logger.log(TestMessages.MessageWithMultipleArguments, "Foo", "Bar"); 73 | } 74 | 75 | @Test 76 | public void log_shouldAllowALogMessageWithAUUIDAsAnArgument() throws Exception { 77 | logger.log(TestMessages.Bar, UUID.randomUUID()); 78 | } 79 | 80 | @Test 81 | public void log_shouldThrowAnException_givenANullLogMessage() throws Exception { 82 | try { 83 | logger.log(null); 84 | fail("expected an exception"); 85 | } catch (AssertionError e) { 86 | assertThat(e.getMessage(), containsString("LogMessage must be provided")); 87 | } 88 | } 89 | 90 | @Test 91 | public void log_shouldThrowAnException_givenANullMessageCode() throws Exception { 92 | try { 93 | logger.log(TestMessages.InvalidNullCode); 94 | fail("expected an exception"); 95 | } catch (AssertionError e) { 96 | assertThat(e.getMessage(), containsString("MessageCode must be provided")); 97 | } 98 | } 99 | 100 | @Test 101 | public void log_shouldThrowAnException_givenAnEmptyMessageCode() throws Exception { 102 | try { 103 | logger.log(TestMessages.InvalidEmptyCode); 104 | fail("expected an exception"); 105 | } catch (AssertionError e) { 106 | assertThat(e.getMessage(), containsString("MessageCode must be provided")); 107 | } 108 | } 109 | 110 | @Test 111 | public void log_shouldThrowAnException_givenANullMessagePattern() throws Exception { 112 | try { 113 | logger.log(TestMessages.InvalidNullFormat); 114 | fail("expected an exception"); 115 | } catch (AssertionError e) { 116 | assertThat(e.getMessage(), containsString("MessagePattern must be provided")); 117 | } 118 | } 119 | 120 | @Test 121 | public void log_shouldThrowAnException_givenAnEmptyMessagePattern() throws Exception { 122 | try { 123 | logger.log(TestMessages.InvalidEmptyFormat); 124 | fail("expected an exception"); 125 | } catch (AssertionError e) { 126 | assertThat(e.getMessage(), containsString("MessagePattern must be provided")); 127 | } 128 | } 129 | 130 | @Test 131 | public void log_shouldThrowAnException_givenAMutableFormatStringArgument() throws Exception { 132 | try { 133 | logger.log(TestMessages.MessageWithMultipleArguments, "foo", new StringBuilder("bar")); 134 | fail("expected an exception"); 135 | } catch (MutabilityAssertionError e) { 136 | assertThat(e.getMessage(), containsString("StringBuilder")); 137 | } 138 | } 139 | 140 | @Test 141 | public void log_shouldNotCallAnOverloadedMethod() throws Exception { 142 | //calling another method inside this log method can cause trouble with spying frameworks 143 | OpsLogger logger = spy(this.logger); 144 | 145 | logger.log(TestMessages.Foo); 146 | 147 | verify(logger).log(TestMessages.Foo); 148 | verifyNoMoreInteractions(logger); 149 | } 150 | 151 | //endregion 152 | 153 | //region tests for logThrowable 154 | 155 | @Test 156 | public void logThrowable_shouldAllowValidCalls_givenAThrowable() throws Exception { 157 | logger.logThrowable(TestMessages.Foo, new RuntimeException()); 158 | } 159 | 160 | @Test 161 | public void logThrowable_shouldThrowAnException_givenAnInvalidFormatStringWithTheRightArgumentsAndAThrowable() throws Exception { 162 | try { 163 | logger.logThrowable(TestMessages.BadFormatString, new RuntimeException(), 42); 164 | fail("expected an exception"); 165 | } catch (IllegalFormatException e) { 166 | //this exception is expected 167 | } 168 | } 169 | 170 | @Test 171 | public void logThrowable_shouldThrowAnException_givenNotEnoughFormatStringArgumentsAndAThrowable() throws Exception { 172 | 173 | try { 174 | logger.logThrowable(TestMessages.Bar, new RuntimeException()); 175 | fail("expected an exception"); 176 | } catch (IllegalFormatException e) { 177 | //this exception is expected 178 | } 179 | } 180 | 181 | @Test 182 | public void logThrowable_shouldThrowAnException_givenTooManyFormatStringArgumentsAndAThrowable() throws Exception { 183 | 184 | try { 185 | logger.logThrowable(TestMessages.Bar, new RuntimeException(), "Foo", "Bar"); 186 | fail("expected an exception"); 187 | } catch (IllegalArgumentException e) { 188 | //this exception is expected 189 | assertEquals("Too many format string arguments provided", e.getMessage()); 190 | } 191 | } 192 | 193 | @Test 194 | public void logThrowable_shouldAllowCorrectLogMessages_givenTwoOrMoreFormatStringArgumentsAndThrowable() throws Exception { 195 | logger.logThrowable(TestMessages.MessageWithMultipleArguments, new RuntimeException(), "Foo", "Bar"); 196 | } 197 | 198 | @Test 199 | public void logThrowable_shouldThrowAnException_givenANullLogMessageAndThrowable() throws Exception { 200 | try { 201 | logger.logThrowable(null, new RuntimeException()); 202 | fail("expected an exception"); 203 | } catch (AssertionError e) { 204 | assertThat(e.getMessage(), containsString("LogMessage must be provided")); 205 | } 206 | } 207 | 208 | @Test 209 | public void logThrowable_shouldThrowAnException_givenANullThrowable() throws Exception { 210 | try { 211 | logger.logThrowable(TestMessages.Bar, null, "a"); 212 | fail("expected an exception"); 213 | } catch (AssertionError e) { 214 | assertThat(e.getMessage(), containsString("Throwable instance must be provided")); 215 | } 216 | } 217 | 218 | @Test 219 | public void logThrowable_shouldThrowAnException_givenAThrowableAndNullMessageCode() throws Exception { 220 | try { 221 | logger.logThrowable(TestMessages.InvalidNullCode, new RuntimeException()); 222 | fail("expected an exception"); 223 | } catch (AssertionError e) { 224 | assertThat(e.getMessage(), containsString("MessageCode must be provided")); 225 | } 226 | } 227 | 228 | @Test 229 | public void logThrowable_shouldThrowAnException_givenAThrowableAndAnEmptyMessageCode() throws Exception { 230 | try { 231 | logger.logThrowable(TestMessages.InvalidEmptyCode, new RuntimeException()); 232 | fail("expected an exception"); 233 | } catch (AssertionError e) { 234 | assertThat(e.getMessage(), containsString("MessageCode must be provided")); 235 | } 236 | } 237 | 238 | @Test 239 | public void logThrowable_shouldThrowAnException_givenAThrowableAndNullMessageFormat() throws Exception { 240 | try { 241 | logger.logThrowable(TestMessages.InvalidNullFormat, new RuntimeException()); 242 | fail("expected an exception"); 243 | } catch (AssertionError e) { 244 | assertThat(e.getMessage(), containsString("MessagePattern must be provided")); 245 | } 246 | } 247 | 248 | @Test 249 | public void logThrowable_shouldThrowAnException_givenAThrowableAndAnEmptyMessageFormat() throws Exception { 250 | try { 251 | logger.logThrowable(TestMessages.InvalidEmptyFormat, new RuntimeException()); 252 | fail("expected an exception"); 253 | } catch (AssertionError e) { 254 | assertThat(e.getMessage(), containsString("MessagePattern must be provided")); 255 | } 256 | } 257 | 258 | @Test 259 | public void logThrowable_shouldThrowAnException_givenAThrowableAndAMutableFormatStringArgument() throws Exception { 260 | try { 261 | logger.logThrowable(TestMessages.MessageWithMultipleArguments, new RuntimeException(), "foo", new StringBuilder("bar")); 262 | fail("expected an exception"); 263 | } catch (MutabilityAssertionError e) { 264 | assertThat(e.getMessage(), containsString("StringBuilder")); 265 | } 266 | } 267 | 268 | @Test 269 | public void logThrowable_shouldNotCallAnOverloadedMethod_givenAThrowable() throws Exception { 270 | //calling another method inside this log method can cause trouble with spying frameworks 271 | OpsLogger logger = spy(this.logger); 272 | RuntimeException ex = new RuntimeException(); 273 | 274 | logger.logThrowable(TestMessages.Foo, ex); 275 | 276 | verify(logger).logThrowable(TestMessages.Foo, ex); 277 | verifyNoMoreInteractions(logger); 278 | } 279 | 280 | //endregion 281 | 282 | //region tests for withSpyFunction 283 | @SuppressWarnings("unchecked") 284 | @Test 285 | public void withSpyFunction_shouldReturnAnOpsLoggerTestDoubleWrappedByTheSpyFunction() throws Exception { 286 | OpsLogger expectedResult = (OpsLogger) mock(OpsLogger.class); 287 | doReturn(expectedResult).when(mockSpyFunction).apply(any(OpsLoggerTestDouble.class)); 288 | 289 | OpsLogger result = OpsLoggerTestDouble.withSpyFunction(mockSpyFunction); 290 | 291 | //noinspection unchecked 292 | verify(mockSpyFunction).apply(captor.capture()); 293 | verifyNoMoreInteractions(mockSpyFunction); 294 | assertSame(expectedResult, result); 295 | assertEquals(mockSpyFunction, captor.getValue().getNestedLoggerDecorator()); 296 | } 297 | //endregion 298 | 299 | //region tests for with 300 | @SuppressWarnings("unchecked") 301 | @Test 302 | public void with_shouldReturnANestedOpsLoggerTestDoubleWrappedByTheSpyFunction() throws Exception { 303 | OpsLogger expectedResult = (OpsLogger) mock(OpsLogger.class); 304 | doReturn(expectedResult).when(mockSpyFunction).apply(any(OpsLoggerTestDouble.class)); 305 | 306 | OpsLoggerTestDouble logger = new OpsLoggerTestDouble<>(mockSpyFunction); 307 | 308 | 309 | OpsLogger result = logger.with(Collections::emptyMap); 310 | 311 | //noinspection unchecked 312 | verify(mockSpyFunction).apply(captor.capture()); 313 | verifyNoMoreInteractions(mockSpyFunction); 314 | assertSame(expectedResult, result); 315 | assertEquals(mockSpyFunction, captor.getValue().getNestedLoggerDecorator()); 316 | } 317 | 318 | @Test 319 | public void with_shouldReturnTheSameNestedLogger_givenAnEquivalentContextSupplier() throws Exception { 320 | OpsLoggerTestDouble logger = new OpsLoggerTestDouble<>(Function.identity()); 321 | 322 | Map context = new HashMap<>(); 323 | context.put("foo", "bar"); 324 | 325 | HashMap equivalentContext = new HashMap<>(context); //a different context with the same contents 326 | 327 | DiagnosticContextSupplier supplier = () -> context; 328 | DiagnosticContextSupplier equivalentSupplier = () -> equivalentContext; 329 | 330 | assertNotEquals("precondition: suppliers should not be equal", supplier, equivalentSupplier); 331 | 332 | assertSame(logger.with(supplier), logger.with(equivalentSupplier)); 333 | } 334 | 335 | @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") 336 | @Test 337 | public void with_shouldReturnADifferentNestedLogger_givenADifferentContextSupplier() throws Exception { 338 | OpsLoggerTestDouble logger = new OpsLoggerTestDouble<>(Function.identity()); 339 | 340 | Map context = new HashMap<>(); 341 | context.put("foo", "bar"); 342 | 343 | HashMap differentContext = new HashMap<>(); 344 | differentContext.put("foo", "baz"); 345 | 346 | DiagnosticContextSupplier supplier = () -> context; 347 | DiagnosticContextSupplier differentSupplier = () -> differentContext; 348 | 349 | assertNotSame(logger.with(supplier), logger.with(differentSupplier)); 350 | } 351 | //endregion 352 | 353 | @Test 354 | public void close_shouldThrowAnException() throws Exception { 355 | /* 356 | Application code shouldn't normally close a real logger, so throw an Exception in the test double 357 | to discourage it 358 | */ 359 | try { 360 | logger.close(); 361 | fail("Expected an exception"); 362 | } catch (IllegalStateException e) { 363 | assertThat(e.getMessage(), containsString("OpsLogger instances should not be closed by application code.")); 364 | } 365 | } 366 | 367 | enum TestMessages implements LogMessage { 368 | Foo("CODE-Foo", "No Fields"), 369 | Bar("CODE-Bar", "One Field: %s"), 370 | BadFormatString("CODE-BFS", "%++d"), 371 | InvalidNullCode(null, "Blah"), 372 | InvalidEmptyCode("", "Blah"), 373 | InvalidNullFormat("CODE-InvalidNullFormat", null), 374 | InvalidEmptyFormat("CODE-InvalidEmptyFormat", ""), 375 | MessageWithMultipleArguments("CODE-MultipleArguments", "Multiple Format String Arguments: %s %s"); 376 | 377 | //region LogMessage implementation guts 378 | private final String messageCode; 379 | private final String messagePattern; 380 | 381 | TestMessages(String messageCode, String messagePattern) { 382 | this.messageCode = messageCode; 383 | this.messagePattern = messagePattern; 384 | } 385 | 386 | @Override 387 | public String getMessageCode() { 388 | return messageCode; 389 | } 390 | 391 | @Override 392 | public String getMessagePattern() { 393 | return messagePattern; 394 | } 395 | 396 | //endregion 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /opslogger/.gitignore: -------------------------------------------------------------------------------- 1 | /test.log -------------------------------------------------------------------------------- /opslogger/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply from: rootProject.file("jacoco-support.gradle") 3 | 4 | sourceCompatibility = "1.8" 5 | targetCompatibility = "1.8" 6 | 7 | dependencies { 8 | testCompile junit, hamcrest, mockito 9 | testCompile dagger, daggerCompiler, spring 10 | } 11 | 12 | group = 'com.equalexperts' 13 | archivesBaseName = 'opslogger' 14 | version = rootProject.version 15 | 16 | apply from: rootProject.file("bintray-support.gradle") 17 | 18 | idea.springFacets << 'src/test/resources/applicationContext.xml' 19 | 20 | javadoc { 21 | exclude "com/equalexperts/logging/impl/*" 22 | } -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/DiagnosticContextSupplier.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | *

Provides contextual information for log messages.

7 | * 8 | *

This is a set of name value pairs that should be 9 | * added to every log message when available. 10 | * Two common examples of contextual information are the current username 11 | * and the current request id.

12 | * 13 | *

Contextual name-value pairs are only added to log messages when the value is not null or empty.

14 | *

For example, in the case of username, a username=XXX name value pair would not be added to log messages 15 | * generated by anonymous users, or to background tasks that could not be easily associated with a user.

16 | */ 17 | @FunctionalInterface 18 | public interface DiagnosticContextSupplier { 19 | /** 20 | *

Provide contextual information about the current log message.

21 | * 22 | *

This method should generally return a different map each time it is called. 23 | * 24 | *

A common implementation strategy is to access thread-local information about the current request to build an 25 | * appropriate map of name-value pairs for each individual request.

26 | * 27 | * @return a map of name-value pairs 28 | */ 29 | Map getMessageContext(); 30 | } -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/LogMessage.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging; 2 | 3 | /** 4 | * To be implemented by enumerations of log messages in applications. 5 | */ 6 | public interface LogMessage { 7 | /** 8 | * @return unique code for this particular log message. Example "QA001" 9 | */ 10 | 11 | String getMessageCode(); 12 | 13 | /** 14 | * @return message pattern for this log message. Example "%d out of %d processed." 15 | */ 16 | 17 | String getMessagePattern(); 18 | } 19 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/OpsLogger.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging; 2 | 3 | import com.equalexperts.logging.impl.ActiveRotationRegistry; 4 | 5 | /** 6 | *

OpsLogger is the interface used to log messages by the application. Instances are usually constructed as singletons 7 | * and injected into application classes via the application's usual dependency injection mechanism.

8 | * 9 | *

All instances produced by {@link OpsLoggerFactory#build()} are thread-safe, and can be safely 10 | * accessed by multiple threads.

11 | */ 12 | public interface OpsLogger & LogMessage> extends AutoCloseable { 13 | /** 14 | * Log message using message.getMessagePattern as the format and details as the format arguments. 15 | * @param message enum to log 16 | * @param details format string arguments to message.getMessagePattern() 17 | */ 18 | void log(T message, Object... details); 19 | 20 | /** 21 | * Log message using message.getMessagePattern as the format and details as the format arguments, with 22 | * the processed cause added. 23 | * @param message enum to log 24 | * @param cause stack trace to process and include in the log message 25 | * @param details format string arguments to message.getMessagePattern() 26 | */ 27 | void logThrowable(T message, Throwable cause, Object... details); 28 | 29 | /** 30 | * Create a nested logger that uses a local DiagnosticContextSupplier, which is often 31 | * convenient during parallel stream processing. 32 | * 33 | * Note: OpsLogger instances created by calling this method should not be closed, 34 | * and will ignore calls to the close() method. 35 | * 36 | * @param override a supplier for a local diagnostic context 37 | * @return a new OpsLogger instance which will use the provided DiagnosticContextSupplier instead of the global one 38 | */ 39 | OpsLogger with(DiagnosticContextSupplier override); 40 | 41 | /** 42 | * Refreshes file handles for all log files, providing active rotation support. 43 | * This method should be called between rotating the original file, and manipulating (archiving, compressing, etc) 44 | * it. The postRotate block in logRotate is an excellent example of when to use this method. 45 | * 46 | * This method will not return until all writing to old file handles has completed. 47 | * 48 | * Exposing this method via JMX or an administrative API some kind is the intended use case. 49 | */ 50 | static void refreshFileHandles() { 51 | ActiveRotationRegistry.getSingletonInstance().refreshFileHandles(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/OpsLoggerFactory.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging; 2 | 3 | import com.equalexperts.logging.impl.AsyncOpsLoggerFactory; 4 | import com.equalexperts.logging.impl.BasicOpsLoggerFactory; 5 | import com.equalexperts.logging.impl.InfrastructureFactory; 6 | 7 | import java.io.PrintStream; 8 | import java.io.UncheckedIOException; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.util.Map; 12 | import java.util.Objects; 13 | import java.util.Optional; 14 | import java.util.function.Consumer; 15 | import java.util.function.Supplier; 16 | 17 | /** 18 | *

Constructs OpsLogger instances.

19 | * 20 | *

Instances of this class are not thread-safe, and should not be accessed by multiple 21 | * threads.

22 | * 23 | * @see OpsLogger 24 | */ 25 | public class OpsLoggerFactory { 26 | 27 | private Optional loggerOutput = Optional.empty(); 28 | private Optional logfilePath = Optional.empty(); 29 | 30 | private boolean async = false; 31 | private Optional storeStackTracesInFilesystem = Optional.empty(); 32 | private Optional stackTraceStoragePath = Optional.empty(); 33 | private Optional> errorHandler = Optional.empty(); 34 | private Optional contextSupplier = Optional.empty(); 35 | 36 | private Optional> cachedInstance = Optional.empty(); 37 | 38 | private AsyncOpsLoggerFactory asyncOpsLoggerFactory = new AsyncOpsLoggerFactory(); 39 | private BasicOpsLoggerFactory basicOpsLoggerFactory = new BasicOpsLoggerFactory(); 40 | 41 | /** 42 | * The destination for the log strings. A typical value is System.out. 43 | * @param printStream destination 44 | * @return this for further configuration 45 | */ 46 | public OpsLoggerFactory setDestination(PrintStream printStream) { 47 | validateParametersForSetDestination(printStream); 48 | clearCachedInstance(); 49 | loggerOutput = Optional.of(printStream); 50 | logfilePath = Optional.empty(); 51 | return this; 52 | } 53 | 54 | /** 55 | * The path of the file to print the log strings to. Is closed and reopened frequently to allow outside log rotation to work. 56 | * The path is used as-is. 57 | * @param path path for log file 58 | * @return this for further configuration 59 | */ 60 | public OpsLoggerFactory setPath(Path path) { 61 | validateParametersForSetPath(path); 62 | clearCachedInstance(); 63 | logfilePath = Optional.of(path).map(Path::toAbsolutePath); 64 | loggerOutput = Optional.empty(); 65 | return this; 66 | } 67 | 68 | /** 69 | *

Should stack traces be placed in individual files or printed along with the log statements?

70 | *

If called with true, each unique stack trace will placed in its own file. (see setStackTraceStoragePath). 71 | * If called with false, stack traces will be printed to main log file.

72 | * @param store true for separate files, false for inlined in log file 73 | * @return this for further configuration 74 | */ 75 | public OpsLoggerFactory setStoreStackTracesInFilesystem(boolean store) { 76 | clearCachedInstance(); 77 | storeStackTracesInFilesystem = Optional.of(store); 78 | if (!store) { 79 | stackTraceStoragePath = Optional.empty(); 80 | } 81 | return this; 82 | } 83 | 84 | /** 85 | *

Path to directory to contain individual stack trace files.

86 | *

87 | * Must be called with a valid path corresponding to a directory, where stack traces will be stored 88 | * (see setStoreStackTracesInFilesystem). If the directory does not exist, it will be created.

89 | * @param directory valid path for target directory 90 | * @return this for further configuration 91 | */ 92 | public OpsLoggerFactory setStackTraceStoragePath(Path directory) { 93 | validateParametersForSetStackTraceStoragePath(directory); 94 | clearCachedInstance(); 95 | setStoreStackTracesInFilesystem(true); 96 | stackTraceStoragePath = Optional.of(directory); 97 | return this; 98 | } 99 | 100 | /** 101 | *

Handler for when exceptions occur when logging.

102 | *

103 | * If any exception is thrown "inside" this logger, it will caught and be passed on to this error handler, 104 | * which then is responsible for any further error handling. The log message causing the error, will not 105 | * be processed further.

106 | * @param handler Consumer of Throwables handleling any exception encountered. 107 | * @return this for further configuration 108 | */ 109 | public OpsLoggerFactory setErrorHandler(Consumer handler) { 110 | clearCachedInstance(); 111 | errorHandler = Optional.ofNullable(handler); 112 | return this; 113 | } 114 | 115 | /** 116 | *

This method will be removed in a future release.

117 | * 118 | * @param supplier the map supplier. (for example: ()->map) 119 | * @deprecated Replaced by {@link #setGlobalDiagnosticContextSupplier(DiagnosticContextSupplier)}. 120 | * @return this for further configuration 121 | */ 122 | @Deprecated 123 | public OpsLoggerFactory setCorrelationIdSupplier(Supplier> supplier) { 124 | return setGlobalDiagnosticContextSupplier(supplier != null ? supplier::get : null); 125 | } 126 | 127 | /** 128 | *

Set the supplier of the map to print for each log entry.

129 | *

The correlation id map is printed out as part of the message logged:

130 | *

Example code: (where setGlobalDiagnosticContextSupplier(()->map) has been invoked in the OpsLoggerFactory 131 | * invocation, and Failure has the message code "FOO-012345")

132 | *
133 |      * map.put("A", "113");
134 |      * logger.log(Failure, new RuntimeException("Argh"));
135 |      * 
136 | * will give 137 | *
138 |      * 2014-10-22T10:59:19.891Z,A=113,FOO-012345,Did not do anything. java.lang.RuntimeException: Argh (file:///tmp/stacktraces/stacktrace_7rSxGtIroLrznTg8bt1BrQ.txt)
139 |      * 
140 | * @param supplier the context supplier. (for example: ()->map) 141 | * @return this for further configuration 142 | */ 143 | public OpsLoggerFactory setGlobalDiagnosticContextSupplier(DiagnosticContextSupplier supplier) { 144 | clearCachedInstance(); 145 | this.contextSupplier = Optional.ofNullable(supplier); 146 | return this; 147 | } 148 | 149 | /** 150 | * Enable/disable asynchronous logging. 151 | * 152 | * When disabled, the log(...) method call does not return until the message has been written to the target 153 | * file/output stream. 154 | * 155 | * When enabled, the log(...) method call pushes the log message object to an internal queue, and returns 156 | * immediately. The queue is emptied in order by a background thread. This can be very useful for keeping response 157 | * times low, but risks losing the log message objects still in the queue if the Java Virtual Machine is for any 158 | * reason abruptly terminated. 159 | * 160 | * If this method is not called, asynchronous logging is disabled. 161 | * 162 | * @param async true=async, false=sync 163 | * @return this for further configuration 164 | */ 165 | public OpsLoggerFactory setAsync(boolean async) { 166 | clearCachedInstance(); 167 | this.async = async; 168 | return this; 169 | } 170 | 171 | /** 172 | * Build and return the OpsLogger corresponding to the configuration provided. 173 | * 174 | * Calling build multiple times on a single instance of this class without 175 | * changing the configuration (by calling a set method) will return the 176 | * same OpsLogger instance each time. 177 | * 178 | * @param LogMessage enum of all possible logger objects. 179 | * @return ready to use OpsLogger 180 | * @throws UncheckedIOException if a problem occurs creating parent directories for log files and/or stack traces 181 | */ 182 | @SuppressWarnings("unchecked") 183 | public & LogMessage> OpsLogger build() throws UncheckedIOException { 184 | if (!cachedInstance.isPresent()) { 185 | cachedInstance = Optional.of(buildNewInstance()); 186 | } 187 | return (OpsLogger) cachedInstance.get(); 188 | } 189 | 190 | private & LogMessage> OpsLogger buildNewInstance() throws UncheckedIOException { 191 | InfrastructureFactory infrastructureFactory = new InfrastructureFactory(logfilePath, loggerOutput, storeStackTracesInFilesystem, stackTraceStoragePath, contextSupplier, errorHandler); 192 | if (async) { 193 | return asyncOpsLoggerFactory.build(infrastructureFactory); 194 | } 195 | return basicOpsLoggerFactory.build(infrastructureFactory); 196 | } 197 | 198 | private void clearCachedInstance() { 199 | cachedInstance = Optional.empty(); 200 | } 201 | 202 | private void validateParametersForSetDestination(PrintStream destination) { 203 | Objects.requireNonNull(destination, "Destination must not be null"); 204 | } 205 | 206 | private void validateParametersForSetStackTraceStoragePath(Path directory) { 207 | Objects.requireNonNull(directory, "path must not be null"); 208 | if (Files.exists(directory) && !Files.isDirectory(directory)) { 209 | throw new IllegalArgumentException("path must be a directory"); 210 | } 211 | } 212 | 213 | private void validateParametersForSetPath(Path path) { 214 | Objects.requireNonNull(path, "path must not be null"); 215 | if (Files.isDirectory(path)) { 216 | throw new IllegalArgumentException("Path must not be a directory"); 217 | } 218 | } 219 | 220 | //region test hooks for spying on internal factories 221 | 222 | void setAsyncOpsLoggerFactory(AsyncOpsLoggerFactory asyncOpsLoggerFactory) { 223 | this.asyncOpsLoggerFactory = asyncOpsLoggerFactory; 224 | } 225 | 226 | void setBasicOpsLoggerFactory(BasicOpsLoggerFactory basicOpsLoggerFactory) { 227 | this.basicOpsLoggerFactory = basicOpsLoggerFactory; 228 | } 229 | 230 | //endregion 231 | } -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/ActiveRotationRegistry.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import java.util.Set; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | 6 | public class ActiveRotationRegistry { 7 | 8 | private static ActiveRotationRegistry singletonInstance = new ActiveRotationRegistry(); 9 | 10 | private final Set registeredInstances = ConcurrentHashMap.newKeySet(); 11 | 12 | public static ActiveRotationRegistry getSingletonInstance() { 13 | return singletonInstance; 14 | } 15 | 16 | public static void setSingletonInstance(ActiveRotationRegistry newRegistry) { 17 | singletonInstance = newRegistry; 18 | } 19 | 20 | public T add(T instance) { 21 | registeredInstances.add(instance); 22 | return instance; 23 | } 24 | 25 | public void remove(ActiveRotationSupport instance) { 26 | registeredInstances.remove(instance); 27 | } 28 | 29 | public boolean contains(ActiveRotationSupport instance) { 30 | return registeredInstances.contains(instance); 31 | } 32 | 33 | public void refreshFileHandles() { 34 | registeredInstances.forEach(ActiveRotationRegistry::safelyCallPostRotate); 35 | } 36 | 37 | private static void safelyCallPostRotate(ActiveRotationSupport instance) { 38 | try { 39 | instance.refreshFileHandles(); 40 | } catch (InterruptedException e) { 41 | throw new RuntimeException(e); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/ActiveRotationSupport.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | public interface ActiveRotationSupport { 4 | void refreshFileHandles() throws InterruptedException; 5 | } 6 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/AsyncExecutor.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import java.util.concurrent.Future; 5 | import java.util.concurrent.ThreadFactory; 6 | 7 | /** 8 | * Like an {@link java.util.concurrent.Executor Executor}, except that the execute method 9 | * returns a {@link java.util.concurrent.Future Future}, and execution is guaranteed to be 10 | * executed asynchronously. 11 | * 12 | * Spawns a new thread when @{link execute execute} is called, and the various methods of the 13 | * Future can be used to control the thread or wait for it to finish. 14 | * 15 | * This is more testable than directly manipulating threads ({@link java.lang.Thread Thread} 16 | * has final methods that can't be intercepted by normal mocking libraries), and is easier than 17 | * dealing with a full {@link java.util.concurrent.ExecutorService ExecutorService}, which has to be 18 | * stopped and is perhaps more useful with a stream of small tasks than a single long-running one. 19 | * 20 | * @see java.util.concurrent.Executor 21 | * @see java.util.concurrent.Future 22 | */ 23 | public class AsyncExecutor { 24 | private final ThreadFactory factory; 25 | 26 | public AsyncExecutor(ThreadFactory factory) { 27 | this.factory = factory; 28 | } 29 | 30 | public Future execute(Runnable runnable) { 31 | CompletableFuture result = new CompletableFuture() { 32 | @Override 33 | public boolean cancel(boolean mayInterruptIfRunning) { 34 | throw new UnsupportedOperationException("Calling thread.stop is deprecated. Arrange termination directly with the runnable"); 35 | } 36 | }; 37 | factory.newThread(() -> { 38 | try { 39 | runnable.run(); 40 | } finally { 41 | result.complete(null); 42 | } 43 | }).start(); 44 | return result; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/AsyncOpsLogger.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.DiagnosticContextSupplier; 4 | import com.equalexperts.logging.LogMessage; 5 | import com.equalexperts.logging.OpsLogger; 6 | 7 | import java.time.Clock; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Optional; 11 | import java.util.concurrent.Future; 12 | import java.util.concurrent.LinkedTransferQueue; 13 | import java.util.function.Consumer; 14 | 15 | import static java.util.stream.Collectors.toList; 16 | 17 | /** 18 | * Asynchronous OpsLogger which puts the record to be logged in a transferQueue and 19 | * returns immediately which allows for better performance at the expense of not 20 | * necessarily having everything logged if the JVM shuts down unexpectedly. 21 | * A background thread is responsible for emptying the transferQueue. 22 | */ 23 | 24 | public class AsyncOpsLogger & LogMessage> implements OpsLogger { 25 | 26 | static final int MAX_BATCH_SIZE = 100; 27 | private final Future processingThread; 28 | private final LinkedTransferQueue>> transferQueue; 29 | private final Clock clock; 30 | private final DiagnosticContextSupplier diagnosticContextSupplier; 31 | private final Destination destination; 32 | private final Consumer errorHandler; 33 | private final boolean closeable; 34 | 35 | public AsyncOpsLogger(Clock clock, DiagnosticContextSupplier diagnosticContextSupplier, Destination destination, Consumer errorHandler, LinkedTransferQueue>> transferQueue, AsyncExecutor executor) { 36 | this.clock = clock; 37 | this.diagnosticContextSupplier = diagnosticContextSupplier; 38 | this.destination = destination; 39 | this.errorHandler = errorHandler; 40 | this.transferQueue = transferQueue; 41 | processingThread = executor.execute(this::process); 42 | this.closeable = true; 43 | } 44 | 45 | private AsyncOpsLogger(Clock clock, DiagnosticContextSupplier diagnosticContextSupplier, Destination destination, Consumer errorHandler, LinkedTransferQueue>> transferQueue, Future processingThread, boolean closeable) { 46 | this.clock = clock; 47 | this.diagnosticContextSupplier = diagnosticContextSupplier; 48 | this.destination = destination; 49 | this.errorHandler = errorHandler; 50 | this.transferQueue = transferQueue; 51 | this.processingThread = processingThread; 52 | this.closeable = closeable; 53 | } 54 | 55 | @Override 56 | public void log(T message, Object... details) { 57 | try { 58 | 59 | DiagnosticContext diagnosticContext = new DiagnosticContext(diagnosticContextSupplier); 60 | LogicalLogRecord record = new LogicalLogRecord<>(clock.instant(), diagnosticContext, message, Optional.empty(), details); 61 | transferQueue.put(Optional.of(record)); 62 | } catch (Throwable t) { 63 | errorHandler.accept(t); 64 | } 65 | } 66 | 67 | @Override 68 | public void logThrowable(T message, Throwable cause, Object... details) { 69 | try { 70 | DiagnosticContext diagnosticContext = new DiagnosticContext(diagnosticContextSupplier); 71 | LogicalLogRecord record = new LogicalLogRecord<>(clock.instant(), diagnosticContext, message, Optional.of(cause), details); 72 | transferQueue.put(Optional.of(record)); 73 | } catch (Throwable t) { 74 | errorHandler.accept(t); 75 | } 76 | } 77 | 78 | @Override 79 | public AsyncOpsLogger with(DiagnosticContextSupplier override) { 80 | return new AsyncOpsLogger<>(clock, override, destination, errorHandler, transferQueue, processingThread, false); 81 | } 82 | 83 | @Override 84 | public void close() throws Exception { 85 | if (closeable) { 86 | try { 87 | transferQueue.put(Optional.empty()); //an empty optional is the shutdown signal 88 | processingThread.get(); 89 | } finally { 90 | destination.close(); 91 | } 92 | } 93 | } 94 | 95 | private void process() { 96 | /* 97 | An empty optional on the queue is the shutdown signal 98 | */ 99 | boolean run = true; 100 | do { 101 | try { 102 | List>> messages = waitForNextBatch(); 103 | List> logRecords = messages.stream() 104 | .filter(Optional::isPresent) 105 | .map(Optional::get) 106 | .collect(toList()); 107 | 108 | if (logRecords.size() < messages.size()) { 109 | run = false; //shutdown signal detected 110 | } 111 | processBatch(logRecords); 112 | } catch (Throwable t) { 113 | errorHandler.accept(t); 114 | } 115 | } while (run); 116 | } 117 | 118 | private void processBatch(List> batch) throws Exception { 119 | if (batch.isEmpty()) { 120 | return; 121 | } 122 | destination.beginBatch(); 123 | for (LogicalLogRecord record : batch) { 124 | try { 125 | destination.publish(record); 126 | } catch (Throwable t) { 127 | errorHandler.accept(t); 128 | } 129 | } 130 | destination.endBatch(); 131 | } 132 | 133 | private List>> waitForNextBatch() throws InterruptedException { 134 | List>> result = new ArrayList<>(); 135 | result.add(transferQueue.take()); //a blocking operation 136 | transferQueue.drainTo(result, MAX_BATCH_SIZE - 1); 137 | return result; 138 | } 139 | 140 | public Clock getClock() { 141 | return clock; 142 | } 143 | 144 | public Destination getDestination() { 145 | return destination; 146 | } 147 | 148 | public DiagnosticContextSupplier getDiagnosticContextSupplier() { 149 | return diagnosticContextSupplier; 150 | } 151 | 152 | public Consumer getErrorHandler() { 153 | return errorHandler; 154 | } 155 | 156 | public LinkedTransferQueue>> getTransferQueue() { 157 | return transferQueue; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/AsyncOpsLoggerFactory.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.DiagnosticContextSupplier; 4 | import com.equalexperts.logging.LogMessage; 5 | 6 | import java.io.IOException; 7 | import java.time.Clock; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.LinkedTransferQueue; 10 | import java.util.function.Consumer; 11 | 12 | public class AsyncOpsLoggerFactory { 13 | 14 | private AsyncExecutor asyncExecutor = new AsyncExecutor(Executors.defaultThreadFactory()); 15 | 16 | public & LogMessage> AsyncOpsLogger build(InfrastructureFactory infrastructureFactory) { 17 | DiagnosticContextSupplier diagnosticContextSupplier = infrastructureFactory.configureContextSupplier(); 18 | Consumer errorHandler = infrastructureFactory.configureErrorHandler(); 19 | Destination destination = infrastructureFactory.configureDestination(); 20 | return new AsyncOpsLogger<>(Clock.systemUTC(), diagnosticContextSupplier, destination, errorHandler, new LinkedTransferQueue<>(), asyncExecutor); 21 | } 22 | 23 | void setAsyncExecutor(AsyncExecutor asyncExecutor) { 24 | this.asyncExecutor = asyncExecutor; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/BasicOpsLogger.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.DiagnosticContextSupplier; 4 | import com.equalexperts.logging.LogMessage; 5 | import com.equalexperts.logging.OpsLogger; 6 | 7 | import java.time.Clock; 8 | import java.util.Optional; 9 | import java.util.concurrent.locks.Lock; 10 | import java.util.function.Consumer; 11 | 12 | /** OpsLogger which writes each entry directly to the Destination */ 13 | 14 | public class BasicOpsLogger & LogMessage> implements OpsLogger { 15 | 16 | private final Clock clock; 17 | private final Consumer errorHandler; 18 | private final Destination destination; 19 | private final Lock lock; 20 | private final DiagnosticContextSupplier diagnosticContextSupplier; 21 | private final boolean closeable; 22 | 23 | public BasicOpsLogger(Clock clock, DiagnosticContextSupplier diagnosticContextSupplier, Destination destination, Lock lock, Consumer errorHandler) { 24 | this(clock, diagnosticContextSupplier, destination, lock, errorHandler, true); 25 | } 26 | 27 | private BasicOpsLogger(Clock clock, DiagnosticContextSupplier diagnosticContextSupplier, Destination destination, Lock lock, Consumer errorHandler, boolean closeable) { 28 | this.clock = clock; 29 | this.diagnosticContextSupplier = diagnosticContextSupplier; 30 | this.destination = destination; 31 | this.lock = lock; 32 | this.errorHandler = errorHandler; 33 | this.closeable = closeable; 34 | } 35 | 36 | @Override 37 | public void close() throws Exception { 38 | if (closeable) { 39 | destination.close(); 40 | } 41 | } 42 | 43 | @Override 44 | public void log(T message, Object... details) { 45 | try { 46 | LogicalLogRecord record = constructLogRecord(message, Optional.empty(), details); 47 | publish(record); 48 | } catch (Throwable t) { 49 | errorHandler.accept(t); 50 | } 51 | } 52 | 53 | @Override 54 | public void logThrowable(T message, Throwable cause, Object... details) { 55 | try { 56 | LogicalLogRecord record = constructLogRecord(message, Optional.of(cause), details); 57 | publish(record); 58 | } catch (Throwable t) { 59 | errorHandler.accept(t); 60 | } 61 | } 62 | 63 | @Override 64 | public BasicOpsLogger with(DiagnosticContextSupplier override) { 65 | return new BasicOpsLogger<>(clock, override, destination, lock, errorHandler, false); 66 | } 67 | 68 | private LogicalLogRecord constructLogRecord(T message, Optional o, Object... details) { 69 | return new LogicalLogRecord<>(clock.instant(), new DiagnosticContext(diagnosticContextSupplier), message, o, details); 70 | } 71 | 72 | private void publish(LogicalLogRecord record) throws Exception { 73 | lock.lock(); 74 | try { 75 | destination.beginBatch(); 76 | try { 77 | destination.publish(record); 78 | } finally { 79 | destination.endBatch(); 80 | } 81 | } finally { 82 | lock.unlock(); 83 | } 84 | } 85 | 86 | public Clock getClock() { 87 | return clock; 88 | } 89 | 90 | public Destination getDestination() { 91 | return destination; 92 | } 93 | 94 | public DiagnosticContextSupplier getDiagnosticContextSupplier() { 95 | return diagnosticContextSupplier; 96 | } 97 | 98 | public Lock getLock() { 99 | return lock; 100 | } 101 | 102 | public Consumer getErrorHandler() { return errorHandler; } 103 | } 104 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/BasicOpsLoggerFactory.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.DiagnosticContextSupplier; 4 | import com.equalexperts.logging.LogMessage; 5 | 6 | import java.io.IOException; 7 | import java.time.Clock; 8 | import java.util.concurrent.locks.ReentrantLock; 9 | import java.util.function.Consumer; 10 | 11 | public class BasicOpsLoggerFactory { 12 | 13 | public & LogMessage> BasicOpsLogger build(InfrastructureFactory infrastructureFactory) { 14 | DiagnosticContextSupplier correlationIdSupplier = infrastructureFactory.configureContextSupplier(); 15 | Consumer errorHandler = infrastructureFactory.configureErrorHandler(); 16 | Destination destination = infrastructureFactory.configureDestination(); 17 | return new BasicOpsLogger<>(Clock.systemUTC(), correlationIdSupplier, destination, new ReentrantLock(), errorHandler); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/Destination.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.LogMessage; 4 | 5 | public interface Destination & LogMessage> extends AutoCloseable { 6 | void beginBatch() throws Exception; 7 | 8 | void publish(LogicalLogRecord record) throws Exception; 9 | 10 | void endBatch() throws Exception; 11 | 12 | StackTraceProcessor getStackTraceProcessor(); 13 | } 14 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/DiagnosticContext.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.DiagnosticContextSupplier; 4 | 5 | import java.util.Collections; 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | import java.util.Objects; 9 | 10 | import static java.util.stream.Collectors.joining; 11 | 12 | public class DiagnosticContext { 13 | 14 | private final Map context; 15 | 16 | public DiagnosticContext(DiagnosticContextSupplier supplier) { 17 | if (supplier == null) { 18 | context = Collections.emptyMap(); 19 | } else { 20 | Map rawContext = supplier.getMessageContext(); 21 | if (rawContext == null) { 22 | context = Collections.emptyMap(); 23 | } else { 24 | context = Collections.unmodifiableMap(new LinkedHashMap<>(rawContext)); 25 | } 26 | } 27 | } 28 | 29 | public Map getContext() { 30 | return context; 31 | } 32 | 33 | public void printContextInformation(StringBuilder result) { 34 | String contextInformation = context.entrySet().stream() 35 | .filter(e -> Objects.nonNull(e.getValue())) 36 | .filter(e -> !e.getValue().isEmpty()) 37 | .map(es -> es.getKey() + "=" + es.getValue()) 38 | .collect(joining(";")); 39 | result.append(contextInformation); 40 | if (!contextInformation.isEmpty()) { 41 | result.append(","); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/FileChannelProvider.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | import java.io.Writer; 6 | import java.nio.channels.Channels; 7 | import java.nio.channels.FileChannel; 8 | import java.nio.file.Path; 9 | 10 | import static java.nio.file.StandardOpenOption.APPEND; 11 | import static java.nio.file.StandardOpenOption.CREATE; 12 | 13 | /** Provide a convenience method to get a file channel pointing to a previously opened 14 | * writable file in UTF-8 corresponding to a given Path. 15 | */ 16 | public class FileChannelProvider { 17 | private final Path path; 18 | 19 | public FileChannelProvider(Path path) { 20 | this.path = path; 21 | } 22 | 23 | public Result getChannel() throws IOException { 24 | FileChannel fileChannel = FileChannel.open(path, CREATE, APPEND); 25 | Writer writer = Channels.newWriter(fileChannel, "UTF-8"); 26 | return new Result(fileChannel, writer); 27 | } 28 | 29 | static class Result implements Closeable { 30 | final FileChannel channel; 31 | final Writer writer; 32 | 33 | Result(FileChannel channel, Writer writer) { 34 | this.channel = channel; 35 | this.writer = writer; 36 | } 37 | 38 | @Override 39 | public void close() throws IOException { 40 | this.writer.close(); 41 | } 42 | } 43 | 44 | public Path getPath() { 45 | return path; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/FilesystemStackTraceProcessor.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import java.io.IOException; 4 | import java.io.PrintStream; 5 | import java.nio.file.FileAlreadyExistsException; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | 9 | import static java.nio.file.StandardOpenOption.CREATE_NEW; 10 | import static java.nio.file.StandardOpenOption.WRITE; 11 | 12 | /** 13 | * A stack trace processor that stores the stack trace in a uniquely fingerprinted file in a given destination. 14 | * The URI of the file (whether new or existing) is included in the log message. 15 | */ 16 | public class FilesystemStackTraceProcessor implements StackTraceProcessor { 17 | private final Path destination; 18 | private final ThrowableFingerprintCalculator fingerprintCalculator; 19 | 20 | public FilesystemStackTraceProcessor(Path destination, ThrowableFingerprintCalculator fingerprintCalculator) { 21 | this.destination = destination; 22 | this.fingerprintCalculator = fingerprintCalculator; 23 | } 24 | 25 | @Override 26 | public void process(Throwable throwable, StringBuilder output) throws Exception { 27 | Path stackTraceFile = calculateFilenameForException(throwable); 28 | writeStacktraceToPathIfNecessary(throwable, stackTraceFile); 29 | printSubstituteMessage(output, throwable, stackTraceFile); 30 | } 31 | 32 | public Path getDestination() { 33 | return destination; 34 | } 35 | 36 | private void writeStacktraceToPathIfNecessary(Throwable throwable, Path stackTraceFile) throws IOException { 37 | if (Files.notExists(stackTraceFile)) { 38 | try(PrintStream out = new PrintStream(Files.newOutputStream(stackTraceFile, CREATE_NEW, WRITE))) { 39 | throwable.printStackTrace(out); 40 | } catch (FileAlreadyExistsException ignore) { 41 | //the exception is being written to (probably right now) 42 | } 43 | } 44 | } 45 | 46 | private void printSubstituteMessage(StringBuilder output, Throwable throwable, Path stackTraceFile) { 47 | output.append(throwable.toString()); 48 | output.append(" ("); 49 | output.append(stackTraceFile.toUri().toString()); 50 | output.append(")"); 51 | } 52 | 53 | private Path calculateFilenameForException(Throwable throwable) { 54 | String fingerprint = fingerprintCalculator.calculateFingerprint(throwable); 55 | String filePath = "stacktrace_" + fingerprint + ".txt"; 56 | return destination.resolve(filePath); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/InfrastructureFactory.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.DiagnosticContextSupplier; 4 | import com.equalexperts.logging.LogMessage; 5 | 6 | import java.io.IOException; 7 | import java.io.PrintStream; 8 | import java.io.UncheckedIOException; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.util.Collections; 12 | import java.util.Optional; 13 | import java.util.function.Consumer; 14 | 15 | /** 16 | * Constructs the various non-trivial dependencies that OpsLogger implementations need. 17 | */ 18 | public class InfrastructureFactory { 19 | public static final Consumer DEFAULT_ERROR_HANDLER = (error) -> error.printStackTrace(System.err); 20 | public static final DiagnosticContextSupplier EMPTY_CONTEXT_SUPPLIER = Collections::emptyMap; 21 | 22 | private final Optional logfilePath; 23 | private final Optional loggerOutput; 24 | private final Optional storeStackTracesInFilesystem; 25 | private final Optional stackTraceStoragePath; 26 | private final Optional correlationIdSupplier; 27 | private final Optional> errorHandler; 28 | 29 | public InfrastructureFactory(Optional logfilePath, Optional loggerOutput, Optional storeStackTracesInFilesystem, Optional stackTraceStoragePath, Optional correlationIdSupplier, Optional> errorHandler) { 30 | this.logfilePath = logfilePath; 31 | this.loggerOutput = loggerOutput; 32 | this.storeStackTracesInFilesystem = storeStackTracesInFilesystem; 33 | this.stackTraceStoragePath = stackTraceStoragePath; 34 | this.correlationIdSupplier = correlationIdSupplier; 35 | this.errorHandler = errorHandler; 36 | } 37 | 38 | public & LogMessage> Destination configureDestination() throws UncheckedIOException { 39 | try { 40 | StackTraceProcessor stackTraceProcessor = this.configureStackTraceProcessor(); 41 | if (logfilePath.isPresent()) { 42 | if (!Files.isSymbolicLink(logfilePath.get().getParent())) { 43 | Files.createDirectories(logfilePath.get().getParent()); 44 | } 45 | FileChannelProvider provider = new FileChannelProvider(logfilePath.get()); 46 | ActiveRotationRegistry registry = ActiveRotationRegistry.getSingletonInstance(); 47 | return registry.add(new PathDestination<>(provider, stackTraceProcessor, registry)); 48 | } 49 | return new OutputStreamDestination<>(loggerOutput.orElse(System.out), stackTraceProcessor); 50 | } catch (IOException e) { 51 | throw new UncheckedIOException(e); 52 | } 53 | } 54 | 55 | public Consumer configureErrorHandler() { 56 | return errorHandler.orElse(DEFAULT_ERROR_HANDLER); 57 | } 58 | 59 | public DiagnosticContextSupplier configureContextSupplier() { 60 | return correlationIdSupplier.orElse(EMPTY_CONTEXT_SUPPLIER); 61 | } 62 | 63 | private StackTraceProcessor configureStackTraceProcessor() throws IOException { 64 | Optional storagePath = this.determineStackTraceProcessorPath(); 65 | if (storagePath.isPresent()) { 66 | if (!Files.isSymbolicLink(storagePath.get())) { 67 | Files.createDirectories(storagePath.get()); 68 | } 69 | return new FilesystemStackTraceProcessor(storagePath.get(), new ThrowableFingerprintCalculator()); 70 | } 71 | return new SimpleStackTraceProcessor(); 72 | } 73 | 74 | private Optional determineStackTraceProcessorPath() { 75 | if (storeStackTracesInFilesystem.isPresent()) { 76 | //storing stack traces in the filesystem has been explicitly configured 77 | 78 | if (!storeStackTracesInFilesystem.get()) { 79 | return Optional.empty(); //explicitly disabled 80 | } 81 | 82 | if (stackTraceStoragePath.isPresent()) { 83 | //use the explicitly provided location when one is set 84 | return stackTraceStoragePath; 85 | } 86 | 87 | if (!logfilePath.isPresent()) { 88 | throw new IllegalStateException("Cannot store stack traces in the filesystem without providing a path"); 89 | } 90 | } 91 | 92 | //No explicit path provided. Store stack traces in the same directory as the log file, if one is specified. 93 | return logfilePath.map(Path::getParent); 94 | } 95 | 96 | //region test hooks: allow tests to determine the values passed into the constructor 97 | public Optional getLogfilePath() { 98 | return logfilePath; 99 | } 100 | 101 | public Optional getLoggerOutput() { 102 | return loggerOutput; 103 | } 104 | 105 | public Optional getStoreStackTracesInFilesystem() { 106 | return storeStackTracesInFilesystem; 107 | } 108 | 109 | public Optional getStackTraceStoragePath() { 110 | return stackTraceStoragePath; 111 | } 112 | 113 | public Optional getContextSupplier() { 114 | return correlationIdSupplier; 115 | } 116 | 117 | public Optional> getErrorHandler() { 118 | return errorHandler; 119 | } 120 | //endregion 121 | } 122 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/LogicalLogRecord.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.LogMessage; 4 | 5 | import java.time.Instant; 6 | import java.time.format.DateTimeFormatter; 7 | import java.time.format.DateTimeFormatterBuilder; 8 | import java.util.*; 9 | 10 | import static java.util.Objects.requireNonNull; 11 | 12 | public class LogicalLogRecord & LogMessage> { 13 | 14 | private static final DateTimeFormatter ISO_ALWAYS_WITH_MILLISECONDS = new DateTimeFormatterBuilder() 15 | .parseStrict() 16 | .parseCaseInsensitive() 17 | .appendInstant(3) 18 | .toFormatter(); 19 | 20 | private final Instant timestamp; 21 | private final T message; 22 | private final Optional cause; 23 | private final Object[] details; 24 | private final DiagnosticContext diagnosticContext; 25 | 26 | public LogicalLogRecord(Instant timestamp, DiagnosticContext diagnosticContext, T message, Optional cause, Object... details) { 27 | this.timestamp = requireNonNull(timestamp, "parameter timestamp must not be null"); 28 | this.diagnosticContext = requireNonNull(diagnosticContext, "parameter diagnosticContext must not be null"); 29 | this.message = requireNonNull(message, "parameter message must not be null"); 30 | this.cause = requireNonNull(cause, "parameter cause must not be null"); 31 | this.details = requireNonNull(details, "parameter details must not be null"); 32 | } 33 | 34 | public String format(StackTraceProcessor processor) throws Exception { 35 | StringBuilder result = new StringBuilder(); 36 | ISO_ALWAYS_WITH_MILLISECONDS.formatTo(timestamp, result); 37 | result.append(","); 38 | diagnosticContext.printContextInformation(result); 39 | result.append(message.getMessageCode()); 40 | result.append(","); 41 | new Formatter(result).format(message.getMessagePattern(), details); 42 | if (cause.isPresent()) { 43 | result.append(" "); //the gap between the basic message and the stack trace 44 | processor.process(cause.get(), result); 45 | } 46 | return result.toString(); 47 | } 48 | 49 | Instant getTimestamp() { 50 | return timestamp; 51 | } 52 | 53 | DiagnosticContext getDiagnosticContext() { return diagnosticContext; } 54 | 55 | T getMessage() { 56 | return message; 57 | } 58 | 59 | Optional getCause() { 60 | return cause; 61 | } 62 | 63 | Object[] getDetails() { 64 | return details; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/OutputStreamDestination.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.LogMessage; 4 | 5 | import java.io.PrintStream; 6 | 7 | /** 8 | * A Destination which formats LogicalLogRecords with the provided stackTraceProcessor and prints it to output. 9 | * Also knows that if output is System.out or System.err, it should not be closed when done. 10 | */ 11 | 12 | public class OutputStreamDestination & LogMessage> implements Destination { 13 | private final PrintStream output; 14 | private final StackTraceProcessor stackTraceProcessor; 15 | 16 | public OutputStreamDestination(PrintStream output, StackTraceProcessor stackTraceProcessor) { 17 | this.output = output; 18 | this.stackTraceProcessor = stackTraceProcessor; 19 | } 20 | 21 | @Override 22 | public void beginBatch() throws Exception { 23 | 24 | } 25 | 26 | @Override 27 | public void publish(LogicalLogRecord record) throws Exception { 28 | output.println(record.format(stackTraceProcessor)); 29 | } 30 | 31 | @Override 32 | public void endBatch() throws Exception { 33 | 34 | } 35 | 36 | @Override 37 | public void close() throws Exception { 38 | if (!streamIsSpecial()) { 39 | output.close(); 40 | } 41 | } 42 | 43 | private boolean streamIsSpecial() { 44 | return (output == System.out) || (output == System.err); 45 | } 46 | 47 | public PrintStream getOutput() { 48 | return output; 49 | } 50 | 51 | @Override 52 | public StackTraceProcessor getStackTraceProcessor() { 53 | return stackTraceProcessor; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/PathDestination.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.LogMessage; 4 | 5 | import java.io.IOException; 6 | import java.nio.channels.FileLock; 7 | import java.util.concurrent.CountDownLatch; 8 | 9 | /** 10 | * Writes batches of log records to a path. 11 | * 12 | * A file lock is acquired and held during the batch and released afterwards. 13 | * This allows external log rotation to work. 14 | * @param 15 | */ 16 | public class PathDestination & LogMessage> implements Destination, ActiveRotationSupport { 17 | private static final String LINE_SEPARATOR = System.getProperty("line.separator"); 18 | 19 | private final FileChannelProvider provider; 20 | private final StackTraceProcessor processor; 21 | private final ActiveRotationRegistry registry; 22 | private FileChannelProvider.Result currentChannel; 23 | private FileLock currentLock; 24 | private volatile CountDownLatch latch = new CountDownLatch(0); 25 | 26 | public PathDestination(FileChannelProvider provider, StackTraceProcessor processor, ActiveRotationRegistry registry) { 27 | this.provider = provider; 28 | this.processor = processor; 29 | this.registry = registry; 30 | } 31 | 32 | @Override 33 | public void beginBatch() throws Exception { 34 | closeAnyOpenBatch(); 35 | latch = new CountDownLatch(1); 36 | currentChannel = provider.getChannel(); 37 | currentLock = currentChannel.channel.lock(); 38 | } 39 | 40 | @Override 41 | public void publish(LogicalLogRecord record) throws Exception { 42 | String physicalRecord = record.format(processor); 43 | currentChannel.writer.write(physicalRecord + LINE_SEPARATOR); //one call avoids a partial flush 44 | } 45 | 46 | @Override 47 | public void endBatch() throws Exception { 48 | closeAnyOpenBatch(); 49 | } 50 | 51 | private void closeAnyOpenBatch() throws IOException { 52 | latch.countDown(); 53 | if (currentChannel != null) { 54 | currentChannel.writer.flush(); 55 | currentLock.release(); 56 | currentChannel.writer.close(); 57 | currentLock = null; 58 | currentChannel = null; 59 | } 60 | } 61 | 62 | @Override 63 | public void close() throws Exception { 64 | closeAnyOpenBatch(); 65 | registry.remove(this); 66 | } 67 | 68 | @Override 69 | public void refreshFileHandles() throws InterruptedException { 70 | latch.await(); 71 | } 72 | 73 | public FileChannelProvider getProvider() { 74 | return provider; 75 | } 76 | 77 | @Override 78 | public StackTraceProcessor getStackTraceProcessor() { 79 | return processor; 80 | } 81 | 82 | public ActiveRotationRegistry getActiveRotationRegistry() { 83 | return registry; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/SimpleStackTraceProcessor.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import java.io.PrintWriter; 4 | import java.io.StringWriter; 5 | 6 | /** 7 | * A StackTraceProcessor implementation that just includes the entire 8 | * stack trace as a multi-line string. 9 | */ 10 | public class SimpleStackTraceProcessor implements StackTraceProcessor { 11 | @Override 12 | public void process(Throwable throwable, StringBuilder out) { 13 | StringWriter sw = new StringWriter(); 14 | throwable.printStackTrace(new PrintWriter(sw)); 15 | out.append(stripLastCharacter(sw)); 16 | } 17 | 18 | String stripLastCharacter(StringWriter sw) { 19 | String result = sw.toString(); 20 | return result.substring(0, result.length() - 1); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/StackTraceProcessor.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | /** 4 | * A processor that turns a throwable (and stack trace) into a representation 5 | * (normally single-line) suitable for a log file. 6 | */ 7 | public interface StackTraceProcessor { 8 | void process(Throwable throwable, StringBuilder output) throws Exception; 9 | } -------------------------------------------------------------------------------- /opslogger/src/main/java/com/equalexperts/logging/impl/ThrowableFingerprintCalculator.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import java.io.OutputStream; 4 | import java.io.PrintStream; 5 | import java.security.DigestOutputStream; 6 | import java.security.MessageDigest; 7 | import java.security.NoSuchAlgorithmException; 8 | import java.util.Base64; 9 | 10 | public class ThrowableFingerprintCalculator { 11 | private final Base64.Encoder base64Encoder = Base64.getUrlEncoder().withoutPadding(); 12 | 13 | /* Return the Base64 representation of the MD5 hash for the printed stack trace of t. */ 14 | 15 | public String calculateFingerprint(Throwable t) { 16 | MessageDigest md5 = getMD5Instance(); 17 | PrintStream ps = new PrintStream(new DigestOutputStream(new DoNothingOutputStream(), md5)); 18 | t.printStackTrace(ps); 19 | return base64Encoder.encodeToString(md5.digest()); 20 | } 21 | 22 | private MessageDigest getMD5Instance() { 23 | try { 24 | return MessageDigest.getInstance("MD5"); 25 | } catch (NoSuchAlgorithmException ignore) { 26 | throw new AssertionError("Every JDK provides an MD5 MessageDigest implementation"); 27 | } 28 | } 29 | 30 | private static class DoNothingOutputStream extends OutputStream { 31 | @Override 32 | public void write(byte[] b, int off, int len) {} 33 | 34 | @Override 35 | public void write(int b) {} 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/OpsLoggerTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging; 2 | 3 | import com.equalexperts.logging.impl.ActiveRotationRegistry; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | 7 | import static org.mockito.Mockito.spy; 8 | import static org.mockito.Mockito.verify; 9 | 10 | public class OpsLoggerTest { 11 | 12 | @Rule 13 | public RestoreActiveRotationRegistryFixture registryFixture = new RestoreActiveRotationRegistryFixture(); 14 | 15 | @Test 16 | public void refreshFileHandles_shouldRefreshFileHandlesOnAllRegisteredDestinationsThatSupportActiveRotation() throws Exception { 17 | ActiveRotationRegistry registry = spy(new ActiveRotationRegistry()); 18 | ActiveRotationRegistry.setSingletonInstance(registry); 19 | 20 | OpsLogger.refreshFileHandles(); 21 | 22 | verify(registry).refreshFileHandles(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/RestoreActiveRotationRegistryFixture.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging; 2 | 3 | import com.equalexperts.logging.impl.ActiveRotationRegistry; 4 | import org.junit.rules.TestRule; 5 | import org.junit.runner.Description; 6 | import org.junit.runners.model.Statement; 7 | 8 | public class RestoreActiveRotationRegistryFixture implements TestRule { 9 | @Override 10 | public Statement apply(Statement base, Description description) { 11 | return new Statement() { 12 | @Override 13 | public void evaluate() throws Throwable { 14 | ActiveRotationRegistry original = ActiveRotationRegistry.getSingletonInstance(); 15 | ActiveRotationRegistry.setSingletonInstance(new ActiveRotationRegistry()); 16 | try { 17 | base.evaluate(); 18 | } finally { 19 | ActiveRotationRegistry.setSingletonInstance(original); 20 | } 21 | } 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/RestoreSystemStreamsFixture.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging; 2 | 3 | import org.junit.rules.TestRule; 4 | import org.junit.runner.Description; 5 | import org.junit.runners.model.Statement; 6 | 7 | import java.io.*; 8 | 9 | /** 10 | * Resets standard input, output, and error at the end of tests, 11 | * and prevents any standard streams from being closed. 12 | */ 13 | public class RestoreSystemStreamsFixture implements TestRule { 14 | 15 | @Override 16 | public Statement apply(final Statement base, Description description) { 17 | return new Statement() { 18 | @Override 19 | public void evaluate() throws Throwable { 20 | InputStream originalSystemIn = System.in; 21 | PrintStream originalSystemOut = System.out; 22 | PrintStream originalSystemErr = System.err; 23 | 24 | System.setIn(new NonCloseableInputStream(originalSystemIn)); 25 | System.setOut(new NonCloseablePrintStream(originalSystemOut)); 26 | System.setErr(new NonCloseablePrintStream(originalSystemErr)); 27 | try { 28 | base.evaluate(); 29 | } finally { 30 | System.setIn(originalSystemIn); 31 | System.setOut(originalSystemOut); 32 | System.setErr(originalSystemErr); 33 | } 34 | } 35 | }; 36 | } 37 | 38 | private static class NonCloseableInputStream extends FilterInputStream { 39 | NonCloseableInputStream(InputStream in) { 40 | super(in); 41 | } 42 | 43 | @Override 44 | public void close() { 45 | //don't close 46 | } 47 | } 48 | 49 | private static class NonCloseablePrintStream extends PrintStream { 50 | NonCloseablePrintStream(OutputStream out) { 51 | super(out); 52 | } 53 | 54 | @Override 55 | public void close() { 56 | super.flush(); 57 | //don't close 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/TempFileFixture.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging; 2 | 3 | import org.junit.rules.TestRule; 4 | import org.junit.runner.Description; 5 | import org.junit.runners.model.Statement; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.io.UncheckedIOException; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.UUID; 16 | 17 | public class TempFileFixture implements TestRule { 18 | private final List tempFiles = new ArrayList<>(); 19 | 20 | @Override 21 | public Statement apply(Statement base, Description description) { 22 | return statement(base, () -> tempFiles.stream() 23 | .filter(Files::exists) 24 | .map(Path::toFile) 25 | .forEach(File::delete)); 26 | } 27 | 28 | public Path createTempFile(String suffix) { 29 | try { 30 | return register(Files.createTempFile("", suffix)); 31 | } catch (IOException e) { 32 | throw new UncheckedIOException(e); 33 | } 34 | } 35 | 36 | public Path createTempDirectory() { 37 | try { 38 | return register(Files.createTempDirectory(null)); 39 | } catch (IOException e) { 40 | throw new UncheckedIOException(e); 41 | } 42 | } 43 | 44 | public Path createTempFileThatDoesNotExist(String suffix) { 45 | Path result = Paths.get(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString() + suffix); 46 | return register(result); 47 | } 48 | 49 | public Path createTempDirectoryThatDoesNotExist() { 50 | Path result = Paths.get(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString()); 51 | return register(result); 52 | } 53 | 54 | public Path register(Path path) { 55 | path.toFile().deleteOnExit(); 56 | tempFiles.add(path); 57 | return path; 58 | } 59 | 60 | /* 61 | Code to make creating Statements Java 8-friendly even though Statement is an abstract class 62 | */ 63 | 64 | @FunctionalInterface 65 | private interface StatementClosure { 66 | void evaluate() throws Throwable; 67 | } 68 | 69 | private static Statement statement(Statement base, StatementClosure closure) { 70 | return new Statement() { 71 | @Override 72 | public void evaluate() throws Throwable { 73 | base.evaluate(); 74 | closure.evaluate(); 75 | } 76 | }; 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/AbstractFileChannelProviderTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import java.io.Writer; 4 | import java.lang.reflect.Field; 5 | import java.nio.channels.FileChannel; 6 | import java.nio.charset.Charset; 7 | import java.nio.charset.StandardCharsets; 8 | import java.nio.file.FileSystem; 9 | import java.nio.file.Path; 10 | import java.nio.file.StandardOpenOption; 11 | import java.nio.file.spi.FileSystemProvider; 12 | import java.util.EnumSet; 13 | import java.util.Set; 14 | 15 | import static java.nio.file.StandardOpenOption.APPEND; 16 | import static java.nio.file.StandardOpenOption.CREATE; 17 | import static org.junit.Assert.assertSame; 18 | import static org.mockito.Mockito.mock; 19 | import static org.mockito.Mockito.when; 20 | 21 | public abstract class AbstractFileChannelProviderTest { 22 | protected static final Set CREATE_AND_APPEND = EnumSet.of(CREATE, APPEND); 23 | 24 | static Path createMockPath() { 25 | Path result = mock(Path.class); 26 | FileSystem mockFileSystem = mock(FileSystem.class); 27 | when(result.getFileSystem()).thenReturn(mockFileSystem); 28 | when(mockFileSystem.provider()).thenReturn(mock(FileSystemProvider.class)); 29 | return result; 30 | } 31 | 32 | protected void ensureAssociated(Writer writer, FileChannel channel) throws Exception { 33 | Class implementationClass = writer.getClass(); 34 | for (Field field : implementationClass.getDeclaredFields()) { 35 | if (!field.getType().isAssignableFrom(channel.getClass())) { 36 | continue; 37 | } 38 | field.setAccessible(true); 39 | Object value = field.get(writer); 40 | if (channel == value) { 41 | return; 42 | } 43 | } 44 | throw new AssertionError("The provided writer is not associated with the FileChannel"); 45 | } 46 | 47 | protected void ensureUtf8Charset(Writer writer) throws Exception { 48 | Class implementationClass = writer.getClass(); 49 | for (Field field : implementationClass.getDeclaredFields()) { 50 | if (!field.getType().isAssignableFrom(Charset.class)) { 51 | continue; 52 | } 53 | field.setAccessible(true); 54 | assertSame(StandardCharsets.UTF_8, field.get(writer)); 55 | return; 56 | } 57 | } 58 | 59 | protected boolean isOpen(Writer writer) throws Exception { 60 | Class implementationClass = writer.getClass(); 61 | for (Field field : implementationClass.getDeclaredFields()) { 62 | if (!field.getName().equals("isOpen")) { 63 | continue; 64 | } 65 | field.setAccessible(true); 66 | return (boolean) field.get(writer); 67 | } 68 | throw new RuntimeException("field not found"); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/ActiveRotationRegistryTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | import static org.mockito.Mockito.*; 7 | 8 | public class ActiveRotationRegistryTest { 9 | 10 | private final ActiveRotationRegistry registry = new ActiveRotationRegistry(); 11 | 12 | @Test 13 | public void add_shouldReturnTheProvidedArgument() throws Exception { 14 | ActiveRotationSupport ars = mock(ActiveRotationSupport.class); 15 | 16 | ActiveRotationSupport result = registry.add(ars); 17 | 18 | assertSame(ars, result); 19 | } 20 | 21 | @Test 22 | public void add_shouldAddAnEntryToTheSetOfInstancesAffectedByRefreshFileHandles() throws Exception { 23 | ActiveRotationSupport ars = mock(ActiveRotationSupport.class); 24 | 25 | registry.add(ars); 26 | 27 | registry.refreshFileHandles(); 28 | verify(ars).refreshFileHandles(); 29 | } 30 | 31 | @Test 32 | public void add_shouldNotRequireACast_givenAnImplementationOfActiveRotationSupport() throws Exception { 33 | @SuppressWarnings("UnusedDeclaration") //assignment necessary for the test — we're really testing that this compiles 34 | Foo foo = registry.add(new Foo()); 35 | } 36 | 37 | @Test 38 | public void remove_shouldRemoveAnEntryFromTheSetOfInstancesAffectedByRefreshFileHandles() throws Exception { 39 | ActiveRotationSupport ars = registry.add(mock(ActiveRotationSupport.class)); 40 | 41 | registry.remove(ars); 42 | 43 | registry.refreshFileHandles(); 44 | verifyZeroInteractions(ars); 45 | } 46 | 47 | @Test 48 | public void remove_shouldNotComplain_givenAnUnregisteredInstance() throws Exception { 49 | ActiveRotationSupport ars = mock(ActiveRotationSupport.class); 50 | 51 | registry.remove(ars); 52 | } 53 | 54 | @Test 55 | public void contains_shouldReturnTrue_givenAnAddedInstance() throws Exception { 56 | ActiveRotationSupport ars = mock(ActiveRotationSupport.class); 57 | 58 | registry.add(ars); 59 | 60 | assertTrue(registry.contains(ars)); 61 | } 62 | 63 | @Test 64 | public void contains_shouldReturnFalse_givenAnInstanceThatHasNotBeenAdded() throws Exception { 65 | ActiveRotationSupport ars = mock(ActiveRotationSupport.class); 66 | 67 | assertFalse(registry.contains(ars)); 68 | } 69 | 70 | @Test 71 | public void contains_shouldReturnFalse_givenAnInstanceThatHasBeenAddedAndRemoved() throws Exception { 72 | ActiveRotationSupport ars = mock(ActiveRotationSupport.class); 73 | 74 | registry.add(ars); 75 | registry.remove(ars); 76 | 77 | assertFalse(registry.contains(ars)); 78 | } 79 | 80 | @Test 81 | public void refreshFileHandles_shouldRefreshFileHandlesOnEveryRegisteredActiveRotationSupportInstance() throws Exception { 82 | ActiveRotationSupport ars = registry.add(mock(ActiveRotationSupport.class)); 83 | ActiveRotationSupport anotherArs = registry.add(mock(ActiveRotationSupport.class)); 84 | 85 | registry.refreshFileHandles(); 86 | 87 | verify(ars).refreshFileHandles(); 88 | verify(anotherArs).refreshFileHandles(); 89 | } 90 | 91 | @Test 92 | public void refreshFileHandles_shouldThrowARuntimeException_whenAnInstanceThrowsAnInterruptedException() throws Exception { 93 | InterruptedException expectedException = new InterruptedException(); 94 | ActiveRotationSupport ars = registry.add(mock(ActiveRotationSupport.class)); 95 | doThrow(expectedException).when(ars).refreshFileHandles(); 96 | 97 | try { 98 | registry.refreshFileHandles(); 99 | fail("expected a RuntimeException"); 100 | } catch (RuntimeException e) { 101 | assertSame(expectedException, e.getCause()); 102 | } 103 | } 104 | 105 | @Test 106 | public void refreshFileHandles_shouldNotRefreshFileHandlesOnAnInstanceThatHasNotBeenAdded() throws Exception { 107 | ActiveRotationSupport ars = mock(ActiveRotationSupport.class); 108 | 109 | registry.refreshFileHandles(); 110 | 111 | verifyZeroInteractions(ars); 112 | } 113 | 114 | @Test 115 | public void refreshFileHandles_shouldNotRefreshFileHandlesOnAnInstanceThatHasBeenRemoved() throws Exception { 116 | ActiveRotationSupport ars = mock(ActiveRotationSupport.class); 117 | registry.add(ars); 118 | registry.remove(ars); 119 | 120 | registry.refreshFileHandles(); 121 | 122 | verifyZeroInteractions(ars); 123 | } 124 | 125 | private static class Foo implements ActiveRotationSupport { 126 | @Override 127 | public void refreshFileHandles() throws InterruptedException { 128 | //not relevant for this test 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/AsyncExecutorTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import org.hamcrest.BaseMatcher; 4 | import org.hamcrest.Description; 5 | import org.hamcrest.Matcher; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.mockito.invocation.InvocationOnMock; 9 | 10 | import java.util.concurrent.*; 11 | 12 | import static org.junit.Assert.*; 13 | import static org.mockito.Matchers.any; 14 | import static org.mockito.Mockito.*; 15 | 16 | public class AsyncExecutorTest { 17 | private final ThreadFactory factory = mock(ThreadFactory.class); 18 | private final AsyncExecutor asyncExecutor = new AsyncExecutor(factory); 19 | private Thread createdThread; 20 | 21 | @Before 22 | public void setup() { 23 | when(factory.newThread(any())).then((InvocationOnMock invocation) -> { 24 | Runnable r = (Runnable) invocation.getArguments()[0]; 25 | createdThread = new Thread(r); 26 | return createdThread; 27 | }); 28 | } 29 | 30 | @Test 31 | public void execute_shouldCreateAndStartAThreadWithTheProvidedRunnableAndReturnAFuture() throws Exception { 32 | CountDownLatch latch = new CountDownLatch(1); 33 | 34 | Future result = asyncExecutor.execute(latch::countDown); 35 | latch.await(5, TimeUnit.SECONDS); //give the new thread time to start to running 36 | 37 | verify(factory).newThread(any()); //The provided runnable will be wrapped, not passed directly 38 | assertEquals(0, latch.getCount()); //but the runnable will be executed 39 | assertNotNull(result); 40 | assertNotNull(createdThread); 41 | assertNotEquals(Thread.State.NEW, createdThread.getState()); 42 | } 43 | 44 | /* 45 | The following tests are for method calls on the Future instance return by the execute method 46 | */ 47 | 48 | @Test 49 | public void get_shouldJoinTheCreatedThread_givenAFutureReturnedByExecute() throws Exception { 50 | long startTime = System.nanoTime(); 51 | Future result = asyncExecutor.execute(suppressCheckedExceptions(() -> Thread.sleep(250L))); 52 | result.get(); 53 | long endTime = System.nanoTime(); 54 | 55 | assertThat(endTime - startTime, isGreaterThan(100 * 1000000L)); 56 | } 57 | 58 | @Test 59 | public void get_shouldSucceed_whenTheThreadThrowsAnException_givenAFutureReturnedByExecute() throws Exception { 60 | Future result = asyncExecutor.execute(() -> {throw new RuntimeException("blah"); }); 61 | result.get(100, TimeUnit.MILLISECONDS); 62 | } 63 | 64 | @Test 65 | public void getWithATimeout_shouldSucceed_whenTheThreadEndsQuickly_givenAFutureReturnedByExecute() throws Exception { 66 | Future result = asyncExecutor.execute(suppressCheckedExceptions(() -> Thread.sleep(50L))); 67 | result.get(100, TimeUnit.MILLISECONDS); 68 | } 69 | 70 | @Test 71 | public void getWithATimeout_shouldThrowATimeoutException_whenTheThreadDoesNotEndQuickly_givenAFutureReturnedByExecute() throws Exception { 72 | Future result = asyncExecutor.execute(suppressCheckedExceptions(() -> Thread.sleep(250L))); 73 | 74 | try { 75 | result.get(50, TimeUnit.MILLISECONDS); 76 | fail("Expected an exception"); 77 | } catch (TimeoutException ignore) {} 78 | } 79 | 80 | @Test 81 | public void cancel_shouldThrowAnUnsupportedOperationException_givenAFutureReturnedByExecute() throws Exception { 82 | Future result = asyncExecutor.execute(() -> {}); 83 | 84 | try { 85 | result.cancel(true); 86 | fail("Expected an exception"); 87 | } catch (UnsupportedOperationException ignore) {} 88 | } 89 | 90 | @Test 91 | public void isCancelled_shouldReturnFalse_givenAFutureReturnedByExecute() throws Exception { 92 | Future result = asyncExecutor.execute(() -> {}); 93 | 94 | assertFalse(result.isCancelled()); 95 | } 96 | 97 | @Test 98 | public void isDone_shouldReturnTrue_whenTheThreadIsAlive_givenAFutureReturnedByExecute() throws Exception { 99 | CountDownLatch latch = new CountDownLatch(1); 100 | 101 | Future result = asyncExecutor.execute(suppressCheckedExceptions(latch::await)); 102 | 103 | try { 104 | assertFalse(result.isDone()); 105 | } finally { 106 | latch.countDown(); //don't want a thread waiting forever... 107 | } 108 | } 109 | 110 | @Test 111 | public void isDone_shouldReturnFalse_whenTheThreadIsFinished_givenAFutureReturnedByExecute() throws Exception { 112 | Future result = asyncExecutor.execute(() -> {}); 113 | 114 | Thread.sleep(50L); //give other thread a chance to finish... 115 | 116 | assertTrue(result.isDone()); 117 | } 118 | 119 | private static Runnable suppressCheckedExceptions(RunnableThatThrows r) { 120 | return () -> { 121 | try { 122 | r.run(); 123 | } catch (RuntimeException e) { 124 | throw e; 125 | } 126 | catch (Exception e) { 127 | throw new RuntimeException(e); 128 | } 129 | }; 130 | } 131 | 132 | private static Matcher isGreaterThan(final long other) { 133 | return new BaseMatcher() { 134 | @Override 135 | public boolean matches(Object item) { 136 | if (!(item instanceof Long)) { 137 | return false; 138 | } 139 | long itemValue = (Long) item; 140 | 141 | return itemValue > other; 142 | } 143 | 144 | @Override 145 | public void describeTo(Description description) { 146 | description.appendText("greater than").appendValue(other); 147 | } 148 | }; 149 | } 150 | 151 | @FunctionalInterface 152 | private static interface RunnableThatThrows { 153 | void run() throws Exception; 154 | } 155 | } -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/AsyncOpsLoggerFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | 4 | import com.equalexperts.logging.DiagnosticContextSupplier; 5 | import com.equalexperts.logging.LogMessage; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | import java.io.IOException; 10 | import java.time.Clock; 11 | import java.util.HashMap; 12 | import java.util.function.Consumer; 13 | 14 | import static org.junit.Assert.*; 15 | import static org.mockito.Matchers.any; 16 | import static org.mockito.Mockito.*; 17 | 18 | public class AsyncOpsLoggerFactoryTest { 19 | private OutputStreamDestination expectedDestination = new OutputStreamDestination<>(System.out, new SimpleStackTraceProcessor()); 20 | private Consumer expectedErrorHandler = t -> {}; 21 | private DiagnosticContextSupplier expectedDiagnosticContextSupplier = HashMap::new; 22 | 23 | private InfrastructureFactory infrastructure = mock(InfrastructureFactory.class); 24 | private AsyncExecutor mockAsyncExecutor = mock(AsyncExecutor.class); 25 | 26 | private AsyncOpsLoggerFactory factory = new AsyncOpsLoggerFactory(); 27 | 28 | @Before 29 | public void setup() throws IOException { 30 | factory.setAsyncExecutor(mockAsyncExecutor); 31 | when(infrastructure.configureDestination()).thenReturn(expectedDestination); 32 | when(infrastructure.configureContextSupplier()).thenReturn(expectedDiagnosticContextSupplier); 33 | when(infrastructure.configureErrorHandler()).thenReturn(expectedErrorHandler); 34 | } 35 | 36 | @Test 37 | public void build_shouldConstructACorrectlyConfiguredAsyncOpsLogger() throws Exception { 38 | 39 | AsyncOpsLogger result = factory.build(infrastructure); 40 | 41 | assertEquals(Clock.systemUTC(), result.getClock()); 42 | assertSame(expectedDiagnosticContextSupplier, result.getDiagnosticContextSupplier()); 43 | assertSame(expectedDestination, result.getDestination()); 44 | assertSame(expectedErrorHandler, result.getErrorHandler()); 45 | assertNotNull(result.getTransferQueue()); 46 | verify(mockAsyncExecutor).execute(any(Runnable.class)); 47 | } 48 | 49 | @Test 50 | public void build_shouldUseANewLinkedTransferQueueForEachConstructedOpsLogger() throws Exception { 51 | AsyncOpsLogger firstResult = factory.build(infrastructure); 52 | AsyncOpsLogger secondResult = factory.build(infrastructure); 53 | 54 | assertNotNull(firstResult.getTransferQueue()); 55 | assertNotNull(secondResult.getTransferQueue()); 56 | assertNotSame(firstResult, secondResult); 57 | } 58 | 59 | private enum TestMessages implements LogMessage { 60 | ; //don't actually need any messages for these tests 61 | 62 | //region LogMessage implementation guts 63 | private final String messageCode; 64 | private final String messagePattern; 65 | 66 | TestMessages(String messageCode, String messagePattern) { 67 | this.messageCode = messageCode; 68 | this.messagePattern = messagePattern; 69 | } 70 | 71 | @Override 72 | public String getMessageCode() { 73 | return messageCode; 74 | } 75 | 76 | @Override 77 | public String getMessagePattern() { 78 | return messagePattern; 79 | } 80 | //endregion 81 | } 82 | } -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/AsyncOpsLoggerTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.DiagnosticContextSupplier; 4 | import com.equalexperts.logging.LogMessage; 5 | import com.equalexperts.logging.OpsLogger; 6 | import org.hamcrest.BaseMatcher; 7 | import org.hamcrest.CoreMatchers; 8 | import org.hamcrest.Description; 9 | import org.hamcrest.Matcher; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.mockito.*; 13 | import org.mockito.stubbing.Answer; 14 | import org.mockito.stubbing.OngoingStubbing; 15 | 16 | import java.time.Clock; 17 | import java.time.Instant; 18 | import java.time.ZoneOffset; 19 | import java.util.*; 20 | import java.util.concurrent.Future; 21 | import java.util.concurrent.LinkedTransferQueue; 22 | import java.util.function.Consumer; 23 | import java.util.stream.IntStream; 24 | 25 | import static java.util.stream.Collectors.toList; 26 | import static org.hamcrest.CoreMatchers.is; 27 | import static org.hamcrest.CoreMatchers.not; 28 | import static org.junit.Assert.*; 29 | import static org.mockito.Matchers.any; 30 | import static org.mockito.Matchers.argThat; 31 | import static org.mockito.Mockito.*; 32 | 33 | public class AsyncOpsLoggerTest { 34 | 35 | public static final int EXPECTED_MAX_BATCH_SIZE = AsyncOpsLogger.MAX_BATCH_SIZE; 36 | private Clock fixedClock = Clock.fixed(Instant.parse("2014-02-01T14:57:12.500Z"), ZoneOffset.UTC); 37 | @Mock private Destination destination; 38 | @Mock private DiagnosticContextSupplier diagnosticContextSupplier; 39 | @Mock private Consumer exceptionConsumer; 40 | @Mock private LinkedTransferQueue>> transferQueue; 41 | @Mock private AsyncExecutor executor; 42 | @Mock private Future processingThread; 43 | 44 | @Captor private ArgumentCaptor>> captor; 45 | @Captor private ArgumentCaptor runnableCaptor; 46 | 47 | private OpsLogger logger; 48 | 49 | @Before 50 | public void setup() { 51 | MockitoAnnotations.initMocks(this); 52 | 53 | when(executor.execute(runnableCaptor.capture())).thenAnswer((i) -> processingThread); 54 | 55 | logger = new AsyncOpsLogger<>(fixedClock, diagnosticContextSupplier, destination, exceptionConsumer, transferQueue, executor); 56 | } 57 | 58 | @Test 59 | public void constructor_shouldSpawnAnAsynchronousProcessingThread() throws Exception { 60 | 61 | verify(executor).execute(any()); 62 | } 63 | 64 | //region tests for log 65 | 66 | @Test 67 | public void log_shouldAddALogicalLogRecordToTheQueue_givenALogMessageInstance() throws Exception { 68 | Map expectedCorrelationIds = generateCorrelationIds(); 69 | when(diagnosticContextSupplier.getMessageContext()).thenReturn(expectedCorrelationIds); 70 | doNothing().when(transferQueue).put(captor.capture()); 71 | 72 | logger.log(TestMessages.Bar, 64, "Hello, World"); 73 | 74 | verify(transferQueue).put(captor.capture()); 75 | 76 | LogicalLogRecord record = captor.getValue().get(); 77 | assertEquals(fixedClock.instant(), record.getTimestamp()); 78 | assertEquals(expectedCorrelationIds, record.getDiagnosticContext().getContext()); 79 | assertEquals(TestMessages.Bar, record.getMessage()); 80 | assertNotNull(record.getCause()); 81 | assertFalse(record.getCause().isPresent()); 82 | assertArrayEquals(new Object[] {64, "Hello, World"}, record.getDetails()); 83 | } 84 | 85 | @Test 86 | public void log_shouldExposeAnExceptionToTheHandler_givenAProblemCreatingTheLogRecord() throws Exception { 87 | logger.log(null); 88 | 89 | verify(exceptionConsumer).accept(Mockito.isA(NullPointerException.class)); 90 | } 91 | 92 | @Test 93 | public void log_shouldExposeAnExceptionToTheHandler_givenAProblemObtainingTheGlobalDiagnosticContext() throws Exception { 94 | Error expectedThrowable = new Error(); 95 | when(diagnosticContextSupplier.getMessageContext()).thenThrow(expectedThrowable); 96 | 97 | logger.log(TestMessages.Foo); 98 | 99 | verify(exceptionConsumer).accept(Mockito.same(expectedThrowable)); 100 | } 101 | 102 | @Test 103 | public void log_shouldExposeAnExceptionToTheHandler_givenAProblemAddingAMessageToTheQueue() throws Exception { 104 | RuntimeException expectedThrowable = new RuntimeException("blah"); 105 | doThrow(expectedThrowable).when(transferQueue).put(any()); 106 | 107 | logger.log(TestMessages.Foo); 108 | 109 | verify(exceptionConsumer).accept(Mockito.same(expectedThrowable)); 110 | } 111 | 112 | //endregion 113 | 114 | //region tests for logThrowable 115 | 116 | @Test 117 | public void logThrowable_shouldAddALogicalLogRecordToTheQueue_givenALogMessageInstanceAndAThrowable() throws Exception { 118 | Map expectedCorrelationIds = generateCorrelationIds(); 119 | when(diagnosticContextSupplier.getMessageContext()).thenReturn(expectedCorrelationIds); 120 | 121 | Throwable expectedCause = new RuntimeException(); 122 | 123 | doNothing().when(transferQueue).put(captor.capture()); 124 | 125 | logger.logThrowable(TestMessages.Bar, expectedCause, 64, "Hello, World"); 126 | 127 | verify(transferQueue).put(captor.capture()); 128 | 129 | LogicalLogRecord record = captor.getValue().get(); 130 | assertEquals(fixedClock.instant(), record.getTimestamp()); 131 | assertEquals(expectedCorrelationIds, record.getDiagnosticContext().getContext()); 132 | assertEquals(TestMessages.Bar, record.getMessage()); 133 | assertNotNull(record.getCause()); 134 | assertTrue(record.getCause().isPresent()); 135 | assertSame(expectedCause, record.getCause().get()); 136 | assertArrayEquals(new Object[] {64, "Hello, World"}, record.getDetails()); 137 | } 138 | 139 | @Test 140 | public void logThrowable_shouldExposeAnExceptionToTheHandler_givenAProblemCreatingTheLogRecordAndAThrowable() throws Exception { 141 | logger.logThrowable(null, new RuntimeException()); 142 | 143 | verify(exceptionConsumer).accept(Mockito.isA(NullPointerException.class)); 144 | } 145 | 146 | @Test 147 | public void logThrowable_shouldExposeAnExceptionToTheHandler_givenAProblemObtainingCorrelationIdsAndAThrowable() throws Exception { 148 | Error expectedThrowable = new Error(); 149 | when(diagnosticContextSupplier.getMessageContext()).thenThrow(expectedThrowable); 150 | 151 | logger.logThrowable(TestMessages.Foo, new RuntimeException()); 152 | 153 | verify(exceptionConsumer).accept(Mockito.same(expectedThrowable)); 154 | } 155 | 156 | @Test 157 | public void logThrowable_shouldExposeAnExceptionToTheHandler_givenAProblemAddingAMessageToTheQueueAndAThrowable() throws Exception { 158 | RuntimeException expectedThrowable = new RuntimeException("blah"); 159 | doThrow(expectedThrowable).when(transferQueue).put(any()); 160 | 161 | logger.logThrowable(TestMessages.Foo, new Exception()); 162 | 163 | verify(exceptionConsumer).accept(Mockito.same(expectedThrowable)); 164 | } 165 | 166 | //endregion 167 | 168 | @Test 169 | public void with_shouldReturnANewAsyncOpsLoggerWithAnOverriddenDiagnosticContextSupplier_givenADiagnosticContextSupplier() throws Exception { 170 | DiagnosticContextSupplier localSupplier = Collections::emptyMap; 171 | AsyncOpsLogger asyncLogger = (AsyncOpsLogger) logger; 172 | 173 | AsyncOpsLogger result = asyncLogger.with(localSupplier); 174 | 175 | assertNotSame(asyncLogger, result); 176 | assertSame(asyncLogger.getClock(), result.getClock()); 177 | assertSame(asyncLogger.getErrorHandler(), result.getErrorHandler()); 178 | assertSame(asyncLogger.getDestination(), result.getDestination()); 179 | assertSame(asyncLogger.getTransferQueue(), result.getTransferQueue()); 180 | assertSame(localSupplier, result.getDiagnosticContextSupplier()); 181 | assertNotSame(asyncLogger.getDiagnosticContextSupplier(), result.getDiagnosticContextSupplier()); 182 | } 183 | 184 | @Test 185 | public void close_shouldSendAStopSignalToTheProcessingThreadWaitForItToFinishAndCloseTheDestination() throws Exception { 186 | logger.close(); 187 | 188 | InOrder order = inOrder(transferQueue, processingThread, destination); 189 | order.verify(transferQueue).put(argThat(isEmpty())); 190 | order.verify(processingThread).get(); 191 | order.verify(destination).close(); 192 | } 193 | 194 | @Test 195 | public void close_shouldCloseTheDestination_whenAnExceptionIsThrownWaitingForTheProcessingThreadToFinish() throws Exception { 196 | when(processingThread.get()).thenThrow(new InterruptedException()); 197 | 198 | try { 199 | logger.close(); 200 | } catch (InterruptedException ignore) {} 201 | 202 | verify(destination).close(); 203 | } 204 | 205 | @Test 206 | public void close_shouldIgnoreCalls_givenANestedLoggerCreatedByWith() throws Exception { 207 | AsyncOpsLogger asyncLogger = (AsyncOpsLogger) logger; 208 | 209 | AsyncOpsLogger nested = asyncLogger.with(Collections::emptyMap); 210 | 211 | nested.close(); 212 | 213 | verifyZeroInteractions(destination, transferQueue, processingThread); 214 | } 215 | 216 | @Test 217 | public void processingThread_shouldTakeBatchesOfUpTo100InALoopUntilItReceivesAnEmptyOptional() throws Exception { 218 | List>> messages = buildMessages(141).stream().map(Optional::of).collect(toList()); 219 | messages.add(130, Optional.empty()); //add in the middle of the last block — loop should process all messages in the last batch, regardless of where the signal is 220 | setupTransferQueueExpectations(messages, 60, 40, 42); 221 | 222 | runnableCaptor.getValue().run(); 223 | 224 | InOrder order = inOrder(transferQueue); 225 | //expect three take/drainTo combination calls (take blocks, drainTo does not) 226 | order.verify(transferQueue).take(); 227 | order.verify(transferQueue).drainTo(any(), eq(EXPECTED_MAX_BATCH_SIZE - 1)); 228 | order.verify(transferQueue).take(); 229 | order.verify(transferQueue).drainTo(any(), eq(EXPECTED_MAX_BATCH_SIZE - 1)); 230 | order.verify(transferQueue).take(); 231 | order.verify(transferQueue).drainTo(any(), eq(EXPECTED_MAX_BATCH_SIZE - 1)); 232 | order.verifyNoMoreInteractions(); 233 | } 234 | 235 | @Test 236 | public void processingThread_shouldSubmitReceivedMessagesToTheDestinationInBatches() throws Exception { 237 | List>> messages = buildMessages(5).stream().map(Optional::of).collect(toList()); 238 | messages.add(Optional.empty()); 239 | setupTransferQueueExpectations(messages, 4, 2); 240 | 241 | runnableCaptor.getValue().run(); 242 | 243 | InOrder order = inOrder(destination); 244 | order.verify(destination).beginBatch(); 245 | order.verify(destination).publish(messages.get(0).get()); 246 | order.verify(destination).publish(messages.get(1).get()); 247 | order.verify(destination).publish(messages.get(2).get()); 248 | order.verify(destination).publish(messages.get(3).get()); 249 | order.verify(destination).endBatch(); 250 | order.verify(destination).beginBatch(); 251 | order.verify(destination).publish(messages.get(4).get()); 252 | order.verify(destination).endBatch(); 253 | order.verifyNoMoreInteractions(); 254 | } 255 | 256 | @Test 257 | public void processingThread_shouldExposeAnExceptionToTheHandlerAndContinueProcessing_givenAnException() throws Exception { 258 | List>> messages = buildMessages(2).stream().map(Optional::of).collect(toList()); 259 | messages.add(Optional.empty()); 260 | setupTransferQueueExpectations(messages, 1, 1, 1); 261 | 262 | Exception expectedException = new Exception("something went wrong"); 263 | doThrow(expectedException).when(destination).publish(messages.get(0).get()); 264 | 265 | runnableCaptor.getValue().run(); 266 | 267 | verify(exceptionConsumer).accept(expectedException); 268 | verify(destination).publish(messages.get(1).get()); 269 | } 270 | 271 | @Test 272 | public void processingThread_shouldSkipTheEntireBatch_whenAnExceptionIsThrownByBeginBatch() throws Exception { 273 | List>> messages = buildMessages(2).stream().map(Optional::of).collect(toList()); 274 | messages.add(Optional.empty()); 275 | setupTransferQueueExpectations(messages, 1, 2); 276 | 277 | Exception expectedException = new Exception("error starting batch"); 278 | doThrow(expectedException).doNothing().when(destination).beginBatch(); 279 | 280 | runnableCaptor.getValue().run(); 281 | 282 | InOrder order = inOrder(destination, exceptionConsumer); 283 | order.verify(destination).beginBatch(); 284 | order.verify(exceptionConsumer).accept(expectedException); 285 | order.verify(destination).beginBatch(); 286 | order.verify(destination).publish(messages.get(1).get()); 287 | } 288 | 289 | @Test 290 | public void processingThread_shouldCloseTheBatch_whenAnExceptionIsThrownPublishingABatchMember() throws Exception { 291 | List>> messages = buildMessages(1).stream().map(Optional::of).collect(toList()); 292 | messages.add(Optional.empty()); 293 | setupTransferQueueExpectations(messages, 1, 1); 294 | 295 | doThrow(new Exception("error starting batch")).when(destination).publish(messages.get(0).get()); 296 | 297 | runnableCaptor.getValue().run(); 298 | 299 | InOrder order = inOrder(destination); 300 | order.verify(destination).beginBatch(); 301 | order.verify(destination).endBatch(); 302 | } 303 | 304 | @Test 305 | public void processingThread_shouldContinueProcessingTheBatch_whenAnExceptionIsThrownPublishingABatchMember() throws Exception { 306 | List>> messages = buildMessages(4).stream().map(Optional::of).collect(toList()); 307 | messages.add(Optional.empty()); 308 | setupTransferQueueExpectations(messages, 4, 1); //send close in a separate batch to stop an infinite loop when the test fails 309 | 310 | Exception expectedException = new Exception("error starting batch"); 311 | doThrow(expectedException).when(destination).publish(messages.get(1).get()); 312 | 313 | runnableCaptor.getValue().run(); 314 | 315 | verify(destination, times(4)).publish(any()); 316 | verify(exceptionConsumer).accept(expectedException); 317 | } 318 | 319 | @Test 320 | public void processingThread_shouldEndEvenIfABatchErrorOccursInTheLastBatch() throws Exception { 321 | List>> messages = buildMessages(1).stream().map(Optional::of).collect(toList()); 322 | messages.add(Optional.empty()); 323 | setupTransferQueueExpectations(messages, 2); 324 | 325 | doThrow(new Exception()).when(destination).beginBatch(); 326 | 327 | runnableCaptor.getValue().run(); 328 | } 329 | 330 | @Test 331 | public void processingThread_shouldNotBeginOrEndAnEmptyBatch() throws Exception { 332 | List>> messages = new ArrayList<>(); 333 | messages.add(Optional.empty()); 334 | setupTransferQueueExpectations(messages, 1); 335 | 336 | runnableCaptor.getValue().run(); 337 | 338 | verifyZeroInteractions(destination); 339 | } 340 | 341 | private void setupTransferQueueExpectations(List>> messages, int... batchSizes) throws Exception { 342 | int totalSize = IntStream.of(batchSizes) 343 | .peek((i) -> assertTrue("A TransferQueue will never return more than asked for", i <= EXPECTED_MAX_BATCH_SIZE)) 344 | .sum(); 345 | assertEquals("precondition: message size must equal total size of all batches", messages.size(), totalSize); 346 | 347 | final List batchSizeList = IntStream.of(batchSizes).mapToObj((i) -> i).collect(toList()); 348 | 349 | /* 350 | batches are retrieved in a sequence of take(), drainTo() calls, 351 | but we can't interleave when calls to the two mocks. 352 | solution: mock the takes calls first, and then the drainTo calls. 353 | */ 354 | 355 | List>> takeCallResults = new ArrayList<>(); 356 | List>>> drainCallResults = new ArrayList<>(); 357 | int positionSoFar = 0; 358 | for (int i : batchSizeList) { 359 | takeCallResults.add(messages.get(positionSoFar)); 360 | drainCallResults.add(messages.subList(positionSoFar + 1, positionSoFar + i)); 361 | positionSoFar += i; 362 | } 363 | 364 | OngoingStubbing>> takeCall = when(transferQueue.take()).thenReturn(takeCallResults.get(0)); 365 | for (Optional> takeResult : takeCallResults.subList(1, takeCallResults.size())) { 366 | takeCall = takeCall.thenReturn(takeResult); 367 | } 368 | 369 | Answer drainAnswer = invocation -> { 370 | 371 | assertThat("too many calls to drainTo", takeCallResults.size(), not(is(0))); 372 | assertThat("some sort of error in mock setup", drainCallResults.size(), is(takeCallResults.size())); 373 | 374 | @SuppressWarnings("unchecked") 375 | List>> collection = (List>>) invocation.getArguments()[0]; 376 | int batchSize = batchSizeList.get(0); 377 | 378 | 379 | assertEquals(1, collection.size()); 380 | assertThat(collection, CoreMatchers.hasItem(takeCallResults.get(0))); 381 | drainCallResults.get(0).stream().forEach(collection::add); 382 | 383 | 384 | takeCallResults.remove(0); 385 | drainCallResults.remove(0); 386 | return batchSize; 387 | }; 388 | 389 | OngoingStubbing drainCall = when(transferQueue.drainTo(any(), eq(EXPECTED_MAX_BATCH_SIZE - 1))).thenAnswer(drainAnswer); 390 | 391 | for (int i = 1; i < drainCallResults.size(); i++) { 392 | drainCall = drainCall.thenAnswer(drainAnswer); 393 | } 394 | } 395 | 396 | private List> buildMessages(int count) { 397 | return IntStream.range(0, count) 398 | .mapToObj((i) -> constructLogicalLogMessage(TestMessages.Bar, i, "Hello")) 399 | .collect(toList()); 400 | } 401 | 402 | private LogicalLogRecord constructLogicalLogMessage(TestMessages message, Object... args) { 403 | return new LogicalLogRecord<>(Instant.now(), new DiagnosticContext(Collections::emptyMap), message, Optional.empty(), args); 404 | } 405 | 406 | private Map generateCorrelationIds() { 407 | Map result = new HashMap<>(); 408 | result.put("foo", UUID.randomUUID().toString()); 409 | result.put(UUID.randomUUID().toString(), UUID.randomUUID().toString()); 410 | result.put(UUID.randomUUID().toString(), UUID.randomUUID().toString()); 411 | 412 | return result; 413 | } 414 | 415 | private static Matcher> isEmpty() { 416 | return new BaseMatcher>() { 417 | @Override 418 | public boolean matches(Object item) { 419 | if (!(item instanceof Optional)) { 420 | return false; 421 | } 422 | Optional optional = (Optional) item; 423 | return !optional.isPresent(); 424 | } 425 | 426 | @Override 427 | public void describeTo(Description description) { 428 | description.appendText("an empty optional"); 429 | } 430 | }; 431 | } 432 | 433 | private enum TestMessages implements LogMessage { 434 | Foo("CODE-Foo", "An event occurred"), 435 | Bar("CODE-Bar", "An event with %d %s messages"); 436 | 437 | //region LogMessage implementation guts 438 | private final String messageCode; 439 | private final String messagePattern; 440 | 441 | TestMessages(String messageCode, String messagePattern) { 442 | this.messageCode = messageCode; 443 | this.messagePattern = messagePattern; 444 | } 445 | 446 | @Override 447 | public String getMessageCode() { 448 | return messageCode; 449 | } 450 | 451 | @Override 452 | public String getMessagePattern() { 453 | return messagePattern; 454 | } 455 | //endregion 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/BasicOpsLoggerFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.DiagnosticContextSupplier; 4 | import com.equalexperts.logging.LogMessage; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import java.io.IOException; 9 | import java.time.Clock; 10 | import java.util.HashMap; 11 | import java.util.concurrent.locks.ReentrantLock; 12 | import java.util.function.Consumer; 13 | 14 | import static org.hamcrest.core.IsInstanceOf.instanceOf; 15 | import static org.junit.Assert.*; 16 | import static org.mockito.Mockito.mock; 17 | import static org.mockito.Mockito.when; 18 | 19 | public class BasicOpsLoggerFactoryTest { 20 | 21 | private OutputStreamDestination expectedDestination = new OutputStreamDestination<>(System.out, new SimpleStackTraceProcessor()); 22 | private Consumer expectedErrorHandler = t -> {}; 23 | private DiagnosticContextSupplier expectedDiagnosticContextSupplier = HashMap::new; 24 | 25 | private InfrastructureFactory infrastructure = mock(InfrastructureFactory.class); 26 | 27 | private BasicOpsLoggerFactory factory = new BasicOpsLoggerFactory(); 28 | 29 | @Before 30 | public void setup() throws IOException { 31 | when(infrastructure.configureDestination()).thenReturn(expectedDestination); 32 | when(infrastructure.configureContextSupplier()).thenReturn(expectedDiagnosticContextSupplier); 33 | when(infrastructure.configureErrorHandler()).thenReturn(expectedErrorHandler); 34 | } 35 | 36 | @Test 37 | public void build_shouldConstructACorrectlyConfiguredBasicOpsLogger() throws Exception { 38 | 39 | 40 | BasicOpsLogger result = factory.build(infrastructure); 41 | 42 | assertEquals(Clock.systemUTC(), result.getClock()); 43 | assertSame(expectedDiagnosticContextSupplier, result.getDiagnosticContextSupplier()); 44 | assertSame(expectedDestination, result.getDestination()); 45 | assertNotNull(result.getLock()); 46 | assertThat(result.getLock(), instanceOf(ReentrantLock.class)); 47 | assertSame(expectedErrorHandler, result.getErrorHandler()); 48 | } 49 | 50 | @Test 51 | public void build_shouldUseADifferentLockForEachConstructedOpsLogger() throws Exception { 52 | 53 | BasicOpsLogger firstResult = factory.build(infrastructure); 54 | BasicOpsLogger secondResult = factory.build(infrastructure); 55 | 56 | assertNotNull(firstResult.getLock()); 57 | assertNotNull(secondResult.getLock()); 58 | assertNotEquals(firstResult.getLock(), secondResult.getLock()); 59 | } 60 | 61 | private enum TestMessages implements LogMessage { 62 | ; //don't actually need any messages for these tests 63 | 64 | //region LogMessage implementation guts 65 | private final String messageCode; 66 | private final String messagePattern; 67 | 68 | TestMessages(String messageCode, String messagePattern) { 69 | this.messageCode = messageCode; 70 | this.messagePattern = messagePattern; 71 | } 72 | 73 | @Override 74 | public String getMessageCode() { 75 | return messageCode; 76 | } 77 | 78 | @Override 79 | public String getMessagePattern() { 80 | return messagePattern; 81 | } 82 | //endregion 83 | } 84 | } -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/BasicOpsLoggerTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.DiagnosticContextSupplier; 4 | import com.equalexperts.logging.LogMessage; 5 | import com.equalexperts.logging.OpsLogger; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.mockito.*; 9 | 10 | import java.io.IOException; 11 | import java.time.Clock; 12 | import java.time.Instant; 13 | import java.time.ZoneOffset; 14 | import java.util.Collections; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | import java.util.UUID; 18 | import java.util.concurrent.locks.Lock; 19 | import java.util.function.Consumer; 20 | 21 | import static org.junit.Assert.*; 22 | import static org.mockito.Matchers.any; 23 | import static org.mockito.Mockito.*; 24 | 25 | public class BasicOpsLoggerTest { 26 | private Clock fixedClock = Clock.fixed(Instant.parse("2014-02-01T14:57:12.500Z"), ZoneOffset.UTC); 27 | @Mock private Destination destination; 28 | @Mock private DiagnosticContextSupplier diagnosticContextSupplier; 29 | @Mock private Consumer exceptionConsumer; 30 | @Mock private Lock lock; 31 | @Captor private ArgumentCaptor> captor; 32 | 33 | private OpsLogger logger; 34 | 35 | @Before 36 | public void setup() { 37 | MockitoAnnotations.initMocks(this); 38 | logger = new BasicOpsLogger<>(fixedClock, diagnosticContextSupplier, destination, lock, exceptionConsumer); 39 | } 40 | 41 | //region tests for log 42 | 43 | @Test 44 | public void log_shouldWriteALogicalLogRecordToTheDestination_givenALogMessageInstance() throws Exception { 45 | Map expectedCorrelationIds = generateCorrelationIds(); 46 | when(diagnosticContextSupplier.getMessageContext()).thenReturn(expectedCorrelationIds); 47 | doNothing().when(destination).publish(captor.capture()); 48 | 49 | logger.log(TestMessages.Bar, 64, "Hello, World"); 50 | 51 | verify(destination, times(1)).publish(any()); 52 | verify(diagnosticContextSupplier).getMessageContext(); 53 | verifyNoMoreInteractions(diagnosticContextSupplier); 54 | 55 | LogicalLogRecord record = captor.getValue(); 56 | assertEquals(fixedClock.instant(), record.getTimestamp()); 57 | assertEquals(expectedCorrelationIds, record.getDiagnosticContext().getContext()); 58 | assertEquals(TestMessages.Bar, record.getMessage()); 59 | assertNotNull(record.getCause()); 60 | assertFalse(record.getCause().isPresent()); 61 | assertArrayEquals(new Object[] {64, "Hello, World"}, record.getDetails()); 62 | } 63 | 64 | @Test 65 | public void log_shouldObtainAndReleaseALockAndBeginAndEndADestinationBatch_givenALogMessageInstance() throws Exception { 66 | logger.log(TestMessages.Foo); 67 | 68 | InOrder inOrder = inOrder(lock, destination); 69 | inOrder.verify(lock).lock(); 70 | inOrder.verify(destination).beginBatch(); 71 | inOrder.verify(destination).publish(any()); 72 | inOrder.verify(destination).endBatch(); 73 | inOrder.verify(lock).unlock(); 74 | } 75 | 76 | @Test 77 | public void log_shouldExposeAnExceptionToTheHandler_givenAProblemCreatingTheLogRecord() throws Exception { 78 | logger.log(null); 79 | 80 | verify(exceptionConsumer).accept(Mockito.isA(NullPointerException.class)); 81 | } 82 | 83 | @Test 84 | public void log_shouldNotAcquireALockOrInteractWithTheDestination_givenAProblemCreatingTheLogRecord() throws Exception { 85 | logger.log(null); 86 | 87 | verifyZeroInteractions(lock, destination); 88 | } 89 | 90 | @Test 91 | public void log_shouldExposeAnExceptionToTheHandler_givenAProblemObtainingCorrelationIds() throws Exception { 92 | Error expectedThrowable = new Error(); 93 | when(diagnosticContextSupplier.getMessageContext()).thenThrow(expectedThrowable); 94 | 95 | logger.log(TestMessages.Foo); 96 | 97 | verify(exceptionConsumer).accept(Mockito.same(expectedThrowable)); 98 | } 99 | 100 | @Test 101 | public void log_shouldNotAcquireALockOrInteractWithTheDestination_givenAProblemObtainingCorrelationIds() throws Exception { 102 | when(diagnosticContextSupplier.getMessageContext()).thenThrow(new RuntimeException()); 103 | 104 | logger.log(TestMessages.Foo); 105 | 106 | verifyZeroInteractions(lock, destination); 107 | } 108 | 109 | @Test 110 | public void log_shouldExposeAnExceptionToTheHandler_givenAProblemPublishingALogRecord() throws Exception { 111 | RuntimeException expectedException = new NullPointerException(); 112 | doThrow(expectedException).when(destination).publish(any()); 113 | 114 | logger.log(TestMessages.Foo); 115 | 116 | verify(exceptionConsumer).accept(Mockito.same(expectedException)); 117 | } 118 | 119 | @Test 120 | public void log_shouldEndTheBatchAndReleaseTheLock_givenAProblemPublishingALogRecord() throws Exception { 121 | doThrow(new RuntimeException()).when(destination).publish(any()); 122 | 123 | logger.log(TestMessages.Foo); 124 | 125 | InOrder inOrder = inOrder(lock, destination); 126 | inOrder.verify(lock).lock(); 127 | inOrder.verify(destination).beginBatch(); 128 | inOrder.verify(destination).publish(any()); 129 | inOrder.verify(destination).endBatch(); 130 | inOrder.verify(lock).unlock(); 131 | } 132 | 133 | //endregion 134 | 135 | //region tests for logThrowable 136 | 137 | @Test 138 | public void logThrowable_shouldWriteALogicalLogRecordToTheDestination_givenALogMessageInstanceAndAThrowable() throws Exception { 139 | Map expectedCorrelationIds = generateCorrelationIds(); 140 | when(diagnosticContextSupplier.getMessageContext()).thenReturn(expectedCorrelationIds); 141 | doNothing().when(destination).publish(captor.capture()); 142 | RuntimeException expectedException = new RuntimeException("expected"); 143 | 144 | logger.logThrowable(TestMessages.Bar, expectedException, 64, "Hello, World"); 145 | 146 | verify(destination, times(1)).publish(any()); 147 | verify(diagnosticContextSupplier).getMessageContext(); 148 | verifyNoMoreInteractions(diagnosticContextSupplier); 149 | 150 | LogicalLogRecord record = captor.getValue(); 151 | assertEquals(fixedClock.instant(), record.getTimestamp()); 152 | assertEquals(expectedCorrelationIds, record.getDiagnosticContext().getContext()); 153 | assertEquals(TestMessages.Bar, record.getMessage()); 154 | assertNotNull(record.getCause()); 155 | assertSame(expectedException, record.getCause().get()); 156 | assertArrayEquals(new Object[]{64, "Hello, World"}, record.getDetails()); 157 | } 158 | 159 | @Test 160 | public void logThrowable_shouldObtainAndReleaseALockAndBeginAndEndADestinationBatch_givenALogMessageInstanceAndAThrowable() throws Exception { 161 | logger.logThrowable(TestMessages.Foo, new RuntimeException()); 162 | 163 | InOrder inOrder = inOrder(lock, destination); 164 | inOrder.verify(lock).lock(); 165 | inOrder.verify(destination).beginBatch(); 166 | inOrder.verify(destination).publish(any()); 167 | inOrder.verify(destination).endBatch(); 168 | inOrder.verify(lock).unlock(); 169 | } 170 | 171 | @Test 172 | public void logThrowable_shouldExposeAnExceptionToTheHandler_givenAProblemCreatingTheLogRecordWithAThrowable() throws Exception { 173 | logger.logThrowable(TestMessages.Foo, null); 174 | 175 | verify(exceptionConsumer).accept(Mockito.isA(NullPointerException.class)); 176 | } 177 | 178 | @Test 179 | public void logThrowable_shouldNotObtainALockOrInteractWithTheDestination_givenAProblemCreatingTheLogRecordWithAThrowable() throws Exception { 180 | logger.logThrowable(null, new Throwable()); 181 | 182 | verifyZeroInteractions(lock, destination); 183 | } 184 | 185 | @Test 186 | public void logThrowable_shouldExposeAnExceptionToTheHandler_givenAProblemPublishingALogRecordWithAThrowable() throws Exception { 187 | Exception expectedException = new IOException("Couldn't write to the output stream"); 188 | doThrow(expectedException).when(destination).publish(any()); 189 | 190 | logger.logThrowable(TestMessages.Foo, new NullPointerException()); 191 | 192 | verify(exceptionConsumer).accept(Mockito.same(expectedException)); 193 | } 194 | 195 | @Test 196 | public void logThrowable_shouldEndTheBatchAndReleaseTheLock_givenAProblemPublishingTheLogRecordWithAThrowable() throws Exception { 197 | doThrow(new RuntimeException()).when(destination).publish(any()); 198 | 199 | logger.logThrowable(TestMessages.Foo, new Error()); 200 | 201 | InOrder inOrder = inOrder(lock, destination); 202 | inOrder.verify(lock).lock(); 203 | inOrder.verify(destination).beginBatch(); 204 | inOrder.verify(destination).publish(any()); 205 | inOrder.verify(destination).endBatch(); 206 | inOrder.verify(lock).unlock(); 207 | } 208 | 209 | @Test 210 | public void logThrowable_shouldExposeAnExceptionToTheHandler_givenAProblemObtainingCorrelationIdsWithAThrowable() throws Exception { 211 | Error expectedThrowable = new Error(); 212 | when(diagnosticContextSupplier.getMessageContext()).thenThrow(expectedThrowable); 213 | 214 | logger.logThrowable(TestMessages.Foo, new RuntimeException()); 215 | 216 | verify(exceptionConsumer).accept(Mockito.same(expectedThrowable)); 217 | } 218 | 219 | @Test 220 | public void logThrowable_shouldNotObtainALockOrInteractWithTheDestination_givenAProblemObtainingCorrelationIdsWithAThrowable() throws Exception { 221 | when(diagnosticContextSupplier.getMessageContext()).thenThrow(new RuntimeException()); 222 | 223 | logger.logThrowable(TestMessages.Foo, new Exception()); 224 | 225 | verifyZeroInteractions(lock, destination); 226 | } 227 | 228 | //endregion 229 | 230 | @Test 231 | public void with_shouldReturnANewBasicOpsLoggerWithAnOverriddenDiagnosticContextSupplier_givenADiagnosticContextSupplier() throws Exception { 232 | DiagnosticContextSupplier localSupplier = Collections::emptyMap; 233 | BasicOpsLogger basicLogger = (BasicOpsLogger) logger; 234 | 235 | BasicOpsLogger result = basicLogger.with(localSupplier); 236 | 237 | assertNotSame(basicLogger, result); 238 | assertSame(basicLogger.getClock(), result.getClock()); 239 | assertSame(basicLogger.getErrorHandler(), result.getErrorHandler()); 240 | assertSame(basicLogger.getDestination(), result.getDestination()); 241 | assertSame(basicLogger.getLock(), result.getLock()); 242 | assertSame(localSupplier, result.getDiagnosticContextSupplier()); 243 | assertNotSame(basicLogger.getDiagnosticContextSupplier(), result.getDiagnosticContextSupplier()); 244 | } 245 | 246 | @Test 247 | public void close_shouldCloseTheDestination() throws Exception { 248 | logger.close(); 249 | 250 | verify(destination).close(); 251 | } 252 | 253 | @Test 254 | public void close_shouldIgnoreCalls_givenANestedLoggerCreatedByWith() throws Exception { 255 | BasicOpsLogger basicLogger = (BasicOpsLogger) logger; 256 | BasicOpsLogger nested = basicLogger.with(Collections::emptyMap); 257 | 258 | nested.close(); 259 | 260 | verifyZeroInteractions(destination); 261 | } 262 | 263 | private Map generateCorrelationIds() { 264 | Map result = new HashMap<>(); 265 | result.put("foo", UUID.randomUUID().toString()); 266 | result.put(UUID.randomUUID().toString(), UUID.randomUUID().toString()); 267 | result.put(UUID.randomUUID().toString(), UUID.randomUUID().toString()); 268 | return result; 269 | } 270 | 271 | private enum TestMessages implements LogMessage { 272 | Foo("CODE-Foo", "An event of some kind occurred"), 273 | Bar("CODE-Bar", "An event with %d %s messages"); 274 | 275 | //region LogMessage implementation guts 276 | private final String messageCode; 277 | private final String messagePattern; 278 | 279 | TestMessages(String messageCode, String messagePattern) { 280 | this.messageCode = messageCode; 281 | this.messagePattern = messagePattern; 282 | } 283 | 284 | @Override 285 | public String getMessageCode() { 286 | return messageCode; 287 | } 288 | 289 | @Override 290 | public String getMessagePattern() { 291 | return messagePattern; 292 | } 293 | //endregion 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/DiagnosticContextTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.HashMap; 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | 9 | import static java.util.Collections.emptyMap; 10 | import static java.util.Collections.unmodifiableMap; 11 | import static org.junit.Assert.*; 12 | 13 | @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") 14 | public class DiagnosticContextTest { 15 | @Test 16 | public void constructor_shouldCreateAnEmptyContext_givenANullProvider() throws Exception { 17 | assertEquals(emptyMap(), new DiagnosticContext(null).getContext()); 18 | } 19 | 20 | @Test 21 | public void constructor_shouldCreateAnEmptyContext_givenAProviderThatReturnsNull() throws Exception { 22 | assertEquals(emptyMap(), new DiagnosticContext(() -> null).getContext()); 23 | } 24 | 25 | @Test 26 | public void constructor_shouldSafelyCopyTheProvidedContextInformationIntoANewMap() throws Exception { 27 | Map expectedContext = new HashMap<>(); 28 | expectedContext.put("baker", "a"); 29 | expectedContext.put("able", "a"); 30 | expectedContext.put("charlie", "a"); 31 | 32 | Map actualContext = new DiagnosticContext(() -> expectedContext).getContext(); 33 | 34 | assertNotSame(expectedContext, actualContext); 35 | assertEquals(expectedContext, actualContext); 36 | 37 | //ensure it's a copy (as opposed to an adapatation) by clearing the original 38 | expectedContext.clear(); 39 | assertNotEquals(expectedContext, actualContext); 40 | } 41 | 42 | @Test 43 | public void constructor_shouldPreserveInsertionOrder() throws Exception { 44 | Map context = new LinkedHashMap<>(); 45 | context.put("baker", "a"); 46 | context.put("able", "b"); 47 | context.put("charlie", "c"); 48 | 49 | StringBuilder sb = new StringBuilder(); 50 | 51 | new DiagnosticContext(() -> context).printContextInformation(sb); 52 | 53 | assertEquals("baker=a;able=b;charlie=c,", sb.toString()); 54 | } 55 | 56 | @Test 57 | public void constructor_shouldCreateAnUnmodifiableMap() throws Exception { 58 | Map expectedContext = new LinkedHashMap<>(); 59 | expectedContext.put("foo", "bar"); 60 | 61 | Map actualContext = new DiagnosticContext(() -> expectedContext).getContext(); 62 | ensureUnmodifiableMap(actualContext); 63 | } 64 | 65 | @Test 66 | public void printContextInformation_shouldPrintNameValuePairsSeparatedBySemiColonsInTheCorrectOrderFollowedByAComma() throws Exception { 67 | Map context = new LinkedHashMap<>(); 68 | context.put("baker", "a"); 69 | context.put("able", "b"); 70 | context.put("charlie", "c"); 71 | 72 | StringBuilder sb = new StringBuilder(); 73 | 74 | new DiagnosticContext(() -> context).printContextInformation(sb); 75 | 76 | assertEquals(sb.toString(), "baker=a;able=b;charlie=c,"); 77 | } 78 | 79 | @Test 80 | public void printContextInformation_shouldExcludeMappingsWithEmptyValues() throws Exception { 81 | Map context = new LinkedHashMap<>(); 82 | context.put("baker", "a"); 83 | context.put("able", ""); 84 | context.put("charlie", "c"); 85 | 86 | StringBuilder sb = new StringBuilder(); 87 | 88 | new DiagnosticContext(() -> context).printContextInformation(sb); 89 | 90 | assertEquals(sb.toString(), "baker=a;charlie=c,"); 91 | } 92 | 93 | @Test 94 | public void printContextInformation_shouldExcludeMappingsWithNullValues() throws Exception { 95 | Map context = new LinkedHashMap<>(); 96 | context.put("baker", "a"); 97 | context.put("able", null); 98 | context.put("charlie", "c"); 99 | 100 | StringBuilder sb = new StringBuilder(); 101 | 102 | new DiagnosticContext(() -> context).printContextInformation(sb); 103 | 104 | assertEquals(sb.toString(), "baker=a;charlie=c,"); 105 | } 106 | 107 | @Test 108 | public void printContextInformation_shouldNotPrintATrailingComma_givenAnEffectivelyEmptyMergedContext() throws Exception { 109 | Map emptyValue = new LinkedHashMap<>(); 110 | emptyValue.put("able", ""); 111 | 112 | Map nullValue = new LinkedHashMap<>(); 113 | nullValue.put("able", null); 114 | 115 | StringBuilder sb = new StringBuilder(); 116 | 117 | new DiagnosticContext(() -> emptyValue).printContextInformation(sb); 118 | new DiagnosticContext(() -> nullValue).printContextInformation(sb); 119 | new DiagnosticContext(() -> null).printContextInformation(sb); 120 | new DiagnosticContext(null).printContextInformation(sb); 121 | 122 | assertEquals(sb.toString(), ""); 123 | } 124 | 125 | private void ensureUnmodifiableMap(Map mergedContext) { 126 | assertSame("map should be unmodifiable", mergedContext.getClass(), unmodifiableMap(new HashMap<>()).getClass()); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/FileChannelProviderTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.TempFileFixture; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | 7 | import java.io.Closeable; 8 | import java.nio.channels.FileChannel; 9 | import java.nio.file.Path; 10 | 11 | import static java.nio.file.StandardOpenOption.CREATE; 12 | import static org.hamcrest.CoreMatchers.instanceOf; 13 | import static org.junit.Assert.*; 14 | import static org.mockito.Matchers.eq; 15 | import static org.mockito.Matchers.same; 16 | import static org.mockito.Mockito.when; 17 | 18 | public class FileChannelProviderTest extends AbstractFileChannelProviderTest { 19 | @Rule 20 | public TempFileFixture tempFiles = new TempFileFixture(); 21 | private final Path mockPath = createMockPath(); 22 | private final FileChannelProvider provider = new FileChannelProvider(mockPath); 23 | 24 | @Test 25 | public void getChannel_shouldReturnAResultWithAChannelAndAssociatedWriter() throws Exception { 26 | FileChannel testFileChannel = FileChannel.open(tempFiles.createTempFile(null), CREATE); 27 | when(mockPath.getFileSystem().provider().newFileChannel(same(mockPath), eq(CREATE_AND_APPEND))).thenReturn(testFileChannel); 28 | 29 | FileChannelProvider.Result result = provider.getChannel(); 30 | 31 | assertSame(testFileChannel, result.channel); 32 | ensureAssociated(result.writer, result.channel); 33 | ensureUtf8Charset(result.writer); 34 | assertTrue(testFileChannel.isOpen()); 35 | } 36 | 37 | @Test 38 | public void close_shouldCloseTheFileChannelAndAssociatedWriter_givenAResultReturnedByGetChannel() throws Exception { 39 | FileChannel testFileChannel = FileChannel.open(tempFiles.createTempFile(null), CREATE); 40 | when(mockPath.getFileSystem().provider().newFileChannel(same(mockPath), eq(CREATE_AND_APPEND))).thenReturn(testFileChannel); 41 | 42 | FileChannelProvider.Result result = provider.getChannel(); 43 | assertThat(result, instanceOf(Closeable.class)); 44 | 45 | assertTrue("precondition: FileChannel should be open", result.channel.isOpen()); 46 | assertTrue("precondition: Writer should be open", isOpen(result.writer)); 47 | 48 | result.close(); 49 | 50 | assertFalse(result.channel.isOpen()); 51 | assertFalse(isOpen(result.writer)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/FilesystemStackTraceProcessorTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.IOException; 7 | import java.net.URI; 8 | import java.nio.file.*; 9 | import java.nio.file.attribute.BasicFileAttributes; 10 | import java.nio.file.spi.FileSystemProvider; 11 | 12 | import static org.junit.Assert.*; 13 | import static org.mockito.Mockito.*; 14 | 15 | import static java.nio.file.StandardOpenOption.*; 16 | 17 | public class FilesystemStackTraceProcessorTest { 18 | 19 | private final Path mockDestinationDirectory = mock(Path.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS)); 20 | private final ThrowableFingerprintCalculator fingerprintCalculator = mock(ThrowableFingerprintCalculator.class); 21 | private final StackTraceProcessor processor = new FilesystemStackTraceProcessor(mockDestinationDirectory, fingerprintCalculator); 22 | 23 | @Test 24 | public void process_shouldStoreTheStackTraceInAFileBasedOnTheFingerPrint() throws Exception { 25 | //setup is hairy — mocking a lot of file IO 26 | Throwable expectedException = new RuntimeException("blah!"); 27 | String expectedFingerprint = "12345"; 28 | String expectedFilename = "stacktrace_" + expectedFingerprint + ".txt"; 29 | String expectedStacktraceUri = "file:///tmp/log/" + expectedFilename; 30 | String expectedMessage = expectedException.toString() + " (" + expectedStacktraceUri + ")"; 31 | 32 | when(fingerprintCalculator.calculateFingerprint(expectedException)).thenReturn(expectedFingerprint); 33 | 34 | Path expectedPath = mock(Path.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS)); 35 | when(mockDestinationDirectory.resolve(expectedFilename)).thenReturn(expectedPath); 36 | when(expectedPath.toUri()).thenReturn(new URI(expectedStacktraceUri)); 37 | pretendMockPathDoesNotExist(expectedPath); 38 | 39 | ByteArrayOutputStream simulatedFileOutputStream = new ByteArrayOutputStream(); 40 | when(Files.newOutputStream(expectedPath, CREATE_NEW, WRITE)).thenReturn(simulatedFileOutputStream); 41 | 42 | TestPrintStream expectedFileContents = new TestPrintStream(); 43 | expectedException.printStackTrace(expectedFileContents); 44 | 45 | StringBuilder output = new StringBuilder(); 46 | 47 | //execute 48 | processor.process(expectedException, output); 49 | 50 | //assert 51 | verify(expectedPath.getFileSystem().provider()).newOutputStream(expectedPath, CREATE_NEW, WRITE); 52 | assertEquals(expectedFileContents.toString(), new String(simulatedFileOutputStream.toByteArray())); 53 | assertEquals(expectedMessage, output.toString()); 54 | } 55 | 56 | @Test 57 | public void process_shouldReuseTheURIOfTheExistingFile_whenItAlreadyExists() throws Exception { 58 | //setup is hairy — mocking a lot of file IO 59 | Throwable expectedException = new RuntimeException("blah!"); 60 | String expectedFingerprint = "12345"; 61 | String expectedFilename = "stacktrace_" + expectedFingerprint + ".txt"; 62 | String expectedStacktraceUri = "file:///tmp/log/" + expectedFilename; 63 | String expectedMessage = expectedException.toString() + " (" + expectedStacktraceUri + ")"; 64 | 65 | when(fingerprintCalculator.calculateFingerprint(expectedException)).thenReturn(expectedFingerprint); 66 | 67 | Path expectedPath = mock(Path.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS)); 68 | when(mockDestinationDirectory.resolve(expectedFilename)).thenReturn(expectedPath); 69 | when(expectedPath.toUri()).thenReturn(new URI(expectedStacktraceUri)); 70 | 71 | StringBuilder output = new StringBuilder(); 72 | 73 | //execute 74 | processor.process(expectedException, output); 75 | 76 | //assert 77 | assertEquals(expectedMessage, output.toString()); 78 | verify(expectedPath.getFileSystem().provider()).checkAccess(expectedPath); 79 | verifyNoMoreInteractions(expectedPath.getFileSystem().provider()); 80 | } 81 | 82 | @Test 83 | public void process_shouldReuseTheURIOfTheExistingFile_whenItAlreadyExistsInARaceCondition() throws Exception { 84 | //setup is hairy — mocking a lot of file IO 85 | Throwable expectedException = new RuntimeException("blah!"); 86 | String expectedFingerprint = "12345"; 87 | String expectedFilename = "stacktrace_" + expectedFingerprint + ".txt"; 88 | String expectedStacktraceUri = "file:///tmp/log/" + expectedFilename; 89 | String expectedMessage = expectedException.toString() + " (" + expectedStacktraceUri + ")"; 90 | 91 | when(fingerprintCalculator.calculateFingerprint(expectedException)).thenReturn(expectedFingerprint); 92 | 93 | Path expectedPath = mock(Path.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS)); 94 | when(mockDestinationDirectory.resolve(expectedFilename)).thenReturn(expectedPath); 95 | when(expectedPath.toUri()).thenReturn(new URI(expectedStacktraceUri)); 96 | pretendMockPathDoesNotExist(expectedPath); 97 | 98 | //filesystem reports that the file doesn't exist, but fail when creating (simulate a race condition) 99 | when(Files.newOutputStream(expectedPath, CREATE_NEW, WRITE)).thenThrow(new FileAlreadyExistsException("blah")); 100 | 101 | TestPrintStream expectedFileContents = new TestPrintStream(); 102 | expectedException.printStackTrace(expectedFileContents); 103 | 104 | StringBuilder output = new StringBuilder(); 105 | 106 | //execute 107 | processor.process(expectedException, output); 108 | 109 | //assert 110 | verify(expectedPath.getFileSystem().provider()).checkAccess(expectedPath); 111 | verify(expectedPath.getFileSystem().provider()).newOutputStream(expectedPath, CREATE_NEW, WRITE); 112 | verifyNoMoreInteractions(expectedPath.getFileSystem().provider()); 113 | assertEquals(expectedMessage, output.toString()); 114 | } 115 | 116 | private void pretendMockPathDoesNotExist(Path expectedPath) throws Exception { 117 | FileSystemProvider mockProvider = expectedPath.getFileSystem().provider(); 118 | doThrow(NoSuchFileException.class).when(mockProvider).checkAccess(expectedPath); //notExists check 119 | 120 | //for exists, simulate a path not existing by failing to read attributes 121 | when(Files.readAttributes(expectedPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(new IOException("File does not exist")); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/LogicalLogRecordTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.LogMessage; 4 | import org.junit.Test; 5 | 6 | import java.time.Instant; 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | 12 | import static java.util.Collections.emptyMap; 13 | import static org.hamcrest.CoreMatchers.containsString; 14 | import static org.junit.Assert.*; 15 | import static org.mockito.Matchers.any; 16 | import static org.mockito.Mockito.spy; 17 | import static org.mockito.Mockito.verify; 18 | 19 | public class LogicalLogRecordTest { 20 | 21 | static final StackTraceProcessor PROCESSOR_SHOULD_NOT_BE_CALLED = (t, out) -> fail("should not be called"); 22 | static final DiagnosticContext SAMPLE_DIAGNOSTIC_CONTEXT = new DiagnosticContext(Collections::emptyMap); 23 | 24 | @Test 25 | public void constructor_shouldThrowANullPointerException_givenANullTimestamp() throws Exception { 26 | try { 27 | new LogicalLogRecord<>(null, SAMPLE_DIAGNOSTIC_CONTEXT, TestMessages.Foo, Optional.empty()); 28 | fail("expected an exception"); 29 | } catch (NullPointerException e) { 30 | assertThat(e.getMessage(), containsString("parameter timestamp")); 31 | } 32 | } 33 | 34 | @Test 35 | public void constructor_shouldThrowANullPointerException_givenANullDiagnosticContext() throws Exception { 36 | try { 37 | new LogicalLogRecord<>(Instant.now(), null, TestMessages.Foo, Optional.empty()); 38 | fail("expected an exception"); 39 | } catch (NullPointerException e) { 40 | assertThat(e.getMessage(), containsString("parameter diagnosticContext")); 41 | } 42 | } 43 | 44 | @Test 45 | public void constructor_shouldThrowANullPointerException_givenANullMessage() throws Exception { 46 | try { 47 | new LogicalLogRecord(Instant.now(), SAMPLE_DIAGNOSTIC_CONTEXT, null, Optional.empty()); 48 | fail("expected an exception"); 49 | } catch (NullPointerException e) { 50 | assertThat(e.getMessage(), containsString("parameter message")); 51 | } 52 | } 53 | 54 | @Test 55 | public void constructor_shouldThrowANullPointerException_givenANullCause() throws Exception { 56 | try { 57 | new LogicalLogRecord<>(Instant.now(), SAMPLE_DIAGNOSTIC_CONTEXT, TestMessages.Foo, null); 58 | fail("expected an exception"); 59 | } catch (NullPointerException e) { 60 | assertThat(e.getMessage(), containsString("parameter cause")); 61 | } 62 | } 63 | 64 | @Test 65 | public void constructor_shouldThrowANullPointerException_givenNullDetails() throws Exception { 66 | try { 67 | new LogicalLogRecord<>(Instant.now(), SAMPLE_DIAGNOSTIC_CONTEXT, TestMessages.Foo, Optional.empty(), (Object[]) null); 68 | fail("expected an exception"); 69 | } catch (NullPointerException e) { 70 | assertThat(e.getMessage(), containsString("parameter details")); 71 | } 72 | } 73 | 74 | @Test 75 | public void format_shouldProduceAnAppropriatelyFormattedMessage() throws Exception { 76 | Instant instant = Instant.parse("2014-04-01T13:37:00.123Z"); 77 | LogicalLogRecord record = new LogicalLogRecord<>(instant, SAMPLE_DIAGNOSTIC_CONTEXT, TestMessages.Bar, Optional.empty(), 42); 78 | 79 | String result = record.format(PROCESSOR_SHOULD_NOT_BE_CALLED); 80 | 81 | assertEquals("2014-04-01T13:37:00.123Z,CODE-Bar,A Bar event occurred, with argument 42", result); 82 | } 83 | 84 | @Test 85 | public void format_shouldIncludeMilliseconds_whenTheTimestampIsAnEvenSecond() throws Exception { 86 | Instant instant = Instant.parse("2014-04-01T13:37:00.000Z"); 87 | LogicalLogRecord record = new LogicalLogRecord<>(instant, SAMPLE_DIAGNOSTIC_CONTEXT, TestMessages.Foo, Optional.empty()); 88 | 89 | String result = record.format(PROCESSOR_SHOULD_NOT_BE_CALLED); 90 | 91 | assertEquals("2014-04-01T13:37:00.000Z,CODE-Foo,An event of some kind occurred", result); 92 | } 93 | 94 | @Test 95 | public void format_shouldProduceAnAppropriatelyFormattedMessage_givenAThrowable() throws Exception { 96 | Instant instant = Instant.parse("2014-04-01T13:37:00.123Z"); 97 | Throwable expectedThrowable = new RuntimeException(); 98 | LogicalLogRecord record = new LogicalLogRecord<>(instant, SAMPLE_DIAGNOSTIC_CONTEXT, TestMessages.Bar, Optional.of(expectedThrowable), 42); 99 | 100 | StackTraceProcessor processor = (t, out) -> { 101 | assertSame(expectedThrowable, t); 102 | out.append("#EXCEPTION_HERE#"); 103 | }; 104 | 105 | String result = record.format(processor); 106 | 107 | assertEquals("2014-04-01T13:37:00.123Z,CODE-Bar,A Bar event occurred, with argument 42 #EXCEPTION_HERE#", result); 108 | } 109 | 110 | @Test 111 | public void format_shouldIncludeTheDiagnosticContextInTheFormattedMessage() throws Exception { 112 | Instant instant = Instant.parse("2014-04-01T13:37:00.123Z"); 113 | 114 | Map correlationIds = new HashMap<>(); 115 | correlationIds.put("user", "joeUser"); 116 | DiagnosticContext dc = spy(new DiagnosticContext(() -> correlationIds)); 117 | 118 | LogicalLogRecord record = new LogicalLogRecord<>(instant, dc, TestMessages.Bar, Optional.empty(), 42); 119 | 120 | String result = record.format(PROCESSOR_SHOULD_NOT_BE_CALLED); 121 | 122 | assertEquals("2014-04-01T13:37:00.123Z,user=joeUser,CODE-Bar,A Bar event occurred, with argument 42", result); 123 | verify(dc).printContextInformation(any()); 124 | } 125 | 126 | private enum TestMessages implements LogMessage { 127 | Foo("CODE-Foo", "An event of some kind occurred"), 128 | Bar("CODE-Bar", "A Bar event occurred, with argument %d"); 129 | 130 | //region LogMessage implementation guts 131 | private final String messageCode; 132 | private final String messagePattern; 133 | 134 | TestMessages(String messageCode, String messagePattern) { 135 | this.messageCode = messageCode; 136 | this.messagePattern = messagePattern; 137 | } 138 | 139 | @Override 140 | public String getMessageCode() { 141 | return messageCode; 142 | } 143 | 144 | @Override 145 | public String getMessagePattern() { 146 | return messagePattern; 147 | } 148 | //endregion 149 | } 150 | } -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/OutputStreamDestinationTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.LogMessage; 4 | import com.equalexperts.logging.RestoreSystemStreamsFixture; 5 | import org.junit.Rule; 6 | import org.junit.Test; 7 | 8 | import java.time.Instant; 9 | import java.util.Collections; 10 | import java.util.Optional; 11 | 12 | import static org.junit.Assert.*; 13 | 14 | public class OutputStreamDestinationTest { 15 | 16 | @Rule 17 | public RestoreSystemStreamsFixture systemStreamsFixture = new RestoreSystemStreamsFixture(); 18 | 19 | private final TestPrintStream output = new TestPrintStream(); 20 | private final StackTraceProcessor processor = new SimpleStackTraceProcessor(); 21 | private final OutputStreamDestination destination = new OutputStreamDestination<>(output, processor); 22 | 23 | @Test 24 | public void publish_shouldPublishAFormattedLogRecord() throws Exception { 25 | LogicalLogRecord record = new LogicalLogRecord<>(Instant.now(), new DiagnosticContext(Collections::emptyMap), TestMessages.Foo, Optional.empty()); 26 | String expectedMessage = record.format(processor) + System.getProperty("line.separator"); 27 | 28 | destination.publish(record); 29 | 30 | assertEquals(expectedMessage, output.toString()); 31 | } 32 | 33 | @Test 34 | public void close_shouldCloseThePrintStream() throws Exception { 35 | destination.close(); 36 | 37 | assertTrue(output.isClosed()); 38 | } 39 | 40 | @Test 41 | public void close_shouldNotCloseThePrintStream_whenThePrintStreamIsSystemOut() throws Exception { 42 | System.setOut(output); 43 | 44 | destination.close(); 45 | 46 | assertFalse(output.isClosed()); 47 | } 48 | 49 | @Test 50 | public void close_shouldNotCloseThePrintStream_whenThePrintStreamIsSystemErr() throws Exception { 51 | System.setErr(output); 52 | 53 | destination.close(); 54 | 55 | assertFalse(output.isClosed()); 56 | } 57 | 58 | @SuppressWarnings("ConstantConditions") 59 | @Test 60 | public void class_shouldImplementAsyncOpsLoggerDestination() throws Exception { 61 | assertTrue(destination instanceof Destination); 62 | } 63 | 64 | @Test 65 | public void beginBatch_shouldDoNothing() throws Exception { 66 | destination.beginBatch(); 67 | 68 | assertEquals("", this.output.toString()); 69 | } 70 | 71 | @Test 72 | public void endBatch_shouldDoNothing() throws Exception { 73 | destination.endBatch(); 74 | 75 | assertEquals("", this.output.toString()); 76 | } 77 | 78 | private enum TestMessages implements LogMessage { 79 | Foo("CODE-Foo", "An event of some kind occurred"); 80 | 81 | //region LogMessage implementation guts 82 | private final String messageCode; 83 | private final String messagePattern; 84 | 85 | TestMessages(String messageCode, String messagePattern) { 86 | this.messageCode = messageCode; 87 | this.messagePattern = messagePattern; 88 | } 89 | 90 | @Override 91 | public String getMessageCode() { 92 | return messageCode; 93 | } 94 | 95 | @Override 96 | public String getMessagePattern() { 97 | return messagePattern; 98 | } 99 | //endregion 100 | } 101 | } -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/PathDestinationTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import com.equalexperts.logging.LogMessage; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.mockito.InOrder; 7 | 8 | import java.io.IOException; 9 | import java.io.StringWriter; 10 | import java.io.Writer; 11 | import java.nio.channels.FileChannel; 12 | import java.nio.channels.FileLock; 13 | import java.time.Instant; 14 | import java.util.Collections; 15 | import java.util.Optional; 16 | import java.util.concurrent.CountDownLatch; 17 | import java.util.concurrent.atomic.AtomicBoolean; 18 | 19 | import static com.equalexperts.logging.impl.FileChannelProvider.Result; 20 | import static org.hamcrest.core.IsInstanceOf.instanceOf; 21 | import static org.junit.Assert.*; 22 | import static org.mockito.Mockito.*; 23 | 24 | public class PathDestinationTest { 25 | 26 | private StringWriter writer = spy(new StringWriter()); 27 | private FileChannel channel = mock(FileChannel.class); 28 | private FileLock lock = mock(FileLock.class); 29 | private FileChannelProvider provider = mock(FileChannelProvider.class); 30 | private StackTraceProcessor processor = mock(StackTraceProcessor.class); 31 | private ActiveRotationRegistry registry = mock(ActiveRotationRegistry.class); 32 | private PathDestination destination = new PathDestination<>(provider, processor, registry); 33 | 34 | @Before 35 | public void setup() throws Exception { 36 | constructResult(writer, channel); 37 | doReturn(lock).when(channel).lock(); 38 | } 39 | 40 | @Test 41 | public void beginBatch_shouldOpenAFileChannelAndLockTheFile() throws Exception { 42 | 43 | destination.beginBatch(); 44 | 45 | verify(provider).getChannel(); 46 | verify(channel).lock(); 47 | verifyZeroInteractions(lock); 48 | } 49 | 50 | @Test 51 | public void publish_shouldFormatTheLogRecordAndWriteItToTheFile() throws Exception { 52 | LogicalLogRecord record = new LogicalLogRecord<>(Instant.now(), new DiagnosticContext(Collections::emptyMap), TestMessages.Foo, Optional.empty()); 53 | record = spy(record); //use a spy so we can verify at the bottom 54 | destination.beginBatch(); 55 | 56 | destination.publish(record); 57 | 58 | verify(record).format(processor); 59 | verify(writer, times(1)).write(isA(String.class)); //write in one pass to avoid a partial flush 60 | assertEquals(record.format(processor) + System.getProperty("line.separator"), writer.toString()); 61 | } 62 | 63 | @Test 64 | public void publish_shouldNotFlushTheWriter() throws Exception { 65 | LogicalLogRecord record = new LogicalLogRecord<>(Instant.now(), new DiagnosticContext(Collections::emptyMap), TestMessages.Foo, Optional.empty()); 66 | destination.beginBatch(); 67 | 68 | destination.publish(record); 69 | 70 | verify(writer, never()).flush(); 71 | } 72 | 73 | @Test 74 | public void endBatch_shouldFlushTheWriterReleaseTheFileLockAndCloseTheFileChannelAndWriter() throws Exception { 75 | destination.beginBatch(); 76 | 77 | destination.endBatch(); 78 | 79 | InOrder order = inOrder(writer, lock); 80 | order.verify(writer).flush(); 81 | order.verify(lock).release(); 82 | order.verify(writer).close(); 83 | } 84 | 85 | @Test 86 | public void beginBatch_shouldCloseAndReopenFileChannelsAndLocks_whenThePreviousBatchWasNotEnded() throws Exception { 87 | reset(provider); 88 | Result firstResult = new Result(channel, writer); 89 | Result secondResult = new Result(mock(FileChannel.class), spy(new StringWriter())); 90 | FileLock secondLock = mock(FileLock.class); 91 | when(provider.getChannel()).thenReturn(firstResult, secondResult); 92 | doReturn(secondLock).when(secondResult.channel).lock(); 93 | destination.beginBatch(); 94 | 95 | destination.beginBatch(); 96 | verify(writer).flush(); 97 | verify(lock).release(); 98 | verify(writer).close(); 99 | verify(secondResult.channel).lock(); 100 | verifyZeroInteractions(secondLock); 101 | verifyZeroInteractions(secondResult.writer); 102 | } 103 | 104 | @Test 105 | public void close_shouldReleaseTheFileLockAndCloseTheFileChannel_whenABatchIsOpen() throws Exception { 106 | destination.beginBatch(); 107 | 108 | destination.close(); 109 | 110 | InOrder order = inOrder(writer, lock); 111 | order.verify(writer).flush(); 112 | order.verify(lock).release(); 113 | order.verify(writer).close(); 114 | } 115 | 116 | @Test 117 | public void close_shouldNotManipulateABatch_whenABatchIsNotOpen() throws Exception { 118 | destination.close(); 119 | } 120 | 121 | @Test 122 | public void close_shouldRemoveThePathDestinationFromTheActiveRotationRegistry() throws Exception { 123 | destination.close(); 124 | 125 | verify(registry).remove(destination); 126 | } 127 | 128 | @Test 129 | public void class_shouldImplementDestination() throws Exception { 130 | assertThat(destination, instanceOf(Destination.class)); 131 | } 132 | 133 | @Test 134 | public void class_shouldImplementActiveRotationSupport() throws Exception { 135 | assertThat(destination, instanceOf(ActiveRotationSupport.class)); 136 | } 137 | 138 | @Test 139 | public void refreshFileHandles_shouldReturnImmediately_whenTheDestinationHasNeverBeenUsed() throws Exception { 140 | ActiveRotationSupport ars = destination; 141 | 142 | ars.refreshFileHandles(); 143 | } 144 | 145 | @Test 146 | public void refreshFileHandles_shouldReturnImmediately_whenTheDestinationIsNotCurrentlyInUse() throws Exception { 147 | LogicalLogRecord record = new LogicalLogRecord<>(Instant.now(), new DiagnosticContext(Collections::emptyMap), TestMessages.Foo, Optional.empty()); 148 | destination.beginBatch(); 149 | destination.publish(record); 150 | destination.endBatch(); 151 | ActiveRotationSupport ars = destination; 152 | 153 | ars.refreshFileHandles(); 154 | } 155 | 156 | @Test 157 | public void refreshFileHandles_shouldBlockUntilABatchIsClosed_givenAnOpenBatch() throws Exception { 158 | destination.beginBatch(); 159 | AtomicBoolean callReturned = new AtomicBoolean(false); 160 | 161 | CountDownLatch startupLatch = new CountDownLatch(1); 162 | Thread rotationThread = new Thread(() -> { 163 | startupLatch.countDown(); 164 | ActiveRotationSupport ars = destination; 165 | try { 166 | ars.refreshFileHandles(); 167 | } catch (InterruptedException e) { 168 | throw new RuntimeException(e); 169 | } 170 | callReturned.set(true); 171 | }); 172 | rotationThread.setDaemon(true); 173 | rotationThread.start(); 174 | startupLatch.await(); //wait until the thread has started 175 | 176 | try { 177 | //the call to refreshFileHandles should not have returned 178 | Thread.sleep(100L); 179 | assertFalse(callReturned.get()); 180 | 181 | destination.endBatch(); 182 | 183 | rotationThread.join(100L); //now the call should return 184 | assertTrue(callReturned.get()); 185 | } finally { 186 | if (rotationThread.isAlive()) { 187 | //need to call this to violently abort the thread if the thread is still alive at the end of the test 188 | //noinspection deprecation 189 | rotationThread.stop(); 190 | } 191 | } 192 | } 193 | 194 | private void constructResult(Writer writer, FileChannel channel) throws IOException { 195 | Result expectedResult = new Result(channel, writer); 196 | when(provider.getChannel()).thenReturn(expectedResult); 197 | } 198 | 199 | private enum TestMessages implements LogMessage { 200 | Foo("CODE-Foo", "An event of some kind occurred"); 201 | 202 | //region LogMessage implementation guts 203 | private final String messageCode; 204 | private final String messagePattern; 205 | 206 | TestMessages(String messageCode, String messagePattern) { 207 | this.messageCode = messageCode; 208 | this.messagePattern = messagePattern; 209 | } 210 | 211 | @Override 212 | public String getMessageCode() { 213 | return messageCode; 214 | } 215 | 216 | @Override 217 | public String getMessagePattern() { 218 | return messagePattern; 219 | } 220 | //endregion 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/PrintStreamTestUtils.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import java.io.FilterOutputStream; 4 | import java.io.OutputStream; 5 | import java.io.PrintStream; 6 | import java.lang.reflect.Field; 7 | import java.util.stream.Stream; 8 | 9 | public class PrintStreamTestUtils { 10 | /** 11 | * Uses reflection to return the OutputStream wrapped by a PrintStream 12 | */ 13 | public static OutputStream getBackingOutputStream(PrintStream stream) throws Exception { 14 | Field field = getInternalField(FilterOutputStream.class, "out"); 15 | return (OutputStream) field.get(stream); 16 | } 17 | 18 | /** 19 | * Uses reflection to determine the autoFlush setting of a PrintStream 20 | */ 21 | public static boolean getAutoFlush(PrintStream stream) throws Exception { 22 | Field field = getInternalField(PrintStream.class, "autoFlush"); 23 | return (boolean) field.get(stream); 24 | } 25 | 26 | /** 27 | * Obtains a reference to a non-public field and makes it accessible 28 | */ 29 | private static Field getInternalField(Class cls, String fieldName) { 30 | Field field = Stream.of(cls.getDeclaredFields()) 31 | .filter(f -> f.getName().equals(fieldName)) 32 | .findFirst().get(); 33 | field.setAccessible(true); 34 | return field; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/SimpleStackTraceProcessorTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.hamcrest.CoreMatchers.containsString; 6 | import static org.hamcrest.CoreMatchers.endsWith; 7 | import static org.hamcrest.CoreMatchers.not; 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertThat; 10 | 11 | public class SimpleStackTraceProcessorTest { 12 | 13 | private final StackTraceProcessor processor = new SimpleStackTraceProcessor(); 14 | 15 | @Test 16 | public void process_shouldPrintTheStackTraceAsAMultilineString_givenAThrowable() throws Exception { 17 | String expectedMessage = "blah blah blah"; 18 | Throwable expectedException = new RuntimeException(expectedMessage); 19 | String expectedProcessedMessage = getExceptionPrintout(expectedException); 20 | StringBuilder actualOutput = new StringBuilder(); 21 | 22 | processor.process(expectedException, actualOutput); 23 | 24 | assertEquals(expectedProcessedMessage, actualOutput.toString()); 25 | assertThat(actualOutput.toString(), containsString("\n")); 26 | assertThat(actualOutput.toString(), containsString(expectedMessage)); 27 | } 28 | 29 | @Test 30 | public void process_shouldStripAnEndingNewLineFromOutput_givenAThrowable() throws Exception { 31 | Throwable expectedException = new RuntimeException(); 32 | StringBuilder output = new StringBuilder(); 33 | 34 | processor.process(expectedException, output); 35 | 36 | assertThat(output.toString(), not(endsWith("\n"))); 37 | } 38 | 39 | private String getExceptionPrintout(Throwable expectedException) { 40 | TestPrintStream testPrintStream = new TestPrintStream(); 41 | expectedException.printStackTrace(testPrintStream); 42 | String result = testPrintStream.toString(); 43 | return result.substring(0, result.length() - 1); //strip last character 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/TestPrintStream.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.PrintStream; 5 | 6 | class TestPrintStream extends PrintStream { 7 | TestPrintStream() { 8 | super(new ByteArrayOutputStream(), true); 9 | } 10 | 11 | private boolean closed = false; 12 | 13 | @Override 14 | public String toString() { 15 | ByteArrayOutputStream out = (ByteArrayOutputStream) super.out; 16 | return new String(out.toByteArray()); 17 | } 18 | 19 | @Override 20 | public void close() { 21 | closed = true; 22 | super.close(); 23 | } 24 | 25 | public boolean isClosed() { 26 | return closed; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /opslogger/src/test/java/com/equalexperts/logging/impl/ThrowableFingerprintCalculatorTest.java: -------------------------------------------------------------------------------- 1 | package com.equalexperts.logging.impl; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class ThrowableFingerprintCalculatorTest { 8 | private final ThrowableFingerprintCalculator calculator = new ThrowableFingerprintCalculator(); 9 | 10 | @Test 11 | public void calculateFingerprint_shouldGenerateTheSameFingerprint_givenTwoIdenticalThrowables() throws Exception { 12 | //ensure the exceptions are identical by giving them the same message, same class, and same stack trace 13 | String exceptionMessage = "message"; 14 | Throwable firstException = new RuntimeException(exceptionMessage); 15 | Throwable secondException = new RuntimeException(exceptionMessage); 16 | firstException.setStackTrace(constructCustomStackTrace()); 17 | secondException.setStackTrace(constructCustomStackTrace()); 18 | 19 | String firstFingerprint = calculator.calculateFingerprint(firstException); 20 | String secondFingerprint = calculator.calculateFingerprint(secondException); 21 | 22 | assertEquals(firstFingerprint, secondFingerprint); 23 | } 24 | 25 | @Test 26 | public void calculateFingerprint_shouldReturnTheSameFingerprint_givenTheSameExceptionTwice() throws Exception { 27 | Throwable t = new RuntimeException(); 28 | String expectedFingerprint = calculator.calculateFingerprint(t); 29 | 30 | String actualFingerprint = calculator.calculateFingerprint(t); 31 | 32 | assertEquals(expectedFingerprint, actualFingerprint); 33 | } 34 | 35 | @Test 36 | public void calculateFingerprint_shouldReturnADifferentFingerprint_whenTheStackTraceChanges() throws Exception { 37 | Throwable t = new RuntimeException(); 38 | String originalFingerprint = calculator.calculateFingerprint(t); 39 | 40 | t.setStackTrace(constructCustomStackTrace()); 41 | String newFingerprint = calculator.calculateFingerprint(t); 42 | 43 | assertNotEquals(originalFingerprint, newFingerprint); 44 | } 45 | 46 | @Test 47 | public void calculateFingerprint_shouldReturnDifferentFingerprints_forDifferentThrowableTypes() throws Exception { 48 | //same message and stack trace to create throwable instances that differ only by type 49 | String message = "foo"; 50 | Throwable a = new Exception(message); 51 | a.setStackTrace(constructCustomStackTrace()); 52 | Throwable b = new RuntimeException(message); 53 | b.setStackTrace(constructCustomStackTrace()); 54 | 55 | String fingerprintA = calculator.calculateFingerprint(a); 56 | String fingerprintB = calculator.calculateFingerprint(b); 57 | 58 | assertNotEquals(fingerprintA, fingerprintB); 59 | } 60 | 61 | @Test 62 | public void calculateFingerprint_shouldReturnDifferentFingerprints_givenDifferentMessages() throws Exception { 63 | //same class and stack trace to create throwable instances that differ only by message 64 | Throwable a = new RuntimeException("a"); 65 | a.setStackTrace(constructCustomStackTrace()); 66 | Throwable b = new RuntimeException("b"); 67 | b.setStackTrace(constructCustomStackTrace()); 68 | 69 | String fingerprintA = calculator.calculateFingerprint(a); 70 | String fingerprintB = calculator.calculateFingerprint(b); 71 | 72 | assertNotEquals(fingerprintA, fingerprintB); 73 | } 74 | 75 | @Test 76 | public void calculateFingerprint_shouldReturnADifferentFingerprint_givenADifferentCause() throws Exception { 77 | Throwable t = new RuntimeException(); 78 | String originalFingerprint = calculator.calculateFingerprint(t); 79 | t.initCause(new RuntimeException()); 80 | 81 | String modifiedFingerprint = calculator.calculateFingerprint(t); 82 | 83 | assertNotEquals(originalFingerprint, modifiedFingerprint); 84 | } 85 | 86 | @Test 87 | public void calculateFingerprint_shouldReturnADifferentFingerprint_givenAChangeInSuppressedExceptions() throws Exception { 88 | Throwable t = new RuntimeException(); 89 | String originalFingerprint = calculator.calculateFingerprint(t); 90 | t.addSuppressed(new RuntimeException()); 91 | 92 | String modifiedFingerprint = calculator.calculateFingerprint(t); 93 | 94 | assertNotEquals(originalFingerprint, modifiedFingerprint); 95 | } 96 | 97 | private StackTraceElement[] constructCustomStackTrace() { 98 | return new StackTraceElement[]{ 99 | new StackTraceElement("org,example.Foo", "baz", "Foo.java", 128), 100 | new StackTraceElement("org,example.Foo", "bar", "Foo.java", 67), 101 | new StackTraceElement("org,example.Foo", "foo", "Foo.java", 42), 102 | new StackTraceElement("org,example.Foo", "main", "Foo.java", 21) 103 | }; 104 | } 105 | } -------------------------------------------------------------------------------- /opslogger/src/test/resources/applicationContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /sample-usage/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply from: rootProject.file("jacoco-support.gradle") 3 | 4 | sourceCompatibility = "1.8" 5 | targetCompatibility = "1.8" 6 | 7 | configurations { 8 | provided { 9 | dependencies.all { dep -> 10 | configurations.default.exclude group: dep.group, module: dep.name 11 | } 12 | } 13 | compile.extendsFrom provided 14 | } 15 | 16 | sourceSets { 17 | main { 18 | compileClasspath += configurations.provided 19 | } 20 | } 21 | 22 | dependencies { 23 | compile project(":opslogger"), spring, dagger 24 | provided daggerCompiler 25 | testCompile project(":opslogger-support"), junit, mockito 26 | } 27 | 28 | group = 'com.equalexperts' 29 | version = rootProject.version 30 | 31 | task generateLoggingDocumentation { 32 | String outputFile = "${buildDir}/log-messages.txt" 33 | doLast { 34 | javaexec { 35 | classpath sourceSets.test.runtimeClasspath 36 | main = 'com.equalexperts.logging.GenerateLogMessageDocumentation' 37 | args = [outputFile, 'build/classes/main'] 38 | } 39 | } 40 | 41 | dependsOn(compileJava, compileTestJava) 42 | inputs.files sourceSets.main.allSource 43 | inputs.files sourceSets.test.runtimeClasspath 44 | outputs.file outputFile 45 | } 46 | assemble.dependsOn(generateLoggingDocumentation) 47 | 48 | idea.springFacets << 'src/main/resources/applicationContext.xml' 49 | -------------------------------------------------------------------------------- /sample-usage/src/main/java/uk/gov/gds/performance/collector/ClassThatLogs.java: -------------------------------------------------------------------------------- 1 | package uk.gov.gds.performance.collector; 2 | 3 | import com.equalexperts.logging.DiagnosticContextSupplier; 4 | import com.equalexperts.logging.OpsLogger; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.UUID; 9 | import java.util.stream.IntStream; 10 | 11 | import static uk.gov.gds.performance.collector.CollectorLogMessages.SUCCESS; 12 | import static uk.gov.gds.performance.collector.CollectorLogMessages.UNKNOWN_ERROR; 13 | 14 | public class ClassThatLogs { 15 | private final OpsLogger logger; 16 | 17 | public ClassThatLogs(OpsLogger logger) { 18 | this.logger = logger; 19 | } 20 | 21 | public void foo() { 22 | logger.log(SUCCESS, 42); 23 | } 24 | 25 | public void bar() { 26 | RuntimeException e = new RuntimeException(); 27 | logger.logThrowable(UNKNOWN_ERROR, e); 28 | throw e; 29 | } 30 | 31 | public String logContextsAcrossThreads() { 32 | LocalContext context = new LocalContext(UUID.randomUUID().toString()); 33 | IntStream.rangeClosed(1, 5).parallel().forEach(i -> logger.with(context).log(SUCCESS, i)); 34 | IntStream.rangeClosed(6, 10).parallel().forEach(i -> logger.with(new LocalContext("fred")).log(SUCCESS, i)); 35 | return context.getJobId(); 36 | } 37 | 38 | public void baz() { 39 | 40 | } 41 | 42 | static class LocalContext implements DiagnosticContextSupplier { 43 | private final String jobId; 44 | 45 | public LocalContext(String jobId) { 46 | this.jobId = jobId; 47 | } 48 | 49 | @Override 50 | public Map getMessageContext() { 51 | Map context = new HashMap<>(); 52 | context.put("jobId", jobId); 53 | return context; 54 | } 55 | 56 | public String getJobId() { 57 | return jobId; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /sample-usage/src/main/java/uk/gov/gds/performance/collector/CollectorLogMessages.java: -------------------------------------------------------------------------------- 1 | package uk.gov.gds.performance.collector; 2 | 3 | import com.equalexperts.logging.LogMessage; 4 | 5 | public enum CollectorLogMessages implements LogMessage { 6 | SUCCESS("GDS-000000", "Successfully published %d records"), 7 | UNKNOWN_ERROR("GDS-000001", "An unknown error occurred:"), 8 | COULD_NOT_CONNECT_TO_PERFORMANCE_PLATFORM("GDS-000002", "Could not connect to the performance platform %s"), 9 | PERFORMANCE_PLATFORM_TEST_QUERY_FAILED("GDS-000003", "Test query to the performance platform %s failed with response code %d"), 10 | NO_RESULTS_FOUND_FOR_DATE_RANGE("GDS-000004", "No results found in the date range %s to %s"), 11 | INVALID_CONFIGURATION_FILE("GDS-000005", "Invalid configuration file format %s"), 12 | CONFIGURATION_FILE_NOT_FOUND("GDS-000006", "Configuration file %s not found"), 13 | COULD_NOT_CONNECT_TO_DATABASE("GDS-000007", "Could not connect to the database:"), 14 | ALL_CONNECTIVITY_CHECKS_PASSED("GDS-000008", "All connectivity checks passed"); 15 | 16 | //region LogMessage implementation 17 | private final String messageCode; 18 | private final String messagePattern; 19 | 20 | CollectorLogMessages(String messageCode, String messagePattern) { 21 | this.messageCode = messageCode; 22 | this.messagePattern = messagePattern; 23 | } 24 | 25 | @Override 26 | public String getMessageCode() { 27 | return messageCode; 28 | } 29 | 30 | @Override 31 | public String getMessagePattern() { 32 | return messagePattern; 33 | } 34 | //endregion 35 | } 36 | -------------------------------------------------------------------------------- /sample-usage/src/main/java/uk/gov/gds/performance/collector/Dagger1Main.java: -------------------------------------------------------------------------------- 1 | package uk.gov.gds.performance.collector; 2 | 3 | import com.equalexperts.logging.OpsLogger; 4 | import com.equalexperts.logging.OpsLoggerFactory; 5 | import dagger.Module; 6 | import dagger.ObjectGraph; 7 | import dagger.Provides; 8 | 9 | import javax.inject.Singleton; 10 | import java.nio.file.Paths; 11 | 12 | /** 13 | * Example using Dagger 1. 14 | * 15 | * Intellij IDEA must have annotation processing explicitly enabled: 16 | * https://www.jetbrains.com/idea/help/compiler-annotation-processors.html 17 | * 18 | */ 19 | public class Dagger1Main { 20 | @Module(injects=ClassThatLogs.class) 21 | public static class SampleApplicationModule { 22 | @Provides 23 | @Singleton 24 | OpsLogger getLogger() { 25 | return new OpsLoggerFactory() 26 | .setDestination(System.out) 27 | .setStackTraceStoragePath(Paths.get("/tmp/stacktraces")) 28 | .build(); 29 | } 30 | 31 | @Provides 32 | ClassThatLogs classThatLogs(OpsLogger logger) { 33 | return new ClassThatLogs(logger); 34 | } 35 | } 36 | 37 | public static void main(String... args) { 38 | ObjectGraph graph = ObjectGraph.create(new SampleApplicationModule()); 39 | ClassThatLogs classThatLogs = graph.get(ClassThatLogs.class); 40 | 41 | classThatLogs.foo(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sample-usage/src/main/java/uk/gov/gds/performance/collector/Main.java: -------------------------------------------------------------------------------- 1 | package uk.gov.gds.performance.collector; 2 | 3 | import com.equalexperts.logging.OpsLogger; 4 | import com.equalexperts.logging.OpsLoggerFactory; 5 | 6 | import java.nio.file.Paths; 7 | 8 | public class Main { 9 | public static void main(String... args) throws Exception { 10 | OpsLogger logger = new OpsLoggerFactory() 11 | .setDestination(System.out) 12 | .setStackTraceStoragePath(Paths.get("/tmp/stacktraces")) 13 | .build(); 14 | 15 | ClassThatLogs cls = new ClassThatLogs(logger); 16 | cls.foo(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sample-usage/src/main/java/uk/gov/gds/performance/collector/SpringMain.java: -------------------------------------------------------------------------------- 1 | package uk.gov.gds.performance.collector; 2 | 3 | import org.springframework.context.support.ClassPathXmlApplicationContext; 4 | 5 | public class SpringMain { 6 | public static void main(String... args) { 7 | ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:/applicationContext.xml"); 8 | ClassThatLogs classThatLogs = context.getBean("classThatLogs", ClassThatLogs.class); 9 | classThatLogs.foo(); 10 | } 11 | } -------------------------------------------------------------------------------- /sample-usage/src/main/resources/applicationContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sample-usage/src/test/java/uk/gov/gds/performance/collector/ClassThatLogsTest.java: -------------------------------------------------------------------------------- 1 | package uk.gov.gds.performance.collector; 2 | 3 | import com.equalexperts.logging.OpsLogger; 4 | import com.equalexperts.logging.OpsLoggerTestDouble; 5 | import org.junit.Test; 6 | import org.mockito.Mockito; 7 | 8 | import static org.junit.Assert.fail; 9 | import static org.mockito.Mockito.*; 10 | 11 | public class ClassThatLogsTest { 12 | private final OpsLogger mockLogger = OpsLoggerTestDouble.withSpyFunction(Mockito::spy); 13 | private final ClassThatLogs theClass = new ClassThatLogs(mockLogger); 14 | 15 | @Test 16 | public void foo_shouldLogASuccessMessage() throws Exception { 17 | theClass.foo(); 18 | 19 | verify(mockLogger).log(CollectorLogMessages.SUCCESS, 42); 20 | verifyNoMoreInteractions(mockLogger); 21 | } 22 | 23 | @Test 24 | public void bar_shouldLogAnUnknownErrorMessageAndThrowAnException() throws Exception { 25 | try { 26 | theClass.bar(); 27 | fail("expected an exception"); 28 | } catch (RuntimeException e) { 29 | verify(mockLogger).logThrowable(CollectorLogMessages.UNKNOWN_ERROR, e); 30 | } 31 | } 32 | 33 | @Test 34 | public void baz_shouldNotLogAnyMessages() throws Exception { 35 | theClass.baz(); 36 | verifyZeroInteractions(mockLogger); 37 | } 38 | 39 | @Test 40 | public void logContextsAcrossThreads_shouldCarryDiagnosticContextAcrossThreads() throws Exception { 41 | String jobId = theClass.logContextsAcrossThreads(); 42 | 43 | OpsLogger nestedLogger = mockLogger.with(new ClassThatLogs.LocalContext(jobId)); 44 | verify(nestedLogger).log(CollectorLogMessages.SUCCESS, 1); 45 | verify(nestedLogger).log(CollectorLogMessages.SUCCESS, 2); 46 | verify(nestedLogger).log(CollectorLogMessages.SUCCESS, 3); 47 | verify(nestedLogger).log(CollectorLogMessages.SUCCESS, 4); 48 | verify(nestedLogger).log(CollectorLogMessages.SUCCESS, 5); 49 | verifyNoMoreInteractions(nestedLogger); 50 | 51 | nestedLogger = mockLogger.with(new ClassThatLogs.LocalContext("fred")); 52 | verify(nestedLogger).log(CollectorLogMessages.SUCCESS, 6); 53 | verify(nestedLogger).log(CollectorLogMessages.SUCCESS, 7); 54 | verify(nestedLogger).log(CollectorLogMessages.SUCCESS, 8); 55 | verify(nestedLogger).log(CollectorLogMessages.SUCCESS, 9); 56 | verify(nestedLogger).log(CollectorLogMessages.SUCCESS, 10); 57 | verifyNoMoreInteractions(nestedLogger); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /sample-usage/src/test/java/uk/gov/gds/performance/collector/CollectorLogMessagesTest.java: -------------------------------------------------------------------------------- 1 | package uk.gov.gds.performance.collector; 2 | 3 | import com.equalexperts.logging.LogMessageContractTest; 4 | 5 | import static com.equalexperts.logging.EnumContractRunner.EnumToTest; 6 | 7 | @EnumToTest(value= CollectorLogMessages.class) 8 | public class CollectorLogMessagesTest extends LogMessageContractTest { 9 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include "opslogger" 2 | include "opslogger-support" 3 | include "sample-usage" 4 | --------------------------------------------------------------------------------