├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml ├── maven-settings.xml └── workflows │ ├── benchmark.yml │ └── build.yml ├── .gitignore ├── .java-version ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── CHANGELOG.md ├── COPYING.txt ├── README.md ├── maven-version-rules.xml ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main └── java │ └── com │ └── vlkan │ └── rfos │ ├── ByteCountingOutputStream.java │ ├── Clock.java │ ├── LoggingRotationCallback.java │ ├── Rotatable.java │ ├── RotatingFileOutputStream.java │ ├── RotatingFilePattern.java │ ├── RotatingFilePatternException.java │ ├── RotationCallback.java │ ├── RotationConfig.java │ ├── SystemClock.java │ └── policy │ ├── ByteMatchingRotationPolicy.java │ ├── DailyRotationPolicy.java │ ├── RotationPolicy.java │ ├── SizeBasedRotationPolicy.java │ ├── TimeBasedRotationPolicy.java │ └── WeeklyRotationPolicy.java └── test ├── java └── com │ └── vlkan │ └── rfos │ ├── ByteCountingOutputStreamTest.java │ ├── RotatingFileOutputStreamTest.java │ ├── RotatingFilePatternTest.java │ ├── SchedulerShutdownTestApp.java │ ├── SystemClockTest.java │ ├── UtcHelper.java │ └── policy │ ├── ByteMatchingRotationPolicyTest.java │ ├── DailyRotationPolicyTest.java │ ├── SizeBasedRotationPolicyTest.java │ └── TimeBasedRotationPolicyTest.java ├── perf └── com │ └── vlkan │ └── rfos │ └── RotatingFileOutputStreamBenchmark.java └── resources ├── log4j2-quiet.xml └── log4j2.xml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2024 Volkan Yazıcı 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 permits and 13 | # limitations under the License. 14 | 15 | github: vy 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Submit a bug report 4 | --- 5 | 6 | ## Description 7 | A clear and concise description of what the bug is. 8 | 9 | ## Configuration 10 | 11 | **Version:** ... 12 | 13 | **Operating system:** ... 14 | 15 | **JDK:** ... 16 | 17 | **Rotation configuration:** 18 | ```java 19 | RotationConfig 20 | .builder() 21 | .file("...") 22 | .filePattern("...") 23 | // ... 24 | .build(); 25 | ``` 26 | 27 | ## Logs 28 | ``` 29 | Stacktraces, errors, etc. relevant applications logs. 30 | `LoggingRotationCallback` logs will be really handy. 31 | ``` 32 | 33 | ## Additional context 34 | - Is access to `RotatingFileOutputStream` multi-threaded? 35 | - Which `RotatingFileOutputStream` methods are used? 36 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2024 Volkan Yazıcı 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 permits and 13 | # limitations under the License. 14 | 15 | version: 2 16 | updates: 17 | 18 | - package-ecosystem: maven 19 | directory: "/" 20 | schedule: 21 | interval: daily 22 | time: "04:00" 23 | open-pull-requests-limit: 10 24 | ignore: 25 | # Mockito 5 requires Java 11 26 | - dependency-name: "org.mockito:*" 27 | update-types: ["version-update:semver-major"] 28 | 29 | - package-ecosystem: github-actions 30 | directory: "/" 31 | schedule: 32 | interval: weekly 33 | -------------------------------------------------------------------------------- /.github/maven-settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 20 | 21 | 22 | sonatype-nexus-snapshots 23 | ${env.NEXUS_USER} 24 | ${env.NEXUS_PASS} 25 | 26 | 27 | sonatype-nexus-staging 28 | ${env.NEXUS_USER} 29 | ${env.NEXUS_PASS} 30 | 31 | 32 | gpg.passphrase 33 | ${env.GPG_PKEY_PASS} 34 | 35 | 36 | 37 | 38 | nexus 39 | 40 | 41 | 42 | 43 | ${env.GPG_PKEY_ID} 44 | ${env.GPG_PKEY_PASS} 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2024 Volkan Yazıcı 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 permits and 13 | # limitations under the License. 14 | 15 | name: benchmark 16 | 17 | on: [workflow_dispatch] 18 | 19 | jobs: 20 | 21 | build: 22 | 23 | if: github.repository == 'vy/rotating-fos' 24 | 25 | runs-on: ${{ matrix.os }} 26 | 27 | strategy: 28 | matrix: 29 | os: [macos-latest, ubuntu-latest, windows-latest] 30 | 31 | steps: 32 | 33 | - name: Checkout repository 34 | uses: actions/checkout@v4 35 | 36 | - name: Setup JDK 8 37 | uses: actions/setup-java@v4 38 | with: 39 | distribution: temurin 40 | java-version: 8 41 | java-package: jdk 42 | architecture: x64 43 | cache: maven 44 | 45 | - name: Build with Maven 46 | shell: bash 47 | run: | 48 | ./mvnw \ 49 | --show-version --batch-mode --errors --no-transfer-progress \ 50 | -DskipTests=true \ 51 | package 52 | 53 | - name: Upload built sources 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: built-sources-${{ runner.os }} 57 | path: target 58 | 59 | run: 60 | 61 | needs: build 62 | 63 | runs-on: ${{ matrix.os }} 64 | 65 | strategy: 66 | matrix: 67 | os: [macos-latest, ubuntu-latest, windows-latest] 68 | jdk: [8, 11, 17] 69 | concurrency: [1, 2] 70 | 71 | steps: 72 | 73 | - name: Checkout repository 74 | uses: actions/checkout@v4 75 | 76 | - name: Download built sources 77 | uses: actions/download-artifact@v4 78 | with: 79 | name: built-sources-${{ runner.os }} 80 | path: target 81 | 82 | - name: Setup JDK ${{ matrix.jdk }} 83 | if: matrix.jdk != 8 84 | uses: actions/setup-java@v4 85 | with: 86 | distribution: temurin 87 | java-version: ${{ matrix.jdk }} 88 | java-package: jdk 89 | architecture: x64 90 | cache: maven 91 | 92 | - name: Run benchmark 93 | timeout-minutes: 60 94 | # pwsh (the default shell on Windows) hijacks "exec" keyword 95 | shell: bash 96 | run: | 97 | ./mvnw \ 98 | --show-version --batch-mode --errors --no-transfer-progress \ 99 | -Dexec.classpathScope=test \ 100 | -Dexec.mainClass=com.vlkan.rfos.RotatingFileOutputStreamBenchmark \ 101 | -Drfos.benchmark.quick \ 102 | -Drfos.benchmark.concurrency=${{ matrix.concurrency }} \ 103 | -Drfos.benchmark.jsonOutputFile=results.json \ 104 | exec:java 105 | 106 | - name: Stage benchmark results for commit 107 | shell: bash 108 | run: | 109 | 110 | # Determine the artifact version. 111 | set -x 112 | ./mvnw \ 113 | --batch-mode --quiet \ 114 | -DforceStdout=true \ 115 | -Dexpression=project.version \ 116 | help:evaluate \ 117 | | tee mvnw-project-version.out 118 | echo 119 | 120 | # Determine the results file path. 121 | export REVISION=$(&1 | tee git-push.out 162 | if [ $? -eq 0 ]; then 163 | exit 0 164 | else 165 | set -e 166 | let RETRY+=1 167 | echo "retry #$RETRY" 168 | git pull -r origin gh-pages 169 | fi 170 | done 171 | 172 | index: 173 | 174 | runs-on: ubuntu-latest 175 | needs: run 176 | 177 | steps: 178 | 179 | - name: Checkout repository 180 | uses: actions/checkout@v4 181 | with: 182 | ref: gh-pages 183 | 184 | - name: Setup Python 3 185 | uses: actions/setup-python@v5 186 | with: 187 | python-version: 3.x 188 | 189 | - name: Index benchmark results 190 | timeout-minutes: 1 191 | shell: bash 192 | run: | 193 | 194 | # Configure the git user. 195 | git config user.name github-actions 196 | git config user.email github-actions@github.com 197 | 198 | # Push changes in a loop to allow concurrent repository modifications. 199 | export RETRY=0 200 | while [ 1 ]; do 201 | 202 | # Generate the index file. 203 | python -c '\ 204 | import json, os, re;\ 205 | filepaths=[re.sub("^benchmark/results/", "", os.path.join(root,filename)) \ 206 | for (root, dirs, filenames) in os.walk("benchmark/results") \ 207 | for filename in filenames]; \ 208 | filepaths.remove("index.json"); \ 209 | print(json.dumps(filepaths))' \ 210 | >benchmark/results/index.json 211 | 212 | # Commit the index file. 213 | git add benchmark/results/index.json 214 | git commit benchmark/results/index.json -m "Update benchmark results index." 215 | 216 | # Push the index file. 217 | set +e 218 | git push origin gh-pages 2>&1 | tee git-push.out 219 | if [ $? -eq 0 ]; then 220 | exit 0 221 | else 222 | set -e 223 | let RETRY+=1 224 | echo "retry #$RETRY" 225 | git pull -r origin gh-pages 226 | fi 227 | 228 | done 229 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2024 Volkan Yazıcı 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 permits and 13 | # limitations under the License. 14 | 15 | name: build 16 | 17 | on: 18 | push: 19 | # Avoid workflow run for _merged_ `dependabot` PRs. 20 | # They were (hopefully!) already tested in PR-triggered workflow. 21 | branches-ignore: "dependabot/**" 22 | pull_request: 23 | paths-ignore: 24 | - "**.adoc" 25 | - "**.md" 26 | - "**.txt" 27 | 28 | permissions: 29 | contents: write 30 | pull-requests: write 31 | 32 | jobs: 33 | 34 | build: 35 | 36 | runs-on: ${{ matrix.os }} 37 | 38 | strategy: 39 | matrix: 40 | os: [ macos-latest, ubuntu-latest, windows-latest ] 41 | 42 | steps: 43 | 44 | - name: Checkout repository 45 | uses: actions/checkout@v4 46 | 47 | - name: Setup JDK 8 48 | uses: actions/setup-java@v4 49 | with: 50 | distribution: temurin 51 | java-version: 8 52 | java-package: jdk 53 | architecture: x64 54 | cache: maven 55 | 56 | - name: Build with Maven 57 | shell: bash 58 | run: | 59 | ./mvnw \ 60 | --show-version --batch-mode --errors --no-transfer-progress \ 61 | -DtrimStackTrace=false \ 62 | -DskipTests=true \ 63 | verify 64 | 65 | - name: Test scheduler shutdown 66 | timeout-minutes: 1 67 | # pwsh (the default shell on Windows) hijacks "exec" keyword 68 | shell: bash 69 | run: | 70 | ./mvnw \ 71 | --show-version --batch-mode --errors --no-transfer-progress \ 72 | -Dexec.classpathScope=test \ 73 | -Dexec.mainClass=com.vlkan.rfos.SchedulerShutdownTestApp \ 74 | exec:java 75 | 76 | merge: 77 | 78 | runs-on: ubuntu-latest 79 | needs: build 80 | 81 | steps: 82 | 83 | - name: "[dependabot] Fetch metadata" 84 | id: metadata 85 | if: github.event_name == 'pull_request' && github.actor == 'dependabot[bot]' 86 | uses: dependabot/fetch-metadata@v2.4.0 87 | with: 88 | github-token: "${{ secrets.GITHUB_TOKEN }}" 89 | 90 | - name: "[dependabot] Auto-merge the PR" 91 | if: github.event_name == 'pull_request' && github.actor == 'dependabot[bot]' 92 | run: gh pr merge --auto --squash "$PR_URL" 93 | env: 94 | PR_URL: ${{ github.event.pull_request.html_url }} 95 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 96 | 97 | deploy: 98 | 99 | runs-on: ubuntu-latest 100 | needs: merge 101 | if: github.repository == 'vy/rotating-fos' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/release/')) 102 | 103 | steps: 104 | 105 | - name: Checkout repository 106 | uses: actions/checkout@v4 107 | 108 | - name: Setup JDK 8 109 | uses: actions/setup-java@v4 110 | with: 111 | distribution: temurin 112 | java-version: 8 113 | java-package: jdk 114 | architecture: x64 115 | cache: maven 116 | 117 | - name: Import GPG private key 118 | run: | 119 | echo -n "$GPG_PKEY" \ 120 | | base64 --decode \ 121 | | gpg -v --batch --import --yes --pinentry-mode error 122 | env: 123 | GPG_PKEY: ${{ secrets.GPG_PKEY }} 124 | 125 | - name: Export artifact version 126 | run: | 127 | ./mvnw \ 128 | --quiet --batch-mode -DforceStdout=true \ 129 | -Dexpression=project.version \ 130 | help:evaluate \ 131 | | tee /tmp/mvnw-project-version.out 132 | 133 | - name: Check version (SNAPSHOT) 134 | if: github.ref == 'refs/heads/master' 135 | run: | 136 | export REVISION=$(&2 139 | exit 1 140 | } 141 | 142 | - name: Check version (RELEASE) 143 | if: startsWith(github.ref, 'refs/heads/release/') 144 | run: | 145 | export REVISION=$(&2 148 | exit 1 149 | } 150 | [[ "$REVISION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { 151 | echo "was expecting a release version, found: \"$REVISION\"" 1>&2 152 | exit 1 153 | } 154 | export CHANGELOG_VERSION=$(grep "^### " CHANGELOG.md | head -n 1 | sed -r 's/^### \(.*\) v(.*)$/\1/') 155 | [[ "$REVISION" == "$CHANGELOG_VERSION" ]] || { 156 | echo "version \"$REVISION\" doesn't match the one in the CHANGELOG: \"$CHANGELOG_VERSION\"" 1>&2 157 | exit 1 158 | } 159 | export CURRENT_DATE=$(date +%Y-%m-%d) 160 | export CHANGELOG_DATE=$(grep "^### " CHANGELOG.md | head -n 1 | sed -r 's/^### \((.*)\) v.*$/\1/') 161 | [[ "$CURRENT_DATE" == "$CHANGELOG_DATE" ]] || { 162 | echo "current date \"$CURRENT_DATE\" doesn't match the one in the CHANGELOG: \"$CHANGELOG_DATE\"" 1>&2 163 | exit 1 164 | } 165 | 166 | - name: Deploy 167 | # "package" phase is necessary before "gpg:sign" goal. 168 | run: | 169 | ./mvnw \ 170 | -V -B --no-transfer-progress -e \ 171 | -DskipTests=true \ 172 | -DperformRelease=true \ 173 | -DdeployAtEnd=true \ 174 | --settings .github/maven-settings.xml \ 175 | package gpg:sign install:install deploy:deploy 176 | env: 177 | GPG_PKEY_ID: ${{ secrets.GPG_PKEY_ID }} 178 | GPG_PKEY_PASS: ${{ secrets.GPG_PKEY_PASS }} 179 | NEXUS_USER: ${{ secrets.NEXUS_USER }} 180 | NEXUS_PASS: ${{ secrets.NEXUS_PASS }} 181 | 182 | - name: Tag version (RELEASE) 183 | if: startsWith(github.ref, 'refs/heads/release/') 184 | run: | 185 | export REVISION=$( 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 permits and 13 | # limitations under the License. 14 | 15 | target/ 16 | .idea 17 | *.iml 18 | *.iws 19 | /out 20 | dependency-reduced-pom.xml 21 | .flattened-pom.xml 22 | -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 1.8 2 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 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 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vy/rotating-fos/ae73feb3c4c44186822e2249e72a22fbb61c9f93/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | ### (2024-05-06) v0.10.0 18 | 19 | - Add `ByteMatchingRotationPolicy` (#222) 20 | 21 | ### (2024-03-18) v0.9.7 22 | 23 | - Fix repeated rotations in time-sensitive policies when time resolution is not good enough (#207) 24 | 25 | ### (2023-10-23) v0.9.6 26 | 27 | - Avoid using `FileInputStream`, since it requires a full-GC for clean-up. (#174, #175) 28 | 29 | ### (2022-06-02) v0.9.5 30 | 31 | - Guard against I/O failures while reading file length. (#70) 32 | 33 | - Propagate `IOException`s caught while opening the file. (#86) 34 | 35 | ### (2022-02-04) v0.9.4 36 | 37 | - Fixed broken rolling support when `maxBackupCount > 2`. (#49) 38 | 39 | ### (2021-07-31) v0.9.3 40 | 41 | - Improved Javadocs. 42 | 43 | - Switched from `File#renameTo(File)` to the more robust 44 | `Files.move(Path, Path, CopyOptions...)` alternative. (#14) 45 | 46 | - Add rolling support via `maxBackupCount`. (#14) 47 | 48 | - Stop policies after stream close. (#26) 49 | 50 | ### (2020-01-10) v0.9.2 51 | 52 | - Shutdown the default `ScheduledExecutorService` at JVM exit. (#12) 53 | 54 | ### (2019-12-08) v0.9.1 55 | 56 | - Added explicit Java 9 module name. (#11) 57 | 58 | ### (2019-12-03) v0.9.0 59 | 60 | - Switched to semantic versioning scheme. (#10) 61 | 62 | - Overhauled tests. 63 | 64 | - Improved documentation. 65 | 66 | - Added `onOpen()` (#6) and `onClose()` (#9) methods to `RotationCallback`. 67 | 68 | - Removed timer-based invocation from `SizeBasedRotationPolicy`. 69 | 70 | - Replaced `Timer` and `Thread` usage with a shared `ScheduledExecutorService`. 71 | 72 | - Fixed license discrepancies. (#3) 73 | 74 | ### (2019-11-05) v0.8 75 | 76 | - Add Windows build to CI pipeline. (#4) 77 | 78 | - Switch from Travis CI to GitHub Actions. (#4) 79 | 80 | - Fix stream handling for Windows. (#4) 81 | 82 | ### (2019-06-13) v0.7 83 | 84 | - Upgraded to Java 8. 85 | 86 | - Replaced Joda Time with Java Date/Time API. 87 | 88 | - Added support for `Locale` and `ZoneId` in `RotatingFilePattern`. 89 | 90 | - Added pre-`write()` (that is, write-sensitive) policy execution support. 91 | 92 | - Switched from GPL v3 to Apache License v2.0. 93 | 94 | - Upgrade dependency versions. 95 | -------------------------------------------------------------------------------- /COPYING.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | [![Actions Status](https://github.com/vy/rotating-fos/workflows/build/badge.svg)](https://github.com/vy/rotating-fos/actions) 18 | [![Maven Central](https://img.shields.io/maven-central/v/com.vlkan.rfos/rotating-fos.svg)](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.vlkan.rfos%22) 19 | [![License](https://img.shields.io/github/license/vy/rotating-fos.svg)](https://www.apache.org/licenses/LICENSE-2.0.txt) 20 | 21 | `rotating-fos` is a Java 8 library providing `RotatingFileOutputStream` which 22 | internally rotates a delegate `FileOutputStream` using provided rotation 23 | policies similar to [logrotate](https://github.com/logrotate/logrotate), 24 | [Log4j](https://logging.apache.org/log4j/) and [Logback](https://logback.qos.ch/). 25 | 26 | # Usage 27 | 28 | You first need to include `rotating-fos` in your Maven/Gradle dependencies: 29 | 30 | ```xml 31 | 32 | com.vlkan.rfos 33 | rotating-fos 34 | ${rotating-fos.version} 35 | 36 | ``` 37 | 38 | (Note that the Java 9 module name is `com.vlkan.rfos`.) 39 | 40 | `RotatingFileOutputStream` does not extend `java.io.FileOutputStream` (as a 41 | deliberate design decision, see [How (not) to extend standard collection 42 | classes](https://javachannel.org/posts/how-not-to-extend-standard-collection-classes/) 43 | and [`FileInputStream`/`FileOutputStream` considered 44 | harmful](https://www.cloudbees.com/blog/fileinputstream-fileoutputstream-considered-harmful)), 45 | but `java.io.OutputStream`. Its basic usage is pretty straightforward: 46 | 47 | ```java 48 | RotationConfig config = RotationConfig 49 | .builder() 50 | .file("/tmp/app.log") 51 | .filePattern("/tmp/app-%d{yyyyMMdd-HHmmss.SSS}.log") 52 | .policy(new SizeBasedRotationPolicy(1024 * 1024 * 100 /* 100MiB */)) 53 | .compress(true) 54 | .policy(DailyRotationPolicy.getInstance()) 55 | .build(); 56 | 57 | try (RotatingFileOutputStream stream = new RotatingFileOutputStream(config)) { 58 | stream.write("Hello, world!".getBytes(StandardCharsets.UTF_8)); 59 | } 60 | ``` 61 | 62 | Using `maxBackupCount`, one can also introduce a rolling scheme where rotated 63 | files will be named as `file.0`, `file.1`, `file.2`, ..., `file.N` in the order 64 | from the newest to the oldest, `N` denoting the `maxBackupCount`: 65 | 66 | ```java 67 | RotationConfig config = RotationConfig 68 | .builder() 69 | .file("/tmp/app.log") 70 | .maxBackupCount(10) // Set `filePattern` to `file.%i` and keep 71 | // the most recent 10 files. 72 | .policy(new SizeBasedRotationPolicy(1024 * 1024 * 100 /* 100MiB */)) 73 | .build(); 74 | 75 | try (RotatingFileOutputStream stream = new RotatingFileOutputStream(config)) { 76 | stream.write("Hello, world!".getBytes(StandardCharsets.UTF_8)); 77 | } 78 | ``` 79 | 80 | `RotationConfig.Builder` supports the following methods: 81 | 82 | | Method(s) | Description | 83 | | --------- | ----------- | 84 | | `file(File)`
`file(String)` | file accessed (e.g., `/tmp/app.log`) | 85 | | `filePattern(RotatingFilePattern)`
`filePattern(String)`| The pattern used to generate files for moving after rotation, e.g., `/tmp/app-%d{yyyyMMdd-HHmmss-SSS}.log`. This option cannot be combined with `maxBackupCount`. | 86 | | `policy(RotationPolicy)`
`policies(Set policies)` | rotation policies | 87 | | `maxBackupCount(int)` | If greater than zero, rotated files will be named as `file.0`, `file.1`, `file.2`, ..., `file.N` in the order from the newest to the oldest, where `N` denoting the `maxBackupCount`. `maxBackupCount` defaults to `-1`, that is, no rolling. This option cannot be combined with `filePattern` or `compress`. | 88 | | `executorService(ScheduledExecutorService)` | scheduler for time-based policies and compression tasks | 89 | | `append(boolean)` | append while opening the `file` (defaults to `true`) | 90 | | `compress(boolean)` | Toggles GZIP compression after rotation and defaults to `false`. This option cannot be combined with `maxBackupCount`. | 91 | | `clock(Clock)` | clock for retrieving date and time (defaults to `SystemClock`) | 92 | | `callback(RotationCallback)`
`callbacks(Set)` | rotation callbacks (defaults to `LoggingRotationCallback`) | 93 | 94 | The default `ScheduledExecutorService` can be retrieved via 95 | `RotationConfig#getDefaultExecutorService()`, which is a 96 | `ScheduledThreadPoolExecutor` of size `Runtime.getRuntime().availableProcessors()`. 97 | Note that unless explicitly specified in `RotationConfig.Builder`, all instances 98 | of `RotationConfig` (and hence of `RotatingFileOutputStream`) will share the 99 | same `ScheduledExecutorService`. You can change the default pool size via 100 | `RotationJanitorCount` system property. 101 | 102 | Packaged rotation policies are listed below. (You can also create your own 103 | rotation policies by implementing `RotationPolicy` interface.) 104 | 105 | - Time-sensitive: 106 | - `DailyRotationPolicy` 107 | - `WeeklyRotationPolicy` 108 | - Byte-sensitive: 109 | - `ByteMatchingRotationPolicy` (can be used to, e.g., rotate after every 1000 `\n` (newline) occurrences, etc.) 110 | - `SizeBasedRotationPolicy` 111 | 112 | Once you have a handle on `RotatingFileOutputStream`, in addition to standard 113 | `java.io.OutputStream` methods (e.g., `write()`, `close()`, etc.), it provides 114 | the following methods: 115 | 116 | | Method | Description | 117 | | ------ | ------------| 118 | | `getConfig()` | employed `RotationConfig` | 119 | | `rotate(RotationPolicy, Instant)` | trigger a rotation | 120 | 121 | `RotatingFilePattern.Builder` supports the following methods: 122 | 123 | | Method | Description | 124 | | ------ | ----------- | 125 | | `pattern(String)` | rotating file pattern (e.g., `/tmp/app-%d{yyyyMMdd-HHmmss-SSS}.log`) | 126 | | `locale(Locale)` | `Locale` used in the `DateTimeFormatter` (defaults to `Locale.getDefault()`) | 127 | | `timeZoneId(ZoneId)` | `ZoneId` denoting the time zone used in the `DateTimeFormatter` (defaults to `TimeZone.getDefault().toZoneId()`) | 128 | 129 | Rotation-triggered custom behaviours can be introduced via `RotationCallback` 130 | passed to `RotationConfig.Builder`. `RotationCallback` provides the following 131 | methods. 132 | 133 | | Method | Description | 134 | | ------ | ----------- | 135 | | `onTrigger(RotationPolicy, Instant)` | invoked at the beginning of every rotation attempt | 136 | | `onOpen(RotationPolicy, Instant, OutputStream)` | invoked at start and during rotation | 137 | | `onClose(RotationPolicy, Instant, OutputStream)` | invoked on stream close and during rotation | 138 | | `onSuccess(RotationPolicy, Instant, File)` | invoked after a successful rotation | 139 | | `onFailure(RotationPolicy, Instant, File, Exception)` | invoked after a failed rotation attempt | 140 | 141 | # Caveats 142 | 143 | - **`append` is enabled for `RotatingFileOutputStream` by default**, whereas 144 | it is disabled (and hence truncates the file at start) for standard 145 | `FileOutputStream` by default. 146 | 147 | - **Rotated file conflicts are not resolved by `rotating-fos`.** Once a 148 | rotation policy gets triggered, `rotating-fos` applies the given 149 | `filePattern` to determine the rotated file name. In order to avoid 150 | previously generated files to be overridden, prefer a sufficiently 151 | fine-grained date-time pattern. 152 | 153 | For instance, given `filePattern` is `/tmp/app-%d{yyyyMMdd}.log`, if 154 | `SizeBasedRotationPolicy` gets triggered multiple times within a day, the last 155 | one will override the earlier generations in the same day. In order to avoid 156 | this, you should use a date-time pattern with a higher resolution, such as 157 | `/tmp/app-%d{yyyyMMdd-HHmmss-SSS}.log`. 158 | 159 | - **Make sure `RotationCallback` methods are not blocking.** Callbacks are 160 | invoked using the `ScheduledExecutorService` passed via `RotationConfig`. 161 | Hence blocking callback methods have a direct impact on time-sensitive 162 | policies and compression tasks. 163 | 164 | - **When `append` is enabled, be cautious while using `onOpen` and `onClose` 165 | callbacks.** These callbacks might be employed to introduce headers and/or 166 | footers to certain type of files, e.g., [CSV](https://en.wikipedia.org/wiki/Comma-separated_values). 167 | Though one needs to avoid injecting the same header and/or footer multiple 168 | times when a file is re-opened for append. Note that this is not a problem 169 | for files opened/closed via rotation. 170 | 171 | - **Byte-sensitive rotation policies can exceed given thresholds.** 172 | They intercept `write(int[])`, `write(byte[])` calls of the output stream 173 | and trigger when a certain condition holds. If you, say, use 174 | `SizeBasedRotationPolicy` with `maxByteCount` configured to `3`, 175 | and invoke `write(new byte[10])`, the rotation will be triggered once, 176 | not more! Likewise, if you use `ByteMatchingRotationPolicy('.', 2)` and 177 | invoke `write("1.2.3.4.5".getBytes())`, rotation will be triggered once. 178 | 179 | # Security policy 180 | 181 | If you have encountered an unlisted security vulnerability or other unexpected behaviour that has security impact, please report them privately to the [volkan@yazi.ci](mailto:volkan@yazi.ci) email address. 182 | 183 | # Contributors 184 | 185 | - [Alen (alturkovic) Turkovic](https://github.com/alturkovic) (reviewing `ByteMatchingRotationPolicy` #222) 186 | - [Christoph (pitschr) Pitschmann](https://github.com/pitschr) (Windows-specific 187 | fixes, `RotationCallback#onOpen()` method, Java 9 module name, scheduler 188 | shutdown at exit) 189 | - [broafka-ottokar](https://github.com/broafka-ottokar) (repeated rotation due to insufficient time resolution #207) 190 | - [David (kc7bfi) Robison](https://github.com/kc7bfi) (NPE due to write after close in #26) 191 | - [Jonas (yawkat) Konrad](https://yawk.at/) (`RotatingFileOutputStream` 192 | thread-safety improvements) 193 | - [Lukas Bradley](https://github.com/lukasbradley/) 194 | - [Liran Mendelovich](https://github.com/liran2000/) (rolling via `maxBackupCount`) 195 | - [Nicolas Clemeur](https://github.com/nclemeur) (avoiding `FileInputStream`) 196 | 197 | # License 198 | 199 | Copyright © 2018-2024 [Volkan Yazıcı](https://volkan.yazi.ci) 200 | 201 | Licensed under the Apache License, Version 2.0 (the "License"); 202 | you may not use this file except in compliance with the License. 203 | You may obtain a copy of the License at 204 | 205 | https://www.apache.org/licenses/LICENSE-2.0 206 | 207 | Unless required by applicable law or agreed to in writing, software 208 | distributed under the License is distributed on an "AS IS" BASIS, 209 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 210 | See the License for the specific language governing permissions and 211 | limitations under the License. 212 | -------------------------------------------------------------------------------- /maven-version-rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | .*-(alpha|beta)[0-9\.-]* 27 | 28 | 29 | 30 | 31 | 32 | 33 | .*atlassian.* 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 20 | 21 | 4.0.0 22 | 23 | com.vlkan.rfos 24 | rotating-fos 25 | ${revision} 26 | 27 | RotatingFileOutputStream 28 | Sneakily rotating your FileOutputStream since 2017. 29 | https://github.com/vy/rotating-fos 30 | 2017 31 | 32 | 33 | 34 | Apache License, Version 2.0 35 | https://www.apache.org/licenses/LICENSE-2.0 36 | repo 37 | 38 | 39 | 40 | 41 | 42 | vy 43 | Volkan Yazıcı 44 | volkan@yazi.ci 45 | Volkan Yazıcı's Soap Co. 46 | https://volkan.yazi.ci 47 | 48 | Project-Administrator 49 | Developer 50 | 51 | Europe/Amsterdam 52 | 53 | 54 | 55 | 56 | https://github.com/vy/rotating-fos 57 | scm:git:git@github.com:vy/rotating-fos.git 58 | scm:git:git@github.com:vy/rotating-fos.git 59 | HEAD 60 | 61 | 62 | 63 | 64 | sonatype-nexus-snapshots 65 | Sonatype Nexus snapshot repository 66 | https://oss.sonatype.org/content/repositories/snapshots 67 | 68 | 69 | sonatype-nexus-staging 70 | Sonatype Nexus release repository 71 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 72 | 73 | 74 | 75 | 76 | 77 | 78 | 0.11.0-SNAPSHOT 79 | UTF-8 80 | UTF-8 81 | UTF-8 82 | 1.8 83 | 84 | 85 | 3.27.3 86 | 2.38.0 87 | 1.37 88 | 5.13.0 89 | 2.24.3 90 | 4.11.0 91 | 2.0.17 92 | 93 | 94 | 3.6.1 95 | 1.7.0 96 | 3.14.0 97 | 3.5.0 98 | 3.2.7 99 | 3.4.2 100 | 3.11.2 101 | 3.0.0-M6 102 | 3.3.1 103 | 3.5.3 104 | 2.18.0 105 | 106 | 107 | 108 | 109 | 110 | 111 | org.slf4j 112 | slf4j-api 113 | ${slf4j.version} 114 | 115 | 116 | 117 | org.apache.logging.log4j 118 | log4j-slf4j2-impl 119 | ${log4j.version} 120 | test 121 | 122 | 123 | 124 | org.junit.jupiter 125 | junit-jupiter-engine 126 | ${junit.version} 127 | test 128 | 129 | 130 | 131 | org.assertj 132 | assertj-core 133 | ${assertj-core.version} 134 | test 135 | 136 | 137 | 138 | org.mockito 139 | mockito-core 140 | ${mockito.version} 141 | test 142 | 143 | 144 | 145 | org.openjdk.jmh 146 | jmh-core 147 | ${jmh.version} 148 | test 149 | 150 | 151 | 152 | org.openjdk.jmh 153 | jmh-generator-annprocess 154 | ${jmh.version} 155 | test 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | org.apache.maven.plugins 166 | maven-compiler-plugin 167 | ${maven-compiler-plugin.version} 168 | 169 | ${java.version} 170 | ${java.version} 171 | ${project.build.sourceEncoding} 172 | 173 | true 174 | 175 | -Xlint:all 176 | 177 | 178 | 179 | com.google.errorprone 180 | error_prone_core 181 | ${errorprone.version} 182 | 183 | 184 | org.openjdk.jmh 185 | jmh-generator-annprocess 186 | ${jmh.version} 187 | 188 | 189 | 190 | 191 | 192 | 193 | org.apache.maven.plugins 194 | maven-enforcer-plugin 195 | ${maven-enforcer-plugin.version} 196 | 197 | 198 | enforce-versions 199 | 200 | enforce 201 | 202 | 203 | 204 | 205 | [3.6.0,4) 206 | 207 | 208 | [1.8.0,9) 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | org.codehaus.mojo 218 | versions-maven-plugin 219 | ${versions-maven-plugin.version} 220 | 221 | ${project.baseUri}/maven-version-rules.xml 222 | 223 | 224 | 225 | 226 | org.apache.maven.plugins 227 | maven-surefire-plugin 228 | ${maven-surefire-plugin.version} 229 | 230 | 231 | 232 | org.apache.maven.plugins 233 | maven-jar-plugin 234 | ${maven-jar-plugin.version} 235 | 236 | 237 | 238 | com.vlkan.rfos 239 | 240 | 241 | 242 | 243 | 244 | 245 | org.codehaus.mojo 246 | flatten-maven-plugin 247 | ${flatten-maven-plugin.version} 248 | 249 | true 250 | resolveCiFriendliesOnly 251 | 252 | 253 | 254 | flatten 255 | process-resources 256 | 257 | flatten 258 | 259 | 260 | 261 | flatten.clean 262 | clean 263 | 264 | clean 265 | 266 | 267 | 268 | 269 | 270 | 271 | org.codehaus.mojo 272 | build-helper-maven-plugin 273 | ${build-helper-maven-plugin.version} 274 | 275 | 276 | add-perf-source 277 | generate-test-sources 278 | 279 | add-test-source 280 | 281 | 282 | 283 | src/test/perf 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | release-artifacts 297 | 298 | 299 | performRelease 300 | true 301 | 302 | 303 | 304 | 305 | 306 | 307 | org.apache.maven.plugins 308 | maven-gpg-plugin 309 | ${maven-gpg-plugin.version} 310 | 311 | false 312 | 313 | 314 | 315 | sign-artifacts 316 | verify 317 | 318 | sign 319 | 320 | 321 | 322 | 323 | 324 | 325 | org.apache.maven.plugins 326 | maven-source-plugin 327 | ${maven-source-plugin.version} 328 | 329 | 330 | attach-sources 331 | 332 | jar-no-fork 333 | 334 | 335 | 336 | 337 | 338 | 339 | org.apache.maven.plugins 340 | maven-javadoc-plugin 341 | ${maven-javadoc-plugin.version} 342 | 343 | 344 | attach-javadocs 345 | 346 | jar 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/ByteCountingOutputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import java.io.IOException; 20 | import java.io.OutputStream; 21 | 22 | class ByteCountingOutputStream extends OutputStream { 23 | 24 | private final OutputStream parent; 25 | 26 | private long size; 27 | 28 | ByteCountingOutputStream(OutputStream parent, long size) { 29 | this.parent = parent; 30 | this.size = size; 31 | } 32 | 33 | long size() { 34 | return size; 35 | } 36 | 37 | @Override 38 | public void write(int b) throws IOException { 39 | parent.write(b); 40 | size += 1; 41 | } 42 | 43 | @Override 44 | public void write(byte[] b) throws IOException { 45 | parent.write(b); 46 | size += b.length; 47 | } 48 | 49 | @Override 50 | public void write(byte[] b, int off, int len) throws IOException { 51 | parent.write(b, off, len); 52 | size += len; 53 | } 54 | 55 | @Override 56 | public void flush() throws IOException { 57 | parent.flush(); 58 | } 59 | 60 | @Override 61 | public void close() throws IOException { 62 | parent.close(); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/Clock.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import java.time.Instant; 20 | 21 | /** 22 | * The clock used by {@link RotatingFileOutputStream}. 23 | */ 24 | public interface Clock { 25 | 26 | /** 27 | * Gets the current instant. 28 | * 29 | * @return the current instant 30 | */ 31 | Instant now(); 32 | 33 | /** 34 | * Gets the instant of the upcoming midnight. 35 | * 36 | * @return the instant of the upcoming midnight 37 | */ 38 | Instant midnight(); 39 | 40 | /** 41 | * Gets the instant of the upcoming Sunday midnight. 42 | * 43 | * @return the instant of the upcoming Sunday midnight. 44 | */ 45 | Instant sundayMidnight(); 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/LoggingRotationCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import com.vlkan.rfos.policy.RotationPolicy; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import java.io.File; 24 | import java.io.OutputStream; 25 | import java.time.Instant; 26 | 27 | /** 28 | * Callback logging every intercepted operation. 29 | */ 30 | public class LoggingRotationCallback implements RotationCallback { 31 | 32 | private static final LoggingRotationCallback INSTANCE = new LoggingRotationCallback(); 33 | 34 | private static final Logger LOGGER = LoggerFactory.getLogger(LoggingRotationCallback.class); 35 | 36 | private LoggingRotationCallback() { 37 | // Do nothing. 38 | } 39 | 40 | public static LoggingRotationCallback getInstance() { 41 | return INSTANCE; 42 | } 43 | 44 | @Override 45 | public void onTrigger(RotationPolicy policy, Instant instant) { 46 | LOGGER.debug("rotation trigger {policy={}, instant={}}", policy, instant); 47 | } 48 | 49 | @Override 50 | public void onOpen(RotationPolicy policy, Instant instant, OutputStream ignored) { 51 | LOGGER.debug("file open {policy={}, instant={}}", policy, instant); 52 | } 53 | 54 | @Override 55 | public void onClose(RotationPolicy policy, Instant instant, OutputStream stream) { 56 | LOGGER.debug("file close {policy={}, instant={}}", policy, instant); 57 | } 58 | 59 | @Override 60 | public void onSuccess(RotationPolicy policy, Instant instant, File file) { 61 | LOGGER.debug("rotation success {policy={}, instant={}, file={}}", policy, instant, file); 62 | } 63 | 64 | @Override 65 | public void onFailure(RotationPolicy policy, Instant instant, File file, Exception error) { 66 | String message = String.format("rotation failure {policy=%s, instant=%s, file=%s}", policy, instant, file); 67 | LOGGER.error(message, error); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/Rotatable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import com.vlkan.rfos.policy.RotationPolicy; 20 | 21 | import java.time.Instant; 22 | 23 | /** 24 | * Interface for representing a rotatable object to policies. 25 | * 26 | * @see RotatingFileOutputStream 27 | * @see RotationPolicy 28 | */ 29 | public interface Rotatable { 30 | 31 | /** 32 | * Triggers a rotation originating from the given policy at the given instant. 33 | * 34 | * @param policy the triggering policy, can be {@code null} 35 | * @param instant the trigger instant 36 | */ 37 | void rotate(RotationPolicy policy, Instant instant); 38 | 39 | /** 40 | * Gets the configuration employed. 41 | * 42 | * @return the configuration employed 43 | */ 44 | RotationConfig getConfig(); 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/RotatingFileOutputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import com.vlkan.rfos.policy.DailyRotationPolicy; 20 | import com.vlkan.rfos.policy.RotationPolicy; 21 | import com.vlkan.rfos.policy.SizeBasedRotationPolicy; 22 | import com.vlkan.rfos.policy.WeeklyRotationPolicy; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | import java.io.File; 27 | import java.io.FileOutputStream; 28 | import java.io.IOException; 29 | import java.io.InputStream; 30 | import java.io.OutputStream; 31 | import java.nio.file.Files; 32 | import java.nio.file.OpenOption; 33 | import java.nio.file.Path; 34 | import java.nio.file.Paths; 35 | import java.nio.file.StandardCopyOption; 36 | import java.nio.file.StandardOpenOption; 37 | import java.time.Instant; 38 | import java.util.ArrayList; 39 | import java.util.List; 40 | import java.util.Objects; 41 | import java.util.Set; 42 | import java.util.concurrent.locks.LockSupport; 43 | import java.util.function.Consumer; 44 | import java.util.zip.GZIPOutputStream; 45 | 46 | /** 47 | * A thread-safe {@link OutputStream} targeting a file where rotation of the 48 | * active stream is supported. 49 | *

50 | * Rotation can be triggered by either manually using 51 | * {@link #rotate(RotationPolicy, Instant)} method or indirectly using the 52 | * registered {@link RotationPolicy} set. 53 | *

54 | * Interception of state changes are supported by the registered 55 | * {@link RotationCallback} set. 56 | *

57 | * 58 | * @see LoggingRotationCallback 59 | * @see DailyRotationPolicy 60 | * @see WeeklyRotationPolicy 61 | * @see SizeBasedRotationPolicy 62 | */ 63 | public class RotatingFileOutputStream extends OutputStream implements Rotatable { 64 | 65 | private static final Logger LOGGER = LoggerFactory.getLogger(RotatingFileOutputStream.class); 66 | 67 | private final RotationConfig config; 68 | 69 | private final List callbacks; 70 | 71 | private final List writeSensitivePolicies; 72 | 73 | private volatile ByteCountingOutputStream stream; 74 | 75 | /** 76 | * Constructs an instance using the given configuration 77 | * 78 | * @param config a configuration instance 79 | */ 80 | public RotatingFileOutputStream(RotationConfig config) { 81 | this.config = Objects.requireNonNull(config, "config"); 82 | this.callbacks = new ArrayList<>(config.getCallbacks()); 83 | this.writeSensitivePolicies = collectWriteSensitivePolicies(config.getPolicies()); 84 | this.stream = open(null, config.getClock().now()); 85 | startPolicies(); 86 | } 87 | 88 | private static List collectWriteSensitivePolicies(Set policies) { 89 | List writeSensitivePolicies = new ArrayList<>(); 90 | for (RotationPolicy policy : policies) { 91 | if (policy.isWriteSensitive()) { 92 | writeSensitivePolicies.add(policy); 93 | } 94 | } 95 | return writeSensitivePolicies; 96 | } 97 | 98 | private void startPolicies() { 99 | for (RotationPolicy policy : config.getPolicies()) { 100 | policy.start(this); 101 | } 102 | } 103 | 104 | private ByteCountingOutputStream open(RotationPolicy policy, Instant instant) { 105 | try { 106 | OpenOption[] openOptions = { 107 | StandardOpenOption.CREATE, 108 | this.config.isAppend() ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING 109 | }; 110 | OutputStream outputStream = Files.newOutputStream(this.config.getFile().toPath(), openOptions); 111 | invokeCallbacks(callback -> callback.onOpen(policy, instant, outputStream)); 112 | long size = config.isAppend() ? readFileLength() : 0; 113 | return new ByteCountingOutputStream(outputStream, size); 114 | } catch (IOException error) { 115 | String message = String.format("file open failure {file=%s}", config.getFile()); 116 | throw new RuntimeException(message, error); 117 | } 118 | } 119 | 120 | @Override 121 | public void rotate(RotationPolicy policy, Instant instant) { 122 | try { 123 | unsafeRotate(policy, instant); 124 | } catch (Exception error) { 125 | String message = String.format("rotation failure {instant=%s}", instant); 126 | RuntimeException extendedError = new RuntimeException(message, error); 127 | invokeCallbacks(callback -> callback.onFailure(policy, instant, null, extendedError)); 128 | } 129 | } 130 | 131 | private synchronized void unsafeRotate(RotationPolicy policy, Instant instant) throws Exception { 132 | 133 | // Check arguments. 134 | Objects.requireNonNull(instant, "instant"); 135 | 136 | // Check the state. 137 | unsafeCheckStream(); 138 | 139 | // Notify the trigger listeners. 140 | invokeCallbacks(callback -> callback.onTrigger(policy, instant)); 141 | 142 | // Skip rotation if the file is empty. 143 | stream.flush(); 144 | if (readFileLength() == 0) { 145 | LOGGER.debug("empty file, skipping rotation {file={}}", config.getFile()); 146 | return; 147 | } 148 | 149 | // Close the file. (Required before rename on Windows!) 150 | invokeCallbacks(callback -> callback.onClose(policy, instant, stream)); 151 | stream.close(); 152 | 153 | // Backup file, if enabled. 154 | File rotatedFile; 155 | if (config.getMaxBackupCount() > 0) { 156 | renameBackups(); 157 | rotatedFile = backupFile(); 158 | } 159 | 160 | // Otherwise, rename using the provided file pattern. 161 | else { 162 | rotatedFile = config.getFilePattern().create(instant).getAbsoluteFile(); 163 | LOGGER.debug("renaming {file={}, rotatedFile={}}", config.getFile(), rotatedFile); 164 | renameFile(config.getFile(), rotatedFile); 165 | } 166 | 167 | // Re-open the file. 168 | LOGGER.debug("re-opening file {file={}}", config.getFile()); 169 | stream = open(policy, instant); 170 | 171 | // Compress the old file, if necessary. 172 | if (config.isCompress()) { 173 | asyncCompress(policy, instant, rotatedFile); 174 | return; 175 | } 176 | 177 | // So far, so good; 178 | invokeCallbacks(callback -> callback.onSuccess(policy, instant, rotatedFile)); 179 | 180 | } 181 | 182 | private long readFileLength() throws IOException { 183 | File file = config.getFile(); 184 | // Reading the file length is a tricky business. 185 | // We will retry some. 186 | Exception lastError = null; 187 | for (int trialIndex = 0; trialIndex < 5; trialIndex++) { 188 | long fileLength = file.length(); 189 | if (fileLength != 0) { 190 | return fileLength; 191 | } 192 | // `File#length()` can return 0 due to I/O failures. 193 | // We are falling back to NIO for a second attempt. 194 | else { 195 | Path path = file.toPath(); 196 | try { 197 | return Files.size(path); 198 | } catch (IOException error) { 199 | lastError = error; 200 | } 201 | } 202 | // Scientifically proven retry practice: wait a bit. 203 | LockSupport.parkNanos(1); 204 | } 205 | String message = String.format("file length read failure {file=%s}", file); 206 | throw new IOException(message, lastError); 207 | } 208 | 209 | private void renameBackups() throws IOException { 210 | File dstFile = getBackupFile(config.getMaxBackupCount() - 1); 211 | for (int backupIndex = config.getMaxBackupCount() - 2; backupIndex >= 0; backupIndex--) { 212 | File srcFile = getBackupFile(backupIndex); 213 | if (srcFile.exists()) { 214 | LOGGER.debug("renaming backup {srcFile={}, dstFile={}}", srcFile, dstFile); 215 | renameFile(srcFile, dstFile); 216 | } 217 | dstFile = srcFile; 218 | } 219 | } 220 | 221 | private File backupFile() throws IOException { 222 | File dstFile = getBackupFile(0); 223 | File srcFile = config.getFile(); 224 | LOGGER.debug("renaming for backup {srcFile={}, dstFile={}}", srcFile, dstFile); 225 | renameFile(srcFile, dstFile); 226 | return dstFile; 227 | } 228 | 229 | private static void renameFile(File srcFile, File dstFile) throws IOException { 230 | Files.move( 231 | srcFile.toPath(), 232 | dstFile.toPath(), 233 | StandardCopyOption.REPLACE_EXISTING/*, // The rest of the arguments (atomic & copy-attr) are pretty 234 | StandardCopyOption.ATOMIC_MOVE, // much platform-dependent and JVM throws an "unsupported 235 | StandardCopyOption.COPY_ATTRIBUTES*/); // option" exception at runtime. 236 | } 237 | 238 | private File getBackupFile(int backupIndex) { 239 | String parent = config.getFile().getParent(); 240 | if (parent == null) { 241 | parent = "."; 242 | } 243 | String fileName = config.getFile().getName() + '.' + backupIndex; 244 | return Paths.get(parent, fileName).toFile(); 245 | } 246 | 247 | private void asyncCompress(RotationPolicy policy, Instant instant, File rotatedFile) { 248 | config.getExecutorService().execute(new Runnable() { 249 | 250 | private final String displayName = 251 | String.format( 252 | "%s.compress(%s)", 253 | RotatingFileOutputStream.class.getSimpleName(), rotatedFile); 254 | 255 | @Override 256 | public void run() { 257 | File compressedFile = getCompressedFile(rotatedFile); 258 | try { 259 | unsafeSyncCompress(rotatedFile, compressedFile); 260 | invokeCallbacks(callback -> callback.onSuccess(policy, instant, compressedFile)); 261 | } catch (Exception error) { 262 | String message = String.format( 263 | "compression failure {instant=%s, rotatedFile=%s, compressedFile=%s}", 264 | instant, rotatedFile, compressedFile); 265 | RuntimeException extendedError = new RuntimeException(message, error); 266 | invokeCallbacks(callback -> callback.onFailure(policy, instant, rotatedFile, extendedError)); 267 | } 268 | } 269 | 270 | @Override 271 | public String toString() { 272 | return displayName; 273 | } 274 | 275 | }); 276 | } 277 | 278 | private File getCompressedFile(File rotatedFile) { 279 | String compressedFileName = String.format("%s.gz", rotatedFile.getAbsolutePath()); 280 | return new File(compressedFileName); 281 | } 282 | 283 | private static void unsafeSyncCompress(File rotatedFile, File compressedFile) throws IOException { 284 | 285 | // Compress the file. 286 | LOGGER.debug("compressing {rotatedFile={}, compressedFile={}}", rotatedFile, compressedFile); 287 | try (InputStream sourceStream = Files.newInputStream(rotatedFile.toPath())) { 288 | try (FileOutputStream targetStream = new FileOutputStream(compressedFile); 289 | GZIPOutputStream gzipTargetStream = new GZIPOutputStream(targetStream)) { 290 | copy(sourceStream, gzipTargetStream); 291 | } 292 | } 293 | 294 | // Delete the rotated file. (On Windows, delete must take place after closing the file input stream!) 295 | LOGGER.debug("deleting old file {rotatedFile={}}", rotatedFile); 296 | boolean deleted = rotatedFile.delete(); 297 | if (!deleted) { 298 | String message = String.format("failed deleting old file {rotatedFile=%s}", rotatedFile); 299 | throw new IOException(message); 300 | } 301 | 302 | } 303 | 304 | private static void copy(InputStream source, OutputStream target) throws IOException { 305 | byte[] buffer = new byte[8192]; 306 | int readByteCount; 307 | while ((readByteCount = source.read(buffer)) > 0) { 308 | target.write(buffer, 0, readByteCount); 309 | } 310 | } 311 | 312 | @Override 313 | public RotationConfig getConfig() { 314 | return config; 315 | } 316 | 317 | @Override 318 | public synchronized void write(int b) throws IOException { 319 | unsafeCheckStream(); 320 | long byteCount = stream.size() + 1; 321 | // noinspection ForLoopReplaceableByForEach (avoid iterator instantion) 322 | for (int writeSensitivePolicyIndex = 0; 323 | writeSensitivePolicyIndex < writeSensitivePolicies.size(); 324 | writeSensitivePolicyIndex++) { 325 | RotationPolicy writeSensitivePolicy = writeSensitivePolicies.get(writeSensitivePolicyIndex); 326 | writeSensitivePolicy.acceptWrite(byteCount); 327 | writeSensitivePolicy.acceptWrite(b); 328 | } 329 | stream.write(b); 330 | } 331 | 332 | @Override 333 | public synchronized void write(byte[] b) throws IOException { 334 | unsafeCheckStream(); 335 | long byteCount = stream.size() + b.length; 336 | // noinspection ForLoopReplaceableByForEach (avoid iterator instantion) 337 | for (int writeSensitivePolicyIndex = 0; 338 | writeSensitivePolicyIndex < writeSensitivePolicies.size(); 339 | writeSensitivePolicyIndex++) { 340 | RotationPolicy writeSensitivePolicy = writeSensitivePolicies.get(writeSensitivePolicyIndex); 341 | writeSensitivePolicy.acceptWrite(byteCount); 342 | writeSensitivePolicy.acceptWrite(b); 343 | } 344 | stream.write(b); 345 | } 346 | 347 | @Override 348 | public synchronized void write(byte[] b, int off, int len) throws IOException { 349 | unsafeCheckStream(); 350 | long byteCount = stream.size() + len; 351 | // noinspection ForLoopReplaceableByForEach (avoid iterator instantion) 352 | for (int writeSensitivePolicyIndex = 0; 353 | writeSensitivePolicyIndex < writeSensitivePolicies.size(); 354 | writeSensitivePolicyIndex++) { 355 | RotationPolicy writeSensitivePolicy = writeSensitivePolicies.get(writeSensitivePolicyIndex); 356 | writeSensitivePolicy.acceptWrite(byteCount); 357 | writeSensitivePolicy.acceptWrite(b, off, len); 358 | } 359 | stream.write(b, off, len); 360 | } 361 | 362 | @Override 363 | public synchronized void flush() throws IOException { 364 | if (stream != null) { 365 | stream.flush(); 366 | } 367 | } 368 | 369 | /** 370 | * Unless the stream is already closed, invokes registered callbacks, 371 | * stops registered policies, and closes the active stream. 372 | */ 373 | @Override 374 | public synchronized void close() throws IOException { 375 | if (stream == null) { 376 | return; 377 | } 378 | invokeCallbacks(callback -> callback.onClose(null, config.getClock().now(), stream)); 379 | stopPolicies(); 380 | stream.close(); 381 | stream = null; 382 | } 383 | 384 | private void stopPolicies() { 385 | config.getPolicies().forEach(RotationPolicy::stop); 386 | } 387 | 388 | private void invokeCallbacks(Consumer invoker) { 389 | // noinspection ForLoopReplaceableByForEach (avoid iterator instantion) 390 | for (int callbackIndex = 0; callbackIndex < callbacks.size(); callbackIndex++) { 391 | RotationCallback callback = callbacks.get(callbackIndex); 392 | invoker.accept(callback); 393 | } 394 | } 395 | 396 | private void unsafeCheckStream() throws IOException { 397 | if (stream == null) { 398 | throw new IOException("either closed or not initialized yet"); 399 | } 400 | } 401 | 402 | @Override 403 | public String toString() { 404 | return String.format("RotatingFileOutputStream{file=%s}", config.getFile()); 405 | } 406 | 407 | } 408 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/RotatingFilePattern.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import java.io.File; 20 | import java.time.Instant; 21 | import java.time.ZoneId; 22 | import java.time.format.DateTimeFormatter; 23 | import java.util.*; 24 | 25 | /** 26 | * Creates file names for rotated files to be moved to. 27 | */ 28 | public class RotatingFilePattern { 29 | 30 | private static final Locale DEFAULT_LOCALE = Locale.getDefault(); 31 | 32 | private static final ZoneId DEFAULT_TIME_ZONE_ID = TimeZone.getDefault().toZoneId(); 33 | 34 | private static final char ESCAPE_CHAR = '%'; 35 | 36 | private static final char DATE_TIME_DIRECTIVE_CHAR = 'd'; 37 | 38 | private static final char DATE_TIME_BLOCK_START_CHAR = '{'; 39 | 40 | private static final char DATE_TIME_BLOCK_END_CHAR = '}'; 41 | 42 | private interface Field { 43 | 44 | void render(StringBuilder builder, Instant instant); 45 | 46 | } 47 | 48 | private static class TextField implements Field { 49 | 50 | private final String text; 51 | 52 | private TextField(String text) { 53 | this.text = text; 54 | } 55 | 56 | @Override 57 | public void render(StringBuilder builder, Instant ignored) { 58 | builder.append(text); 59 | } 60 | 61 | } 62 | 63 | private static class DateTimeField implements Field { 64 | 65 | private final DateTimeFormatter dateTimeFormatter; 66 | 67 | private DateTimeField(DateTimeFormatter dateTimeFormatter) { 68 | this.dateTimeFormatter = dateTimeFormatter; 69 | } 70 | 71 | @Override 72 | public void render(StringBuilder builder, Instant instant) { 73 | String formattedDateTime = dateTimeFormatter.format(instant); 74 | builder.append(formattedDateTime); 75 | } 76 | 77 | } 78 | 79 | private final String pattern; 80 | 81 | private final Locale locale; 82 | 83 | private final ZoneId timeZoneId; 84 | 85 | private final List fields; 86 | 87 | private RotatingFilePattern(Builder builder) { 88 | this.pattern = builder.pattern; 89 | this.locale = builder.locale; 90 | this.timeZoneId = builder.timeZoneId; 91 | this.fields = readPattern(pattern, locale, timeZoneId); 92 | } 93 | 94 | private static List readPattern(String pattern, Locale locale, ZoneId timeZoneId) { 95 | 96 | List fields = new LinkedList<>(); 97 | StringBuilder textBuilder = new StringBuilder(); 98 | int totalCharCount = pattern.length(); 99 | boolean foundDateTimeDirective = false; 100 | for (int charIndex = 0; charIndex < totalCharCount;) { 101 | 102 | char c0 = pattern.charAt(charIndex); 103 | if (c0 == ESCAPE_CHAR) { 104 | 105 | // Check if escape character is escaped. 106 | boolean hasOneMoreChar = (totalCharCount - charIndex - 1) > 0; 107 | if (hasOneMoreChar) { 108 | char c1 = pattern.charAt(charIndex + 1); 109 | if (c1 == ESCAPE_CHAR) { 110 | textBuilder.append(c1); 111 | charIndex += 2; 112 | continue; 113 | } 114 | } 115 | 116 | // Append collected text so far, if there is any. 117 | if (textBuilder.length() > 0) { 118 | String text = textBuilder.toString(); 119 | TextField field = new TextField(text); 120 | fields.add(field); 121 | textBuilder = new StringBuilder(); 122 | } 123 | 124 | // Try to read the directive. 125 | boolean hasSufficientDateTimeChars = (totalCharCount - charIndex - 3) > 0; 126 | if (hasSufficientDateTimeChars) { 127 | char c1 = pattern.charAt(charIndex + 1); 128 | if (c1 == DATE_TIME_DIRECTIVE_CHAR) { 129 | int blockStartIndex = charIndex + 2; 130 | char c2 = pattern.charAt(blockStartIndex); 131 | if (c2 == DATE_TIME_BLOCK_START_CHAR) { 132 | int blockEndIndex = pattern.indexOf(DATE_TIME_BLOCK_END_CHAR, blockStartIndex + 1); 133 | if (blockEndIndex >= 0) { 134 | String dateTimePattern = pattern.substring(blockStartIndex + 1, blockEndIndex); 135 | DateTimeFormatter dateTimeFormatter; 136 | try { 137 | dateTimeFormatter = DateTimeFormatter 138 | .ofPattern(dateTimePattern) 139 | .withLocale(locale) 140 | .withZone(timeZoneId); 141 | } catch (Exception error) { 142 | String message = String.format( 143 | "invalid date time pattern (position=%d, pattern=%s, dateTimePattern=%s)", 144 | charIndex, pattern, dateTimePattern); 145 | throw new RotatingFilePatternException(message, error); 146 | } 147 | DateTimeField dateTimeField = new DateTimeField(dateTimeFormatter); 148 | fields.add(dateTimeField); 149 | foundDateTimeDirective = true; 150 | charIndex = blockEndIndex + 1; 151 | continue; 152 | } 153 | } 154 | } 155 | 156 | } 157 | 158 | // Escape character leads to a dead end. 159 | String message = String.format("invalid escape character (position=%d, pattern=%s)", charIndex, pattern); 160 | throw new RotatingFilePatternException(message); 161 | 162 | } else { 163 | textBuilder.append(c0); 164 | charIndex += 1; 165 | } 166 | 167 | } 168 | 169 | // Append collected text so far, if there is any. 170 | if (textBuilder.length() > 0) { 171 | String text = textBuilder.toString(); 172 | TextField field = new TextField(text); 173 | fields.add(field); 174 | } 175 | 176 | // Bail out if could not locate any date time directives. 177 | if (!foundDateTimeDirective) { 178 | String message = String.format("missing date time directive (pattern=%s)", pattern); 179 | throw new RotatingFilePatternException(message); 180 | } 181 | 182 | // Return collected fields so far. 183 | return fields; 184 | 185 | } 186 | 187 | /** 188 | * @param instant an instant used to format timestamps in the pattern 189 | * 190 | * @return a file, where the name is formatted by this pattern and the given instant 191 | */ 192 | public File create(Instant instant) { 193 | StringBuilder pathNameBuilder = new StringBuilder(); 194 | for (Field field : fields) { 195 | field.render(pathNameBuilder, instant); 196 | } 197 | String pathName = pathNameBuilder.toString(); 198 | return new File(pathName); 199 | } 200 | 201 | /** 202 | * @return the formatting pattern used 203 | */ 204 | public String getPattern() { 205 | return pattern; 206 | } 207 | 208 | /** 209 | * @return the default locale used for formatting timestamps, unless specified 210 | */ 211 | public static Locale getDefaultLocale() { 212 | return DEFAULT_LOCALE; 213 | } 214 | 215 | /** 216 | * @return the locale used for formatting timestamps 217 | */ 218 | public Locale getLocale() { 219 | return locale; 220 | } 221 | 222 | /** 223 | * @return the default time zone ID used for formatting timestamps, unless specified 224 | */ 225 | public static ZoneId getDefaultTimeZoneId() { 226 | return DEFAULT_TIME_ZONE_ID; 227 | } 228 | 229 | /** 230 | * @return the time zone ID used for formatting timestamps 231 | */ 232 | public ZoneId getTimeZoneId() { 233 | return timeZoneId; 234 | } 235 | 236 | @Override 237 | public boolean equals(Object instance) { 238 | if (this == instance) return true; 239 | if (instance == null || getClass() != instance.getClass()) return false; 240 | RotatingFilePattern that = (RotatingFilePattern) instance; 241 | return Objects.equals(pattern, that.pattern) && 242 | Objects.equals(locale, that.locale) && 243 | Objects.equals(timeZoneId, that.timeZoneId); 244 | } 245 | 246 | @Override 247 | public int hashCode() { 248 | return Objects.hash(pattern, locale, timeZoneId); 249 | } 250 | 251 | @Override 252 | public String toString() { 253 | return String.format("RotatingFilePattern{pattern=%s, locale=%s, timeZoneId=%s}", pattern, locale, timeZoneId); 254 | } 255 | 256 | /** 257 | * @return a {@link Builder builder} to construct {@link RotatingFilePattern}s 258 | */ 259 | public static Builder builder() { 260 | return new Builder(); 261 | } 262 | 263 | /** 264 | * {@link Builder builder} to construct {@link RotatingFilePattern}s. 265 | */ 266 | public static final class Builder { 267 | 268 | private String pattern; 269 | 270 | private Locale locale = DEFAULT_LOCALE; 271 | 272 | private ZoneId timeZoneId = DEFAULT_TIME_ZONE_ID; 273 | 274 | private Builder() {} 275 | 276 | /** 277 | * Sets the pattern for formatting the created file names, e.g., 278 | * {@code /tmp/app-%d{yyyyMMdd-HHmmss.SSS}.log}. The value passed inside 279 | * {@code %d{...}} directive will be rendered using a 280 | * {@link DateTimeFormatter}. {@code %%} directive renders a single 281 | * {@code %} character. 282 | * 283 | * @param pattern the formatting pattern, e.g., 284 | * {@code /tmp/app-%d{yyyyMMdd-HHmmss.SSS}.log} 285 | * 286 | * @return this builder 287 | */ 288 | public Builder pattern(String pattern) { 289 | this.pattern = Objects.requireNonNull(pattern, "pattern"); 290 | return this; 291 | } 292 | 293 | /** 294 | * Sets the locale used while formatting timestamps passed in the pattern. 295 | * 296 | * @param locale the locale 297 | * 298 | * @return this builder 299 | */ 300 | public Builder locale(Locale locale) { 301 | this.locale = Objects.requireNonNull(locale, "locale"); 302 | return this; 303 | } 304 | 305 | /** 306 | * Sets the time zone ID used while formatting timestamps passed in the pattern. 307 | * 308 | * @param timeZoneId the time zone ID 309 | * 310 | * @return this builder 311 | */ 312 | public Builder timeZoneId(ZoneId timeZoneId) { 313 | this.timeZoneId = Objects.requireNonNull(timeZoneId, "timeZoneId"); 314 | return this; 315 | } 316 | 317 | /** 318 | * @return a {@link RotatingFilePattern} instance constructed using this configuration 319 | */ 320 | public RotatingFilePattern build() { 321 | validate(); 322 | return new RotatingFilePattern(this); 323 | } 324 | 325 | private void validate() { 326 | Objects.requireNonNull(pattern, "file"); 327 | } 328 | 329 | } 330 | 331 | } 332 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/RotatingFilePatternException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | /** 20 | * Used by {@link RotatingFilePattern} for communicating failures. 21 | * 22 | * @deprecated This class will be removed in the next major release and 23 | * {@link IllegalArgumentException} will be used instead. 24 | */ 25 | @Deprecated 26 | public class RotatingFilePatternException extends IllegalArgumentException { 27 | 28 | @Deprecated 29 | public RotatingFilePatternException(String message) { 30 | super(message); 31 | } 32 | 33 | @Deprecated 34 | public RotatingFilePatternException(String message, Throwable cause) { 35 | super(message, cause); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/RotationCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import com.vlkan.rfos.policy.RotationPolicy; 20 | 21 | import java.io.File; 22 | import java.io.OutputStream; 23 | import java.time.Instant; 24 | 25 | /** 26 | * Callback intercepting {@link RotatingFileOutputStream} operations. 27 | *

28 | * Callbacks are registered to {@link RotationConfig}s used to construct 29 | * {@link RotatingFileOutputStream}s. 30 | *

31 | */ 32 | public interface RotationCallback { 33 | 34 | /** 35 | * Invoked by {@link RotatingFileOutputStream} at the beginning of every 36 | * {@link RotatingFileOutputStream#rotate(RotationPolicy, Instant)} call. 37 | * The callback will be awaited in a synchronized block to proceed further. 38 | * 39 | * @param policy the triggering policy; {@code null}, if the rotation is 40 | * manually triggered 41 | * @param instant the trigger instant 42 | */ 43 | void onTrigger(RotationPolicy policy, Instant instant); 44 | 45 | /** 46 | * Invoked by {@link RotatingFileOutputStream} at start or during rotation. 47 | * At start or if the rotation is manually triggered, {@code policy} 48 | * argument will be {@code null}. The callback will be awaited in a 49 | * synchronized block to proceed further. 50 | * 51 | * @param policy the triggering policy; {@code null} at start or if 52 | * the rotation is manually triggered 53 | * @param instant the trigger instant 54 | * @param stream the active stream 55 | */ 56 | void onOpen(RotationPolicy policy, Instant instant, OutputStream stream); 57 | 58 | /** 59 | * Invoked by {@link RotatingFileOutputStream} prior to closing the internal 60 | * {@link OutputStream}. The callback might be awaited in a synchronized 61 | * block to complete the rotation. Modifications to the file at this stage 62 | * will not be reflected back to the policies. 63 | * 64 | * @param policy the triggering policy; {@code null} on 65 | * {@link RotatingFileOutputStream#close()} or if the 66 | * rotation is manually triggered 67 | * @param instant the trigger instant 68 | * @param stream the active stream 69 | */ 70 | void onClose(RotationPolicy policy, Instant instant, OutputStream stream); 71 | 72 | /** 73 | * Invoked by {@link RotatingFileOutputStream} after a successful rotation 74 | * including the compression, if enabled. The callback will be awaited in a 75 | * synchronized block to proceed further. 76 | * 77 | * @param policy the triggering policy; {@code null}, if the rotation is 78 | * manually triggered 79 | * @param instant the trigger instant 80 | * @param file the rotated file 81 | */ 82 | void onSuccess(RotationPolicy policy, Instant instant, File file); 83 | 84 | /** 85 | * Invoked by {@link RotatingFileOutputStream} after a failed rotation 86 | * attempt. The call might be awaited in a synchronized block to proceed 87 | * further. 88 | * 89 | * @param policy the triggering policy, if there is one; otherwise, 90 | * {@code null} 91 | * @param instant the trigger instant 92 | * @param file the rotated file 93 | * @param error the failure 94 | */ 95 | void onFailure(RotationPolicy policy, Instant instant, File file, Exception error); 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/RotationConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import com.vlkan.rfos.policy.RotationPolicy; 20 | 21 | import java.io.File; 22 | import java.util.Collections; 23 | import java.util.LinkedHashSet; 24 | import java.util.Objects; 25 | import java.util.Set; 26 | import java.util.concurrent.ScheduledExecutorService; 27 | import java.util.concurrent.ScheduledThreadPoolExecutor; 28 | import java.util.concurrent.ThreadFactory; 29 | 30 | /** 31 | * Configuration for constructing {@link RotatingFileOutputStream} instances. 32 | */ 33 | public class RotationConfig { 34 | 35 | private static final boolean DEFAULT_APPEND = true; 36 | 37 | private static final boolean DEFAULT_COMPRESS = false; 38 | 39 | private static final Clock DEFAULT_CLOCK = SystemClock.getInstance(); 40 | 41 | private static final Set DEFAULT_CALLBACKS = 42 | Collections.singleton(LoggingRotationCallback.getInstance()); 43 | 44 | private static final int DEFAULT_MAX_BACKUP_COUNT = -1; 45 | 46 | private enum DefaultExecutorServiceHolder {; 47 | 48 | private static final ScheduledExecutorService INSTANCE = createDefaultExecutorService(); 49 | 50 | private static ScheduledThreadPoolExecutor createDefaultExecutorService() { 51 | int threadCount = readDefaultThreadCount(); 52 | return new ScheduledThreadPoolExecutor( 53 | threadCount, 54 | new ThreadFactory() { 55 | 56 | private int threadCount = 0; 57 | 58 | @Override 59 | public synchronized Thread newThread(Runnable runnable) { 60 | String name = String.format("RotationJanitor-%02d", ++threadCount); 61 | Thread thread = new Thread(runnable, name); 62 | thread.setDaemon(true); 63 | return thread; 64 | } 65 | 66 | }); 67 | } 68 | 69 | private static int readDefaultThreadCount() { 70 | String threadCountProperty = System.getProperty("RotationJanitorCount"); 71 | return threadCountProperty != null 72 | ? Integer.parseInt(threadCountProperty) 73 | : Runtime.getRuntime().availableProcessors(); 74 | } 75 | 76 | } 77 | 78 | private final File file; 79 | 80 | private final RotatingFilePattern filePattern; 81 | 82 | private final ScheduledExecutorService executorService; 83 | 84 | private final Set policies; 85 | 86 | private final boolean append; 87 | 88 | private final boolean compress; 89 | 90 | private final int maxBackupCount; 91 | 92 | private final Clock clock; 93 | 94 | private final Set callbacks; 95 | 96 | private RotationConfig(Builder builder) { 97 | this.file = builder.file; 98 | this.filePattern = builder.filePattern; 99 | this.executorService = builder.executorService; 100 | this.policies = Collections.unmodifiableSet(builder.policies); 101 | this.append = builder.append; 102 | this.compress = builder.compress; 103 | this.maxBackupCount = builder.maxBackupCount; 104 | this.clock = builder.clock; 105 | this.callbacks = Collections.unmodifiableSet(builder.callbacks); 106 | } 107 | 108 | /** 109 | * @return the file to be written by the stream 110 | */ 111 | public File getFile() { 112 | return file; 113 | } 114 | 115 | /** 116 | * Gets the file pattern to be used while rotating files, if set; otherwise, 117 | * rotated files will be named by other means, e.g., backup indices. 118 | * 119 | * @return the file pattern to be used, if set; otherwise, {@code null}, 120 | * denoting that rotated files will be named by other means, e.g., backup 121 | * indices 122 | * 123 | * @see #getMaxBackupCount() 124 | */ 125 | public RotatingFilePattern getFilePattern() { 126 | return filePattern; 127 | } 128 | 129 | /** 130 | * Gets the default scheduler for time-based policies and compression tasks. 131 | *

132 | * Note that, this scheduler is shared between {@link RotatingFileOutputStream} 133 | * instances, unless it is explicitly set to another one in the 134 | * configuration. Hence, avoid shutting this scheduler instance down. 135 | *

136 | * By default, the scheduler thread pool size is equal to the amount of CPU 137 | * cores available. This can be overridden by setting the {@code RotationJanitorCount} 138 | * system property. 139 | *

140 | * 141 | * @return the default scheduler for time-based policies and compression tasks 142 | */ 143 | public static ScheduledExecutorService getDefaultExecutorService() { 144 | return DefaultExecutorServiceHolder.INSTANCE; 145 | } 146 | 147 | /** 148 | * Gets the scheduler for time-based policies and compression tasks. 149 | *

150 | * Unless explicitly set, this defaults to the one returned by {@link #getDefaultExecutorService()}. 151 | * Since the default scheduler can be shared by multiple streams, be 152 | * cautious while closing the scheduler returned by this method. 153 | *

154 | * 155 | * @return the scheduler for time-based policies and compression tasks 156 | */ 157 | public ScheduledExecutorService getExecutorService() { 158 | return executorService; 159 | } 160 | 161 | /** 162 | * @return the registered set of rotation policies 163 | */ 164 | public Set getPolicies() { 165 | return policies; 166 | } 167 | 168 | /** 169 | * @return the default value of the {@code append} flag, indicating, if 170 | * true, then bytes will be written to the end of the file rather than the 171 | * beginning 172 | */ 173 | public static boolean getDefaultAppend() { 174 | return DEFAULT_APPEND; 175 | } 176 | 177 | /** 178 | * @return the {@code append} flag, indicating, if true, then bytes will be 179 | * written to the end of the file rather than the beginning 180 | */ 181 | public boolean isAppend() { 182 | return append; 183 | } 184 | 185 | /** 186 | * @return the default value of the {@code compress} flag, indicating, if 187 | * true, rotated files will be compressed in the background 188 | */ 189 | public static boolean getDefaultCompress() { 190 | return DEFAULT_COMPRESS; 191 | } 192 | 193 | /** 194 | * Gets the {@code compress} flag, indicating, if true, rotated files will 195 | * be compressed in the background. 196 | *

197 | * Note that this option cannot be combined with {@code maxBackupCount}. 198 | *

199 | * 200 | * @return the {@code compress} flag, indicating, if true, rotated files 201 | * will be compressed in the background. 202 | * 203 | * @see #getMaxBackupCount() 204 | */ 205 | public boolean isCompress() { 206 | return compress; 207 | } 208 | 209 | /** 210 | * @return the default value of the {@code maxBackupCount}, indicating, if 211 | * greater than zero, rotated files will be named as {@code file.0}, 212 | * {@code file.1}, {@code file.2}, ..., {@code file.N} in the order from the 213 | * newest to the oldest, where {@code N} denoting the {@code maxBackupCount} 214 | */ 215 | public static int getDefaultMaxBackupCount() { 216 | return DEFAULT_MAX_BACKUP_COUNT; 217 | } 218 | 219 | /** 220 | * Gets the {@code maxBackupCount}, indicating, if greater than zero, 221 | * rotated files will be named as {@code file.0}, {@code file.1}, 222 | * {@code file.2}, ..., {@code file.N} in the order from the newest to the 223 | * oldest, where {@code N} denoting the {@code maxBackupCount}. 224 | *

225 | * Note that this option cannot be combined with {@code filePattern} or 226 | * {@code compress}. 227 | *

228 | * 229 | * @return the {@code maxBackupCount}, indicating, if greater than zero, 230 | * rotated files will be named as {@code file.0}, {@code file.1}, 231 | * {@code file.2}, ..., {@code file.N} in the order from the newest to the 232 | * oldest, where {@code N} denoting the {@code maxBackupCount} 233 | */ 234 | public int getMaxBackupCount() { 235 | return maxBackupCount; 236 | } 237 | 238 | /** 239 | * @return the default clock implementation 240 | */ 241 | public static Clock getDefaultClock() { 242 | return DEFAULT_CLOCK; 243 | } 244 | 245 | /** 246 | * @return the clock implementation 247 | */ 248 | public Clock getClock() { 249 | return clock; 250 | } 251 | 252 | /** 253 | * @return the default callbacks 254 | */ 255 | public static Set getDefaultCallbacks() { 256 | return DEFAULT_CALLBACKS; 257 | } 258 | 259 | /** 260 | * @return the first callback in the registered set of callbacks 261 | * 262 | * @deprecated This method is kept for backward-compatibility reasons, use {@link #getCallbacks()} instead. 263 | */ 264 | @Deprecated 265 | public RotationCallback getCallback() { 266 | return callbacks.iterator().next(); 267 | } 268 | 269 | /** 270 | * @return the registered set of callbacks 271 | */ 272 | public Set getCallbacks() { 273 | return callbacks; 274 | } 275 | 276 | @Override 277 | public boolean equals(Object instance) { 278 | if (this == instance) return true; 279 | if (instance == null || getClass() != instance.getClass()) return false; 280 | RotationConfig that = (RotationConfig) instance; 281 | return append == that.append && 282 | compress == that.compress && 283 | maxBackupCount == that.maxBackupCount && 284 | Objects.equals(file, that.file) && 285 | Objects.equals(filePattern, that.filePattern) && 286 | Objects.equals(executorService, that.executorService) && 287 | Objects.equals(policies, that.policies) && 288 | Objects.equals(clock, that.clock) && 289 | Objects.equals(callbacks, that.callbacks); 290 | } 291 | 292 | @Override 293 | public int hashCode() { 294 | return Objects.hash( 295 | file, 296 | filePattern, 297 | executorService, 298 | policies, 299 | append, 300 | compress, 301 | maxBackupCount, 302 | clock, 303 | callbacks); 304 | } 305 | 306 | @Override 307 | public String toString() { 308 | return String.format("RotationConfig{file=%s}", file); 309 | } 310 | 311 | /** 312 | * @param config a rotation configuration 313 | * 314 | * @return a rotation configuration builder using the given configuration 315 | */ 316 | public static Builder builder(RotationConfig config) { 317 | Objects.requireNonNull(config, "config"); 318 | return new Builder(config); 319 | } 320 | 321 | /** 322 | * @return a rotation configuration builder where optional fields are populated with defaults 323 | */ 324 | public static Builder builder() { 325 | return new Builder(); 326 | } 327 | 328 | /** 329 | * The rotation configuration builder. 330 | */ 331 | public static class Builder { 332 | 333 | private File file; 334 | 335 | private RotatingFilePattern filePattern; 336 | 337 | private ScheduledExecutorService executorService; 338 | 339 | private Set policies; 340 | 341 | private boolean append = DEFAULT_APPEND; 342 | 343 | private boolean compress = DEFAULT_COMPRESS; 344 | 345 | private int maxBackupCount = DEFAULT_MAX_BACKUP_COUNT; 346 | 347 | private Clock clock = DEFAULT_CLOCK; 348 | 349 | private Set callbacks = 350 | // We need a defensive copy for Set#add() in 351 | // callback(RotationCallback) setter. 352 | new LinkedHashSet<>(DEFAULT_CALLBACKS); 353 | 354 | private Builder(RotationConfig config) { 355 | this.file = config.file; 356 | this.filePattern = config.filePattern; 357 | this.executorService = config.executorService; 358 | this.policies = config.policies; 359 | this.append = config.append; 360 | this.compress = config.append; 361 | this.maxBackupCount = config.maxBackupCount; 362 | this.clock = config.clock; 363 | this.callbacks = config.callbacks; 364 | } 365 | 366 | private Builder() {} 367 | 368 | /** 369 | * Sets the file to be written by the stream. 370 | * 371 | * @param file the file to be written by the stream 372 | * 373 | * @return this builder 374 | */ 375 | public Builder file(File file) { 376 | this.file = Objects.requireNonNull(file, "file"); 377 | return this; 378 | } 379 | 380 | /** 381 | * Sets the file to be written by the stream. 382 | * 383 | * @param fileName the file to be written by the stream 384 | * 385 | * @return this builder 386 | */ 387 | public Builder file(String fileName) { 388 | Objects.requireNonNull(fileName, "fileName"); 389 | this.file = new File(fileName); 390 | return this; 391 | } 392 | 393 | /** 394 | * Sets the file pattern to be used while rotating files, if set; 395 | * otherwise, rotated files will be named by other means, e.g., backup 396 | * indices. 397 | *

398 | * Note that this option cannot be combined with {@code maxBackupCount}. 399 | *

400 | * 401 | * @param filePattern the file pattern to be used while rotating files 402 | * 403 | * @return this builder 404 | */ 405 | public Builder filePattern(RotatingFilePattern filePattern) { 406 | this.filePattern = Objects.requireNonNull(filePattern, "filePattern"); 407 | return this; 408 | } 409 | 410 | /** 411 | * Sets the file pattern to be used while rotating files, e.g., 412 | * {@code /tmp/app-%d{yyyyMMdd-HHmmss.SSS}.log}. Use the setter 413 | * {@link #filePattern(RotatingFilePattern)} to have full control 414 | * on other available file pattern settings. 415 | *

416 | * If undefined, rotated files will be named by other means, e.g., 417 | * backup indices. 418 | *

419 | * Note that this option cannot be combined with {@code maxBackupCount}. 420 | *

421 | * 422 | * @param filePattern the file pattern to be used while rotating files 423 | * 424 | * @return this builder 425 | * 426 | * @see RotatingFilePattern 427 | */ 428 | public Builder filePattern(String filePattern) { 429 | Objects.requireNonNull(filePattern, "filePattern"); 430 | this.filePattern = RotatingFilePattern 431 | .builder() 432 | .pattern(filePattern) 433 | .build(); 434 | return this; 435 | } 436 | 437 | /** 438 | * Sets the scheduler for time-based policies and compression tasks. 439 | *

440 | * If unset, the default one will be used, whose thread pool size is 441 | * equal to the amount of CPU cores available. This can be overridden by 442 | * setting the {@code RotationJanitorCount} system property. 443 | *

444 | * 445 | * @param executorService the scheduler for time-based policies and compression tasks 446 | * 447 | * @return this builder 448 | * 449 | * @see #getDefaultExecutorService() 450 | */ 451 | public Builder executorService(ScheduledExecutorService executorService) { 452 | this.executorService = Objects.requireNonNull(executorService, "executorService"); 453 | return this; 454 | } 455 | 456 | /** 457 | * Sets the rotation policies to be employed. 458 | * 459 | * @param policies the rotation policies to be employed 460 | * 461 | * @return this builder 462 | */ 463 | public Builder policies(Set policies) { 464 | Objects.requireNonNull(policies, "policies"); 465 | // We need a defensive copy for Set#add() in policy(Policy) setter. 466 | this.policies = new LinkedHashSet<>(policies); 467 | return this; 468 | } 469 | 470 | /** 471 | * Adds a rotation policy to be employed along with the ones already registered. 472 | * 473 | * @param policy a rotation policy 474 | * 475 | * @return this builder 476 | */ 477 | public Builder policy(RotationPolicy policy) { 478 | Objects.requireNonNull(policy, "policy"); 479 | if (policies == null) { 480 | policies = new LinkedHashSet<>(); 481 | } 482 | policies.add(policy); 483 | return this; 484 | } 485 | 486 | /** 487 | * Sets the {@code append} flag, indicating, if true, then bytes will be 488 | * written to the end of the file rather than the beginning. 489 | * 490 | * @param append the {@code append} flag, indicating, if true, then 491 | * bytes will be written to the end of the file rather 492 | * than the beginning 493 | * 494 | * @return this builder 495 | * 496 | * @see #getDefaultAppend() 497 | */ 498 | public Builder append(boolean append) { 499 | this.append = append; 500 | return this; 501 | } 502 | 503 | /** 504 | * Sets the {@code compress} flag, indicating, if true, rotated files 505 | * will be compressed in the background. 506 | *

507 | * Note that this option cannot be combined with {@code maxBackupCount}. 508 | *

509 | * 510 | * @param compress if true, rotated files will be compressed in the 511 | * background 512 | * 513 | * @return this builder 514 | * 515 | * @see #getDefaultCompress() 516 | */ 517 | public Builder compress(boolean compress) { 518 | this.compress = compress; 519 | return this; 520 | } 521 | 522 | /** 523 | * Gets the {@code maxBackupCount}, indicating, if greater than zero, 524 | * rotated files will be named as {@code file.0}, {@code file.1}, 525 | * {@code file.2}, ..., {@code file.N} in the order from the newest to the 526 | * oldest, where {@code N} denoting the {@code maxBackupCount}. 527 | *

528 | * Note that this option cannot be combined with {@code filePattern} or 529 | * {@code compress}. 530 | *

531 | * 532 | * @param maxBackupCount if greater than zero, rotated files will be 533 | * named as {@code file.0}, {@code file.1}, 534 | * {@code file.2}, ..., {@code file.N} in the 535 | * order from the newest to the oldest, where 536 | * {@code N} denoting the {@code maxBackupCount} 537 | * 538 | * @return this builder 539 | */ 540 | public Builder maxBackupCount(int maxBackupCount) { 541 | this.maxBackupCount = maxBackupCount; 542 | return this; 543 | } 544 | 545 | /** 546 | * Sets the clock implementation to be used. 547 | * 548 | * @param clock a clock instance 549 | * 550 | * @return this builder 551 | */ 552 | public Builder clock(Clock clock) { 553 | this.clock = Objects.requireNonNull(clock, "clock"); 554 | return this; 555 | } 556 | 557 | /** 558 | * Adds a callback to be employed along with the ones already registered. 559 | * 560 | * @param callback a callback to be employed along with the ones already 561 | * registered 562 | * 563 | * @return this builder 564 | * 565 | * @see #getDefaultCallbacks() 566 | */ 567 | public Builder callback(RotationCallback callback) { 568 | Objects.requireNonNull(callback, "callback"); 569 | callbacks.add(callback); 570 | return this; 571 | } 572 | 573 | /** 574 | * Sets the callbacks to employed. 575 | * 576 | * @param callbacks the callbacks to employed 577 | * 578 | * @return this builder 579 | */ 580 | public Builder callbacks(Set callbacks) { 581 | Objects.requireNonNull(callbacks, "callbacks"); 582 | // We need a defensive copy for Set#add() in callback(RotationCallback) setter. 583 | this.callbacks = new LinkedHashSet<>(callbacks); 584 | return this; 585 | } 586 | 587 | /** 588 | * @return a {@link RotationConfig} constructed using the given properties 589 | */ 590 | public RotationConfig build() { 591 | prepare(); 592 | validate(); 593 | return new RotationConfig(this); 594 | } 595 | 596 | private void prepare() { 597 | if (executorService == null) { 598 | executorService = getDefaultExecutorService(); 599 | } 600 | } 601 | 602 | private void validate() { 603 | Objects.requireNonNull(file, "file"); 604 | if (maxBackupCount > 0) { 605 | String conflictingField = null; 606 | if (filePattern != null) { 607 | conflictingField = "filePattern"; 608 | } else if (compress) { 609 | conflictingField = "compress"; 610 | } 611 | if (conflictingField != null) { 612 | throw new IllegalArgumentException( 613 | "maxBackupCount and " + conflictingField + " cannot be combined"); 614 | } 615 | } else if (filePattern == null) { 616 | throw new IllegalArgumentException( 617 | "one of either maxBackupCount or filePattern must be provided"); 618 | } 619 | if (policies == null || policies.isEmpty()) { 620 | throw new IllegalArgumentException("no rotation policy is provided"); 621 | } 622 | } 623 | 624 | } 625 | 626 | } 627 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/SystemClock.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import java.time.*; 20 | 21 | /** 22 | * A {@link Clock} implementation using the system time. 23 | */ 24 | public class SystemClock implements Clock { 25 | 26 | private static final SystemClock INSTANCE = new SystemClock(); 27 | 28 | private static final ZoneId UTC_ZONE_ID = ZoneId.of("UTC"); 29 | 30 | SystemClock() { 31 | // Do nothing. 32 | } 33 | 34 | public static SystemClock getInstance() { 35 | return INSTANCE; 36 | } 37 | 38 | @Override 39 | public Instant now() { 40 | return Instant.now(); 41 | } 42 | 43 | @Override 44 | public Instant midnight() { 45 | Instant instant = now(); 46 | ZonedDateTime utcInstant = instant.atZone(UTC_ZONE_ID); 47 | return LocalDate 48 | .from(utcInstant) 49 | .atStartOfDay() 50 | .plusDays(1) 51 | .toInstant(ZoneOffset.UTC); 52 | } 53 | 54 | @Override 55 | public Instant sundayMidnight() { 56 | Instant instant = now(); 57 | ZonedDateTime utcInstant = instant.atZone(UTC_ZONE_ID); 58 | LocalDateTime todayStart = LocalDate.from(utcInstant).atStartOfDay(); 59 | int todayIndex = todayStart.getDayOfWeek().getValue() - 1; 60 | int sundayOffset = 7 - todayIndex; 61 | return todayStart 62 | .plusDays(sundayOffset) 63 | .toInstant(ZoneOffset.UTC); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/policy/ByteMatchingRotationPolicy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos.policy; 18 | 19 | import com.vlkan.rfos.Rotatable; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import java.io.File; 24 | import java.io.InputStream; 25 | import java.nio.file.Files; 26 | import java.nio.file.Path; 27 | import java.time.Instant; 28 | import java.util.Objects; 29 | 30 | public class ByteMatchingRotationPolicy implements RotationPolicy { 31 | 32 | private static final Logger LOGGER = LoggerFactory.getLogger(SizeBasedRotationPolicy.class); 33 | 34 | private final byte targetByte; 35 | 36 | private final int maxOccurrenceCount; 37 | 38 | private long occurrenceCount; 39 | 40 | private Rotatable rotatable; 41 | 42 | public ByteMatchingRotationPolicy(byte targetByte, int maxOccurrenceCount) { 43 | if (maxOccurrenceCount < 1) { 44 | String message = String.format("invalid count {maxOccurrenceCount=%d}", maxOccurrenceCount); 45 | throw new IllegalArgumentException(message); 46 | } 47 | this.targetByte = targetByte; 48 | this.maxOccurrenceCount = maxOccurrenceCount; 49 | } 50 | 51 | @Override 52 | public void start(Rotatable rotatable) { 53 | this.rotatable = rotatable; 54 | occurrenceCount = countOccurrences(rotatable.getConfig().getFile()); 55 | if (occurrenceCount > 0) { 56 | LOGGER.debug("starting with non-zero line count {lineCount={}}", occurrenceCount); 57 | } 58 | 59 | } 60 | 61 | private long countOccurrences(File file) { 62 | // No need to check if file exists, since policies get started after opening the file. 63 | final Path path = file.toPath(); 64 | try (InputStream inputStream = Files.newInputStream(path)) { 65 | long lineCount = 0; 66 | byte[] buffer = new byte[8192]; 67 | for (;;) { 68 | int length = inputStream.read(buffer); 69 | if (length < 0) { 70 | break; 71 | } 72 | for (int i = 0; i < length; i++) { 73 | if (buffer[i] == targetByte) { 74 | lineCount++; 75 | } 76 | } 77 | } 78 | return lineCount; 79 | } catch (Exception error) { 80 | final String message = String.format("read failure {file=%s}", file); 81 | throw new RuntimeException(message, error); 82 | } 83 | } 84 | 85 | /** 86 | * @return {@code true}, always. 87 | */ 88 | @Override 89 | public boolean isWriteSensitive() { 90 | return true; 91 | } 92 | 93 | @Override 94 | public void acceptWrite(int b) { 95 | if (b == targetByte) { 96 | ++occurrenceCount; 97 | rotateIfNecessary(); 98 | } 99 | } 100 | 101 | @Override 102 | public void acceptWrite(byte[] buf) { 103 | int matchCount = 0; 104 | for (byte b : buf) { 105 | if (b == targetByte) { 106 | ++matchCount; 107 | } 108 | } 109 | occurrenceCount += matchCount; 110 | rotateIfNecessary(); 111 | } 112 | 113 | @Override 114 | public void acceptWrite(byte[] buf, int off, int len) { 115 | int matchCount = 0; 116 | int maxIdx = off + len; 117 | for (int idx = off; idx < maxIdx; idx++) { 118 | if (buf[idx] == targetByte) { 119 | ++matchCount; 120 | } 121 | } 122 | occurrenceCount += matchCount; 123 | rotateIfNecessary(); 124 | } 125 | 126 | private void rotateIfNecessary() { 127 | if (occurrenceCount >= maxOccurrenceCount) { 128 | LOGGER.debug("triggering {occurrenceCount={}}", occurrenceCount); 129 | Instant instant = rotatable.getConfig().getClock().now(); 130 | rotatable.rotate(this, instant); 131 | occurrenceCount = 0; 132 | } 133 | } 134 | 135 | @Override 136 | public boolean equals(Object instance) { 137 | if (this == instance) return true; 138 | if (instance == null || getClass() != instance.getClass()) { 139 | return false; 140 | } 141 | ByteMatchingRotationPolicy policy = (ByteMatchingRotationPolicy) instance; 142 | return maxOccurrenceCount == policy.maxOccurrenceCount; 143 | } 144 | 145 | @Override 146 | public int hashCode() { 147 | return Objects.hash(maxOccurrenceCount); 148 | } 149 | 150 | @Override 151 | public String toString() { 152 | return String.format( 153 | "ByteMatchingRotationPolicy{targetByte=0x%X, maxOccurrenceCount=%d}", 154 | targetByte, maxOccurrenceCount); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/policy/DailyRotationPolicy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos.policy; 18 | 19 | import com.vlkan.rfos.Clock; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import java.time.Instant; 24 | 25 | /** 26 | * Policy for triggering a rotation at midnight every day. 27 | */ 28 | public class DailyRotationPolicy extends TimeBasedRotationPolicy { 29 | 30 | private static final Logger LOGGER = LoggerFactory.getLogger(DailyRotationPolicy.class); 31 | 32 | private static final DailyRotationPolicy INSTANCE = new DailyRotationPolicy(); 33 | 34 | private DailyRotationPolicy() { 35 | // Do nothing. 36 | } 37 | 38 | /** 39 | * @return an instance of this policy 40 | */ 41 | public static DailyRotationPolicy getInstance() { 42 | return INSTANCE; 43 | } 44 | 45 | /** 46 | * @return the instant of the upcoming midnight 47 | */ 48 | @Override 49 | public Instant getTriggerInstant(Clock clock) { 50 | return clock.midnight(); 51 | } 52 | 53 | @Override 54 | protected Logger getLogger() { 55 | return LOGGER; 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return "DailyRotationPolicy"; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/policy/RotationPolicy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos.policy; 18 | 19 | import com.vlkan.rfos.Rotatable; 20 | 21 | /** 22 | * The policy to trigger a file rotation in {@link Rotatable}. 23 | */ 24 | public interface RotationPolicy { 25 | 26 | /** 27 | * Starts the policy. That is, if it is a time-based policy, it can schedule 28 | * the next rotation. 29 | * 30 | * @param rotatable the rotatable accessing this policy 31 | */ 32 | default void start(Rotatable rotatable) {} 33 | 34 | /** 35 | * Stops the policy. That is, if it is a time-based policy, it can cancel 36 | * the scheduled next rotation task. 37 | */ 38 | default void stop() {} 39 | 40 | /** 41 | * @return {@code true}, if the policy intercepts write operations via 42 | * {@code #acceptWrite()} methods 43 | */ 44 | default boolean isWriteSensitive() { 45 | return false; 46 | } 47 | 48 | /** 49 | * Invoked before every write operation, if {@link #isWriteSensitive()} 50 | * returns {@code true}. 51 | * 52 | * @param byteCount the number of bytes written to the active file so far, 53 | * including the ones about to be written right now 54 | */ 55 | default void acceptWrite(long byteCount) {} 56 | 57 | /** 58 | * Invoked before every {@link java.io.OutputStream#write(int)} operation, 59 | * if {@link #isWriteSensitive()} returns {@code true}. 60 | * 61 | * @param b a byte 62 | */ 63 | default void acceptWrite(int b) {} 64 | 65 | /** 66 | * Invoked before every {@link java.io.OutputStream#write(byte[], int, int)} 67 | * operation, if {@link #isWriteSensitive()} returns {@code true}. 68 | * 69 | * @param b the byte array 70 | */ 71 | default void acceptWrite(byte[] b) {} 72 | 73 | /** 74 | * Invoked before every write operation, if {@link #isWriteSensitive()} 75 | * returns {@code true}. 76 | * 77 | * @param b the byte passed to {@link java.io.OutputStream#write(byte[], int, int)} 78 | */ 79 | default void acceptWrite(byte[] b, int off, int len) {} 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/policy/SizeBasedRotationPolicy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos.policy; 18 | 19 | import com.vlkan.rfos.Rotatable; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import java.time.Instant; 24 | import java.util.Objects; 25 | 26 | /** 27 | * Policy for triggering a rotation when a certain byte count threshold is exceeded. 28 | */ 29 | public class SizeBasedRotationPolicy implements RotationPolicy { 30 | 31 | private static final Logger LOGGER = LoggerFactory.getLogger(SizeBasedRotationPolicy.class); 32 | 33 | private final long maxByteCount; 34 | 35 | private Rotatable rotatable; 36 | 37 | /** 38 | * Constructs an instance using the given threshold. 39 | * 40 | * @param maxByteCount the maximum byte count threshold triggering a rotation when exceeded 41 | */ 42 | public SizeBasedRotationPolicy(long maxByteCount) { 43 | if (maxByteCount < 1) { 44 | String message = String.format("invalid size {maxByteCount=%d}", maxByteCount); 45 | throw new IllegalArgumentException(message); 46 | } 47 | this.maxByteCount = maxByteCount; 48 | } 49 | 50 | /** 51 | * @return the maximum byte count threshold triggering a rotation when exceeded 52 | */ 53 | public long getMaxByteCount() { 54 | return maxByteCount; 55 | } 56 | 57 | /** 58 | * @return {@code true}, always. 59 | */ 60 | @Override 61 | public boolean isWriteSensitive() { 62 | return true; 63 | } 64 | 65 | /** 66 | * Triggers a rotation if the given byte count exceeds the set threshold. 67 | * 68 | * @param byteCount the number of bytes written to the active stream so far 69 | */ 70 | @Override 71 | public void acceptWrite(long byteCount) { 72 | if (byteCount > maxByteCount) { 73 | LOGGER.debug("triggering {byteCount={}}", byteCount); 74 | final Instant instant = rotatable.getConfig().getClock().now(); 75 | rotatable.rotate(this, instant); 76 | } 77 | } 78 | 79 | @Override 80 | public void start(Rotatable rotatable) { 81 | this.rotatable = rotatable; 82 | } 83 | 84 | @Override 85 | public boolean equals(Object instance) { 86 | if (this == instance) return true; 87 | if (instance == null || getClass() != instance.getClass()) return false; 88 | SizeBasedRotationPolicy that = (SizeBasedRotationPolicy) instance; 89 | return maxByteCount == that.maxByteCount; 90 | } 91 | 92 | @Override 93 | public int hashCode() { 94 | return Objects.hash(maxByteCount); 95 | } 96 | 97 | @Override 98 | public String toString() { 99 | return String.format("SizeBasedRotationPolicy{maxByteCount=%d}", maxByteCount); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/policy/TimeBasedRotationPolicy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos.policy; 18 | 19 | import com.vlkan.rfos.Clock; 20 | import com.vlkan.rfos.Rotatable; 21 | import com.vlkan.rfos.RotationConfig; 22 | import org.slf4j.Logger; 23 | 24 | import java.time.Duration; 25 | import java.time.Instant; 26 | import java.util.concurrent.ScheduledFuture; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | /** 30 | * Base class for implementing periodically triggered time-based policies. 31 | * 32 | * @see DailyRotationPolicy 33 | * @see WeeklyRotationPolicy 34 | */ 35 | public abstract class TimeBasedRotationPolicy implements RotationPolicy { 36 | 37 | private volatile ScheduledFuture scheduledFuture; 38 | 39 | /** 40 | * @return {@code false}, always 41 | */ 42 | @Override 43 | public boolean isWriteSensitive() { 44 | return false; 45 | } 46 | 47 | /** 48 | * Throws an exception, always, since this is not a write-sensitive policy. 49 | * 50 | * @throws UnsupportedOperationException thrown upon every call 51 | */ 52 | @Override 53 | public void acceptWrite(long byteCount) { 54 | throw new UnsupportedOperationException(); 55 | } 56 | 57 | @Override 58 | public synchronized void start(Rotatable rotatable) { 59 | start(rotatable, null); 60 | } 61 | 62 | private void start(Rotatable rotatable, Instant lastTriggerInstant) { 63 | RotationConfig config = rotatable.getConfig(); 64 | Clock clock = config.getClock(); 65 | Instant currentInstant = clock.now(); 66 | Instant triggerInstant = getTriggerInstant(clock); 67 | long triggerDelayNanos = Duration.between(currentInstant, triggerInstant).toNanos(); 68 | Runnable task = createTask(rotatable, lastTriggerInstant, triggerInstant); 69 | this.scheduledFuture = config 70 | .getExecutorService() 71 | .schedule(task, triggerDelayNanos, TimeUnit.NANOSECONDS); 72 | } 73 | 74 | private Runnable createTask(Rotatable rotatable, Instant lastTriggerInstant, Instant triggerInstant) { 75 | return () -> { 76 | // Avoid triggering repeatedly for the very same instant. 77 | // This can happen due to: 78 | // 1. Code execution is faster than the time resolution provided by the clock 79 | // 2. Clocks can return a value twice (due to daylight time savings, monotonically-increasing design, etc.) 80 | boolean validTriggerInstant = lastTriggerInstant == null || triggerInstant.isAfter(lastTriggerInstant); 81 | if (validTriggerInstant) { 82 | getLogger().debug("triggering {triggerInstant={}}", triggerInstant); 83 | rotatable.rotate(TimeBasedRotationPolicy.this, triggerInstant); 84 | } 85 | start(rotatable, triggerInstant); 86 | }; 87 | } 88 | 89 | @Override 90 | public synchronized void stop() { 91 | if (scheduledFuture != null) { 92 | scheduledFuture.cancel(true); 93 | } 94 | } 95 | 96 | /** 97 | * @param clock a clock implementation 98 | * 99 | * @return the upcoming rotation trigger instant 100 | */ 101 | abstract public Instant getTriggerInstant(Clock clock); 102 | 103 | /** 104 | * @return the logger used 105 | */ 106 | abstract protected Logger getLogger(); 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/vlkan/rfos/policy/WeeklyRotationPolicy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos.policy; 18 | 19 | import com.vlkan.rfos.Clock; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import java.time.Instant; 24 | 25 | /** 26 | * Policy for triggering a rotation at Sunday midnight every day. 27 | */ 28 | public class WeeklyRotationPolicy extends TimeBasedRotationPolicy { 29 | 30 | private static final Logger LOGGER = LoggerFactory.getLogger(WeeklyRotationPolicy.class); 31 | 32 | private static final WeeklyRotationPolicy INSTANCE = new WeeklyRotationPolicy(); 33 | 34 | private WeeklyRotationPolicy() { 35 | // Do nothing. 36 | } 37 | 38 | /** 39 | * @return an instance of this policy 40 | */ 41 | public static WeeklyRotationPolicy getInstance() { 42 | return INSTANCE; 43 | } 44 | 45 | /** 46 | * @return the instant of the upcoming Sunday midnight 47 | */ 48 | @Override 49 | public Instant getTriggerInstant(Clock clock) { 50 | return clock.sundayMidnight(); 51 | } 52 | 53 | @Override 54 | protected Logger getLogger() { 55 | return LOGGER; 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return "WeeklyRotationPolicy"; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/vlkan/rfos/ByteCountingOutputStreamTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import org.assertj.core.api.Assertions; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import java.io.ByteArrayOutputStream; 23 | import java.io.IOException; 24 | import java.io.PrintStream; 25 | import java.util.Random; 26 | 27 | class ByteCountingOutputStreamTest { 28 | 29 | private static final Random RANDOM = new Random(0); 30 | 31 | @Test 32 | void test() throws IOException { 33 | for (int testIndex = 0; testIndex < 100; testIndex++) { 34 | int textSize = RANDOM.nextInt(1024 * 1024); 35 | String text = generateRandomString(textSize); 36 | test(text); 37 | } 38 | } 39 | 40 | private void test(String text) throws IOException { 41 | String encoding = "UTF-8"; 42 | try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { 43 | try (ByteCountingOutputStream byteCountingOutputStream = new ByteCountingOutputStream(byteArrayOutputStream, 0)) { 44 | try (PrintStream printStream = new PrintStream(byteCountingOutputStream, false, encoding)) { 45 | printStream.print(text); 46 | } 47 | long actualSize = byteCountingOutputStream.size(); 48 | int expectedSize = text.getBytes(encoding).length; 49 | Assertions.assertThat(actualSize).isEqualTo(expectedSize); 50 | } 51 | } 52 | } 53 | 54 | private static String generateRandomString(int length) { 55 | int maxOffset = Character.MAX_VALUE - Character.MIN_VALUE; 56 | StringBuilder builder = new StringBuilder(); 57 | while (length-- > 0) { 58 | int offset = RANDOM.nextInt(maxOffset); 59 | char c = (char) (Character.MIN_VALUE + offset); 60 | builder.append(c); 61 | } 62 | return builder.toString(); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/com/vlkan/rfos/RotatingFilePatternTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import org.assertj.core.api.ThrowableAssert; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import java.io.File; 23 | import java.time.Instant; 24 | import java.time.format.DateTimeFormatter; 25 | import java.util.LinkedHashMap; 26 | import java.util.Locale; 27 | import java.util.Map; 28 | 29 | import static org.assertj.core.api.Assertions.assertThat; 30 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 31 | 32 | class RotatingFilePatternTest { 33 | 34 | @Test 35 | void test_invalid_patterns() { 36 | String[] invalidPatterns = new String[]{ 37 | "", 38 | "-", 39 | "%", 40 | "%d", 41 | "%d{", 42 | "%d}", 43 | "%%", 44 | "foo%", 45 | "foo%d", 46 | "foo%d{", 47 | "foo%d{T}", 48 | "foo%%" 49 | }; 50 | for (String invalidPattern : invalidPatterns) { 51 | ThrowableAssert.ThrowingCallable callable = () -> RotatingFilePattern 52 | .builder() 53 | .pattern(invalidPattern) 54 | .locale(Locale.US) 55 | .timeZoneId(UtcHelper.ZONE_ID) 56 | .build(); 57 | assertThatThrownBy(callable) 58 | .as("pattern=%s", invalidPattern) 59 | .isInstanceOf(RotatingFilePatternException.class); 60 | } 61 | } 62 | 63 | @Test 64 | void test_valid_patterns() { 65 | Instant instant = Instant.now(); 66 | Map fileByPattern = new LinkedHashMap<>(); 67 | fileByPattern.put("%d{yyyy-MM-dd}", new File(formatInstant("yyyy-MM-dd", instant))); 68 | fileByPattern.put("%d{yyyy-MM-dd}.log/foo%%", new File(String.format("%s.log/foo%%", formatInstant("yyyy-MM-dd", instant)))); 69 | fileByPattern.put("tmp/%d{yyyyMMdd-HH}", new File(String.format("tmp/%s", formatInstant("yyyyMMdd-HH", instant)))); 70 | fileByPattern.put("/tmp/%d{yyyyMMdd-HH}", new File(String.format("/tmp/%s", formatInstant("yyyyMMdd-HH", instant)))); 71 | fileByPattern.put( 72 | "%d{yyyy}/%d{MM}/%d{yyyyMMdd}.log", 73 | new File(String.format( 74 | "%s/%s/%s.log", 75 | formatInstant("yyyy", instant), 76 | formatInstant("MM", instant), 77 | formatInstant("yyyyMMdd", instant)))); 78 | for (String pattern : fileByPattern.keySet()) { 79 | File expectedFile = fileByPattern.get(pattern); 80 | File actualFile = RotatingFilePattern 81 | .builder() 82 | .pattern(pattern) 83 | .locale(Locale.US) 84 | .timeZoneId(UtcHelper.ZONE_ID) 85 | .build() 86 | .create(instant); 87 | assertThat(actualFile).as("pattern=%s", pattern).isEqualTo(expectedFile); 88 | } 89 | } 90 | 91 | private static String formatInstant(String pattern, Instant instant) { 92 | return DateTimeFormatter 93 | .ofPattern(pattern) 94 | .withZone(UtcHelper.ZONE_ID) 95 | .format(instant); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/test/java/com/vlkan/rfos/SchedulerShutdownTestApp.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import com.vlkan.rfos.policy.TimeBasedRotationPolicy; 20 | import org.mockito.Mockito; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import java.io.File; 25 | import java.io.IOException; 26 | import java.nio.charset.StandardCharsets; 27 | import java.time.Duration; 28 | import java.time.Instant; 29 | import java.util.Arrays; 30 | import java.util.LinkedList; 31 | import java.util.Objects; 32 | import java.util.Queue; 33 | import java.util.stream.Collectors; 34 | 35 | public enum SchedulerShutdownTestApp {; 36 | 37 | private static final Logger LOGGER = LoggerFactory.getLogger(SchedulerShutdownTestApp.class); 38 | 39 | private static final class DelayedRotationPolicy extends TimeBasedRotationPolicy { 40 | 41 | private static final Logger LOGGER = LoggerFactory.getLogger(DelayedRotationPolicy.class); 42 | 43 | private final Queue delays; 44 | 45 | private DelayedRotationPolicy(Long... delays) { 46 | this.delays = Arrays 47 | .stream(delays) 48 | .map(Duration::ofMillis) 49 | .collect(Collectors.toCollection(LinkedList::new)); 50 | } 51 | 52 | @Override 53 | public Instant getTriggerInstant(Clock clock) { 54 | Duration delay = delays.poll(); 55 | Objects.requireNonNull(delay, "delay"); 56 | LOGGER.info("setting trigger with delay {}", delay); 57 | return clock.now().plus(delay); 58 | } 59 | 60 | @Override 61 | protected Logger getLogger() { 62 | return LOGGER; 63 | } 64 | 65 | } 66 | 67 | public static void main(String[] args) throws IOException { 68 | 69 | // Determine file names. 70 | String filePrefix = 71 | RotatingFileOutputStream.class.getSimpleName() 72 | + "-" 73 | + SchedulerShutdownTestApp.class.getSimpleName(); 74 | File tmpDir = new File(System.getProperty("java.io.tmpdir")); 75 | File file = new File(tmpDir, filePrefix + ".log"); 76 | String fileName = file.getAbsolutePath(); 77 | String fileNamePattern = new File(tmpDir, filePrefix + "-%d{yyyy}.log").getAbsolutePath(); 78 | 79 | // Create the stream config. 80 | long rotationDelay1Millis = 500L; 81 | long rotationDelay2Millis = 5L * 60L * 1_000L; // 5 minutes 82 | DelayedRotationPolicy policy = new DelayedRotationPolicy(rotationDelay1Millis, rotationDelay2Millis); 83 | RotationCallback callback = Mockito.spy(LoggingRotationCallback.getInstance()); 84 | RotationConfig config = RotationConfig 85 | .builder() 86 | .file(fileName) 87 | .filePattern(fileNamePattern) 88 | .policy(policy) 89 | .callback(callback) 90 | .build(); 91 | 92 | // Create the stream. 93 | LOGGER.info("creating the stream"); 94 | RotatingFileOutputStream stream = new RotatingFileOutputStream(config); 95 | 96 | // Write something to stream to avoid rotation being skipped. 97 | stream.write(filePrefix.getBytes(StandardCharsets.US_ASCII)); 98 | 99 | // Verify the 1st rotation. 100 | LOGGER.info("verifying the 1st rotation"); 101 | long expectedRotationDelay1Millis1 = Math.addExact( 102 | rotationDelay1Millis, 103 | /* extra threshold */ 1_000L); 104 | Mockito 105 | .verify(callback, Mockito.timeout(expectedRotationDelay1Millis1)) 106 | .onTrigger(Mockito.eq(policy), Mockito.any(Instant.class)); 107 | 108 | // Close the stream. 109 | LOGGER.info("closing stream"); 110 | stream.close(); 111 | LOGGER.info("closed stream"); 112 | 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/com/vlkan/rfos/SystemClockTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | import java.time.Instant; 22 | import java.util.Arrays; 23 | import java.util.LinkedHashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | 27 | import static org.assertj.core.api.Assertions.assertThat; 28 | 29 | class SystemClockTest { 30 | 31 | private abstract static class GenericTest implements Runnable { 32 | 33 | abstract protected Map> getCurrentInstantTextsByExpectedInstantText(); 34 | 35 | abstract protected Instant getActualInstant(Clock clock); 36 | 37 | @Override 38 | public void run() { 39 | Map> currentInstantTextsByExpectedInstantText = getCurrentInstantTextsByExpectedInstantText(); 40 | for (String expectedInstantText : currentInstantTextsByExpectedInstantText.keySet()) { 41 | List currentInstantTexts = currentInstantTextsByExpectedInstantText.get(expectedInstantText); 42 | for (String currentInstantText : currentInstantTexts) { 43 | testInstant(currentInstantText, expectedInstantText); 44 | } 45 | } 46 | } 47 | 48 | private void testInstant(String currentInstantText, String expectedInstantText) { 49 | Instant currentInstant = Instant.parse(currentInstantText); 50 | SystemClock clock = new SystemClock() { 51 | @Override 52 | public Instant now() { 53 | return currentInstant; 54 | } 55 | }; 56 | Instant actualInstant = getActualInstant(clock); 57 | String actualInstantText = UtcHelper.INSTANT_FORMATTER.format(actualInstant); 58 | assertThat(actualInstantText) 59 | .as("currentInstantText=%s", currentInstantText) 60 | .isEqualTo(expectedInstantText); 61 | } 62 | 63 | } 64 | 65 | @Test 66 | void test_midnight() { 67 | 68 | // Create test cases. 69 | Map> currentInstantTextsByExpectedInstantText = new LinkedHashMap<>(); 70 | currentInstantTextsByExpectedInstantText.put( 71 | "2017-01-02T00:00:00.000Z", 72 | Arrays.asList( 73 | "2017-01-01T00:00:00.000Z", 74 | "2017-01-01T01:00:00.000Z", 75 | "2017-01-01T23:59:59.999Z")); 76 | currentInstantTextsByExpectedInstantText.put( 77 | "2017-12-30T00:00:00.000Z", 78 | Arrays.asList( 79 | "2017-12-29T00:00:00.000Z", 80 | "2017-12-29T01:00:00.000Z", 81 | "2017-12-29T23:59:59.999Z")); 82 | currentInstantTextsByExpectedInstantText.put( 83 | "2018-01-01T00:00:00.000Z", 84 | Arrays.asList( 85 | "2017-12-31T00:00:00.000Z", 86 | "2017-12-31T01:00:00.000Z", 87 | "2017-12-31T23:59:59.999Z")); 88 | 89 | // Execute tests. 90 | new GenericTest() { 91 | 92 | @Override 93 | protected Map> getCurrentInstantTextsByExpectedInstantText() { 94 | return currentInstantTextsByExpectedInstantText; 95 | } 96 | 97 | @Override 98 | protected Instant getActualInstant(Clock clock) { 99 | return clock.midnight(); 100 | } 101 | 102 | }.run(); 103 | 104 | } 105 | 106 | @Test 107 | void test_sundayMidnight() { 108 | 109 | // Create test cases. 110 | Map> currentInstantTextsByExpectedInstantText = new LinkedHashMap<>(); 111 | currentInstantTextsByExpectedInstantText.put( 112 | "2017-01-02T00:00:00.000Z", 113 | Arrays.asList( 114 | "2017-01-01T00:00:00.000Z", 115 | "2017-01-01T01:00:00.000Z", 116 | "2017-01-01T23:59:59.999Z")); 117 | currentInstantTextsByExpectedInstantText.put( 118 | "2018-01-01T00:00:00.000Z", 119 | Arrays.asList( 120 | "2017-12-25T00:00:00.000Z", 121 | "2017-12-25T01:00:00.000Z", 122 | "2017-12-25T01:59:59.999Z", 123 | "2017-12-26T00:00:00.000Z", 124 | "2017-12-26T01:00:00.000Z", 125 | "2017-12-26T01:59:59.999Z", 126 | "2017-12-31T00:00:00.000Z", 127 | "2017-12-31T01:00:00.000Z", 128 | "2017-12-31T01:59:59.999Z")); 129 | 130 | // Execute tests. 131 | new GenericTest() { 132 | 133 | @Override 134 | protected Map> getCurrentInstantTextsByExpectedInstantText() { 135 | return currentInstantTextsByExpectedInstantText; 136 | } 137 | 138 | @Override 139 | protected Instant getActualInstant(Clock clock) { 140 | return clock.sundayMidnight(); 141 | } 142 | 143 | }.run(); 144 | 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/test/java/com/vlkan/rfos/UtcHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import java.time.ZoneId; 20 | import java.time.format.DateTimeFormatter; 21 | import java.util.Locale; 22 | 23 | public enum UtcHelper {; 24 | 25 | public static final ZoneId ZONE_ID = ZoneId.of("UTC"); 26 | 27 | public static final DateTimeFormatter INSTANT_FORMATTER = DateTimeFormatter 28 | .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") 29 | .withLocale(Locale.US) 30 | .withZone(ZONE_ID); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/vlkan/rfos/policy/ByteMatchingRotationPolicyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos.policy; 18 | 19 | import com.vlkan.rfos.Clock; 20 | import com.vlkan.rfos.Rotatable; 21 | import com.vlkan.rfos.RotationConfig; 22 | import org.assertj.core.api.Assertions; 23 | import org.junit.jupiter.api.Test; 24 | import org.junit.jupiter.api.io.CleanupMode; 25 | import org.junit.jupiter.api.io.TempDir; 26 | import org.mockito.InOrder; 27 | import org.mockito.Mockito; 28 | 29 | import java.io.IOException; 30 | import java.nio.charset.StandardCharsets; 31 | import java.nio.file.Files; 32 | import java.nio.file.Path; 33 | import java.nio.file.StandardOpenOption; 34 | import java.time.Instant; 35 | 36 | class ByteMatchingRotationPolicyTest { 37 | 38 | @Test 39 | void test_invalid_maxOccurrenceCount() { 40 | for (int invalidMaxOccurrenceCount : new int[] {-1, 0}) { 41 | Assertions 42 | .assertThatThrownBy(() -> new ByteMatchingRotationPolicy((byte) '.', invalidMaxOccurrenceCount)) 43 | .isInstanceOf(IllegalArgumentException.class) 44 | .hasMessageContaining("invalid count"); 45 | } 46 | } 47 | 48 | @Test 49 | void test_maxOccurrenceCount_1(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) throws IOException { 50 | 51 | // Create a non-empty initial file 52 | final Path tempFilePath = tempDir.resolve("test.log"); 53 | Files.write( 54 | tempFilePath, 55 | new byte[]{'a', '.'}, 56 | StandardOpenOption.CREATE_NEW, 57 | StandardOpenOption.TRUNCATE_EXISTING); 58 | 59 | // Create a `Rotatable` mock 60 | Rotatable rotatable = Mockito.mock(Rotatable.class); 61 | RotationConfig rotationConfig = Mockito.mock(RotationConfig.class); 62 | Mockito.when(rotationConfig.getFile()).thenReturn(tempFilePath.toFile()); 63 | Clock clock = Mockito.mock(Clock.class); 64 | Mockito.when(clock.now()).thenReturn(Instant.EPOCH); 65 | Mockito.when(rotationConfig.getClock()).thenReturn(clock); 66 | Mockito.when(rotatable.getConfig()).thenReturn(rotationConfig); 67 | 68 | // Create and start the policy 69 | ByteMatchingRotationPolicy policy = new ByteMatchingRotationPolicy((byte) '.', 1); 70 | policy.start(rotatable); 71 | 72 | // Verify the `Rotatable` in order 73 | InOrder inOrder = Mockito.inOrder(rotatable); 74 | 75 | // Test `acceptWrite(int)` 76 | policy.acceptWrite('b'); 77 | policy.acceptWrite('.'); 78 | inOrder.verify(rotatable).rotate(Mockito.same(policy), Mockito.any()); 79 | policy.acceptWrite('c'); 80 | policy.acceptWrite('.'); 81 | inOrder.verify(rotatable).rotate(Mockito.same(policy), Mockito.any()); 82 | policy.acceptWrite('d'); 83 | inOrder.verify(rotatable, Mockito.never()).rotate(Mockito.same(policy), Mockito.any()); 84 | 85 | // Test `acceptWrite(byte[])` 86 | policy.acceptWrite("e.f.g".getBytes(StandardCharsets.US_ASCII)); 87 | inOrder.verify(rotatable).rotate(Mockito.same(policy), Mockito.any()); 88 | 89 | // Test `acceptWrite(byte[],int,int)` 90 | policy.acceptWrite((".." + "h.i.j" + "..").getBytes(StandardCharsets.US_ASCII), 2, 5); 91 | inOrder.verify(rotatable).rotate(Mockito.same(policy), Mockito.any()); 92 | 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/com/vlkan/rfos/policy/DailyRotationPolicyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos.policy; 18 | 19 | import com.vlkan.rfos.Clock; 20 | import com.vlkan.rfos.Rotatable; 21 | import com.vlkan.rfos.RotatingFilePattern; 22 | import com.vlkan.rfos.RotationConfig; 23 | import org.junit.jupiter.api.Test; 24 | import org.mockito.Mockito; 25 | import org.mockito.invocation.InvocationOnMock; 26 | import org.mockito.stubbing.Answer; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | import java.io.File; 31 | import java.time.Duration; 32 | import java.time.Instant; 33 | import java.util.concurrent.ScheduledExecutorService; 34 | import java.util.concurrent.ScheduledFuture; 35 | import java.util.concurrent.TimeUnit; 36 | 37 | class DailyRotationPolicyTest { 38 | 39 | private static final Logger LOGGER = LoggerFactory.getLogger(DailyRotationPolicyTest.class); 40 | 41 | @Test 42 | void test() { 43 | 44 | // Create the scheduler mock. 45 | ScheduledFuture scheduledFuture = Mockito.mock(ScheduledFuture.class); 46 | ScheduledExecutorService executorService = Mockito.mock(ScheduledExecutorService.class); 47 | Mockito 48 | .when(executorService.schedule( 49 | Mockito.any(Runnable.class), 50 | Mockito.anyLong(), 51 | Mockito.same(TimeUnit.NANOSECONDS))) 52 | .thenAnswer(new Answer>() { 53 | 54 | private int invocationCount = 0; 55 | 56 | @Override 57 | public ScheduledFuture answer(InvocationOnMock invocation) { 58 | Runnable runnable = invocation.getArgument(0); 59 | if (++invocationCount < 3) { 60 | runnable.run(); 61 | } else { 62 | LOGGER.trace("skipping execution {invocationCount={}}", invocationCount); 63 | } 64 | return scheduledFuture; 65 | } 66 | 67 | }); 68 | 69 | // Create the clock mock. 70 | Clock clock = Mockito.mock(Clock.class); 71 | Instant midnight1 = Instant.parse("2017-12-29T00:00:00.000Z"); 72 | Duration waitPeriod1 = Duration.ofSeconds(1); 73 | Instant now1 = midnight1.minus(waitPeriod1); 74 | Instant midnight2 = Instant.parse("2017-12-30T00:00:00.000Z"); 75 | Duration waitPeriod2 = Duration.ofSeconds(2); 76 | Instant now2 = midnight2.minus(waitPeriod2); 77 | Mockito 78 | .when(clock.now()) 79 | .thenReturn(now1) 80 | .thenReturn(now2); 81 | Mockito 82 | .when(clock.midnight()) 83 | .thenReturn(midnight1) 84 | .thenReturn(midnight2); 85 | 86 | // Create the config. 87 | DailyRotationPolicy policy = DailyRotationPolicy.getInstance(); 88 | File file = Mockito.mock(File.class); 89 | RotatingFilePattern filePattern = Mockito.mock(RotatingFilePattern.class); 90 | RotationConfig config = RotationConfig 91 | .builder() 92 | .file(file) 93 | .filePattern(filePattern) 94 | .clock(clock) 95 | .executorService(executorService) 96 | .policy(policy) 97 | .build(); 98 | 99 | // Create the rotatable mock. 100 | Rotatable rotatable = Mockito.mock(Rotatable.class); 101 | Mockito.when(rotatable.getConfig()).thenReturn(config); 102 | 103 | // Start policy. 104 | policy.start(rotatable); 105 | 106 | // Verify the 1st execution. 107 | Mockito 108 | .verify(executorService) 109 | .schedule( 110 | Mockito.any(Runnable.class), 111 | Mockito.eq(waitPeriod1.toNanos()), 112 | Mockito.same(TimeUnit.NANOSECONDS)); 113 | 114 | // Verify the 1st rotation. 115 | Mockito 116 | .verify(rotatable) 117 | .rotate(Mockito.same(policy), Mockito.eq(midnight1)); 118 | 119 | // Verify the 2nd execution. 120 | Mockito 121 | .verify(executorService, Mockito.atLeastOnce()) 122 | .schedule( 123 | Mockito.any(Runnable.class), 124 | Mockito.eq(waitPeriod2.toNanos()), 125 | Mockito.same(TimeUnit.NANOSECONDS)); 126 | 127 | // Verify the 2nd rotation. 128 | Mockito 129 | .verify(rotatable) 130 | .rotate(Mockito.same(policy), Mockito.eq(midnight2)); 131 | 132 | // Close the policy. 133 | policy.stop(); 134 | 135 | // Verify the task cancellation. 136 | Mockito 137 | .verify(scheduledFuture) 138 | .cancel(Mockito.same(true)); 139 | 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/test/java/com/vlkan/rfos/policy/SizeBasedRotationPolicyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos.policy; 18 | 19 | import org.assertj.core.api.Assertions; 20 | import org.junit.jupiter.api.Test; 21 | 22 | class SizeBasedRotationPolicyTest { 23 | 24 | @Test 25 | void test_invalid_maxByteCount() { 26 | for (int invalidMaxByteCount : new int[] {-1, 0}) { 27 | Assertions 28 | .assertThatThrownBy(() -> new SizeBasedRotationPolicy(invalidMaxByteCount)) 29 | .isInstanceOf(IllegalArgumentException.class) 30 | .hasMessageContaining("invalid size"); 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/vlkan/rfos/policy/TimeBasedRotationPolicyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos.policy; 18 | 19 | import com.vlkan.rfos.Clock; 20 | import com.vlkan.rfos.Rotatable; 21 | import com.vlkan.rfos.RotationConfig; 22 | import org.junit.jupiter.api.Test; 23 | import org.mockito.invocation.InvocationOnMock; 24 | import org.mockito.stubbing.Answer; 25 | import org.slf4j.Logger; 26 | 27 | import java.time.Instant; 28 | import java.util.concurrent.ScheduledExecutorService; 29 | import java.util.concurrent.ScheduledFuture; 30 | import java.util.concurrent.atomic.AtomicInteger; 31 | 32 | import static java.time.Instant.EPOCH; 33 | import static org.mockito.ArgumentMatchers.any; 34 | import static org.mockito.ArgumentMatchers.anyLong; 35 | import static org.mockito.Mockito.mock; 36 | import static org.mockito.Mockito.times; 37 | import static org.mockito.Mockito.verify; 38 | import static org.mockito.Mockito.when; 39 | 40 | class TimeBasedRotationPolicyTest { 41 | 42 | @Test 43 | void quick_task_scheduling_should_not_cause_repeated_rotations() { 44 | 45 | // Mock a system where everything happens very fast, that is, at the very same time! 46 | // This is a contrived assumption, but probable. 47 | // This can happen due to: 48 | // 1. Code execution is faster than the time resolution provided by the clock 49 | // 2. Clocks can return a value twice (due to daylight time savings, monotonically-increasing design, etc.) 50 | Clock clock = mock(Clock.class); 51 | when(clock.now()).thenReturn(EPOCH); 52 | 53 | // Create an executor that runs *the first two* tasks immediately (i.e., without any delay!) and discards the rest. 54 | // Why the first two? 55 | // 1. The first time `start()` is run manually (by `RotatingFileOutputStream`), it will schedule a task. 56 | // 2. When `start()` is run by the scheduled task, it will schedule a task, again. 57 | // 3. We need to stop here, otherwise we will be looping around step #2. 58 | ScheduledExecutorService executor = mock(ScheduledExecutorService.class); 59 | when(executor.schedule(any(Runnable.class), anyLong(), any())) 60 | .thenAnswer(new Answer>() { 61 | 62 | private final AtomicInteger invocationCounter = new AtomicInteger(0); 63 | 64 | @Override 65 | public ScheduledFuture answer(InvocationOnMock invocation) { 66 | if (invocationCounter.getAndIncrement() < 2) { 67 | Runnable task = invocation.getArgument(0); 68 | task.run(); 69 | } 70 | return null; 71 | } 72 | 73 | }); 74 | 75 | // Create the rotation configuration 76 | RotationConfig config = mock(RotationConfig.class); 77 | when(config.getClock()).thenReturn(clock); 78 | when(config.getExecutorService()).thenReturn(executor); 79 | 80 | // Create the rotatable 81 | Rotatable rotatable = mock(Rotatable.class); 82 | when(rotatable.getConfig()).thenReturn(config); 83 | 84 | // Create and start the policy 85 | PerNanoRotationPolicy policy = new PerNanoRotationPolicy(); 86 | policy.start(rotatable); 87 | 88 | // Verify there was only a single rotation 89 | verify(rotatable, times(1)).rotate(any(), any()); 90 | 91 | } 92 | 93 | private static class PerNanoRotationPolicy extends TimeBasedRotationPolicy { 94 | 95 | private static final Logger LOGGER = mock(Logger.class); 96 | 97 | @Override 98 | public Instant getTriggerInstant(Clock clock) { 99 | // Choose a sub-millisecond delay 100 | return clock.now().plusNanos(1); 101 | } 102 | 103 | @Override 104 | protected Logger getLogger() { 105 | return LOGGER; 106 | } 107 | 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/test/perf/com/vlkan/rfos/RotatingFileOutputStreamBenchmark.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 Volkan Yazıcı 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 permits and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.vlkan.rfos; 18 | 19 | import org.openjdk.jmh.annotations.Benchmark; 20 | import org.openjdk.jmh.annotations.Scope; 21 | import org.openjdk.jmh.annotations.State; 22 | import org.openjdk.jmh.results.format.ResultFormatType; 23 | import org.openjdk.jmh.runner.Runner; 24 | import org.openjdk.jmh.runner.options.ChainedOptionsBuilder; 25 | import org.openjdk.jmh.runner.options.Options; 26 | import org.openjdk.jmh.runner.options.OptionsBuilder; 27 | import org.openjdk.jmh.runner.options.TimeValue; 28 | 29 | import java.io.*; 30 | import java.net.URL; 31 | import java.net.URLClassLoader; 32 | import java.util.concurrent.ThreadLocalRandom; 33 | 34 | public class RotatingFileOutputStreamBenchmark { 35 | 36 | public static void main(String[] args) throws Exception { 37 | fixJavaClassPath(); 38 | setLog4jConfig(); 39 | ChainedOptionsBuilder optionsBuilder = new OptionsBuilder() 40 | .include(RotatingFileOutputStreamBenchmark.class.getSimpleName()) 41 | .forks(2) 42 | .warmupIterations(3) 43 | .warmupTime(TimeValue.seconds(20)) 44 | .measurementIterations(3) 45 | .measurementTime(TimeValue.seconds(30)); 46 | configJmhQuickRun(optionsBuilder); 47 | configJmhJsonOutput(optionsBuilder); 48 | configJmhConcurrency(optionsBuilder); 49 | Options options = optionsBuilder.build(); 50 | new Runner(options).run(); 51 | } 52 | 53 | /** 54 | * Add project dependencies to java.class.path property used by JMH. 55 | * 56 | * @see How to Run a JMH Benchmark in Maven Using exec:java Instead of exec:exec 57 | */ 58 | private static void fixJavaClassPath() { 59 | URLClassLoader classLoader = (URLClassLoader) RotatingFileOutputStreamBenchmark.class.getClassLoader(); 60 | StringBuilder classpathBuilder = new StringBuilder(); 61 | for (URL url : classLoader.getURLs()) { 62 | String urlPath = url.getPath(); 63 | classpathBuilder.append(urlPath).append(File.pathSeparator); 64 | } 65 | String classpath = classpathBuilder.toString(); 66 | System.setProperty("java.class.path", classpath); 67 | } 68 | 69 | private static void setLog4jConfig() { 70 | System.setProperty("log4j.configurationFile", "log4j2-quiet.xml"); 71 | } 72 | 73 | private static void configJmhQuickRun(ChainedOptionsBuilder optionsBuilder) { 74 | String quick = System.getProperty("rfos.benchmark.quick"); 75 | if (quick != null) { 76 | optionsBuilder 77 | .forks(0) 78 | .warmupIterations(0) 79 | .measurementIterations(1) 80 | .measurementTime(TimeValue.seconds(3)); 81 | } 82 | } 83 | 84 | private static void configJmhJsonOutput(ChainedOptionsBuilder optionsBuilder) { 85 | String jsonOutputFile = System.getProperty("rfos.benchmark.jsonOutputFile"); 86 | if (jsonOutputFile != null) { 87 | optionsBuilder 88 | .resultFormat(ResultFormatType.JSON) 89 | .result(jsonOutputFile); 90 | } 91 | } 92 | 93 | private static void configJmhConcurrency(ChainedOptionsBuilder optionsBuilder) { 94 | String concurrencyString = System.getProperty("rfos.benchmark.concurrency"); 95 | if (concurrencyString != null) { 96 | int concurrency = Integer.parseInt(concurrencyString); 97 | optionsBuilder.threads(concurrency); 98 | } 99 | } 100 | 101 | @State(Scope.Thread) 102 | public static class Writer { 103 | 104 | private static final int BUFFER_SIZE = 16 * 1_024; 105 | 106 | private final byte[] buffer = new byte[BUFFER_SIZE]; 107 | 108 | private int index = 0; 109 | 110 | public Writer() { 111 | ThreadLocalRandom.current().nextBytes(buffer); 112 | } 113 | 114 | private int writeByte(OutputStream outputStream) throws IOException { 115 | byte b = buffer[index]; 116 | outputStream.write(b); 117 | return shiftIndex(); 118 | } 119 | 120 | private int writeByteArray(OutputStream outputStream) throws IOException { 121 | outputStream.write(buffer, index, buffer.length - index); 122 | return shiftIndex(); 123 | } 124 | 125 | private int shiftIndex() { 126 | return (index = ++index % BUFFER_SIZE); 127 | } 128 | 129 | } 130 | 131 | @State(Scope.Benchmark) 132 | public static class FosSource { 133 | 134 | private final OutputStream outputStream; 135 | 136 | public FosSource() { 137 | String path = System.getProperty("os.name").toLowerCase().contains("win") 138 | ? "C:\\nul" // https://stackoverflow.com/a/27773642/1278899 139 | : "/dev/null"; 140 | try { 141 | this.outputStream = new FileOutputStream(path); 142 | } catch (FileNotFoundException error) { 143 | throw new IllegalStateException(error); 144 | } 145 | } 146 | 147 | } 148 | 149 | @Benchmark 150 | public static int fos_1b(FosSource source, Writer writer) throws IOException { 151 | return writer.writeByte(source.outputStream); 152 | } 153 | 154 | @Benchmark 155 | public static int fos_ba(FosSource source, Writer writer) throws IOException { 156 | return writer.writeByteArray(source.outputStream); 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /src/test/resources/log4j2-quiet.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/test/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | --------------------------------------------------------------------------------