├── .gitignore ├── AUTHORS.txt ├── LICENSE.txt ├── README.md ├── build.gradle ├── circle.yml ├── gradle.properties ├── gradle ├── deploy.gradle ├── readme.gradle ├── readme.template └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── loggly ├── build.gradle ├── gradle.properties └── src │ ├── main │ └── java │ │ └── ch │ │ └── qos │ │ └── logback │ │ └── ext │ │ └── loggly │ │ ├── AbstractLogglyAppender.java │ │ ├── LogglyAppender.java │ │ ├── LogglyBatchAppender.java │ │ ├── LogglyBatchAppenderMBean.java │ │ └── io │ │ ├── DiscardingRollingOutputStream.java │ │ └── IoUtils.java │ └── test │ ├── java │ └── ch │ │ └── qos │ │ └── logback │ │ └── ext │ │ └── loggly │ │ ├── HttpTestServer.java │ │ ├── LogglyBatchAppenderTest.java │ │ ├── LogglyHttpAppenderIntegratedTest.java │ │ └── LogglySendOnlyWhenMaxBucketsFullTest.java │ └── resources │ └── logback-test.xml ├── scripts ├── deploysnapshot.sh ├── nexus.sh └── release.sh ├── settings.gradle └── spring ├── build.gradle ├── gradle.properties └── src └── main └── java └── ch └── qos └── logback └── ext └── spring ├── ApplicationContextHolder.java ├── DelegatingLogbackAppender.java ├── EventCacheMode.java ├── ILoggingEventCache.java ├── LogbackConfigurer.java └── web ├── LogbackConfigListener.java ├── LogbackConfigServlet.java └── WebLogbackConfigurer.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.idea 2 | *.iml 3 | *.ipr 4 | *.iws 5 | .classpath 6 | .externalToolBuilders 7 | .gradle 8 | .project 9 | .settings 10 | .gradle 11 | target 12 | out 13 | build 14 | local.properties 15 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Original author 2 | --------------- 3 | 4 | Hazlewood, Les { 5 | Loggly Extension 6 | Spring Extension 7 | } 8 | 9 | 10 | Contributors (alphabetical) 11 | --------------------------- 12 | 13 | Gulcu, Ceki { 14 | Maintainer 15 | } 16 | 17 | Trinh, Anthony { 18 | Maintainer 19 | } 20 | 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2014 The logback-extensions developers. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # logback-extensions [![CircleCI branch](https://img.shields.io/circleci/project/qos-ch/logback-extensions/master.svg)](https://circleci.com/gh/https://circleci.com/gh/qos-ch/logback-extensions) 2 | v0.1.5 3 | 4 | https://github.com/qos-ch/logback-extensions/wiki 5 | 6 | #### Build Instructions 7 | Run the following command to build: 8 | 9 | ``` 10 | ./gradlew clean assemble 11 | ``` 12 | 13 | #### License 14 | ``` 15 | Licensed under the Apache License, Version 2.0 (the "License"); 16 | you may not use this file except in compliance with the License. 17 | You may obtain a copy of the License at 18 | 19 | http://www.apache.org/licenses/LICENSE-2.0 20 | 21 | Unless required by applicable law or agreed to in writing, software 22 | distributed under the License is distributed on an "AS IS" BASIS, 23 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | See the License for the specific language governing permissions and 25 | limitations under the License. 26 | ``` 27 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | dependencies { 7 | // classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' 8 | classpath 'org.ajoberstar:grgit:2.1.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | plugins { 15 | id 'io.codearte.nexus-staging' version '0.11.0' // must be in root project 16 | id 'net.researchgate.release' version '2.6.0' 17 | } 18 | apply plugin: 'org.ajoberstar.grgit' 19 | apply from: 'gradle/readme.gradle' 20 | 21 | allprojects { 22 | apply plugin: 'maven' 23 | 24 | group = 'org.logback-extensions' 25 | version = VERSION_NAME 26 | } 27 | 28 | subprojects { 29 | apply plugin: 'java' 30 | apply from: "${rootProject.rootDir}/gradle/deploy.gradle" 31 | 32 | sourceCompatibility = JavaVersion.VERSION_1_8 33 | targetCompatibility = JavaVersion.VERSION_1_8 34 | tasks.withType(JavaCompile) { 35 | options.encoding = 'UTF-8' 36 | // Warn about deprecations 37 | //options.compilerArgs << '-Xlint:deprecation' 38 | // Warn about unchecked usages 39 | options.compilerArgs << '-Xlint:unchecked' 40 | // Don't warn about using source/target 1.5 option 41 | options.compilerArgs << '-Xlint:-options' 42 | 43 | options.debug(['debugLevel': 'source,lines,vars']) 44 | options.debug = VERSION_NAME.contains('SNAPSHOT') 45 | } 46 | 47 | repositories { 48 | mavenLocal() 49 | jcenter() 50 | maven { url "https://oss.sonatype.org/content/repositories/snapshots" } 51 | } 52 | 53 | dependencies { 54 | testCompile 'org.mockito:mockito-core:1.9.0' 55 | testCompile 'junit:junit:4.12' 56 | } 57 | 58 | task sourcesJar(type: Jar, dependsOn: classes) { 59 | classifier = 'sources' 60 | from sourceSets.main.allSource 61 | } 62 | 63 | javadoc { 64 | failOnError false 65 | // disable javadoc lint warnings (e.g., missing javadoc) 66 | options.addBooleanOption('Xdoclint:none', true) 67 | } 68 | 69 | task javadocJar(type: Jar, dependsOn: javadoc) { 70 | classifier = 'javadoc' 71 | from javadoc.destinationDir 72 | } 73 | 74 | artifacts { 75 | archives sourcesJar 76 | archives javadocJar 77 | } 78 | } 79 | 80 | release { 81 | tagTemplate = 'v_${version}' 82 | preTagCommitMessage = ':cloud: Release' 83 | tagCommitMessage = ':cloud: Release' 84 | newVersionCommitMessage = ':cloud: Bump' 85 | 86 | // versionPropertyFile = '../gradle.properties' 87 | versionProperties = ['VERSION_NAME'] 88 | 89 | git { 90 | requireBranch = '' 91 | } 92 | } 93 | 94 | nexusStaging { 95 | packageGroup = 'org.logback-extensions' 96 | } 97 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | # Configuration for CircleCI 2 | # https://circleci.com/gh/qos-ch/logback-extensions 3 | version: 2.0 4 | 5 | jobs: 6 | build: 7 | branches: 8 | ignore: gh-pages 9 | working_directory: ~/code 10 | docker: 11 | - image: circleci/openjdk:8-jdk-browsers 12 | environment: 13 | JVM_OPTS: -Xmx3200m 14 | steps: 15 | - checkout 16 | - restore_cache: 17 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "loggly/build.gradle" }}-{{ checksum "spring/build.gradle" }} 18 | # - run: 19 | # name: Chmod permissions #if permission for Gradlew Dependencies fail, use this. 20 | # command: sudo chmod +x ./gradlew 21 | - save_cache: 22 | paths: 23 | - ~/.gradle 24 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "loggly/build.gradle" }}-{{ checksum "spring/build.gradle" }} 25 | - run: 26 | name: Run Tests 27 | command: ./gradlew check assemble --parallel 28 | - store_artifacts: 29 | path: loggly/build/reports 30 | destination: reports 31 | - store_artifacts: 32 | path: spring/build/reports 33 | destination: reports 34 | - store_artifacts: 35 | path: loggly/build/outputs 36 | destination: outputs 37 | - store_artifacts: 38 | path: spring/build/outputs 39 | destination: outputs 40 | - store_test_results: 41 | path: loggly/build/test-results 42 | - store_test_results: 43 | path: spring/build/test-results 44 | - run: 45 | name: Deploy snapshot 46 | command: scripts/deploysnapshot.sh 47 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # `version` is needed by gradle-release-plugin and always matches 2 | # `VERSION_NAME`. The plugin updates both automatically. 3 | version=0.1.6-SNAPSHOT 4 | VERSION_NAME=0.1.6-SNAPSHOT 5 | 6 | GROUP=org.logback-extensions 7 | 8 | POM_URL=https://github.com/qos-ch/logback-extensions 9 | POM_SCM_URL=https://github.com/qos-ch/logback-extensions 10 | POM_SCM_ISSUES_URL=https://github.com/qos-ch/logback-extensions/issues 11 | POM_SCM_CONNECTION=scm:git@github.com:qos-ch/logback-extensions.git 12 | POM_SCM_DEV_CONNECTION=scm:git@github.com:qos-ch/logback-extensions.git 13 | POM_LICENCE_NAME="The Apache Software License, Version 2.0" 14 | POM_LICENCE_ID="Apache-2.0" 15 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 16 | POM_LICENCE_DIST=repo 17 | POM_DEVELOPER_ID=tony19 18 | POM_DEVELOPER_NAME=tony19 19 | POM_DEVELOPER_EMAIL=tony19@gmail.com 20 | -------------------------------------------------------------------------------- /gradle/deploy.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Chris Banes 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | apply plugin: 'maven' 17 | apply plugin: 'signing' 18 | //apply plugin: 'com.jfrog.bintray' 19 | // 20 | //bintray { 21 | // user = hasProperty('BINTRAY_USER') ? BINTRAY_USER : '' 22 | // key = hasProperty('BINTRAY_KEY') ? BINTRAY_KEY : '' 23 | // configurations = ['archives'] 24 | // 25 | // publish = true 26 | // override = true 27 | // 28 | // pkg { 29 | // repo = 'generic' 30 | // name = "${GROUP}:${POM_NAME}" 31 | // desc = POM_DESCRIPTION 32 | // licenses = [POM_LICENCE_ID] 33 | // labels = ['android', 'logging'] 34 | // websiteUrl = POM_URL 35 | // issueTrackerUrl = POM_SCM_ISSUES_URL 36 | // vcsUrl = POM_SCM_URL 37 | // githubRepo = POM_SCM_URL 38 | // 39 | // version { 40 | // name = VERSION_NAME 41 | // released = new Date() 42 | // vcsTag = "v_${VERSION_NAME}" 43 | // gpg { 44 | // sign = true 45 | // } 46 | // } 47 | // } 48 | //} 49 | 50 | def isReleaseBuild() { 51 | return !VERSION_NAME.contains("SNAPSHOT") 52 | } 53 | 54 | def getReleaseRepositoryUrl() { 55 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 56 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 57 | } 58 | 59 | def getSnapshotRepositoryUrl() { 60 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 61 | : "https://oss.sonatype.org/content/repositories/snapshots/" 62 | } 63 | 64 | def getRepositoryUsername() { 65 | return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : '' 66 | } 67 | 68 | def getRepositoryPassword() { 69 | return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : '' 70 | } 71 | 72 | afterEvaluate { project -> 73 | uploadArchives { 74 | repositories { 75 | mavenDeployer { 76 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 77 | 78 | pom.groupId = GROUP 79 | pom.artifactId = POM_ARTIFACT_ID 80 | pom.version = VERSION_NAME 81 | 82 | repository(url: getReleaseRepositoryUrl()) { 83 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 84 | } 85 | snapshotRepository(url: getSnapshotRepositoryUrl()) { 86 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 87 | } 88 | 89 | pom.project { 90 | name POM_NAME 91 | packaging POM_PACKAGING 92 | description POM_DESCRIPTION 93 | url POM_URL 94 | 95 | scm { 96 | url POM_SCM_URL 97 | connection POM_SCM_CONNECTION 98 | developerConnection POM_SCM_DEV_CONNECTION 99 | } 100 | 101 | licenses { 102 | license { 103 | name POM_LICENCE_NAME 104 | url POM_LICENCE_URL 105 | distribution POM_LICENCE_DIST 106 | } 107 | } 108 | 109 | developers { 110 | developer { 111 | id POM_DEVELOPER_ID 112 | name POM_DEVELOPER_NAME 113 | email POM_DEVELOPER_EMAIL 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | 121 | signing { 122 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 123 | sign configurations.archives 124 | } 125 | } -------------------------------------------------------------------------------- /gradle/readme.gradle: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates README.md based on project properties, and 3 | * pushes it to GitHub if rootProject.hasProperty('push') 4 | */ 5 | task readme { 6 | description 'Updates README.md, and pushes it to GitHub.' 7 | 8 | doLast { 9 | def updateReadme = { 10 | def text = new File('gradle/readme.template').getText('UTF-8') 11 | def template = new groovy.text.StreamingTemplateEngine().createTemplate(text) 12 | 13 | def binding = [ 14 | version : rootProject.VERSION_NAME - ~/-SNAPSHOT/, 15 | ] 16 | 17 | String newText = template.make(binding) 18 | newText = newText.replace('\\u007B', '{') 19 | 20 | def updated = false 21 | def readmeFile = new File('README.md') 22 | if (readmeFile.text != newText) { 23 | readmeFile.text = newText 24 | updated = true 25 | } 26 | return updated 27 | } 28 | 29 | def commitReadme = { 30 | grgit.add(patterns: ['README.md']) 31 | grgit.commit(message: ":books: Update README for ${rootProject.version}") 32 | grgit.push() 33 | } 34 | 35 | if (updateReadme() && rootProject.hasProperty('push')) { 36 | logger.info "committing README for ${rootProject.version}" 37 | commitReadme() 38 | } else { 39 | logger.info "no README changes for ${rootProject.version}" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /gradle/readme.template: -------------------------------------------------------------------------------- 1 | # logback-extensions [![CircleCI branch](https://img.shields.io/circleci/project/qos-ch/logback-extensions/master.svg)](https://circleci.com/gh/https://circleci.com/gh/qos-ch/logback-extensions) 2 | v${version} 3 | 4 | https://github.com/qos-ch/logback-extensions/wiki 5 | 6 | #### Build Instructions 7 | Run the following command to build: 8 | 9 | ``` 10 | ./gradlew clean assemble 11 | ``` 12 | 13 | #### License 14 | ``` 15 | Licensed under the Apache License, Version 2.0 (the "License"); 16 | you may not use this file except in compliance with the License. 17 | You may obtain a copy of the License at 18 | 19 | http://www.apache.org/licenses/LICENSE-2.0 20 | 21 | Unless required by applicable law or agreed to in writing, software 22 | distributed under the License is distributed on an "AS IS" BASIS, 23 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | See the License for the specific language governing permissions and 25 | limitations under the License. 26 | ``` 27 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qos-ch/logback-extensions/2d7dea74a2585d2ec724e3c3da2e9f4bf6fca99d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-bin.zip 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 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 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /loggly/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | description = POM_DESCRIPTION 3 | dependencies { 4 | testCompile 'de.sven-jacobs:loremipsum:1.0' 5 | testCompile 'org.simpleframework:simple:5.0.4' 6 | compile 'ch.qos.logback:logback-classic:1.2.3' 7 | } 8 | -------------------------------------------------------------------------------- /loggly/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=logback-ext-loggly 2 | POM_ARTIFACT_ID=logback-ext-loggly 3 | POM_PACKAGING=jar 4 | POM_DESCRIPTION="Logback Extensions :: Loggly" 5 | -------------------------------------------------------------------------------- /loggly/src/main/java/ch/qos/logback/ext/loggly/AbstractLogglyAppender.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.loggly; 17 | 18 | import ch.qos.logback.classic.PatternLayout; 19 | import ch.qos.logback.core.Context; 20 | import ch.qos.logback.core.Layout; 21 | import ch.qos.logback.core.UnsynchronizedAppenderBase; 22 | 23 | import java.io.ByteArrayOutputStream; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.net.InetSocketAddress; 27 | import java.net.Proxy; 28 | import java.nio.charset.Charset; 29 | 30 | /** 31 | * Common base for Loggly appenders. 32 | * 33 | * @author Mårten Gustafson 34 | * @author Les Hazlewood 35 | * @author Cyrille Le Clerc 36 | */ 37 | public abstract class AbstractLogglyAppender extends UnsynchronizedAppenderBase { 38 | public static final String DEFAULT_ENDPOINT_PREFIX = "https://logs-01.loggly.com/"; 39 | public static final String DEFAULT_LAYOUT_PATTERN = "%d{\"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\",UTC} %-5level [%thread] %logger: %m%n"; 40 | protected static final Charset UTF_8 = Charset.forName("UTF-8"); 41 | protected String endpointUrl; 42 | protected String inputKey; 43 | protected Layout layout; 44 | protected boolean layoutCreatedImplicitly = false; 45 | private String pattern; 46 | 47 | private int proxyPort; 48 | private String proxyHost; 49 | protected Proxy proxy; 50 | private int httpReadTimeoutInMillis = 1000; 51 | 52 | @Override 53 | public void start() { 54 | ensureLayout(); 55 | if (!this.layout.isStarted()) { 56 | this.layout.start(); 57 | } 58 | if (this.endpointUrl == null) { 59 | if (this.inputKey == null) { 60 | addError("inputKey (or alternatively, endpointUrl) must be configured"); 61 | } else { 62 | this.endpointUrl = buildEndpointUrl(this.inputKey); 63 | } 64 | } 65 | 66 | if (this.proxyHost == null || this.proxyHost.isEmpty()) { 67 | // don't set it to Proxy.NO_PROXY (i.e. Proxy.Type.DIRECT) as the meaning is different (user-jvm-proxy-config vs. don't use proxy) 68 | this.proxy = null; 69 | } else { 70 | this.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); 71 | } 72 | super.start(); 73 | } 74 | 75 | @Override 76 | public void stop() { 77 | super.stop(); 78 | if (this.layoutCreatedImplicitly) { 79 | try { 80 | this.layout.stop(); 81 | } finally { 82 | this.layout = null; 83 | this.layoutCreatedImplicitly = false; 84 | } 85 | } 86 | } 87 | 88 | protected byte[] toBytes(final InputStream is) throws IOException { 89 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 90 | int count; 91 | byte[] buf = new byte[512]; 92 | 93 | while((count = is.read(buf, 0, buf.length)) != -1) { 94 | baos.write(buf, 0, count); 95 | } 96 | baos.flush(); 97 | 98 | return baos.toByteArray(); 99 | } 100 | 101 | protected String readResponseBody(final InputStream input) throws IOException { 102 | try { 103 | final byte[] bytes = toBytes(input); 104 | return new String(bytes, UTF_8); 105 | } finally { 106 | input.close(); 107 | } 108 | } 109 | 110 | protected final void ensureLayout() { 111 | if (this.layout == null) { 112 | this.layout = createLayout(); 113 | this.layoutCreatedImplicitly = true; 114 | } 115 | if (this.layout != null) { 116 | Context context = this.layout.getContext(); 117 | if (context == null) { 118 | this.layout.setContext(getContext()); 119 | } 120 | } 121 | } 122 | 123 | @SuppressWarnings("unchecked") 124 | protected Layout createLayout() { 125 | PatternLayout layout = new PatternLayout(); 126 | String pattern = getPattern(); 127 | if (pattern == null) { 128 | pattern = DEFAULT_LAYOUT_PATTERN; 129 | } 130 | layout.setPattern(pattern); 131 | return (Layout) layout; 132 | } 133 | 134 | protected String buildEndpointUrl(String inputKey) { 135 | return new StringBuilder(DEFAULT_ENDPOINT_PREFIX).append(getEndpointPrefix()) 136 | .append(inputKey).toString(); 137 | } 138 | 139 | /** 140 | * Returns the URL path prefix for the Loggly endpoint to which the 141 | * implementing class will send log events. This path prefix varies 142 | * for the different Loggly services. The final endpoint URL is built 143 | * by concatenating the {@link #DEFAULT_ENDPOINT_PREFIX} with the 144 | * endpoint prefix from {@link #getEndpointPrefix()} and the 145 | * {@link #inputKey}. 146 | * 147 | * @return the URL path prefix for the Loggly endpoint 148 | */ 149 | protected abstract String getEndpointPrefix(); 150 | 151 | public String getEndpointUrl() { 152 | return endpointUrl; 153 | } 154 | 155 | public void setEndpointUrl(String endpointUrl) { 156 | this.endpointUrl = endpointUrl; 157 | } 158 | 159 | public String getInputKey() { 160 | return inputKey; 161 | } 162 | 163 | public void setInputKey(String inputKey) { 164 | String cleaned = inputKey; 165 | if (cleaned != null) { 166 | cleaned = cleaned.trim(); 167 | } 168 | if ("".equals(cleaned)) { 169 | cleaned = null; 170 | } 171 | this.inputKey = cleaned; 172 | } 173 | 174 | public String getPattern() { 175 | return pattern; 176 | } 177 | 178 | public void setPattern(String pattern) { 179 | this.pattern = pattern; 180 | } 181 | 182 | public Layout getLayout() { 183 | return layout; 184 | } 185 | 186 | public void setLayout(Layout layout) { 187 | this.layout = layout; 188 | } 189 | 190 | public int getProxyPort() { 191 | return proxyPort; 192 | } 193 | 194 | public void setProxyPort(int proxyPort) { 195 | this.proxyPort = proxyPort; 196 | } 197 | public void setProxyPort(String proxyPort) { 198 | if(proxyPort == null || proxyPort.trim().isEmpty()) { 199 | // handle logback configuration default value like "${logback.loggly.proxy.port:-}" 200 | proxyPort = "0"; 201 | } 202 | this.proxyPort = Integer.parseInt(proxyPort); 203 | } 204 | 205 | public String getProxyHost() { 206 | return proxyHost; 207 | } 208 | 209 | public void setProxyHost(String proxyHost) { 210 | if(proxyHost == null || proxyHost.trim().isEmpty()) { 211 | // handle logback configuration default value like "${logback.loggly.proxy.host:-}" 212 | proxyHost = null; 213 | } 214 | this.proxyHost = proxyHost; 215 | } 216 | 217 | public int getHttpReadTimeoutInMillis() { 218 | return httpReadTimeoutInMillis; 219 | } 220 | 221 | public void setHttpReadTimeoutInMillis(int httpReadTimeoutInMillis) { 222 | this.httpReadTimeoutInMillis = httpReadTimeoutInMillis; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /loggly/src/main/java/ch/qos/logback/ext/loggly/LogglyAppender.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.loggly; 17 | 18 | import java.io.IOException; 19 | import java.io.OutputStream; 20 | import java.net.HttpURLConnection; 21 | import java.net.URL; 22 | 23 | /** 24 | * An Appender that posts logging messages to Loggly, a cloud logging service. 25 | * 26 | * @author Mårten Gustafson 27 | * @author Les Hazlewood 28 | * @since 0.1 29 | */ 30 | public class LogglyAppender extends AbstractLogglyAppender { 31 | 32 | public static final String ENDPOINT_URL_PATH = "inputs/"; 33 | 34 | public LogglyAppender() { 35 | } 36 | 37 | @Override 38 | protected void append(E eventObject) { 39 | String msg = this.layout.doLayout(eventObject); 40 | postToLoggly(msg); 41 | } 42 | 43 | private void postToLoggly(final String event) { 44 | try { 45 | assert endpointUrl != null; 46 | URL endpoint = new URL(endpointUrl); 47 | final HttpURLConnection connection; 48 | if (proxy == null) { 49 | connection = (HttpURLConnection) endpoint.openConnection(); 50 | } else { 51 | connection = (HttpURLConnection) endpoint.openConnection(proxy); 52 | } 53 | connection.setRequestMethod("POST"); 54 | connection.setDoOutput(true); 55 | connection.addRequestProperty("Content-Type", this.layout.getContentType()); 56 | connection.connect(); 57 | sendAndClose(event, connection.getOutputStream()); 58 | connection.disconnect(); 59 | final int responseCode = connection.getResponseCode(); 60 | if (responseCode != 200) { 61 | final String message = readResponseBody(connection.getInputStream()); 62 | addError("Loggly post failed (HTTP " + responseCode + "). Response body:\n" + message); 63 | } 64 | } catch (final IOException e) { 65 | addError("IOException while attempting to communicate with Loggly", e); 66 | } 67 | } 68 | 69 | private void sendAndClose(final String event, final OutputStream output) throws IOException { 70 | try { 71 | output.write(event.getBytes("UTF-8")); 72 | } finally { 73 | output.close(); 74 | } 75 | } 76 | 77 | @Override 78 | protected String getEndpointPrefix() { 79 | return ENDPOINT_URL_PATH; 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /loggly/src/main/java/ch/qos/logback/ext/loggly/LogglyBatchAppender.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.loggly; 17 | 18 | import java.io.BufferedOutputStream; 19 | import java.io.ByteArrayInputStream; 20 | import java.io.ByteArrayOutputStream; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.lang.management.ManagementFactory; 24 | import java.net.HttpURLConnection; 25 | import java.net.URL; 26 | import java.nio.charset.Charset; 27 | import java.sql.Timestamp; 28 | import java.util.concurrent.BlockingDeque; 29 | import java.util.concurrent.Executors; 30 | import java.util.concurrent.ScheduledExecutorService; 31 | import java.util.concurrent.ThreadFactory; 32 | import java.util.concurrent.TimeUnit; 33 | import java.util.concurrent.atomic.AtomicInteger; 34 | import java.util.concurrent.atomic.AtomicLong; 35 | 36 | import javax.management.MBeanServer; 37 | import javax.management.ObjectName; 38 | 39 | import ch.qos.logback.ext.loggly.io.DiscardingRollingOutputStream; 40 | import ch.qos.logback.ext.loggly.io.IoUtils; 41 | 42 | /** 43 | *

44 | * Logback batch appender for Loggly HTTP API. 45 | *

46 | *

Note:Loggly's Syslog API is much more scalable than the HTTP API which should mostly be used in 47 | * low-volume or non-production systems. The HTTP API can be very convenient to workaround firewalls.

48 | *

If the {@link LogglyBatchAppender} saturates and discards log messages, the following warning message is 49 | * appended to both Loggly and {@link System#err}:
50 | * "$date - OutputStream is full, discard previous logs"

51 | *

Configuration settings

52 | * 53 | * 54 | * 55 | * 56 | * 57 | * 58 | * 59 | * 60 | * 61 | * 63 | * 64 | * 65 | * 66 | * 67 | * 69 | * 70 | * 71 | * 72 | * 73 | * 75 | * 76 | * 77 | * 78 | * 79 | * 80 | * 81 | * 82 | * 83 | * 84 | * 85 | * 86 | * 87 | * 88 | * 89 | * 91 | * 92 | * 93 | * 94 | * 95 | * 96 | * 97 | * 98 | * 99 | * 100 | * 101 | * 102 | * 103 | * 104 | * 105 | * 106 | * 107 | * 108 | * 109 | * 110 | * 111 | * 112 | *
Property NameTypeDescription
inputKeyStringLoggly input key. "inputKey" or endpointUrl is required. Sample 62 | * "12345678-90ab-cdef-1234-567890abcdef"
endpointUrlStringLoggly HTTP API endpoint URL. "inputKey" or endpointUrl is required. Sample: 68 | * "https://logs.loggly.com/inputs/12345678-90ab-cdef-1234-567890abcdef"
patternStringPattern used for Loggly log messages. Default value is: 74 | * %d{"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",UTC} %-5level [%thread] %logger: %m%n.
proxyHostStringhostname of a proxy server. If blank, no proxy is used (See {@link URL#openConnection(java.net.Proxy)}.
proxyPortintport of a proxy server. Must be a valid int but is ignored if proxyHost is blank or null.
jmxMonitoringbooleanEnable registration of a monitoring MBean named 90 | * "ch.qos.logback:type=LogglyBatchAppender,name=LogglyBatchAppender@#hashcode#". Default: true.
maxNumberOfBucketsintMax number of buckets of in the byte buffer. Default value: 8.
maxBucketSizeInKilobytesintMax size of each bucket. Default value: 1024 Kilobytes (1MB).
flushIntervalInSecondsintInterval of the buffer flush to Loggly API. Default value: 3.
connReadTimeoutSecondsintHow Long the HTTP Connection will wait on reads. Default value: 1 second.
113 | * Default configuration consumes up to 8 buffers of 1024 Kilobytes (1MB) each, which seems very reasonable even for small JVMs. 114 | * If logs are discarded, try first to shorten the flushIntervalInSeconds parameter to "2s" or event "1s". 115 | *

116 | *

Configuration Sample

117 | *

118 |  * <configuration scan="true" scanPeriod="30 seconds" debug="true">
119 |  *   <if condition='isDefined("logback.loggly.inputKey")'>
120 |  *     <then>
121 |  *       <appender name="loggly" class="ch.qos.logback.ext.loggly.LogglyBatchAppender">
122 |  *         <inputKey>${logback.loggly.inputKey}</inputKey>
123 |  *         <pattern>%d{yyyy/MM/dd HH:mm:ss,SSS} [${HOSTNAME}] [%thread] %-5level %logger{36} - %m %throwable{5}%n</pattern>
124 |  *         <proxyHost>${logback.loggly.proxy.host:-}</proxyHost>
125 |  *         <proxyPort>${logback.loggly.proxy.port:-8080}</proxyPort>
126 |  *         <debug>${logback.loggly.debug:-false}</debug>
127 |  *       </appender>
128 |  *       <root level="WARN">
129 |  *         <appender-ref ref="loggly"/>
130 |  *       </root>
131 |  *     </then>
132 |  *   </if>
133 |  * </configuration>
134 |  * 
135 | *

136 | *

137 | *

Implementation decisions

138 | *
    139 | *
  • Why buffer the generated log messages as bytes instead of using the 140 | * {@code ch.qos.logback.core.read.CyclicBufferAppender} and buffering the {@code ch.qos.logback.classic.spi.ILoggingEvent} ? 141 | * Because it is much easier to control the size in memory
  • 142 | *
  • 143 | * Why buffer in a byte array instead of directly writing in a {@link BufferedOutputStream} on the {@link HttpURLConnection} ? 144 | * Because the Loggly API may not like such kind of streaming approach. 145 | *
  • 146 | *
147 | * 148 | * @author Cyrille Le Clerc 149 | */ 150 | public class LogglyBatchAppender extends AbstractLogglyAppender implements LogglyBatchAppenderMBean { 151 | 152 | public static final String ENDPOINT_URL_PATH = "bulk/"; 153 | 154 | private boolean debug = false; 155 | 156 | private int flushIntervalInSeconds = 3; 157 | 158 | private DiscardingRollingOutputStream outputStream; 159 | 160 | protected final AtomicLong sendDurationInNanos = new AtomicLong(); 161 | 162 | protected final AtomicLong sentBytes = new AtomicLong(); 163 | 164 | protected final AtomicInteger sendSuccessCount = new AtomicInteger(); 165 | 166 | protected final AtomicInteger sendExceptionCount = new AtomicInteger(); 167 | 168 | private ScheduledExecutorService scheduledExecutor; 169 | 170 | private boolean jmxMonitoring = true; 171 | 172 | private MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); 173 | 174 | private ObjectName registeredObjectName; 175 | 176 | private int maxNumberOfBuckets = 8; 177 | 178 | private int maxBucketSizeInKilobytes = 1024; 179 | 180 | private Charset charset = Charset.forName("UTF-8"); 181 | 182 | /* Store Connection Read Timeout */ 183 | private int connReadTimeoutSeconds = 1; 184 | 185 | @Override 186 | protected void append(E eventObject) { 187 | if (!isStarted()) { 188 | return; 189 | } 190 | String msg = this.layout.doLayout(eventObject); 191 | 192 | // Issue #21: Make sure messages end with new-line to delimit 193 | // individual log events within the batch sent to loggly. 194 | if (!msg.endsWith("\n")) { 195 | msg += "\n"; 196 | } 197 | 198 | try { 199 | outputStream.write(msg.getBytes(charset)); 200 | } catch (IOException e) { 201 | throw new RuntimeException(e); 202 | } 203 | } 204 | 205 | @Override 206 | public void start() { 207 | 208 | // OUTPUTSTREAM 209 | outputStream = new DiscardingRollingOutputStream( 210 | maxBucketSizeInKilobytes * 1024, 211 | maxNumberOfBuckets) { 212 | @Override 213 | protected void onBucketDiscard(ByteArrayOutputStream discardedBucket) { 214 | if (isDebug()) { 215 | addInfo("Discard bucket - " + getDebugInfo()); 216 | } 217 | String s = new Timestamp(System.currentTimeMillis()) + " - OutputStream is full, discard previous logs" + LINE_SEPARATOR; 218 | try { 219 | getFilledBuckets().peekLast().write(s.getBytes(charset)); 220 | addWarn(s); 221 | } catch (IOException e) { 222 | addWarn("Exception appending warning message '" + s + "'", e); 223 | } 224 | } 225 | 226 | @Override 227 | protected void onBucketRoll(ByteArrayOutputStream rolledBucket) { 228 | if (isDebug()) { 229 | addInfo("Roll bucket - " + getDebugInfo()); 230 | } 231 | } 232 | 233 | }; 234 | 235 | // SCHEDULER 236 | ThreadFactory threadFactory = new ThreadFactory() { 237 | @Override 238 | public Thread newThread(Runnable r) { 239 | Thread thread = Executors.defaultThreadFactory().newThread(r); 240 | thread.setName("logback-loggly-appender"); 241 | thread.setDaemon(true); 242 | return thread; 243 | } 244 | }; 245 | scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory); 246 | scheduledExecutor.scheduleWithFixedDelay(new LogglyExporter(), flushIntervalInSeconds, flushIntervalInSeconds, TimeUnit.SECONDS); 247 | 248 | // MONITORING 249 | if (jmxMonitoring) { 250 | String objectName = "ch.qos.logback:type=LogglyBatchAppender,name=LogglyBatchAppender@" + System.identityHashCode(this); 251 | try { 252 | registeredObjectName = mbeanServer.registerMBean(this, new ObjectName(objectName)).getObjectName(); 253 | } catch (Exception e) { 254 | addWarn("Exception registering mbean '" + objectName + "'", e); 255 | } 256 | } 257 | 258 | // super.setOutputStream() must be defined before calling super.start() 259 | super.start(); 260 | } 261 | 262 | @Override 263 | public void stop() { 264 | scheduledExecutor.shutdown(); 265 | 266 | processLogEntries(); 267 | 268 | if (registeredObjectName != null) { 269 | try { 270 | mbeanServer.unregisterMBean(registeredObjectName); 271 | } catch (Exception e) { 272 | addWarn("Exception unRegistering mbean " + registeredObjectName, e); 273 | } 274 | } 275 | 276 | try { 277 | scheduledExecutor.awaitTermination(2 * this.flushIntervalInSeconds, TimeUnit.SECONDS); 278 | } catch (InterruptedException e) { 279 | addWarn("Exception waiting for termination of LogglyAppender scheduler", e); 280 | } 281 | 282 | // stop appender (ie close outputStream) after sending it to Loggly 283 | outputStream.close(); 284 | 285 | super.stop(); 286 | } 287 | 288 | /** 289 | * Send log entries to Loggly 290 | */ 291 | @Override 292 | public void processLogEntries() { 293 | if (isDebug()) { 294 | addInfo("Process log entries - " + getDebugInfo()); 295 | } 296 | 297 | outputStream.rollCurrentBucketIfNotEmpty(); 298 | BlockingDeque filledBuckets = outputStream.getFilledBuckets(); 299 | 300 | ByteArrayOutputStream bucket; 301 | 302 | while ((bucket = filledBuckets.poll()) != null) { 303 | try { 304 | InputStream in = new ByteArrayInputStream(bucket.toByteArray()); 305 | processLogEntries(in); 306 | } catch (Exception e) { 307 | addWarn("Internal error", e); 308 | } 309 | outputStream.recycleBucket(bucket); 310 | } 311 | } 312 | 313 | /** 314 | * Creates a configured HTTP connection to a URL (does not open the 315 | * connection) 316 | * 317 | * @param url target URL 318 | * @return the newly created HTTP connection 319 | * @throws IOException connection error 320 | */ 321 | protected HttpURLConnection getHttpConnection(URL url) throws IOException { 322 | HttpURLConnection conn; 323 | if (proxy == null) { 324 | conn = (HttpURLConnection) url.openConnection(); 325 | } else { 326 | conn = (HttpURLConnection) url.openConnection(proxy); 327 | } 328 | 329 | conn.setDoOutput(true); 330 | conn.setDoInput(true); 331 | conn.setRequestProperty("Content-Type", layout.getContentType() + "; charset=" + charset.name()); 332 | conn.setRequestMethod("POST"); 333 | conn.setReadTimeout(getHttpReadTimeoutInMillis()); 334 | return conn; 335 | } 336 | 337 | /** 338 | * Send log entries to Loggly 339 | * @param in log input stream 340 | */ 341 | protected void processLogEntries(InputStream in) { 342 | long nanosBefore = System.nanoTime(); 343 | try { 344 | 345 | HttpURLConnection conn = getHttpConnection(new URL(endpointUrl)); 346 | /* Set connection Read Timeout */ 347 | conn.setReadTimeout(connReadTimeoutSeconds*1000); 348 | BufferedOutputStream out = new BufferedOutputStream(conn.getOutputStream()); 349 | 350 | long len = IoUtils.copy(in, out); 351 | sentBytes.addAndGet(len); 352 | 353 | out.flush(); 354 | out.close(); 355 | 356 | int responseCode = conn.getResponseCode(); 357 | String response = super.readResponseBody(conn.getInputStream()); 358 | switch (responseCode) { 359 | case HttpURLConnection.HTTP_OK: 360 | case HttpURLConnection.HTTP_ACCEPTED: 361 | sendSuccessCount.incrementAndGet(); 362 | break; 363 | default: 364 | sendExceptionCount.incrementAndGet(); 365 | addError("LogglyAppender server-side exception: " + responseCode + ": " + response); 366 | } 367 | // force url connection recycling 368 | try { 369 | conn.getInputStream().close(); 370 | conn.disconnect(); 371 | } catch (Exception e) { 372 | // swallow exception 373 | } 374 | } catch (Exception e) { 375 | sendExceptionCount.incrementAndGet(); 376 | addError("LogglyAppender client-side exception", e); 377 | } finally { 378 | sendDurationInNanos.addAndGet(System.nanoTime() - nanosBefore); 379 | } 380 | } 381 | 382 | public int getFlushIntervalInSeconds() { 383 | return flushIntervalInSeconds; 384 | } 385 | 386 | public void setFlushIntervalInSeconds(int flushIntervalInSeconds) { 387 | this.flushIntervalInSeconds = flushIntervalInSeconds; 388 | } 389 | 390 | @Override 391 | public long getSentBytes() { 392 | return sentBytes.get(); 393 | } 394 | 395 | @Override 396 | public long getSendDurationInNanos() { 397 | return sendDurationInNanos.get(); 398 | } 399 | 400 | @Override 401 | public int getSendSuccessCount() { 402 | return sendSuccessCount.get(); 403 | } 404 | 405 | @Override 406 | public int getSendExceptionCount() { 407 | return sendExceptionCount.get(); 408 | } 409 | 410 | @Override 411 | public int getDiscardedBucketsCount() { 412 | return outputStream.getDiscardedBucketCount(); 413 | } 414 | 415 | @Override 416 | public long getCurrentLogEntriesBufferSizeInBytes() { 417 | return outputStream.getCurrentOutputStreamSize(); 418 | } 419 | 420 | public void setDebug(boolean debug) { 421 | this.debug = debug; 422 | } 423 | 424 | public boolean isDebug() { 425 | return debug; 426 | } 427 | 428 | public void setJmxMonitoring(boolean jmxMonitoring) { 429 | this.jmxMonitoring = jmxMonitoring; 430 | } 431 | 432 | public void setMbeanServer(MBeanServer mbeanServer) { 433 | this.mbeanServer = mbeanServer; 434 | } 435 | 436 | public void setMaxNumberOfBuckets(int maxNumberOfBuckets) { 437 | this.maxNumberOfBuckets = maxNumberOfBuckets; 438 | } 439 | 440 | public void setMaxBucketSizeInKilobytes(int maxBucketSizeInKilobytes) { 441 | this.maxBucketSizeInKilobytes = maxBucketSizeInKilobytes; 442 | } 443 | 444 | /** 445 | * set method for Logback to allow Connection Read Timeout to be exposed 446 | */ 447 | public void setConnReadTimeoutSeconds(int connReadTimeoutSeconds) { 448 | this.connReadTimeoutSeconds = connReadTimeoutSeconds; 449 | } 450 | 451 | private String getDebugInfo() { 452 | return "{" + 453 | "sendDurationInMillis=" + TimeUnit.MILLISECONDS.convert(sendDurationInNanos.get(), TimeUnit.NANOSECONDS) + 454 | ", sendSuccessCount=" + sendSuccessCount + 455 | ", sendExceptionCount=" + sendExceptionCount + 456 | ", sentBytes=" + sentBytes + 457 | ", discardedBucketsCount=" + getDiscardedBucketsCount() + 458 | ", currentLogEntriesBufferSizeInBytes=" + getCurrentLogEntriesBufferSizeInBytes() + 459 | '}'; 460 | } 461 | 462 | public class LogglyExporter implements Runnable { 463 | @Override 464 | public void run() { 465 | try { 466 | processLogEntries(); 467 | } catch (Exception e) { 468 | addWarn("Exception processing log entries", e); 469 | } 470 | } 471 | } 472 | 473 | @Override 474 | protected String getEndpointPrefix() { 475 | return ENDPOINT_URL_PATH; 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /loggly/src/main/java/ch/qos/logback/ext/loggly/LogglyBatchAppenderMBean.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.loggly; 17 | 18 | /** 19 | * JMX Mbean interface for the {@link LogglyBatchAppender}. 20 | * 21 | * @author Cyrille Le Clerc 22 | */ 23 | public interface LogglyBatchAppenderMBean { 24 | 25 | void processLogEntries(); 26 | 27 | /** 28 | * Number of bytes sent to Loggly. 29 | */ 30 | long getSentBytes(); 31 | 32 | /** 33 | * Duration spent sending logs to Loggly. 34 | */ 35 | long getSendDurationInNanos(); 36 | 37 | /** 38 | * Number of successful invocations to Loggly's send logs API. 39 | */ 40 | int getSendSuccessCount(); 41 | 42 | /** 43 | * Number of failing invocations to Loggly's send logs API. 44 | */ 45 | int getSendExceptionCount(); 46 | 47 | /** 48 | * Number of discarded buckets 49 | */ 50 | int getDiscardedBucketsCount(); 51 | 52 | /** 53 | * Size in bytes of the log entries that have not yet been sent to Loggly. 54 | */ 55 | long getCurrentLogEntriesBufferSizeInBytes(); 56 | 57 | boolean isDebug(); 58 | 59 | /** 60 | * Enable debugging 61 | */ 62 | void setDebug(boolean debug); 63 | } 64 | -------------------------------------------------------------------------------- /loggly/src/main/java/ch/qos/logback/ext/loggly/io/DiscardingRollingOutputStream.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.loggly.io; 17 | 18 | import java.io.ByteArrayOutputStream; 19 | import java.io.IOException; 20 | import java.io.OutputStream; 21 | import java.util.concurrent.BlockingDeque; 22 | import java.util.concurrent.ConcurrentLinkedQueue; 23 | import java.util.concurrent.LinkedBlockingDeque; 24 | import java.util.concurrent.atomic.AtomicInteger; 25 | import java.util.concurrent.locks.ReentrantLock; 26 | 27 | /** 28 | *

29 | * Capped in-memory {@linkplain OutputStream} composed of a chain of {@linkplain ByteArrayOutputStream} called 'buckets'. 30 | *

31 | *

32 | * Each 'bucket' is limited in size (see {@link #maxBucketSizeInBytes}) and the total size of the {@linkplain OutputStream} 33 | * is bounded thanks to a discarding policy. An external component is expected to consume the filled buckets thanks to 34 | * {@link #getFilledBuckets()}. 35 | *

36 | *

37 | * Implementation decisions: 38 | *

39 | *
    40 | *
  • Why in-memory without offload on disk: offload on disk was possible with Google Guava's 41 | * FileBackedOutputStream but had the drawback to introduce a dependency. Loggly batch appender use case 42 | * should be OK with a pure in-memory approach.
  • 43 | *
44 | * 45 | * @author Cyrille Le Clerc 46 | */ 47 | public class DiscardingRollingOutputStream extends OutputStream { 48 | 49 | public static final String LINE_SEPARATOR = System.getProperty("line.separator"); 50 | 51 | private ByteArrayOutputStream currentBucket; 52 | 53 | private final ReentrantLock currentBucketLock = new ReentrantLock(); 54 | 55 | private final BlockingDeque filledBuckets; 56 | 57 | private final ConcurrentLinkedQueue recycledBucketPool; 58 | 59 | private long maxBucketSizeInBytes; 60 | 61 | private final AtomicInteger discardedBucketCount = new AtomicInteger(); 62 | 63 | /** 64 | * @param maxBucketSizeInBytes maximum byte size of each bucket 65 | * @param maxBucketCount maximum number of buckets 66 | */ 67 | public DiscardingRollingOutputStream(int maxBucketSizeInBytes, int maxBucketCount) { 68 | if (maxBucketCount < 2) { 69 | throw new IllegalArgumentException("'maxBucketCount' must be >1"); 70 | } 71 | 72 | this.maxBucketSizeInBytes = maxBucketSizeInBytes; 73 | this.filledBuckets = new LinkedBlockingDeque(maxBucketCount); 74 | 75 | this.recycledBucketPool = new ConcurrentLinkedQueue(); 76 | this.currentBucket = newBucket(); 77 | } 78 | 79 | 80 | @Override 81 | public void write(int b) throws IOException { 82 | currentBucketLock.lock(); 83 | try { 84 | currentBucket.write(b); 85 | rollCurrentBucketIfNeeded(); 86 | } finally { 87 | currentBucketLock.unlock(); 88 | } 89 | } 90 | 91 | @Override 92 | public void write(byte[] b) throws IOException { 93 | currentBucketLock.lock(); 94 | try { 95 | currentBucket.write(b); 96 | rollCurrentBucketIfNeeded(); 97 | } finally { 98 | currentBucketLock.unlock(); 99 | } 100 | } 101 | 102 | @Override 103 | public void write(byte[] b, int off, int len) throws IOException { 104 | currentBucketLock.lock(); 105 | try { 106 | currentBucket.write(b, off, len); 107 | rollCurrentBucketIfNeeded(); 108 | } finally { 109 | currentBucketLock.unlock(); 110 | } 111 | } 112 | 113 | @Override 114 | public void flush() throws IOException { 115 | currentBucketLock.lock(); 116 | try { 117 | currentBucket.flush(); 118 | } finally { 119 | currentBucketLock.unlock(); 120 | } 121 | } 122 | 123 | /** 124 | * Close all the underlying buckets (current bucket, filled buckets and buckets from the recycled buckets pool). 125 | */ 126 | @Override 127 | public void close() { 128 | // no-op as ByteArrayOutputStream#close() is no op 129 | } 130 | 131 | /** 132 | * Roll current bucket if size threshold has been reached. 133 | */ 134 | private void rollCurrentBucketIfNeeded() { 135 | if (currentBucket.size() < maxBucketSizeInBytes) { 136 | return; 137 | } 138 | rollCurrentBucket(); 139 | } 140 | 141 | /** 142 | * Roll current bucket if size threshold has been reached. 143 | */ 144 | public void rollCurrentBucketIfNotEmpty() { 145 | if (currentBucket.size() == 0) { 146 | return; 147 | } 148 | rollCurrentBucket(); 149 | } 150 | 151 | /** 152 | * Moves the current active bucket to the list of filled buckets and defines a new one. 153 | * 154 | * The new active bucket is reused from the {@link #recycledBucketPool} pool if one is available or recreated. 155 | */ 156 | public void rollCurrentBucket() { 157 | currentBucketLock.lock(); 158 | try { 159 | boolean offered = filledBuckets.offer(currentBucket); 160 | if (offered) { 161 | onBucketRoll(currentBucket); 162 | } else { 163 | onBucketDiscard(currentBucket); 164 | discardedBucketCount.incrementAndGet(); 165 | } 166 | 167 | currentBucket = newBucket(); 168 | } finally { 169 | currentBucketLock.unlock(); 170 | } 171 | } 172 | 173 | /** 174 | * Designed for extension. 175 | * 176 | * @param discardedBucket the discarded bucket 177 | */ 178 | protected void onBucketDiscard(ByteArrayOutputStream discardedBucket) { 179 | 180 | } 181 | 182 | /** 183 | * The rolled bucket. Designed for extension. 184 | * 185 | * @param rolledBucket the discarded bucket 186 | */ 187 | protected void onBucketRoll(ByteArrayOutputStream rolledBucket) { 188 | 189 | } 190 | 191 | /** 192 | * Get a new bucket from the {@link #recycledBucketPool} or instantiate a new one if none available 193 | * in the free bucket pool. 194 | * 195 | * @return the bucket ready to use 196 | */ 197 | protected ByteArrayOutputStream newBucket() { 198 | ByteArrayOutputStream bucket = recycledBucketPool.poll(); 199 | if (bucket == null) { 200 | bucket = new ByteArrayOutputStream(); 201 | } 202 | return bucket; 203 | } 204 | 205 | /** 206 | * Returns the given bucket to the pool of free buckets. 207 | * 208 | * @param bucket the bucket to recycle 209 | */ 210 | public void recycleBucket(ByteArrayOutputStream bucket) { 211 | bucket.reset(); 212 | recycledBucketPool.offer(bucket); 213 | } 214 | 215 | /** 216 | * Return the filled buckets 217 | */ 218 | public BlockingDeque getFilledBuckets() { 219 | return filledBuckets; 220 | } 221 | 222 | /** 223 | * @return Number of discarded buckets. Monitoring oriented metric. 224 | */ 225 | public int getDiscardedBucketCount() { 226 | return discardedBucketCount.get(); 227 | } 228 | 229 | public long getCurrentOutputStreamSize() { 230 | long sizeInBytes = 0; 231 | for (ByteArrayOutputStream bucket : filledBuckets) { 232 | sizeInBytes += bucket.size(); 233 | } 234 | sizeInBytes += currentBucket.size(); 235 | return sizeInBytes; 236 | } 237 | 238 | @Override 239 | public String toString() { 240 | return "DiscardingRollingOutputStream{" + 241 | "currentBucket.bytesWritten=" + currentBucket.size() + 242 | ", filledBuckets.size=" + filledBuckets.size() + 243 | ", discardedBucketCount=" + discardedBucketCount + 244 | ", recycledBucketPool.size=" + recycledBucketPool.size() + 245 | '}'; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /loggly/src/main/java/ch/qos/logback/ext/loggly/io/IoUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.loggly.io; 17 | 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.io.OutputStream; 21 | 22 | /** 23 | * @author Cyrille Le Clerc 24 | */ 25 | public class IoUtils { 26 | 27 | public static long copy(InputStream in, OutputStream out) throws IOException { 28 | byte[] buffer = new byte[1024]; // 1k 29 | long totalSize = 0; 30 | while (true) { 31 | int size = in.read(buffer); 32 | if (size == -1) { 33 | return totalSize; 34 | } 35 | out.write(buffer, 0, size); 36 | totalSize += size; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /loggly/src/test/java/ch/qos/logback/ext/loggly/HttpTestServer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.loggly; 17 | 18 | import java.io.IOException; 19 | import java.io.PrintStream; 20 | import java.net.InetSocketAddress; 21 | import java.net.SocketAddress; 22 | import java.nio.ByteBuffer; 23 | import java.util.concurrent.atomic.AtomicInteger; 24 | 25 | import org.simpleframework.http.Request; 26 | import org.simpleframework.http.Response; 27 | import org.simpleframework.http.core.Container; 28 | import org.simpleframework.http.core.ContainerServer; 29 | import org.simpleframework.transport.Server; 30 | import org.simpleframework.transport.connect.Connection; 31 | import org.simpleframework.transport.connect.SocketConnection; 32 | 33 | /** 34 | * HTTP test server that tracks the number of requests received 35 | */ 36 | public class HttpTestServer implements Container { 37 | static private final int MAX_RXBUF_SIZE = 2048; 38 | private int port; 39 | private Connection connection; 40 | private Server server; 41 | AtomicInteger numRequests; 42 | 43 | /** 44 | * Initializes the HTTP server 45 | * @param port 46 | */ 47 | public HttpTestServer(int port) { 48 | this.port = port; 49 | numRequests = new AtomicInteger(0); 50 | } 51 | 52 | /** 53 | * Opens the HTTP server 54 | * @throws IOException 55 | */ 56 | public void start() throws IOException { 57 | stop(); 58 | this.server = new ContainerServer(this); 59 | this.connection = new SocketConnection(server); 60 | SocketAddress address = new InetSocketAddress(this.port); 61 | this.connection.connect(address); 62 | } 63 | 64 | /** 65 | * Closes the HTTP server 66 | */ 67 | public void stop() { 68 | if (this.connection != null) { 69 | try { 70 | this.connection.close(); 71 | } catch (IOException e) { 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * Gets the number of requests received 78 | * @return the request count 79 | */ 80 | public int requestCount() { 81 | return numRequests.get(); 82 | } 83 | 84 | /** 85 | * Resets the request count 86 | */ 87 | public void clearRequests() { 88 | numRequests.set(0); 89 | } 90 | 91 | /** 92 | * Waits indefinitely for the specified number of requests to be received 93 | * @param count the number of received requests to wait for 94 | */ 95 | public void waitForRequests(int count) { 96 | waitForRequests(count, 0, 0); 97 | } 98 | 99 | /** 100 | * Waits for the specified number of requests to be received 101 | * @param count the number of received requests to wait for 102 | * @param interval the wait time between polls, checking the receive count; 103 | * use 0 to wait indefinitely 104 | * @param maxPolls the maximum number of polls; use 0 for no limit 105 | */ 106 | public void waitForRequests(int count, long interval, int maxPolls) { 107 | synchronized (this) { 108 | while (requestCount() < count) { 109 | System.out.println("requests " + requestCount() + "/" + count); 110 | if (maxPolls > 0 && maxPolls-- > 0) { 111 | break; 112 | } 113 | try { 114 | this.wait(interval); 115 | } catch (InterruptedException e) { 116 | } 117 | } 118 | System.out.println("requests " + requestCount() + "/" + count); 119 | } 120 | } 121 | 122 | /** 123 | * Handles incoming HTTP requests by responding with the 124 | * message index and size of the received message. 125 | * @see org.simpleframework.http.core.Container#handle(org.simpleframework.http.Request, org.simpleframework.http.Response) 126 | */ 127 | @Override 128 | public void handle(Request request, Response response) { 129 | try { 130 | PrintStream body = response.getPrintStream(); 131 | long time = System.currentTimeMillis(); 132 | 133 | response.setValue("Content-Type", "text/html"); 134 | response.setValue("Server", "HttpTestServer/1.0 (Simple 4.0)"); 135 | response.setDate("Date", time); 136 | response.setDate("Last-Modified", time); 137 | 138 | ByteBuffer buf = ByteBuffer.allocate(MAX_RXBUF_SIZE); 139 | int len = request.getByteChannel().read(buf); 140 | int count = numRequests.incrementAndGet(); 141 | 142 | // warn if RX buffer exceeded (message truncated) 143 | String warning = ""; 144 | if (len > MAX_RXBUF_SIZE) { 145 | warning = "(" + (len - MAX_RXBUF_SIZE) + " bytes truncated)"; 146 | } 147 | 148 | body.println("Request #" + count + "\n" + len + " bytes read" + warning); 149 | body.close(); 150 | 151 | synchronized (this) { 152 | notify(); 153 | } 154 | } catch (Exception e) { 155 | e.printStackTrace(); 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /loggly/src/test/java/ch/qos/logback/ext/loggly/LogglyBatchAppenderTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.loggly; 17 | 18 | import java.io.IOException; 19 | import java.net.InetAddress; 20 | import java.net.UnknownHostException; 21 | 22 | import static org.junit.Assert.assertTrue; 23 | import static org.junit.Assert.assertEquals; 24 | 25 | import org.junit.AfterClass; 26 | import org.junit.Before; 27 | import org.junit.BeforeClass; 28 | import org.junit.Ignore; 29 | import org.junit.Test; 30 | 31 | import ch.qos.logback.classic.LoggerContext; 32 | import ch.qos.logback.core.layout.EchoLayout; 33 | 34 | /** 35 | * Tests the LogglyBatchAppender 36 | */ 37 | @Ignore 38 | public class LogglyBatchAppenderTest { 39 | 40 | static private final int PORT = 10800; 41 | static private final int MAX_BUCKETS = 4; 42 | static private final int BUCKET_KB_SIZE = 1; 43 | static private final int MSG_SIZE = BUCKET_KB_SIZE * 1024; 44 | static private HttpTestServer httpServer; 45 | static private LogglyBatchAppender appender; 46 | static private LoggerContext context; 47 | 48 | /** 49 | * Starts the HTTP test server, initializes the appender, 50 | * and creates a context with a status listener 51 | * @throws IOException 52 | */ 53 | @BeforeClass 54 | static public void beforeClass() throws IOException { 55 | httpServer = new HttpTestServer(PORT); 56 | httpServer.start(); 57 | context = new LoggerContext(); 58 | } 59 | 60 | /** 61 | * Shuts down the HTTP test server 62 | */ 63 | @AfterClass 64 | static public void afterClass() { 65 | httpServer.stop(); 66 | appender.stop(); 67 | } 68 | 69 | @Before 70 | public void before() throws UnknownHostException { 71 | httpServer.clearRequests(); 72 | 73 | appender = new LogglyBatchAppender(); 74 | appender.setContext(context); 75 | appender.setEndpointUrl("http://" + InetAddress.getLocalHost().getHostAddress() + ":" + PORT + "/"); 76 | 77 | appender.setLayout(new EchoLayout()); 78 | appender.setDebug(true); 79 | appender.setMaxBucketSizeInKilobytes(BUCKET_KB_SIZE); 80 | appender.setMaxNumberOfBuckets(MAX_BUCKETS); 81 | appender.start(); 82 | } 83 | 84 | @Test 85 | public void starts() { 86 | assertTrue(appender.isStarted()); 87 | } 88 | 89 | private void appendFullBuckets(int count) { 90 | for (int i = 0; i < count; i++) { 91 | appender.doAppend(new String(new char[MSG_SIZE]).replace("\0", "X")); 92 | } 93 | } 94 | 95 | @Test(timeout = 180000) 96 | public void sendsOnlyWhenMaxBucketsFull() { 97 | // assert nothing yet sent/received 98 | assertEquals(0, appender.getSendSuccessCount()); 99 | assertEquals(0, httpServer.requestCount()); 100 | 101 | // send stuff and wait for it to be received 102 | appendFullBuckets(MAX_BUCKETS); 103 | httpServer.waitForRequests(MAX_BUCKETS); 104 | 105 | // assert stuff sent/received 106 | assertEquals(MAX_BUCKETS, appender.getSendSuccessCount()); 107 | assertEquals(MAX_BUCKETS, httpServer.requestCount()); 108 | } 109 | 110 | @Test(timeout = 180000) 111 | public void excessBucketsGetDiscarded() { 112 | // assert nothing yet discarded (because nothing is yet sent) 113 | assertEquals(0, appender.getDiscardedBucketsCount()); 114 | 115 | // send stuff and wait for it to be received 116 | final int NUM_MSGS = 40; 117 | appendFullBuckets(NUM_MSGS); 118 | httpServer.waitForRequests(MAX_BUCKETS); 119 | 120 | // assert excess buckets (those > MAX_BUCKETS) were discarded 121 | assertEquals(NUM_MSGS - MAX_BUCKETS, appender.getDiscardedBucketsCount()); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /loggly/src/test/java/ch/qos/logback/ext/loggly/LogglyHttpAppenderIntegratedTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.loggly; 17 | 18 | import ch.qos.logback.core.layout.EchoLayout; 19 | import ch.qos.logback.ext.loggly.io.IoUtils; 20 | import de.svenjacobs.loremipsum.LoremIpsum; 21 | 22 | import java.io.FileOutputStream; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.io.OutputStream; 26 | import java.sql.Timestamp; 27 | import java.text.SimpleDateFormat; 28 | import java.util.Date; 29 | import java.util.Random; 30 | import java.util.concurrent.TimeUnit; 31 | 32 | import static org.junit.Assert.*; 33 | 34 | /** 35 | * @author Cyrille Le Clerc 36 | */ 37 | public class LogglyHttpAppenderIntegratedTest { 38 | 39 | public static void main(String[] args) throws Exception { 40 | 41 | Random random = new Random(); 42 | LoremIpsum loremIpsum = new LoremIpsum(); 43 | 44 | String file = "/tmp/loggly-appender-test-" + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + ".log"; 45 | System.out.println("Generate " + file); 46 | final OutputStream out = new FileOutputStream(file); 47 | 48 | LogglyBatchAppender appender = new LogglyBatchAppender() { 49 | @Override 50 | protected void processLogEntries(InputStream in) { 51 | // super.processLogEntries(in); 52 | try { 53 | IoUtils.copy(in, out); 54 | } catch (IOException e) { 55 | e.printStackTrace(); 56 | } 57 | } 58 | }; 59 | appender.setInputKey("YOUR LOGGLY INPUT KEY"); 60 | appender.setLayout(new EchoLayout()); 61 | appender.setDebug(true); 62 | 63 | // Start 64 | appender.start(); 65 | 66 | assertTrue("Appender failed to start", appender.isStarted()); 67 | 68 | appender.doAppend("# Test " + new Timestamp(System.currentTimeMillis())); 69 | 70 | for (int i = 0; i < 100000; i++) { 71 | appender.doAppend(i + " -- " + new Timestamp(System.currentTimeMillis()) + " - " + loremIpsum.getWords(random.nextInt(50), random.nextInt(50))); 72 | TimeUnit.MILLISECONDS.sleep(random.nextInt(30)); 73 | if (i % 100 == 0) { 74 | System.out.println(i + " - " + appender); 75 | } 76 | } 77 | // stop 78 | appender.stop(); 79 | 80 | out.close(); 81 | System.out.println(appender); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /loggly/src/test/java/ch/qos/logback/ext/loggly/LogglySendOnlyWhenMaxBucketsFullTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.loggly; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.mockito.Mockito.*; 20 | 21 | import java.io.BufferedInputStream; 22 | import java.io.BufferedOutputStream; 23 | import java.io.ByteArrayInputStream; 24 | import java.io.ByteArrayOutputStream; 25 | import java.io.IOException; 26 | import java.io.InputStream; 27 | import java.net.HttpURLConnection; 28 | import java.net.InetAddress; 29 | import java.net.URL; 30 | import java.net.UnknownHostException; 31 | 32 | import org.junit.After; 33 | import org.junit.AfterClass; 34 | import org.junit.Before; 35 | import org.junit.BeforeClass; 36 | import org.junit.Test; 37 | import org.junit.runner.RunWith; 38 | import org.mockito.Mock; 39 | import org.mockito.runners.MockitoJUnitRunner; 40 | 41 | import ch.qos.logback.classic.LoggerContext; 42 | import ch.qos.logback.core.layout.EchoLayout; 43 | 44 | @RunWith(MockitoJUnitRunner.class) 45 | public class LogglySendOnlyWhenMaxBucketsFullTest { 46 | static private final int PORT = 10800; 47 | static private final int MAX_BUCKETS = 4; 48 | static private final int BUCKET_KB_SIZE = 1; 49 | static private final int MSG_SIZE = BUCKET_KB_SIZE * 1024; 50 | static private LogglyBatchAppender appender; 51 | static private LoggerContext context; 52 | private ByteArrayOutputStream byteOutputStream; 53 | private ByteArrayInputStream byteInputStream; 54 | 55 | @Mock 56 | HttpURLConnection connection; 57 | 58 | /** 59 | * Creates a context with a status listener 60 | */ 61 | @BeforeClass 62 | static public void beforeClass() { 63 | context = new LoggerContext(); 64 | } 65 | 66 | /** 67 | * Shuts down the appender's processing thread 68 | */ 69 | @AfterClass 70 | static public void afterClass() { 71 | appender.stop(); 72 | } 73 | 74 | @Before 75 | public void before() throws UnknownHostException { 76 | byteOutputStream = new ByteArrayOutputStream(); 77 | byteInputStream = new ByteArrayInputStream(new byte[0]); 78 | 79 | appender = new LogglyBatchAppenderWithMockConnection(); 80 | appender.setContext(context); 81 | appender.setEndpointUrl("http://" + InetAddress.getLocalHost().getHostAddress() + ":" + PORT + "/"); 82 | 83 | appender.setLayout(new EchoLayout()); 84 | appender.setDebug(true); 85 | appender.setMaxBucketSizeInKilobytes(BUCKET_KB_SIZE); 86 | appender.setMaxNumberOfBuckets(MAX_BUCKETS); 87 | appender.start(); 88 | 89 | assertNoTrafficYet(); 90 | 91 | } 92 | 93 | @After 94 | public void after() { 95 | System.out.println("BYTES ------>"); 96 | System.out.println(byteOutputStream.toString()); 97 | } 98 | 99 | @Test 100 | public void successCountMatchesSentCount() throws Exception { 101 | sendMessagesAndConfirmRx(MAX_BUCKETS); 102 | assertEquals(MAX_BUCKETS, appender.getSendSuccessCount()); 103 | } 104 | 105 | @Test 106 | public void rxCountMatchesSentCount() throws Exception { 107 | sendMessagesAndConfirmRx(MAX_BUCKETS); 108 | assertEquals(MAX_BUCKETS, getRxMessageCount()); 109 | } 110 | 111 | /** 112 | * Mirrors LogglyBatchAppender but uses a mock HTTP connection to 113 | * feed byte streams that we can control. Also calls notifyAll() 114 | * after log entries are processed. 115 | */ 116 | private class LogglyBatchAppenderWithMockConnection extends LogglyBatchAppender { 117 | 118 | @Override 119 | protected HttpURLConnection getHttpConnection(URL url) throws IOException { 120 | when(connection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); 121 | when(connection.getOutputStream()).thenReturn(new BufferedOutputStream(byteOutputStream)); 122 | when(connection.getInputStream()).thenReturn(new BufferedInputStream(byteInputStream)); 123 | return connection; 124 | } 125 | 126 | @Override 127 | protected void processLogEntries(InputStream in) { 128 | super.processLogEntries(in); 129 | synchronized(appender) { 130 | notifyAll(); 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * Verifies that no messages have been sent or received yet 137 | */ 138 | private void assertNoTrafficYet() { 139 | assertEquals(0, appender.getSendSuccessCount()); 140 | assertEquals(0, getRxMessageCount()); 141 | } 142 | 143 | /** 144 | * Sends bucket-full messages to a remote server (which is just a mock connection) 145 | * and confirms delivery (by checking for method calls in the mock connection) 146 | * 147 | * @param count number of full buckets to send 148 | * @throws IOException 149 | * @throws InterruptedException 150 | */ 151 | private void sendMessagesAndConfirmRx(int count) throws IOException, InterruptedException { 152 | appendFullBuckets(count); 153 | 154 | for (int i = 0; i < count; i++) { 155 | synchronized(appender) { 156 | appender.wait(10000); 157 | } 158 | } 159 | 160 | verify(connection, atLeast(MAX_BUCKETS)).getOutputStream(); 161 | verify(connection, atLeast(MAX_BUCKETS)).getInputStream(); 162 | verify(connection, atLeast(MAX_BUCKETS)).disconnect(); 163 | } 164 | 165 | /** 166 | * Gets the number of messages "received" 167 | * @return the message count 168 | */ 169 | private int getRxMessageCount() { 170 | String str = byteOutputStream.toString(); 171 | return str.isEmpty() ? 0 : str.split("\n").length; 172 | } 173 | 174 | /** 175 | * Fills the appender's buckets with max-length messages 176 | * @param count number of buckets to fill 177 | */ 178 | private void appendFullBuckets(int count) { 179 | for (int i = 0; i < count; i++) { 180 | appender.doAppend(i + ")" + new String(new char[MSG_SIZE]).replace("\0", "X")); 181 | } 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /loggly/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %throwable{5}%n 22 | 23 | 24 | 25 | 26 | 27 | 28 | %d{yyyy/MM/dd HH:mm:ss,SSS} [${HOSTNAME}] [%thread] %-5level %logger{36} - %msg %throwable{5}%n 29 | 30 | ${logback.loggly.inputKey:-} 31 | ${logback.loggly.proxy.host:-} 32 | ${logback.loggly.proxy.port:-8080} 33 | ${logback.loggly.debug:-false} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /scripts/deploysnapshot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | user=${NEXUS_USERNAME} 4 | pass=${NEXUS_PASSWORD} 5 | [ -z "$user" ] && read -p "Nexus username: " user 6 | [ -z "$pass" ] && read -p "Nexus password: " -s pass 7 | echo '' 8 | 9 | ./gradlew uploadArchives -x test -x build -PNEXUS_USERNAME=${user} -PNEXUS_PASSWORD=${pass} 10 | -------------------------------------------------------------------------------- /scripts/nexus.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash -e 2 | 3 | user=${NEXUS_USERNAME} 4 | pass=${NEXUS_PASSWORD} 5 | [ -z "$user" ] && read -p "Nexus username: " user 6 | [ -z "$pass" ] && read -p "Nexus password: " -s pass 7 | echo '' 8 | 9 | ./gradlew closeAndReleaseRepository -PnexusUsername=$user -PnexusPassword=$pass 10 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash -e 2 | 3 | . gradle.properties 4 | . local.properties 5 | 6 | version=${VERSION_NAME%*-SNAPSHOT} 7 | baseVersion=${version%*.*} 8 | nextBuild=$((${version##*.} + 1)) 9 | nextVersion="${baseVersion}.${nextBuild}-SNAPSHOT" 10 | 11 | echo "Starting release for logback-ext-${version} ..." 12 | 13 | fail() { 14 | echo "error: $1" >&2 15 | exit 1 16 | } 17 | 18 | # Run Git integrity checks early (gradle-release-plugin does this 19 | # after we update the readme) to avoid premature push of new readme 20 | [[ "$(git rev-parse master)" != "$(git rev-parse origin/master)" ]] && fail "branches out of sync" 21 | [[ -n "$(git status -u -s)" ]] && fail "found unstaged changes" 22 | 23 | # gradle-release-plugin prompts for your Nexus credentials 24 | # with "Please specify username" (no mention of Nexus). 25 | # Use our own prompt to remind the user where they're 26 | # logging into to. 27 | user=${NEXUS_USERNAME} 28 | pass=${NEXUS_PASSWORD} 29 | [ -z "$user" ] && read -p "Nexus username: " user 30 | [ -z "$pass" ] && read -p "Nexus password: " -s pass 31 | 32 | #bintray_user=${BINTRAY_USER} 33 | #bintray_key=${BINTRAY_KEY} 34 | #[ -z "$bintray_user" ] && read -p "Bintray username: " bintray_user 35 | #[ -z "$bintray_key" ] && read -p "Bintray API key: " bintray_key 36 | #echo '' 37 | 38 | ./gradlew -Prelease.useAutomaticVersion=true \ 39 | -Prelease.releaseVersion=${version} \ 40 | -Prelease.newVersion=${nextVersion} \ 41 | -Pversion=${version} \ 42 | -PVERSION_NAME=${version} \ 43 | -PNEXUS_USERNAME=${user} \ 44 | -PNEXUS_PASSWORD=${pass} \ 45 | -Ppush \ 46 | -x test \ 47 | clean \ 48 | readme \ 49 | release \ 50 | uploadArchives 51 | 52 | #./gradlew -PBINTRAY_USER=${BINTRAY_USER} \ 53 | # -PBINTRAY_KEY=${BINTRAY_KEY} \ 54 | # bintrayUpload 55 | 56 | # To deploy archives without git transactions (tagging, etc.), 57 | # replace the `release` task above with `assembleRelease`. 58 | 59 | echo -e "\n\n" 60 | 61 | # FIXME: In test repo, this can't checkout 'gh-pages' -- no error provided 62 | #./gradlew uploadDocs 63 | echo TODO: upload javadocs to gh-pages with: 64 | echo scripts/deploydocs.sh ${version} 65 | 66 | # FIXME: hub is no longer able to find tagged releases for some reason. 67 | #hub release edit -m '' v_${version} -a build/logback-extensions-${version}.jar 68 | echo TODO: attach uber jar to release at: 69 | echo https://github.com/qos-ch/logback-extensions/releases/tag/v_${version} 70 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'logback-ext-parent' 2 | include ':logback-ext-loggly' 3 | include ':logback-ext-spring' 4 | 5 | project(':logback-ext-loggly').projectDir = "$rootDir/loggly" as File 6 | project(':logback-ext-spring').projectDir = "$rootDir/spring" as File -------------------------------------------------------------------------------- /spring/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | description = POM_DESCRIPTION 3 | dependencies { 4 | compileOnly 'ch.qos.logback:logback-classic:1.2.3' 5 | compileOnly('org.springframework:spring-context:3.2.2.RELEASE') { 6 | exclude(module: 'commons-logging') 7 | } 8 | compileOnly('org.springframework:spring-web:3.2.2.RELEASE') { 9 | exclude(module: 'commons-logging') 10 | } 11 | compileOnly 'javax.servlet:servlet-api:2.5' 12 | } 13 | -------------------------------------------------------------------------------- /spring/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=logback-ext-spring 2 | POM_ARTIFACT_ID=logback-ext-spring 3 | POM_PACKAGING=jar 4 | POM_DESCRIPTION="Logback Extensions :: Spring" -------------------------------------------------------------------------------- /spring/src/main/java/ch/qos/logback/ext/spring/ApplicationContextHolder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.spring; 17 | 18 | import org.springframework.beans.BeansException; 19 | import org.springframework.context.ApplicationContext; 20 | import org.springframework.context.ApplicationContextAware; 21 | import org.springframework.context.ApplicationListener; 22 | import org.springframework.context.event.ContextRefreshedEvent; 23 | 24 | /** 25 | * A special bean which may be defined in the Spring {@code ApplicationContext} to make the context available statically 26 | * to objects which, for whatever reason, cannot be wired up in Spring (for example, logging appenders which must be 27 | * defined in XML or properties files used to initialize the logging system). 28 | *

29 | * To use this holder, exactly one bean should be declared as follows: 30 | *

 31 |  *     <bean class="ch.qos.logback.ext.spring.ApplicationContextHolder"/>
 32 |  * 
33 | * Note that no ID is necessary because this holder should always be used via its static accessors, rather than being 34 | * injected. Any Spring bean which wishes to access the {@code ApplicationContext} should not rely on this holder; it 35 | * should simply implement {@code ApplicationContextAware}. 36 | *

37 | * WARNING: This object uses static memory to retain the ApplicationContext. This means this bean (and the 38 | * related configuration strategy) is only usable when no other Logback-enabled Spring applications exist in the same 39 | * JVM. 40 | * 41 | * @author Bryan Turner 42 | * @author Les Hazlewood 43 | * @since 0.1 44 | */ 45 | public class ApplicationContextHolder implements ApplicationContextAware, ApplicationListener { 46 | 47 | private static ApplicationContext applicationContext; 48 | private static volatile boolean refreshed; 49 | 50 | @Override 51 | public void onApplicationEvent(ContextRefreshedEvent event) { 52 | refreshed = true; 53 | } 54 | 55 | /** 56 | * Ensures that the {@code ApplicationContext} has been set and that it has been refreshed. The refresh 57 | * event is sent when the context has completely finished starting up, meaning all beans have been created and 58 | * initialized successfully. 59 | *

60 | * This method has a loosely defined relationship with {@link #getApplicationContext()}. When this method returns 61 | * {@code true}, calling {@link #getApplicationContext()} is guaranteed to return a non-{@code null} context which 62 | * has been completely initialized. When this method returns {@code false}, {@link #getApplicationContext()} may 63 | * return {@code null}, or it may return a non-{@code null} context which is not yet completely initialized. 64 | * 65 | * @return {@code true} if the context has been set and refreshed; otherwise, {@code false} 66 | */ 67 | public static boolean hasApplicationContext() { 68 | return (refreshed && applicationContext != null); 69 | } 70 | 71 | /** 72 | * Retrieves the {@code ApplicationContext} set when Spring created and initialized the holder bean. If the 73 | * holder has not been created (see the class documentation for details on how to wire up the holder), or if 74 | * the holder has not been initialized, this accessor may return {@code null}. 75 | *

76 | * As a general usage pattern, callers should wrap this method in a check for {@link #hasApplicationContext()}. 77 | * That ensures both that the context is set and also that it has fully initialized. Using a context which has 78 | * not been fully initialized can result in unexpected initialization behaviors for some beans. The most common 79 | * example of this behavior is receiving unproxied references to some beans, such as beans which were supposed 80 | * to have transactional semantics applied by AOP. By waiting for the context refresh event, the likelihood of 81 | * encountering such behavior is greatly reduced. 82 | * 83 | * @return the set context, or {@code null} if the holder bean has not been initialized 84 | */ 85 | public static ApplicationContext getApplicationContext() { 86 | return applicationContext; 87 | } 88 | 89 | @Override 90 | public void setApplicationContext(ApplicationContext context) throws BeansException { 91 | applicationContext = context; 92 | } 93 | 94 | /** 95 | * Returns a flag indicating whether the {@code ApplicationContext} has been refreshed. Theoretically, it is 96 | * possible for this method to return {@code true} when {@link #hasApplicationContext()} returns {@code false}, 97 | * but in practice that is very unlikely since the bean for the holder should have been created and initialized 98 | * before the refresh event was raised. 99 | * 100 | * @return {@code true} if the context refresh event has been received; otherwise, {@code false} 101 | */ 102 | public static boolean isRefreshed() { 103 | return refreshed; 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /spring/src/main/java/ch/qos/logback/ext/spring/DelegatingLogbackAppender.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.spring; 17 | 18 | import ch.qos.logback.classic.spi.ILoggingEvent; 19 | import ch.qos.logback.core.Appender; 20 | import ch.qos.logback.core.UnsynchronizedAppenderBase; 21 | import org.springframework.beans.factory.NoSuchBeanDefinitionException; 22 | import org.springframework.context.ApplicationContext; 23 | 24 | import java.util.List; 25 | 26 | /** 27 | * A Logback {@code Appender} implementation which delegates the actual appending to a named bean contained in a Spring 28 | * {@code ApplicationContext}. 29 | *

30 | * This appender is similar in spirit to Spring's {@code DelegatingFilterProxy}, which allows servlet filters to be 31 | * created and wired in the ApplicationContext and then accessed in the filter chain. As with the filter proxy, the 32 | * delegating appender uses its own name to find the target appender in the context. 33 | *

34 | * Because the logging framework is usually started before the Spring context, this appender supports caching for 35 | * {@code ILoggingEvent}s which are received before the {@code ApplicationContext} is available. This caching has 36 | * 3 possible modes: 37 | *

    38 | *
  • off - Events are discarded until the {@code ApplicationContext} is available.
  • 39 | *
  • on - Events are cached with strong references until the {@code ApplicationContext} is available, at 40 | * which time they will be forwarded to the delegate appender. In systems which produce substantial amounts of log 41 | * events while starting up the {@code ApplicationContext} this mode may result in heavy memory usage.
  • 42 | *
  • soft - Events are wrapped in {@code SoftReference}s and cached until the {@code ApplicationContext} 43 | * is available. Memory pressure may cause the garbage collector to collect some or all of the cached events before 44 | * the {@code ApplicationContext} is available, so some or all events may be lost. However, in systems with heavy 45 | * logging, this mode may result in more efficient memory usage.
  • 46 | *
47 | * Caching is {@code on} by default, so strong references will be used for all events. 48 | *

49 | * An example of how to use this appender in {@code logback.xml}: 50 | *

 51 |  * <appender name="appenderBeanName" class="ch.qos.logback.ext.spring.DelegatingLogbackAppender"/>
 52 |  * 
53 | *

54 | * Or, if specifying a different cache mode, e.g.: 55 | *

 56 |  * <appender name="appenderBeanName" class="ch.qos.logback.ext.spring.DelegatingLogbackAppender">
 57 |  *     <cacheMode>soft</cacheMode>
 58 |  * </appender>
 59 |  * 
60 | * Using this appender requires that the {@link ApplicationContextHolder} be included in the {@code ApplicationContext}. 61 | * 62 | * @author Bryan Turner 63 | * @since 0.1 64 | */ 65 | public class DelegatingLogbackAppender extends UnsynchronizedAppenderBase { 66 | 67 | private final Object lock; 68 | 69 | private String beanName; 70 | private ILoggingEventCache cache; 71 | private EventCacheMode cacheMode; 72 | private volatile Appender delegate; 73 | 74 | public DelegatingLogbackAppender() { 75 | cacheMode = EventCacheMode.ON; 76 | lock = new Object(); 77 | } 78 | 79 | public void setCacheMode(String mode) { 80 | cacheMode = Enum.valueOf(EventCacheMode.class, mode.toUpperCase()); 81 | } 82 | 83 | @Override 84 | public void start() { 85 | if (isStarted()) { 86 | return; 87 | } 88 | 89 | if (beanName == null || beanName.trim().isEmpty()) { 90 | if (name == null || name.trim().isEmpty()) { 91 | throw new IllegalStateException("A 'name' or 'beanName' is required for DelegatingLogbackAppender"); 92 | } 93 | beanName = name; 94 | } 95 | cache = cacheMode.createCache(); 96 | 97 | super.start(); 98 | } 99 | 100 | @Override 101 | public void stop() { 102 | super.stop(); 103 | 104 | if (cache != null) { 105 | cache = null; 106 | } 107 | if (delegate != null) { 108 | delegate.stop(); 109 | delegate = null; 110 | } 111 | } 112 | 113 | @Override 114 | protected void append(ILoggingEvent event) { 115 | //Double-check locking here to optimize out the synchronization after the delegate is in place. This also has 116 | //the benefit of dealing with the race condition where 2 threads are trying to log and one gets the lock with 117 | //the other waiting and the lead thread sets the delegate, logs all cached events and then returns, allowing 118 | //the blocked thread to acquire the lock. At that time, the delegate is no longer null and the event is logged 119 | //directly to it, rather than being cached. 120 | if (delegate == null) { 121 | synchronized (lock) { 122 | //Note the isStarted() check here. If multiple threads are logging at the time the ApplicationContext 123 | //becomes available, the first thread to acquire the lock _may_ stop this appender if the context does 124 | //not contain an Appender with the expected name. If that happens, when the lock is released and other 125 | //threads acquire it, isStarted() will return false and those threads should return without trying to 126 | //use either the delegate or the cache--both of which will be null. 127 | if (!isStarted()) { 128 | return; 129 | } 130 | //If we're still started either no thread has attempted to load the delegate yet, or the delegate has 131 | //been loaded successfully. If the latter, the delegate will no longer be null 132 | if (delegate == null) { 133 | if (ApplicationContextHolder.hasApplicationContext()) { 134 | //First, load the delegate Appender from the ApplicationContext. If it cannot be loaded, this 135 | //appender will be stopped and null will be returned. 136 | Appender appender = getDelegate(); 137 | if (appender == null) { 138 | return; 139 | } 140 | 141 | //Once we have the appender, unload the cache to it. 142 | List cachedEvents = cache.get(); 143 | for (ILoggingEvent cachedEvent : cachedEvents) { 144 | appender.doAppend(cachedEvent); 145 | } 146 | 147 | //If we've found our delegate appender, we no longer need the cache. 148 | cache = null; 149 | delegate = appender; 150 | } else { 151 | //Otherwise, if the ApplicationContext is not ready yet, cache this event and wait 152 | cache.put(event); 153 | 154 | return; 155 | } 156 | } 157 | } 158 | } 159 | 160 | //If we make it here, the delegate should always be non-null and safe to append to. 161 | delegate.doAppend(event); 162 | } 163 | 164 | private Appender getDelegate() { 165 | ApplicationContext context = ApplicationContextHolder.getApplicationContext(); 166 | 167 | try { 168 | @SuppressWarnings("unchecked") 169 | Appender appender = context.getBean(beanName, Appender.class); 170 | appender.setContext(getContext()); 171 | if (!appender.isStarted()) { 172 | appender.start(); 173 | } 174 | return appender; 175 | } catch (NoSuchBeanDefinitionException e) { 176 | stop(); 177 | addError("The ApplicationContext does not contain an Appender named [" + beanName + 178 | "]. This delegating appender will now stop processing events.", e); 179 | } 180 | return null; 181 | } 182 | 183 | public String getBeanName() { 184 | return beanName; 185 | } 186 | 187 | public void setBeanName(String beanName) { 188 | this.beanName = beanName; 189 | } 190 | } 191 | 192 | -------------------------------------------------------------------------------- /spring/src/main/java/ch/qos/logback/ext/spring/EventCacheMode.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.spring; 17 | 18 | import ch.qos.logback.classic.spi.ILoggingEvent; 19 | 20 | import java.lang.ref.SoftReference; 21 | import java.util.ArrayList; 22 | import java.util.Collections; 23 | import java.util.List; 24 | 25 | /** 26 | * @author Bryan Turner 27 | * @since 0.1 28 | */ 29 | public enum EventCacheMode { 30 | 31 | OFF { 32 | @Override 33 | public ILoggingEventCache createCache() { 34 | return new ILoggingEventCache() { 35 | 36 | @Override 37 | public List get() { 38 | return Collections.emptyList(); 39 | } 40 | 41 | @Override 42 | public void put(ILoggingEvent event) { 43 | //When caching is off, events are discarded as they are received 44 | } 45 | }; 46 | } 47 | }, 48 | ON { 49 | @Override 50 | public ILoggingEventCache createCache() { 51 | return new ILoggingEventCache() { 52 | 53 | private List events = new ArrayList(); 54 | 55 | @Override 56 | public List get() { 57 | List list = Collections.unmodifiableList(events); 58 | events = null; 59 | return list; 60 | } 61 | 62 | @Override 63 | public void put(ILoggingEvent event) { 64 | events.add(event); 65 | } 66 | }; 67 | } 68 | }, 69 | SOFT { 70 | @Override 71 | public ILoggingEventCache createCache() { 72 | return new ILoggingEventCache() { 73 | 74 | private List> references = new ArrayList>(); 75 | 76 | @Override 77 | public List get() { 78 | List events = new ArrayList(references.size()); 79 | for (SoftReference reference : references) { 80 | ILoggingEvent event = reference.get(); 81 | if (event != null) { 82 | events.add(event); 83 | } 84 | } 85 | references = null; 86 | return Collections.unmodifiableList(events); 87 | } 88 | 89 | @Override 90 | public void put(ILoggingEvent event) { 91 | references.add(new SoftReference(event)); 92 | } 93 | }; 94 | } 95 | }; 96 | 97 | public abstract ILoggingEventCache createCache(); 98 | } 99 | -------------------------------------------------------------------------------- /spring/src/main/java/ch/qos/logback/ext/spring/ILoggingEventCache.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.spring; 17 | 18 | import ch.qos.logback.classic.spi.ILoggingEvent; 19 | 20 | import java.util.List; 21 | 22 | /** 23 | * Abstraction interface for defining a cache for Logback {@code ILoggingEvent} instances. 24 | * 25 | * @author Bryan Turner 26 | * @since 0.1 27 | */ 28 | public interface ILoggingEventCache { 29 | 30 | /** 31 | * Retrieves a list containing 0 or more cached {@code ILoggingEvent}s. 32 | *

33 | * Note: Implementations of this method must return a non-{@code null} list, even if the list is empty, and the 34 | * returned list must not contain any {@code null} elements. If the caching implementation has discarded any of 35 | * the events that were passed to {@link #put(ILoggingEvent)}, they should be completely omitted from the event 36 | * list returned. 37 | * 38 | * @return a non-{@code null} list containing 0 or more cached events 39 | */ 40 | List get(); 41 | 42 | /** 43 | * Stores the provided event in the cache. 44 | *

45 | * Note: Implementations are free to "store" the event in a destructive or potentially-destructive way. This means 46 | * the "cache" may actually just discard any events it receives, or it may wrap them in a {@code SoftReference} or 47 | * other {@code java.lang.ref} type which could potentially result in the event being garbage collected before the 48 | * {@link #get()} method is called. 49 | * 50 | * @param event the event to cache 51 | */ 52 | void put(ILoggingEvent event); 53 | } 54 | -------------------------------------------------------------------------------- /spring/src/main/java/ch/qos/logback/ext/spring/LogbackConfigurer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.spring; 17 | 18 | import java.io.File; 19 | import java.io.FileNotFoundException; 20 | import java.net.URL; 21 | 22 | import org.slf4j.impl.StaticLoggerBinder; 23 | import org.springframework.util.ResourceUtils; 24 | import org.springframework.util.SystemPropertyUtils; 25 | 26 | import ch.qos.logback.classic.LoggerContext; 27 | import ch.qos.logback.classic.selector.ContextSelector; 28 | import ch.qos.logback.classic.util.ContextInitializer; 29 | import ch.qos.logback.classic.util.ContextSelectorStaticBinder; 30 | import ch.qos.logback.core.joran.spi.JoranException; 31 | 32 | /** 33 | * Convenience class that features simple methods for custom Log4J configuration. 34 | *

35 | * Only needed for non-default Logback initialization with a custom 36 | * config location. By default, Logback will simply read its 37 | * configuration from a "logback.xml" or "logback_test.xml" file in the root of the classpath. 38 | *

39 | * For web environments, the analogous LogbackWebConfigurer class can be found 40 | * in the web package, reading in its configuration from context-params in web.xml. 41 | * In a JEE web application, Logback is usually set up via LogbackConfigListener or 42 | * LogbackConfigServlet, delegating to LogbackWebConfigurer underneath. 43 | * 44 | * @author Juergen Hoeller 45 | * @author Bryan Turner 46 | * @author Les Hazlewood 47 | * @author Knute Axelson 48 | * @see ch.qos.logback.ext.spring.web.WebLogbackConfigurer WebLogbackConfigurer 49 | * @see ch.qos.logback.ext.spring.web.LogbackConfigListener LogbackConfigListener 50 | * @see ch.qos.logback.ext.spring.web.LogbackConfigServlet LogbackConfigServlet 51 | * @since 0.1 52 | */ 53 | public class LogbackConfigurer { 54 | 55 | private LogbackConfigurer() { 56 | } 57 | 58 | /** 59 | * Initialize logback from the given file. 60 | * 61 | * @param location the location of the config file: either a "classpath:" location 62 | * (e.g. "classpath:logback.xml"), an absolute file URL 63 | * (e.g. "file:C:/logback.xml), or a plain absolute path in the file system 64 | * (e.g. "C:/logback.xml") 65 | * @throws java.io.FileNotFoundException if the location specifies an invalid file path 66 | * @throws ch.qos.logback.core.joran.spi.JoranException 67 | * Thrown 68 | */ 69 | public static void initLogging(String location) throws FileNotFoundException, JoranException { 70 | String resolvedLocation = SystemPropertyUtils.resolvePlaceholders(location); 71 | URL url = ResourceUtils.getURL(resolvedLocation); 72 | LoggerContext loggerContext = (LoggerContext)StaticLoggerBinder.getSingleton().getLoggerFactory(); 73 | 74 | // in the current version logback automatically configures at startup the context, so we have to reset it 75 | loggerContext.reset(); 76 | 77 | // reinitialize the logger context. calling this method allows configuration through groovy or xml 78 | new ContextInitializer(loggerContext).configureByResource(url); 79 | } 80 | 81 | /** 82 | * Set the specified system property to the current working directory. 83 | *

84 | * This can be used e.g. for test environments, for applications that leverage 85 | * LogbackWebConfigurer's "webAppRootKey" support in a web environment. 86 | * 87 | * @param key system property key to use, as expected in Logback configuration 88 | * (for example: "demo.root", used as "${demo.root}/WEB-INF/demo.log") 89 | * @see ch.qos.logback.ext.spring.web.WebLogbackConfigurer WebLogbackConfigurer 90 | */ 91 | public static void setWorkingDirSystemProperty(String key) { 92 | System.setProperty(key, new File("").getAbsolutePath()); 93 | } 94 | 95 | /** 96 | * Shut down Logback. 97 | *

98 | * This isn't strictly necessary, but recommended for shutting down 99 | * logback in a scenario where the host VM stays alive (for example, when 100 | * shutting down an application in a J2EE environment). 101 | */ 102 | public static void shutdownLogging() { 103 | ContextSelector selector = ContextSelectorStaticBinder.getSingleton().getContextSelector(); 104 | LoggerContext loggerContext = selector.getLoggerContext(); 105 | String loggerContextName = loggerContext.getName(); 106 | LoggerContext context = selector.detachLoggerContext(loggerContextName); 107 | context.reset(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /spring/src/main/java/ch/qos/logback/ext/spring/web/LogbackConfigListener.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.spring.web; 17 | 18 | import javax.servlet.ServletContextEvent; 19 | import javax.servlet.ServletContextListener; 20 | 21 | /** 22 | * Bootstrap listener for custom Logback initialization in a web environment. 23 | * Delegates to WebLogbackConfigurer (see its javadoc for configuration details). 24 | *

25 | * WARNING: Assumes an expanded WAR file, both for loading the configuration 26 | * file and for writing the log files. If you want to keep your WAR unexpanded or 27 | * don't need application-specific log files within the WAR directory, don't use 28 | * Logback setup within the application (thus, don't use Log4jConfigListener or 29 | * LogbackConfigServlet). Instead, use a global, VM-wide Log4J setup (for example, 30 | * in JBoss) or JDK 1.4's java.util.logging (which is global too). 31 | *

32 | * This listener should be registered before ContextLoaderListener in web.xml, 33 | * when using custom Logback initialization. 34 | *

35 | * For Servlet 2.2 containers and Servlet 2.3 ones that do not initialize listeners before servlets, use 36 | * LogbackConfigServlet. See the ContextLoaderServlet javadoc for details. 37 | * 38 | * @author Juergen Hoeller 39 | * @author Les Hazlewood 40 | * @see WebLogbackConfigurer 41 | * @see LogbackConfigListener 42 | * @see LogbackConfigServlet 43 | * @since 0.1 44 | */ 45 | public class LogbackConfigListener implements ServletContextListener { 46 | 47 | @Override 48 | public void contextDestroyed(ServletContextEvent event) { 49 | WebLogbackConfigurer.shutdownLogging(event.getServletContext()); 50 | } 51 | 52 | @Override 53 | public void contextInitialized(ServletContextEvent event) { 54 | WebLogbackConfigurer.initLogging(event.getServletContext()); 55 | } 56 | } -------------------------------------------------------------------------------- /spring/src/main/java/ch/qos/logback/ext/spring/web/LogbackConfigServlet.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.spring.web; 17 | 18 | import javax.servlet.http.HttpServlet; 19 | import javax.servlet.http.HttpServletRequest; 20 | import javax.servlet.http.HttpServletResponse; 21 | import java.io.IOException; 22 | 23 | /** 24 | * Bootstrap servlet for custom Logback initialization in a web environment. 25 | * Delegates to LogbackWebConfigurer (see its javadoc for configuration details). 26 | *

27 | * WARNING: Assumes an expanded WAR file, both for loading the configuration 28 | * file and for writing the log files. If you want to keep your WAR unexpanded or 29 | * don't need application-specific log files within the WAR directory, don't use 30 | * Logback setup within the application (thus, don't use LogbackConfigListener or 31 | * LogbackConfigServlet). Instead, use a global, VM-wide Logback setup (for example, 32 | * in JBoss) or JDK 1.4's java.util.logging (which is global too). 33 | *

34 | * Note: This servlet should have a lower load-on-startup value 35 | * in web.xml than ContextLoaderServlet, when using custom Logback 36 | * initialization. 37 | *

38 | * Note that this class has been deprecated for containers implementing 39 | * Servlet API 2.4 or higher, in favor of LogbackConfigListener.
40 | * According to Servlet 2.4, listeners must be initialized before load-on-startup 41 | * servlets. Many Servlet 2.3 containers already enforce this behavior 42 | * (see ContextLoaderServlet javadocs for details). If you use such a container, 43 | * this servlet can be replaced with LogbackConfigListener. Else or if working 44 | * with a Servlet 2.2 container, stick with this servlet. 45 | * 46 | * @author Juergen Hoeller 47 | * @author Les Hazlewood 48 | * @see WebLogbackConfigurer 49 | * @see LogbackConfigListener 50 | * @since 0.1 51 | */ 52 | public class LogbackConfigServlet extends HttpServlet { 53 | 54 | @Override 55 | public void init() { 56 | WebLogbackConfigurer.initLogging(getServletContext()); 57 | } 58 | 59 | @Override 60 | public void destroy() { 61 | WebLogbackConfigurer.shutdownLogging(getServletContext()); 62 | } 63 | 64 | @Override 65 | public String getServletInfo() { 66 | return "LogbackConfigServlet for Servlet API 2.2/2.3 " + 67 | "(deprecated in favor of LogbackConfigListener for Servlet API 2.4+)"; 68 | } 69 | 70 | /** 71 | * This should never even be called since no mapping to this servlet should 72 | * ever be created in web.xml. That's why a correctly invoked Servlet 2.3 73 | * listener is much more appropriate for initialization work ;-) 74 | */ 75 | @Override 76 | public void service(HttpServletRequest request, HttpServletResponse response) throws IOException { 77 | getServletContext().log( 78 | "Attempt to call service method on LogbackConfigServlet as [" + 79 | request.getRequestURI() + "] was ignored"); 80 | response.sendError(HttpServletResponse.SC_BAD_REQUEST); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /spring/src/main/java/ch/qos/logback/ext/spring/web/WebLogbackConfigurer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014 The logback-extensions developers (logback-user@qos.ch) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package ch.qos.logback.ext.spring.web; 17 | 18 | import ch.qos.logback.core.joran.spi.JoranException; 19 | import ch.qos.logback.ext.spring.LogbackConfigurer; 20 | 21 | import org.springframework.context.ConfigurableApplicationContext; 22 | import org.springframework.util.ClassUtils; 23 | import org.springframework.util.ReflectionUtils; 24 | import org.springframework.util.ResourceUtils; 25 | import org.springframework.util.StringUtils; 26 | import org.springframework.web.util.ServletContextPropertyUtils; 27 | import org.springframework.web.util.WebUtils; 28 | 29 | import javax.servlet.ServletContext; 30 | import java.io.FileNotFoundException; 31 | import java.lang.reflect.Method; 32 | 33 | /** 34 | * Convenience class that performs custom Logback initialization for web environments, 35 | * allowing for log file paths within the web application. 36 | *

37 | * WARNING: Assumes an expanded WAR file, both for loading the configuration 38 | * file and for writing the log files. If you want to keep your WAR unexpanded or 39 | * don't need application-specific log files within the WAR directory, don't use 40 | * Logback setup within the application (thus, don't use LogbackConfigListener or 41 | * LogbackConfigServlet). Instead, use a global, VM-wide Logback setup (for example, 42 | * in JBoss) or JDK 1.4's java.util.logging (which is global too). 43 | *

44 | * Supports two init parameters at the servlet context level (that is, 45 | * context-param entries in web.xml): 46 | *

    47 | *
  • "logbackConfigLocation":
    48 | * Location of the Logback config file; either a "classpath:" location (e.g. 49 | * "classpath:myLogback.xml"), an absolute file URL (e.g. "file:C:/logback.properties), 50 | * or a plain path relative to the web application root directory (e.g. 51 | * "/WEB-INF/logback.xml"). If not specified, default Logback initialization will 52 | * apply ("logback.xml" or "logback_test.xml" in the class path; see Logback documentation for details). 53 | *
  • "logbackExposeWebAppRoot":
    54 | * Whether the web app root system property should be exposed, allowing for log 55 | * file paths relative to the web application root directory. Default is "true"; 56 | * specify "false" to suppress expose of the web app root system property. See 57 | * below for details on how to use this system property in log file locations. 58 | *
59 | *

60 | * Note: initLogging should be called before any other Spring activity 61 | * (when using Logback), for proper initialization before any Spring logging attempts. 62 | *

63 | * By default, this configurer automatically sets the web app root system property, 64 | * for "${key}" substitutions within log file locations in the Logback config file, 65 | * allowing for log file paths relative to the web application root directory. 66 | * The default system property key is "webapp.root", to be used in a Logback config 67 | * file like as follows: 68 | *

69 | * 70 | * 71 | * 72 | * %-4relative [%thread] %-5level %class - %msg%n 73 | * 74 | * ${webapp.root}/WEB-INF/demo.log 75 | * 76 | * 77 | *

78 | * Alternatively, specify a unique context-param "webAppRootKey" per web application. 79 | * For example, with "webAppRootKey = "demo.root": 80 | *

81 | * 82 | * 83 | * 84 | * %-4relative [%thread] %-5level %class - %msg%n 85 | * 86 | * ${demo.root}/WEB-INF/demo.log 87 | * 88 | * 89 | *

90 | * WARNING: Some containers (like Tomcat) do not keep system properties 91 | * separate per web app. You have to use unique "webAppRootKey" context-params per web 92 | * app then, to avoid clashes. Other containers like Resin do isolate each web app's 93 | * system properties: Here you can use the default key (i.e. no "webAppRootKey" 94 | * context-param at all) without worrying. 95 | * 96 | * @author Juergen Hoeller 97 | * @author Les Hazlewood 98 | * @see org.springframework.util.Log4jConfigurer 99 | * @see org.springframework.web.util.Log4jConfigListener 100 | * @since 0.1 101 | */ 102 | public class WebLogbackConfigurer { 103 | 104 | /** 105 | * Parameter specifying the location of the logback config file 106 | */ 107 | public static final String CONFIG_LOCATION_PARAM = "logbackConfigLocation"; 108 | /** 109 | * Parameter specifying whether to expose the web app root system property 110 | */ 111 | public static final String EXPOSE_WEB_APP_ROOT_PARAM = "logbackExposeWebAppRoot"; 112 | 113 | private WebLogbackConfigurer() { 114 | } 115 | 116 | /** 117 | * Initialize Logback, including setting the web app root system property. 118 | * 119 | * @param servletContext the current ServletContext 120 | * @see org.springframework.web.util.WebUtils#setWebAppRootSystemProperty 121 | */ 122 | public static void initLogging(ServletContext servletContext) { 123 | // Expose the web app root system property. 124 | if (exposeWebAppRoot(servletContext)) { 125 | WebUtils.setWebAppRootSystemProperty(servletContext); 126 | } 127 | 128 | // Only perform custom Logback initialization in case of a config file. 129 | String locationParam = servletContext.getInitParameter(CONFIG_LOCATION_PARAM); 130 | if (locationParam != null) { 131 | // Perform Logback initialization; else rely on Logback's default initialization. 132 | for (String location : StringUtils.tokenizeToStringArray(locationParam, 133 | ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)) { 134 | try { 135 | // Resolve context property placeholders before potentially resolving real path. 136 | location = ServletContextPropertyUtils.resolvePlaceholders(location, servletContext); 137 | // Return a URL (e.g. "classpath:" or "file:") as-is; 138 | // consider a plain file path as relative to the web application root directory. 139 | if (!ResourceUtils.isUrl(location)) { 140 | location = WebUtils.getRealPath(servletContext, location); 141 | } 142 | 143 | // Write log message to server log. 144 | servletContext.log("Initializing Logback from [" + location + "]"); 145 | 146 | // Initialize 147 | LogbackConfigurer.initLogging(location); 148 | break; 149 | } catch (FileNotFoundException ex) { 150 | servletContext.log("No logback configuration file found at [" + location + "]"); 151 | //throw new IllegalArgumentException("Invalid 'logbackConfigLocation' parameter: " + ex.getMessage()); 152 | } catch (JoranException e) { 153 | throw new RuntimeException("Unexpected error while configuring logback", e); 154 | } 155 | } 156 | } 157 | 158 | //If SLF4J's java.util.logging bridge is available in the classpath, install it. This will direct any messages 159 | //from the Java Logging framework into SLF4J. When logging is terminated, the bridge will need to be uninstalled 160 | try { 161 | Class julBridge = ClassUtils.forName("org.slf4j.bridge.SLF4JBridgeHandler", ClassUtils.getDefaultClassLoader()); 162 | 163 | Method removeHandlers = ReflectionUtils.findMethod(julBridge, "removeHandlersForRootLogger"); 164 | if (removeHandlers != null) { 165 | servletContext.log("Removing all previous handlers for JUL to SLF4J bridge"); 166 | ReflectionUtils.invokeMethod(removeHandlers, null); 167 | } 168 | 169 | Method install = ReflectionUtils.findMethod(julBridge, "install"); 170 | if (install != null) { 171 | servletContext.log("Installing JUL to SLF4J bridge"); 172 | ReflectionUtils.invokeMethod(install, null); 173 | } 174 | } catch (ClassNotFoundException ignored) { 175 | //Indicates the java.util.logging bridge is not in the classpath. This is not an indication of a problem. 176 | servletContext.log("JUL to SLF4J bridge is not available on the classpath"); 177 | } 178 | } 179 | 180 | /** 181 | * Shut down Logback, properly releasing all file locks 182 | * and resetting the web app root system property. 183 | * 184 | * @param servletContext the current ServletContext 185 | * @see WebUtils#removeWebAppRootSystemProperty 186 | */ 187 | public static void shutdownLogging(ServletContext servletContext) { 188 | //Uninstall the SLF4J java.util.logging bridge *before* shutting down the Logback framework. 189 | try { 190 | Class julBridge = ClassUtils.forName("org.slf4j.bridge.SLF4JBridgeHandler", ClassUtils.getDefaultClassLoader()); 191 | Method uninstall = ReflectionUtils.findMethod(julBridge, "uninstall"); 192 | if (uninstall != null) { 193 | servletContext.log("Uninstalling JUL to SLF4J bridge"); 194 | ReflectionUtils.invokeMethod(uninstall, null); 195 | } 196 | } catch (ClassNotFoundException ignored) { 197 | //No need to shutdown the java.util.logging bridge. If it's not on the classpath, it wasn't started either. 198 | } 199 | 200 | try { 201 | servletContext.log("Shutting down Logback"); 202 | LogbackConfigurer.shutdownLogging(); 203 | } finally { 204 | // Remove the web app root system property. 205 | if (exposeWebAppRoot(servletContext)) { 206 | WebUtils.removeWebAppRootSystemProperty(servletContext); 207 | } 208 | } 209 | } 210 | 211 | /** 212 | * Return whether to expose the web app root system property, 213 | * checking the corresponding ServletContext init parameter. 214 | * 215 | * @param servletContext the servlet context 216 | * @return {@code true} if the webapp's root should be exposed; otherwise, {@code false} 217 | * @see #EXPOSE_WEB_APP_ROOT_PARAM 218 | */ 219 | @SuppressWarnings({"BooleanMethodNameMustStartWithQuestion"}) 220 | private static boolean exposeWebAppRoot(ServletContext servletContext) { 221 | String exposeWebAppRootParam = servletContext.getInitParameter(EXPOSE_WEB_APP_ROOT_PARAM); 222 | return (exposeWebAppRootParam == null || Boolean.valueOf(exposeWebAppRootParam)); 223 | } 224 | } 225 | --------------------------------------------------------------------------------