├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── release.yml │ └── run-ui-tests.yml ├── .gitignore ├── .idea └── gradle.xml ├── .run ├── Run Plugin.run.xml ├── Run Tests.run.xml └── Run Verifications.run.xml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── example └── Example.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── media ├── context-menu.png ├── log-colors.png ├── screenshot-installation.png ├── screenshot_basic.png └── screenshot_expanded.png ├── qodana.yml ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── io │ │ └── github │ │ └── orangain │ │ └── prettyjsonlog │ │ ├── MyBundle.kt │ │ ├── action │ │ └── ToggleEnabledAction.kt │ │ ├── console │ │ ├── MyConsoleFolding.kt │ │ └── MyConsoleInputFilterProvider.kt │ │ ├── json │ │ ├── Mapper.kt │ │ ├── Parse.kt │ │ └── PrettyPrint.kt │ │ ├── listeners │ │ └── MyApplicationActivationListener.kt │ │ ├── logentry │ │ ├── Level.kt │ │ ├── Message.kt │ │ ├── NodeExtractor.kt │ │ ├── StackTrace.kt │ │ └── Timestamp.kt │ │ └── service │ │ └── EphemeralStateService.kt └── resources │ ├── META-INF │ ├── plugin.xml │ └── pluginIcon.svg │ └── messages │ └── MyBundle.properties └── test └── kotlin └── io └── github └── orangain └── prettyjsonlog ├── json ├── FoldingTest.kt ├── NotFoldingTest.kt └── ParseTest.kt └── logentry ├── ExtractTest.kt └── TimestampTest.kt /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration: 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Maintain dependencies for Gradle dependencies 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | # Maintain dependencies for GitHub Actions 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow is created for testing and preparing the plugin release in the following steps: 2 | # - Validate Gradle Wrapper. 3 | # - Run 'test' and 'verifyPlugin' tasks. 4 | # - Run Qodana inspections. 5 | # - Run the 'buildPlugin' task and prepare artifact for further tests. 6 | # - Run the 'runPluginVerifier' task. 7 | # - Create a draft release. 8 | # 9 | # The workflow is triggered on push and pull_request events. 10 | # 11 | # GitHub Actions reference: https://help.github.com/en/actions 12 | # 13 | ## JBIJPPTPL 14 | 15 | name: Build 16 | on: 17 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g., for dependabot pull requests) 18 | push: 19 | branches: [ main ] 20 | # Trigger the workflow on any pull request 21 | pull_request: 22 | 23 | concurrency: 24 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 25 | cancel-in-progress: true 26 | 27 | jobs: 28 | 29 | # Prepare environment and build the plugin 30 | build: 31 | name: Build 32 | runs-on: ubuntu-latest 33 | outputs: 34 | version: ${{ steps.properties.outputs.version }} 35 | changelog: ${{ steps.properties.outputs.changelog }} 36 | pluginVerifierHomeDir: ${{ steps.properties.outputs.pluginVerifierHomeDir }} 37 | steps: 38 | 39 | # Check out the current repository 40 | - name: Fetch Sources 41 | uses: actions/checkout@v4 42 | 43 | # Validate wrapper 44 | - name: Gradle Wrapper Validation 45 | uses: gradle/wrapper-validation-action@v3.5.0 46 | 47 | # Set up Java environment for the next steps 48 | - name: Setup Java 49 | uses: actions/setup-java@v4 50 | with: 51 | distribution: temurin 52 | java-version: 21 53 | 54 | # Setup Gradle 55 | - name: Setup Gradle 56 | uses: gradle/actions/setup-gradle@v4 57 | 58 | # Set environment variables 59 | - name: Export Properties 60 | id: properties 61 | shell: bash 62 | run: | 63 | PROPERTIES="$(./gradlew properties --console=plain -q)" 64 | VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" 65 | CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" 66 | 67 | echo "version=$VERSION" >> $GITHUB_OUTPUT 68 | echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT 69 | 70 | echo "changelog<> $GITHUB_OUTPUT 71 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 72 | echo "EOF" >> $GITHUB_OUTPUT 73 | 74 | # Build plugin 75 | - name: Build plugin 76 | run: ./gradlew buildPlugin 77 | 78 | # Prepare plugin archive content for creating artifact 79 | - name: Prepare Plugin Artifact 80 | id: artifact 81 | shell: bash 82 | run: | 83 | cd ${{ github.workspace }}/build/distributions 84 | FILENAME=`ls *.zip` 85 | unzip "$FILENAME" -d content 86 | 87 | echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT 88 | 89 | # Store already-built plugin as an artifact for downloading 90 | - name: Upload artifact 91 | uses: actions/upload-artifact@v4 92 | with: 93 | name: ${{ steps.artifact.outputs.filename }} 94 | path: ./build/distributions/content/*/* 95 | 96 | # Run tests and upload a code coverage report 97 | test: 98 | name: Test 99 | needs: [ build ] 100 | runs-on: ubuntu-latest 101 | steps: 102 | 103 | # Check out the current repository 104 | - name: Fetch Sources 105 | uses: actions/checkout@v4 106 | 107 | # Set up Java environment for the next steps 108 | - name: Setup Java 109 | uses: actions/setup-java@v4 110 | with: 111 | distribution: temurin 112 | java-version: 21 113 | 114 | # Setup Gradle 115 | - name: Setup Gradle 116 | uses: gradle/actions/setup-gradle@v4 117 | 118 | # Run tests 119 | - name: Run Tests 120 | run: ./gradlew check 121 | 122 | # Collect Tests Result of failed tests 123 | - name: Collect Tests Result 124 | if: ${{ failure() }} 125 | uses: actions/upload-artifact@v4 126 | with: 127 | name: tests-result 128 | path: ${{ github.workspace }}/build/reports/tests 129 | 130 | # Upload the Kover report to CodeCov 131 | - name: Upload Code Coverage Report 132 | uses: codecov/codecov-action@v5 133 | with: 134 | files: ${{ github.workspace }}/build/reports/kover/report.xml 135 | 136 | # Run Qodana inspections and provide report 137 | inspectCode: 138 | name: Inspect code 139 | needs: [ build ] 140 | runs-on: ubuntu-latest 141 | permissions: 142 | contents: write 143 | checks: write 144 | pull-requests: write 145 | steps: 146 | 147 | # Free GitHub Actions Environment Disk Space 148 | - name: Maximize Build Space 149 | uses: jlumbroso/free-disk-space@main 150 | with: 151 | tool-cache: false 152 | large-packages: false 153 | 154 | # Check out the current repository 155 | - name: Fetch Sources 156 | uses: actions/checkout@v4 157 | with: 158 | ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit 159 | fetch-depth: 0 # a full history is required for pull request analysis 160 | 161 | # Set up Java environment for the next steps 162 | - name: Setup Java 163 | uses: actions/setup-java@v4 164 | with: 165 | distribution: temurin 166 | java-version: 21 167 | 168 | # Run Qodana inspections 169 | - name: Qodana - Code Inspection 170 | uses: JetBrains/qodana-action@v2024.3 171 | with: 172 | cache-default-branch-only: true 173 | 174 | # Run plugin structure verification along with IntelliJ Plugin Verifier 175 | verify: 176 | name: Verify plugin 177 | needs: [ build ] 178 | runs-on: ubuntu-latest 179 | steps: 180 | 181 | # Free GitHub Actions Environment Disk Space 182 | - name: Maximize Build Space 183 | uses: jlumbroso/free-disk-space@main 184 | with: 185 | tool-cache: false 186 | large-packages: false 187 | 188 | # Check out the current repository 189 | - name: Fetch Sources 190 | uses: actions/checkout@v4 191 | 192 | # Set up Java environment for the next steps 193 | - name: Setup Java 194 | uses: actions/setup-java@v4 195 | with: 196 | distribution: temurin 197 | java-version: 21 198 | 199 | # Setup Gradle 200 | - name: Setup Gradle 201 | uses: gradle/actions/setup-gradle@v4 202 | 203 | # Cache Plugin Verifier IDEs 204 | - name: Setup Plugin Verifier IDEs Cache 205 | uses: actions/cache@v4 206 | with: 207 | path: ${{ needs.build.outputs.pluginVerifierHomeDir }}/ides 208 | key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} 209 | 210 | # Run Verify Plugin task and IntelliJ Plugin Verifier tool 211 | - name: Run Plugin Verification tasks 212 | run: ./gradlew verifyPlugin -Dplugin.verifier.home.dir=${{ needs.build.outputs.pluginVerifierHomeDir }} 213 | 214 | # Collect Plugin Verifier Result 215 | - name: Collect Plugin Verifier Result 216 | if: ${{ always() }} 217 | uses: actions/upload-artifact@v4 218 | with: 219 | name: pluginVerifier-result 220 | path: ${{ github.workspace }}/build/reports/pluginVerifier 221 | 222 | # Prepare a draft release for GitHub Releases page for the manual verification 223 | # If accepted and published, release workflow would be triggered 224 | releaseDraft: 225 | name: Release draft 226 | if: github.event_name != 'pull_request' 227 | needs: [ build, test, inspectCode, verify ] 228 | runs-on: ubuntu-latest 229 | permissions: 230 | contents: write 231 | steps: 232 | 233 | # Check out the current repository 234 | - name: Fetch Sources 235 | uses: actions/checkout@v4 236 | 237 | # Remove old release drafts by using the curl request for the available releases with a draft flag 238 | - name: Remove Old Release Drafts 239 | env: 240 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 241 | run: | 242 | gh api repos/{owner}/{repo}/releases \ 243 | --jq '.[] | select(.draft == true) | .id' \ 244 | | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} 245 | 246 | # Create a new release draft which is not publicly visible and requires manual acceptance 247 | - name: Create Release Draft 248 | env: 249 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 250 | run: | 251 | gh release create "v${{ needs.build.outputs.version }}" \ 252 | --draft \ 253 | --title "v${{ needs.build.outputs.version }}" \ 254 | --notes "$(cat << 'EOM' 255 | ${{ needs.build.outputs.changelog }} 256 | EOM 257 | )" 258 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow. 2 | # Running the publishPlugin task requires all following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN. 3 | # See https://plugins.jetbrains.com/docs/intellij/plugin-signing.html for more information. 4 | 5 | name: Release 6 | on: 7 | release: 8 | types: [ prereleased, released ] 9 | 10 | jobs: 11 | 12 | # Prepare and publish the plugin to JetBrains Marketplace repository 13 | release: 14 | name: Publish Plugin 15 | runs-on: ubuntu-latest 16 | environment: release 17 | permissions: 18 | contents: write 19 | pull-requests: write 20 | steps: 21 | 22 | # Free GitHub Actions Environment Disk Space 23 | - name: Maximize Build Space 24 | uses: jlumbroso/free-disk-space@main 25 | with: 26 | tool-cache: false 27 | large-packages: false 28 | 29 | # Check out the current repository 30 | - name: Fetch Sources 31 | uses: actions/checkout@v4 32 | with: 33 | ref: ${{ github.event.release.tag_name }} 34 | 35 | # Set up Java environment for the next steps 36 | - name: Setup Java 37 | uses: actions/setup-java@v4 38 | with: 39 | distribution: temurin 40 | java-version: 21 41 | 42 | # Setup Gradle 43 | - name: Setup Gradle 44 | uses: gradle/actions/setup-gradle@v4 45 | 46 | # Set environment variables 47 | - name: Export Properties 48 | id: properties 49 | shell: bash 50 | run: | 51 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 52 | ${{ github.event.release.body }} 53 | EOM 54 | )" 55 | 56 | echo "changelog<> $GITHUB_OUTPUT 57 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 58 | echo "EOF" >> $GITHUB_OUTPUT 59 | 60 | # Update the Unreleased section with the current release note 61 | - name: Patch Changelog 62 | if: ${{ steps.properties.outputs.changelog != '' }} 63 | env: 64 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 65 | run: | 66 | ./gradlew patchChangelog --release-note="$CHANGELOG" 67 | 68 | # Publish the plugin to JetBrains Marketplace 69 | - name: Publish Plugin 70 | env: 71 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 72 | CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} 73 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 74 | PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} 75 | run: ./gradlew publishPlugin 76 | 77 | # Upload artifact as a release asset 78 | - name: Upload Release Asset 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* 82 | 83 | # Create a pull request 84 | - name: Create Pull Request 85 | if: ${{ steps.properties.outputs.changelog != '' }} 86 | env: 87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 88 | run: | 89 | VERSION="${{ github.event.release.tag_name }}" 90 | BRANCH="changelog-update-$VERSION" 91 | LABEL="release changelog" 92 | 93 | git config user.email "action@github.com" 94 | git config user.name "GitHub Action" 95 | 96 | git checkout -b $BRANCH 97 | git commit -am "Changelog update - $VERSION" 98 | git push --set-upstream origin $BRANCH 99 | 100 | gh label create "$LABEL" \ 101 | --description "Pull requests with release changelog update" \ 102 | --force \ 103 | || true 104 | 105 | gh pr create \ 106 | --title "Changelog update - \`$VERSION\`" \ 107 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 108 | --label "$LABEL" \ 109 | --head $BRANCH 110 | -------------------------------------------------------------------------------- /.github/workflows/run-ui-tests.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps: 2 | # - Prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with the UI. 3 | # - Wait for IDE to start. 4 | # - Run UI tests with a separate Gradle task. 5 | # 6 | # Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform. 7 | # 8 | # Workflow is triggered manually. 9 | 10 | name: Run UI Tests 11 | on: 12 | workflow_dispatch 13 | 14 | jobs: 15 | 16 | testUI: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - os: ubuntu-latest 23 | runIde: | 24 | export DISPLAY=:99.0 25 | Xvfb -ac :99 -screen 0 1920x1080x16 & 26 | gradle runIdeForUiTests & 27 | - os: windows-latest 28 | runIde: start gradlew.bat runIdeForUiTests 29 | - os: macos-latest 30 | runIde: ./gradlew runIdeForUiTests & 31 | 32 | steps: 33 | 34 | # Check out the current repository 35 | - name: Fetch Sources 36 | uses: actions/checkout@v4 37 | 38 | # Set up Java environment for the next steps 39 | - name: Setup Java 40 | uses: actions/setup-java@v4 41 | with: 42 | distribution: temurin 43 | java-version: 21 44 | 45 | # Setup Gradle 46 | - name: Setup Gradle 47 | uses: gradle/actions/setup-gradle@v4 48 | 49 | # Run IDEA prepared for UI testing 50 | - name: Run IDE 51 | run: ${{ matrix.runIde }} 52 | 53 | # Wait for IDEA to be started 54 | - name: Health Check 55 | uses: jtalk/url-health-check-action@v4 56 | with: 57 | url: http://127.0.0.1:8082 58 | max-attempts: 15 59 | retry-delay: 30s 60 | 61 | # Run tests 62 | - name: Tests 63 | run: ./gradlew test 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | .intellijPlatform 4 | .qodana 5 | build 6 | .kotlin 7 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | -------------------------------------------------------------------------------- /.run/Run Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | false 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.run/Run Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | true 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.run/Run Verifications.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | false 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # pretty-json-log-plugin Changelog 4 | 5 | ## [Unreleased] 6 | 7 | ## [0.5.0] - 2024-10-17 8 | 9 | ### Added 10 | 11 | - Support for timestampNanos and timestampSeconds of StackdriverJsonLayout by @LeeGilbert 12 | - Support for IntelliJ IDEA 2024.3 13 | 14 | ## [0.4.1] - 2024-10-05 15 | 16 | ### Fixed 17 | 18 | - Fix error when opening context menu, reported by @sabob 19 | 20 | ## [0.4.0] - 2024-09-08 21 | 22 | ### Added 23 | 24 | - Support for Serilog reported by @Twinki14 25 | - Context menu item to toggle formatting in the console 26 | 27 | ## [0.3.0] - 2024-08-16 28 | 29 | ### Added 30 | 31 | - Support for uber-go/zap by @Sxtanna 32 | - Detect epoch precision automatically 33 | - Prevent folding of stacktrace inside JSON object by default 34 | 35 | ## [0.2.0] - 2024-07-18 36 | 37 | ### Added 38 | 39 | - Showing stacktrace when present by @vonProteus 40 | - Support for log level in Elastic Common Schema (ECS) by @vonProteus 41 | 42 | ## [0.1.0] - 2024-06-22 43 | 44 | ### Added 45 | 46 | - Prepare for publishing plugin 47 | 48 | ## [0.0.1] - 2024-05-11 49 | 50 | ### Added 51 | 52 | - Initial scaffold created 53 | from [IntelliJ Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template) 54 | 55 | [Unreleased]: https://github.com/orangain/pretty-json-log-plugin/compare/v0.5.0...HEAD 56 | [0.5.0]: https://github.com/orangain/pretty-json-log-plugin/compare/v0.4.1...v0.5.0 57 | [0.4.1]: https://github.com/orangain/pretty-json-log-plugin/compare/v0.4.0...v0.4.1 58 | [0.4.0]: https://github.com/orangain/pretty-json-log-plugin/compare/v0.3.0...v0.4.0 59 | [0.3.0]: https://github.com/orangain/pretty-json-log-plugin/compare/v0.2.0...v0.3.0 60 | [0.2.0]: https://github.com/orangain/pretty-json-log-plugin/compare/v0.1.0...v0.2.0 61 | [0.1.0]: https://github.com/orangain/pretty-json-log-plugin/compare/v0.0.1...v0.1.0 62 | [0.0.1]: https://github.com/orangain/pretty-json-log-plugin/commits/v0.0.1 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 orangain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pretty JSON Log plugin 2 | 3 | ![Build](https://github.com/orangain/pretty-json-log-plugin/workflows/Build/badge.svg) 4 | [![Version](https://img.shields.io/jetbrains/plugin/v/io.github.orangain.prettyjsonlog.svg)](https://plugins.jetbrains.com/plugin/24693-pretty-json-log/versions) 5 | [![Downloads](https://img.shields.io/jetbrains/plugin/d/io.github.orangain.prettyjsonlog.svg)](https://plugins.jetbrains.com/plugin/24693-pretty-json-log) 6 | 7 | ![Plugin screenshot](media/screenshot_expanded.png) 8 | 9 | 10 | Pretty JSON Log plugin for IntelliJ Platform makes NDJSON (Newline Delimited JSON a.k.a. JSON Lines) logs more readable 11 | in the console. It has the following features: 12 | 13 | - **JSON Parsing**: Automatically parses each log line as JSON and extracts essential log information such as timestamp, 14 | log level, and message. 15 | - **Colorful Display**: Displays essential log information in different colors depending on the log level to make it 16 | easier to read. 17 | - **Readable Timestamp**: Formats the timestamp in a human-friendly format. 18 | - **Expandable Pretty JSON**: Prints a well-formatted JSON string following the log message. The JSON string is folded 19 | by default, but you can expand it when you need to view the full details. 20 | - **Seamless Integration**: Supports various log formats such 21 | as [Logstash](https://github.com/logfellow/logstash-logback-encoder), [Bunyan](https://github.com/trentm/node-bunyan), 22 | [Pino](https://github.com/pinojs/pino), [log/slog](https://pkg.go.dev/log/slog), 23 | [Cloud Logging](https://cloud.google.com/logging/docs/structured-logging), etc. with no additional configuration or 24 | software. 25 | 26 | This plugin is useful when you are developing a modern system that outputs logs in JSON format. You no longer need to 27 | switch log formats between production and local development environments. 28 | 29 | 30 | ## Installation 31 | 32 | - Using the IDE built-in plugin system: 33 | 34 | Settings/Preferences > Plugins > Marketplace > Search for "Pretty JSON 35 | Log" > Install 36 | 37 | ![Installation dialog](media/screenshot-installation.png) 38 | 39 | - Manually: 40 | 41 | Download the [latest release](https://github.com/orangain/pretty-json-log-plugin/releases/latest) and install it 42 | manually using 43 | Settings/Preferences > Plugins > ⚙️ > Install plugin from disk... 44 | 45 | After installation, newly written JSON lines logs in the console will be automatically formatted by the plugin. 46 | 47 | ## Limitations 48 | 49 | JetBrains IDEs provide different kinds of consoles for different run configurations, but this plugin does not currently 50 | support all consoles due 51 | to [the lack of extension points in the IDE](https://youtrack.jetbrains.com/issue/IJPL-60196/TerminalExecutionConsole-Extension-points-support). 52 | If the console does not have the context menu item "Pretty JSON Log", the plugin is not supported in that console. 53 | 54 | Context menu item with text 'Pretty JSON Log' 55 | 56 | ## How to configure log colors 57 | 58 | You can configure log colors from the Settings > Editor > Color Scheme > Console 59 | Colors > Log console. 60 | 61 | ![Settings for log colors](media/log-colors.png) 62 | 63 | ## How to see the debug log of the plugin 64 | 65 | 1. Help > Diagnostic Tools > Debug Log Settings.... 66 | 2. Add line `#io.github.orangain.prettyjsonlog` to the text area of the dialog and click OK. 67 | 3. Reproduce the issue. 68 | 4. Help > Show Log in Finder/Explorer to open the log directory. 69 | 5. Open the `idea.log` file and find the log of the plugin by searching for `#io.github.orangain.prettyjsonlog`. 70 | 71 | Enabling debug logging may slow down the IDE, so it is recommended to disable it after reproducing the issue. 72 | 73 | ## Acknowledgements 74 | 75 | This plugin is inspired by the [pino-pretty](https://github.com/pinojs/pino-pretty) 76 | and [bunyan CLI](https://github.com/trentm/node-bunyan). The great idea behind these tools is that applications should 77 | write logs in machine-readable format (JSON) and pretty-printing for human readability should be done by another tool. I 78 | am grateful to the authors of these tools. 79 | 80 | Thanks to the [IntelliJ Platform Plugin Template][template], I was able to quickly start developing the plugin. 81 | 82 | [template]: https://github.com/JetBrains/intellij-platform-plugin-template 83 | 84 | [docs:plugin-description]: https://plugins.jetbrains.com/docs/intellij/plugin-user-experience.html#plugin-description-and-presentation 85 | 86 | ## Internal 87 | 88 | ### How to release 89 | 90 | 1. Update `pluginVersion` in `gradle.properties` and merge it into the main branch. 91 | 2. Publish a release note with the new version. 92 | 3. Approve deployment. 93 | 4. The new version will be reviewed by JetBrains and if there are no problems, it will be published in Marketplace. 94 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.changelog.Changelog 2 | import org.jetbrains.changelog.markdownToHTML 3 | import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType 4 | import org.jetbrains.intellij.platform.gradle.TestFrameworkType 5 | import org.jetbrains.intellij.platform.gradle.models.ProductRelease 6 | 7 | plugins { 8 | id("java") // Java support 9 | alias(libs.plugins.kotlin) // Kotlin support 10 | alias(libs.plugins.intelliJPlatform) // IntelliJ Platform Gradle Plugin 11 | alias(libs.plugins.changelog) // Gradle Changelog Plugin 12 | alias(libs.plugins.qodana) // Gradle Qodana Plugin 13 | alias(libs.plugins.kover) // Gradle Kover Plugin 14 | } 15 | 16 | group = providers.gradleProperty("pluginGroup").get() 17 | version = providers.gradleProperty("pluginVersion").get() 18 | 19 | // Set the JVM language level used to build the project. 20 | kotlin { 21 | jvmToolchain(21) 22 | } 23 | 24 | // Configure project's dependencies 25 | repositories { 26 | mavenCentral() 27 | 28 | // IntelliJ Platform Gradle Plugin Repositories Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-repositories-extension.html 29 | intellijPlatform { 30 | defaultRepositories() 31 | } 32 | } 33 | 34 | // Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog 35 | dependencies { 36 | testImplementation(libs.junit) 37 | implementation(libs.jacksonModuleKotlin) 38 | 39 | // IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html 40 | intellijPlatform { 41 | create(providers.gradleProperty("platformType"), providers.gradleProperty("platformVersion")) 42 | 43 | // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. 44 | bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(',') }) 45 | 46 | // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file for plugin from JetBrains Marketplace. 47 | plugins(providers.gradleProperty("platformPlugins").map { it.split(',') }) 48 | 49 | pluginVerifier() 50 | zipSigner() 51 | testFramework(TestFrameworkType.Platform) 52 | } 53 | } 54 | 55 | // Configure IntelliJ Platform Gradle Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-extension.html 56 | intellijPlatform { 57 | pluginConfiguration { 58 | version = providers.gradleProperty("pluginVersion") 59 | 60 | // Extract the section from README.md and provide for the plugin's manifest 61 | description = providers.fileContents(layout.projectDirectory.file("README.md")).asText.map { 62 | val start = "" 63 | val end = "" 64 | 65 | with(it.lines()) { 66 | if (!containsAll(listOf(start, end))) { 67 | throw GradleException("Plugin description section not found in README.md:\n$start ... $end") 68 | } 69 | subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML) 70 | } 71 | } 72 | 73 | val changelog = project.changelog // local variable for configuration cache compatibility 74 | // Get the latest available change notes from the changelog file 75 | changeNotes = providers.gradleProperty("pluginVersion").map { pluginVersion -> 76 | with(changelog) { 77 | renderItem( 78 | (getOrNull(pluginVersion) ?: getUnreleased()) 79 | .withHeader(false) 80 | .withEmptySections(false), 81 | Changelog.OutputType.HTML, 82 | ) 83 | } 84 | } 85 | 86 | ideaVersion { 87 | sinceBuild = providers.gradleProperty("pluginSinceBuild") 88 | untilBuild = providers.gradleProperty("pluginUntilBuild") 89 | } 90 | } 91 | 92 | signing { 93 | certificateChain = providers.environmentVariable("CERTIFICATE_CHAIN") 94 | privateKey = providers.environmentVariable("PRIVATE_KEY") 95 | password = providers.environmentVariable("PRIVATE_KEY_PASSWORD") 96 | } 97 | 98 | publishing { 99 | token = providers.environmentVariable("PUBLISH_TOKEN") 100 | // The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 101 | // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: 102 | // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel 103 | channels = providers.gradleProperty("pluginVersion") 104 | .map { listOf(it.substringAfter('-', "").substringBefore('.').ifEmpty { "default" }) } 105 | } 106 | 107 | pluginVerification { 108 | ides { 109 | recommended() 110 | } 111 | } 112 | } 113 | 114 | // Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin 115 | changelog { 116 | groups.empty() 117 | repositoryUrl = providers.gradleProperty("pluginRepositoryUrl") 118 | } 119 | 120 | // Configure Gradle Kover Plugin - read more: https://github.com/Kotlin/kotlinx-kover#configuration 121 | kover { 122 | reports { 123 | total { 124 | xml { 125 | onCheck = true 126 | } 127 | } 128 | } 129 | } 130 | 131 | tasks { 132 | wrapper { 133 | gradleVersion = providers.gradleProperty("gradleVersion").get() 134 | } 135 | 136 | publishPlugin { 137 | dependsOn(patchChangelog) 138 | } 139 | } 140 | 141 | intellijPlatformTesting { 142 | runIde { 143 | register("runIdeForUiTests") { 144 | task { 145 | jvmArgumentProviders += CommandLineArgumentProvider { 146 | listOf( 147 | "-Drobot-server.port=8082", 148 | "-Dide.mac.message.dialogs.as.sheets=false", 149 | "-Djb.privacy.policy.text=", 150 | "-Djb.consents.confirmation.enabled=false", 151 | ) 152 | } 153 | } 154 | 155 | plugins { 156 | robotServerPlugin() 157 | } 158 | } 159 | } 160 | } 161 | 162 | // https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-faq.html#how-to-check-the-latest-available-eap-release 163 | tasks { 164 | printProductsReleases { 165 | channels = listOf(ProductRelease.Channel.EAP) 166 | types = listOf(IntelliJPlatformType.IntellijIdeaCommunity) 167 | untilBuild = provider { null } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /example/Example.kts: -------------------------------------------------------------------------------- 1 | import java.time.Instant 2 | 3 | fun String.escapeJsonString(): String { 4 | return this 5 | .replace("\\", "\\\\") 6 | .replace("\"", "\\\"") 7 | .replace("\n", "\\n") 8 | .replace("\r", "\\r") 9 | .replace("\t", "\\t") 10 | .replace("\b", "\\b") 11 | } 12 | 13 | fun log(level: String, message: String, traceId: String) { 14 | val timestamp = Instant.now().toString() 15 | val pid = ProcessHandle.current().pid() 16 | val thread = Thread.currentThread().name 17 | val escapedMessage = message.escapeJsonString() 18 | val json = 19 | """{"timestamp":"$timestamp","pid":$pid,"thread":"$thread","level":"$level","message":"$escapedMessage","traceId":"$traceId"}""" 20 | println(json) 21 | } 22 | 23 | fun logException(message: String, e: Exception, traceId: String) { 24 | val level = "ERROR" 25 | val timestamp = Instant.now().toString() 26 | val pid = ProcessHandle.current().pid() 27 | val thread = Thread.currentThread().name 28 | val escapedMessage = message.escapeJsonString() 29 | val escapedStackTrace = e.stackTraceToString().escapeJsonString() 30 | val json = 31 | """{"timestamp":"$timestamp","pid":$pid,"thread":"$thread","level":"$level","message":"$escapedMessage","stack_trace":"$escapedStackTrace","traceId":"$traceId"}""" 32 | println(json) 33 | } 34 | 35 | val trace1 = "0af7651916cd43dd8448eb211c80319c" 36 | log("INFO", "Starting request", trace1) 37 | log("DEBUG", "Processing request", trace1) 38 | log("WARN", "Request is too large", trace1) 39 | log("DEBUG", "Still processing request", trace1) 40 | log("INFO", "200 OK: GET /", trace1) 41 | 42 | Thread.sleep(200) 43 | 44 | val trace2 = "4bf92f3577b34da6a3ce929d0e0e4736" 45 | try { 46 | log("INFO", "Starting request", trace2) 47 | log("DEBUG", "Processing request", trace2) 48 | throw RuntimeException("Something went wrong!") 49 | } catch (e: Exception) { 50 | log("ERROR", e.stackTraceToString(), trace2) 51 | logException(e.message ?: "", e, trace2) 52 | log("INFO", "500 Internal Server Error: GET /", trace2) 53 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 2 | pluginGroup=io.github.orangain.prettyjsonlog 3 | pluginName=pretty-json-log-plugin 4 | pluginRepositoryUrl=https://github.com/orangain/pretty-json-log-plugin 5 | # SemVer format -> https://semver.org 6 | pluginVersion=0.6.0 7 | # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 8 | pluginSinceBuild=242 9 | pluginUntilBuild=252.* 10 | # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension 11 | platformType=IC 12 | platformVersion=2025.1 13 | #platformVersion=243.18137.10 14 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 15 | # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP 16 | platformPlugins= 17 | # Example: platformBundledPlugins = com.intellij.java 18 | platformBundledPlugins= 19 | # Gradle Releases -> https://github.com/gradle/gradle/releases 20 | gradleVersion=8.10.2 21 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 22 | kotlin.stdlib.default.dependency=false 23 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 24 | org.gradle.configuration-cache=true 25 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 26 | org.gradle.caching=true 27 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # libraries 3 | junit = "4.13.2" 4 | jacksonModuleKotlin = "2.18.3" 5 | 6 | # plugins 7 | changelog = "2.2.1" 8 | intelliJPlatform = "2.5.0" 9 | kotlin = "2.1.20" 10 | kover = "0.9.1" 11 | qodana = "2024.3.4" 12 | 13 | [libraries] 14 | junit = { group = "junit", name = "junit", version.ref = "junit" } 15 | jacksonModuleKotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" } 16 | 17 | [plugins] 18 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } 19 | intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } 20 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 21 | kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } 22 | qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } 23 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangain/pretty-json-log-plugin/554e4c008bfb0775e60f0046b254160aa84f8e7a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /media/context-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangain/pretty-json-log-plugin/554e4c008bfb0775e60f0046b254160aa84f8e7a/media/context-menu.png -------------------------------------------------------------------------------- /media/log-colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangain/pretty-json-log-plugin/554e4c008bfb0775e60f0046b254160aa84f8e7a/media/log-colors.png -------------------------------------------------------------------------------- /media/screenshot-installation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangain/pretty-json-log-plugin/554e4c008bfb0775e60f0046b254160aa84f8e7a/media/screenshot-installation.png -------------------------------------------------------------------------------- /media/screenshot_basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangain/pretty-json-log-plugin/554e4c008bfb0775e60f0046b254160aa84f8e7a/media/screenshot_basic.png -------------------------------------------------------------------------------- /media/screenshot_expanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangain/pretty-json-log-plugin/554e4c008bfb0775e60f0046b254160aa84f8e7a/media/screenshot_expanded.png -------------------------------------------------------------------------------- /qodana.yml: -------------------------------------------------------------------------------- 1 | # Qodana configuration: 2 | # https://www.jetbrains.com/help/qodana/qodana-yaml.html 3 | 4 | version: 1.0 5 | linter: jetbrains/qodana-jvm-community:2024.2 6 | projectJDK: "17" 7 | profile: 8 | name: qodana.recommended 9 | exclude: 10 | - name: All 11 | paths: 12 | - .qodana 13 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "pretty-json-log-plugin" 2 | 3 | plugins { 4 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.10.0" 5 | } 6 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/MyBundle.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog 2 | 3 | import com.intellij.DynamicBundle 4 | import org.jetbrains.annotations.NonNls 5 | import org.jetbrains.annotations.PropertyKey 6 | 7 | @NonNls 8 | private const val BUNDLE = "messages.MyBundle" 9 | 10 | object MyBundle : DynamicBundle(BUNDLE) { 11 | 12 | @JvmStatic 13 | fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 14 | getMessage(key, *params) 15 | 16 | @Suppress("unused") 17 | @JvmStatic 18 | fun messagePointer(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 19 | getLazyMessage(key, *params) 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/action/ToggleEnabledAction.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.action 2 | 3 | import com.intellij.openapi.actionSystem.ActionUpdateThread 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import com.intellij.openapi.actionSystem.LangDataKeys 6 | import com.intellij.openapi.components.service 7 | import com.intellij.openapi.diagnostic.thisLogger 8 | import com.intellij.openapi.project.DumbAwareToggleAction 9 | import io.github.orangain.prettyjsonlog.service.EphemeralStateService 10 | 11 | class ToggleEnabledAction : DumbAwareToggleAction() { 12 | override fun getActionUpdateThread(): ActionUpdateThread { 13 | return ActionUpdateThread.BGT 14 | } 15 | 16 | override fun isSelected(e: AnActionEvent): Boolean { 17 | val consoleView = e.getData(LangDataKeys.CONSOLE_VIEW) ?: return false 18 | val project = e.project ?: return false 19 | val service = project.service() 20 | return service.isEnabled(consoleView) 21 | } 22 | 23 | override fun setSelected(e: AnActionEvent, state: Boolean) { 24 | thisLogger().debug("setSelected: $state") 25 | val consoleView = e.getData(LangDataKeys.CONSOLE_VIEW) ?: return 26 | val project = e.project ?: return 27 | val service = project.service() 28 | service.setEnabled(consoleView, state) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/console/MyConsoleFolding.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.console 2 | 3 | import com.intellij.execution.ConsoleFolding 4 | import com.intellij.execution.ui.ConsoleView 5 | import com.intellij.openapi.components.service 6 | import com.intellij.openapi.diagnostic.thisLogger 7 | import com.intellij.openapi.project.Project 8 | import io.github.orangain.prettyjsonlog.json.isPartOfPrettyJson 9 | import io.github.orangain.prettyjsonlog.service.EphemeralStateService 10 | 11 | class MyConsoleFolding : ConsoleFolding() { 12 | private var consoleView: ConsoleView? = null 13 | 14 | override fun getPlaceholderText(project: Project, lines: List): String { 15 | return "{...}" 16 | } 17 | 18 | override fun shouldFoldLine(project: Project, line: String): Boolean { 19 | thisLogger().debug("shouldFoldLine: $line") 20 | if (!isEnabled(project)) { 21 | return false 22 | } 23 | return isPartOfPrettyJson(line) 24 | } 25 | 26 | override fun isEnabledForConsole(consoleView: ConsoleView): Boolean { 27 | // This method "isEnabledForConsole" is not for storing consoleView, but we use it for that purpose because 28 | // there is no other way to get consoleView reference in "shouldFoldLine" method. 29 | this.consoleView = consoleView 30 | return true 31 | } 32 | 33 | private fun isEnabled(project: Project): Boolean { 34 | val service = project.service() 35 | val consoleView = this.consoleView ?: return false 36 | return service.isEnabled(consoleView) 37 | } 38 | 39 | // override fun shouldBeAttachedToThePreviousLine(): Boolean { 40 | // return false 41 | // } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/console/MyConsoleInputFilterProvider.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.console 2 | 3 | import com.intellij.execution.filters.ConsoleDependentInputFilterProvider 4 | import com.intellij.execution.filters.InputFilter 5 | import com.intellij.execution.ui.ConsoleView 6 | import com.intellij.execution.ui.ConsoleViewContentType 7 | import com.intellij.openapi.components.service 8 | import com.intellij.openapi.diagnostic.thisLogger 9 | import com.intellij.openapi.project.Project 10 | import com.intellij.openapi.util.Pair 11 | import com.intellij.psi.search.GlobalSearchScope 12 | import io.github.orangain.prettyjsonlog.json.parseJson 13 | import io.github.orangain.prettyjsonlog.json.prettyPrintJson 14 | import io.github.orangain.prettyjsonlog.logentry.* 15 | import io.github.orangain.prettyjsonlog.service.EphemeralStateService 16 | import java.time.ZoneId 17 | import java.time.format.DateTimeFormatter 18 | 19 | // We use ConsoleDependentInputFilterProvider instead of ConsoleInputFilterProvider because we need to access 20 | // ConsoleView and Project in the filter. 21 | class MyConsoleInputFilterProvider : ConsoleDependentInputFilterProvider() { 22 | override fun getDefaultFilters( 23 | consoleView: ConsoleView, 24 | project: Project, 25 | scope: GlobalSearchScope 26 | ): MutableList { 27 | thisLogger().debug("getDefaultFilters") 28 | return mutableListOf(MyConsoleInputFilter(consoleView, project)) 29 | } 30 | } 31 | 32 | private val zoneId = ZoneId.systemDefault() 33 | private val timestampFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") 34 | 35 | class MyConsoleInputFilter( 36 | private val consoleView: ConsoleView, 37 | private val project: Project 38 | ) : InputFilter { 39 | override fun applyFilter( 40 | text: String, 41 | contentType: ConsoleViewContentType 42 | ): MutableList>? { 43 | thisLogger().debug("contentType: $contentType, applyFilter: $text") 44 | if (!isEnabled()) { 45 | return null 46 | } 47 | val (node, suffixWhitespaces) = parseJson(text) ?: return null 48 | 49 | val timestamp = extractTimestamp(node) 50 | val level = extractLevel(node) 51 | val message = extractMessage(node) 52 | val stackTrace = extractStackTrace(node) 53 | // .trimEnd('\n') is necessary because of the following reasons: 54 | // - When stackTrace is null or empty, we don't want to add an extra newline. 55 | // - When stackTrace ends with a newline, trimming the last newline makes a folding marker look better. 56 | val coloredMessage = "$level: $message\n${stackTrace ?: ""}".trimEnd('\n') 57 | 58 | val jsonString = prettyPrintJson(node) 59 | return mutableListOf( 60 | Pair("[${timestamp?.format(zoneId, timestampFormatter)}] ", contentType), 61 | Pair(coloredMessage, contentTypeOf(level, contentType)), 62 | Pair( 63 | " \n$jsonString$suffixWhitespaces", // Adding a space at the end of line makes a folding marker look better. 64 | contentType 65 | ), 66 | ) 67 | } 68 | 69 | private fun isEnabled(): Boolean { 70 | val service = project.service() 71 | return service.isEnabled(consoleView) 72 | } 73 | } 74 | 75 | private fun contentTypeOf(level: Level?, inputContentType: ConsoleViewContentType): ConsoleViewContentType { 76 | return when (level) { 77 | Level.TRACE, Level.DEBUG -> ConsoleViewContentType.LOG_DEBUG_OUTPUT 78 | Level.INFO -> ConsoleViewContentType.LOG_INFO_OUTPUT 79 | Level.WARN -> ConsoleViewContentType.LOG_WARNING_OUTPUT 80 | Level.ERROR, Level.FATAL -> ConsoleViewContentType.LOG_ERROR_OUTPUT 81 | else -> inputContentType 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/json/Mapper.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.json 2 | 3 | import com.fasterxml.jackson.databind.SerializationFeature 4 | import com.fasterxml.jackson.databind.cfg.JsonNodeFeature 5 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 6 | 7 | val mapper = jacksonObjectMapper().apply { 8 | configure(SerializationFeature.INDENT_OUTPUT, true) 9 | configure(JsonNodeFeature.WRITE_PROPERTIES_SORTED, true) 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/json/Parse.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.json 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException 4 | import com.fasterxml.jackson.databind.JsonNode 5 | 6 | private val jsonPattern = Regex("""^\s*(\{.*})(\s*)$""") 7 | 8 | fun parseJson(text: String): Pair? { 9 | val result = jsonPattern.matchEntire(text) ?: return null 10 | 11 | return try { 12 | Pair(mapper.readTree(result.groups[1]!!.value), result.groups[2]!!.value) 13 | } catch (e: JsonProcessingException) { 14 | null 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/json/PrettyPrint.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.json 2 | 3 | import com.fasterxml.jackson.core.util.DefaultPrettyPrinter 4 | import com.fasterxml.jackson.databind.JsonNode 5 | 6 | private val writer = mapper.writer(MyPrettyPrinter()) 7 | 8 | fun prettyPrintJson(node: JsonNode): String { 9 | return writer.writeValueAsString(node) 10 | } 11 | 12 | class MyPrettyPrinter : DefaultPrettyPrinter() { 13 | init { 14 | _objectFieldValueSeparatorWithSpaces = ": " 15 | } 16 | 17 | override fun createInstance(): DefaultPrettyPrinter { 18 | return MyPrettyPrinter() 19 | } 20 | } 21 | 22 | private val prettyJsonPartRegex = Regex("""^([{}]| {2,}(".*": |}))""") 23 | 24 | fun isPartOfPrettyJson(line: String): Boolean { 25 | return prettyJsonPartRegex.containsMatchIn(line) 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/listeners/MyApplicationActivationListener.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.listeners 2 | 3 | import com.intellij.openapi.application.ApplicationActivationListener 4 | import com.intellij.openapi.diagnostic.thisLogger 5 | import com.intellij.openapi.wm.IdeFrame 6 | import io.github.orangain.prettyjsonlog.MyBundle 7 | 8 | internal class MyApplicationActivationListener : ApplicationActivationListener { 9 | 10 | override fun applicationActivated(ideFrame: IdeFrame) { 11 | thisLogger().debug(MyBundle.message("applicationActivated")) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/logentry/Level.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.logentry 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | 5 | enum class Level { 6 | TRACE, DEBUG, INFO, WARN, ERROR, FATAL; 7 | 8 | companion object { 9 | fun fromInt(level: Int): Level { 10 | // Use bunyan's level as a reference. 11 | // See: https://github.com/trentm/node-bunyan?tab=readme-ov-file#levels 12 | return when { 13 | level < 20 -> TRACE 14 | level < 30 -> DEBUG 15 | level < 40 -> INFO 16 | level < 50 -> WARN 17 | level < 60 -> ERROR 18 | else -> FATAL 19 | } 20 | } 21 | 22 | fun fromString(level: String): Level? { 23 | // Bunyan's levels: TRACE, DEBUG, INFO, WARN, ERROR, FATAL 24 | // https://github.com/trentm/node-bunyan?tab=readme-ov-file#levels 25 | // Cloud Logging's levels: DEFAULT, DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY 26 | // https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity 27 | // java.util.logging's levels: FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE 28 | // https://docs.oracle.com/en/java/javase/21/docs/api/java.logging/java/util/logging/Level.html 29 | return when (level.uppercase()) { 30 | "TRACE", "FINEST", "FINER", "FINE" -> TRACE 31 | "DEBUG", "CONFIG" -> DEBUG 32 | "INFO", "NOTICE" -> INFO 33 | "WARN", "WARNING" -> WARN 34 | "ERROR", "CRITICAL", "SEVERE" -> ERROR 35 | "FATAL", "ALERT", "EMERGENCY" -> FATAL 36 | else -> null // This includes "DEFAULT" 37 | } 38 | } 39 | } 40 | } 41 | 42 | private val levelKeys = listOf("level", "severity", "log.level", "@l", "Level") 43 | 44 | fun extractLevel(node: JsonNode): Level? { 45 | return levelKeys.firstNotNullOfOrNull { node.get(it) }?.let { levelNode -> 46 | if (levelNode.isNumber) { 47 | Level.fromInt(levelNode.asInt()) 48 | } else { 49 | Level.fromString(levelNode.asText()) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/logentry/Message.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.logentry 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | 5 | private val messageKeys = listOf("message", "msg", "error.message", "@m", "RenderedMessage") 6 | 7 | fun extractMessage(node: JsonNode): String? { 8 | return messageKeys.firstNotNullOfOrNull { node.get(it) }?.asText() 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/logentry/NodeExtractor.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.logentry 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | 5 | typealias NodeExtractor = (JsonNode) -> JsonNode? 6 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/logentry/StackTrace.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.logentry 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | 5 | private val stackTraceNodeExtractors: List = listOf( 6 | { it.get("stack_trace") }, 7 | { it.get("exception") }, 8 | { it.get("error.stack_trace") }, 9 | { it.get("err")?.get("stack") }, 10 | { it.get("@x") }, 11 | { it.get("Exception") }, 12 | ) 13 | 14 | fun extractStackTrace(node: JsonNode): String? { 15 | return stackTraceNodeExtractors.firstNotNullOfOrNull { it(node) }?.asText() 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/logentry/Timestamp.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.logentry 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import java.time.Instant 5 | import java.time.OffsetDateTime 6 | import java.time.ZoneId 7 | import java.time.format.DateTimeFormatter 8 | import java.time.format.DateTimeParseException 9 | 10 | sealed interface Timestamp { 11 | fun format(zoneId: ZoneId, formatter: DateTimeFormatter): String 12 | 13 | data class Parsed(val value: Instant) : Timestamp { 14 | override fun format(zoneId: ZoneId, formatter: DateTimeFormatter): String { 15 | return value.atZone(zoneId).format(formatter) 16 | } 17 | } 18 | 19 | data class Fallback(val value: String) : Timestamp { 20 | override fun format(zoneId: ZoneId, formatter: DateTimeFormatter): String { 21 | return value 22 | } 23 | } 24 | 25 | companion object { 26 | fun fromEpoch(value: Long): Parsed { 27 | return Parsed( 28 | when { 29 | value < 10_000_000_000L -> Instant.ofEpochSecond(value) 30 | value < 10_000_000_000_000L -> Instant.ofEpochMilli(value) 31 | value < 10_000_000_000_000_000L -> Instant.ofEpochSecond( 32 | value / 1_000_000, 33 | (value % 1_000_000) * 1_000 34 | ) // microseconds 35 | else -> Instant.ofEpochSecond(value / 1_000_000_000, value % 1_000_000_000) // nanoseconds 36 | } 37 | ) 38 | } 39 | 40 | fun fromString(value: String): Timestamp { 41 | return try { 42 | // Use OffsetDateTime.parse instead of Instant.parse because Instant.parse in JDK <= 11 does not support non-UTC offset like "-05:00". 43 | // See: https://stackoverflow.com/questions/68217689/how-to-use-instant-java-class-to-parse-a-date-time-with-offset-from-utc/68221614#68221614 44 | Parsed(OffsetDateTime.parse(value).toInstant()) 45 | } catch (e: DateTimeParseException) { 46 | Fallback(value) 47 | } 48 | } 49 | } 50 | } 51 | 52 | private val timestampKeys = listOf("timestamp", "time", "@timestamp", "ts", "@t", "Timestamp") 53 | 54 | fun extractTimestamp(node: JsonNode): Timestamp? { 55 | val timestampNode = timestampKeys.firstNotNullOfOrNull { node.get(it) } 56 | if (timestampNode != null) { 57 | return if (timestampNode.isNumber) { 58 | // We assume that the number is a Unix timestamp in seconds, milliseconds, microseconds, or nanoseconds. 59 | Timestamp.fromEpoch(timestampNode.asLong()) 60 | } else { 61 | Timestamp.fromString(timestampNode.asText()) 62 | } 63 | } 64 | // Fallback to google GCP timestampSeconds and timestampNanos 65 | return extractTimestampWithSecondsAndNanos(node) 66 | } 67 | 68 | fun extractTimestampWithSecondsAndNanos(node: JsonNode): Timestamp? { 69 | val timestampSeconds = node.get("timestampSeconds")?.asLong() 70 | val timestampNanos = node.get("timestampNanos")?.asLong() 71 | return if (timestampNanos != null && timestampSeconds != null) { 72 | Timestamp.Parsed(Instant.ofEpochSecond(timestampSeconds, timestampNanos)) 73 | } else { 74 | null 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/orangain/prettyjsonlog/service/EphemeralStateService.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.service 2 | 3 | import com.intellij.execution.ui.ConsoleView 4 | import com.intellij.openapi.components.Service 5 | import java.util.* 6 | 7 | @Service(Service.Level.PROJECT) 8 | class EphemeralStateService { 9 | private val enabledMap = WeakHashMap() 10 | 11 | /** 12 | * Returns true if the formatting is enabled for the given console view. 13 | */ 14 | fun isEnabled(consoleView: ConsoleView): Boolean { 15 | return enabledMap[consoleView] ?: true // default is true 16 | } 17 | 18 | /** 19 | * Sets the enabled state of the formatting for the given console view. 20 | */ 21 | fun setEnabled(consoleView: ConsoleView, value: Boolean) { 22 | enabledMap[consoleView] = value 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.github.orangain.prettyjsonlog 4 | Pretty JSON Log 5 | orangain 6 | 7 | com.intellij.modules.platform 8 | 9 | messages.MyBundle 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/messages/MyBundle.properties: -------------------------------------------------------------------------------- 1 | applicationActivated=Application activated 2 | action.io.github.orangain.prettyjsonlog.action.ToggleEnabledAction.text=Formatting Enabled 3 | action.io.github.orangain.prettyjsonlog.action.ToggleEnabledAction.description=Enable or disable formatting of JSON logs in the console 4 | -------------------------------------------------------------------------------- /src/test/kotlin/io/github/orangain/prettyjsonlog/json/FoldingTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.json 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import com.fasterxml.jackson.databind.node.ObjectNode 5 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 6 | import junit.framework.TestCase 7 | 8 | class FoldingTest : TestCase() { 9 | private data class Param( 10 | val description: String, 11 | val jsonCreator: (ObjectNode) -> Unit 12 | ) 13 | 14 | private val mapper = jacksonObjectMapper() 15 | 16 | private val params = listOf( 17 | Param("Empty object") { }, 18 | Param("Simple object") { 19 | it.put("string", "x") 20 | it.put("number", 1) 21 | it.put("true", true) 22 | it.put("false", false) 23 | it.putNull("null") 24 | }, 25 | Param("Nested object") { 26 | it.putObject("nested").apply { 27 | put("string", "x") 28 | put("number", 1) 29 | put("true", true) 30 | put("false", false) 31 | putNull("null") 32 | } 33 | }, 34 | Param("Array") { 35 | it.putArray("array").apply { 36 | add("x") 37 | add(1) 38 | add(true) 39 | add(false) 40 | addNull() 41 | addObject().apply { 42 | put("foo", "bar") 43 | } 44 | } 45 | }, 46 | Param("Nested array") { 47 | it.putObject("nested").apply { 48 | putArray("array").apply { 49 | add("x") 50 | add(1) 51 | add(true) 52 | add(false) 53 | addNull() 54 | } 55 | } 56 | }, 57 | Param("Array with many elements") { 58 | it.putArray("array").apply { 59 | for (i in 1..100) { 60 | add(i) 61 | } 62 | } 63 | }, 64 | ) 65 | 66 | fun testFolding() { 67 | params.forEach { param -> 68 | val node = mapper.createObjectNode().apply(param.jsonCreator) 69 | assertAllLinesFolded(param.description, node) 70 | } 71 | } 72 | 73 | private fun assertAllLinesFolded(description: String, node: JsonNode) { 74 | val json = prettyPrintJson(node) 75 | println(json) 76 | val lines = json.lines() 77 | assertTrue("[$description] Lines should not be empty", lines.isNotEmpty()) 78 | lines.forEachIndexed { index, line -> 79 | assertTrue("[$description] Line $index should be folded", isPartOfPrettyJson(line)) 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/github/orangain/prettyjsonlog/json/NotFoldingTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.json 2 | 3 | import junit.framework.TestCase 4 | 5 | 6 | class NotFoldingTest : TestCase() { 7 | private data class Param( 8 | val description: String, 9 | val text: String 10 | ) 11 | 12 | private val params = listOf( 13 | Param("Log message", "[12:34:56.789] INFO: This is a log message"), 14 | Param("Empty line", ""), 15 | Param( 16 | "Multiline SQL", """ 17 | SELECT 18 | id, 19 | name 20 | FROM 21 | table 22 | WHERE 23 | id = 1 24 | """.trimIndent() 25 | ), 26 | Param("Starts with space and double quote", """ "foo" """), 27 | ) 28 | 29 | fun testNotFolding() { 30 | params.forEach { param -> 31 | assertAllLinesNotFolded(param.description, param.text) 32 | } 33 | } 34 | 35 | private fun assertAllLinesNotFolded(description: String, text: String) { 36 | val lines = text.lines() 37 | assertTrue("[$description] Lines should not be empty", lines.isNotEmpty()) 38 | lines.forEachIndexed { index, line -> 39 | assertFalse("[$description] Line $index should not be folded", isPartOfPrettyJson(line)) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/github/orangain/prettyjsonlog/json/ParseTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.json 2 | 3 | import junit.framework.TestCase 4 | 5 | class ParseTest : TestCase() { 6 | fun testParseJsonLine() { 7 | val result = parseJson("""{"key": "value"}""") 8 | assertNotNull(result) 9 | val (node, rest) = result!! 10 | assertEquals("""{"key":"value"}""", node.toString()) 11 | assertEquals("", rest) 12 | } 13 | 14 | fun testParseJsonLineWithSpaces() { 15 | val result = parseJson(""" {"key": "value"} """) 16 | assertNotNull(result) 17 | val (node, rest) = result!! 18 | assertEquals("""{"key":"value"}""", node.toString()) 19 | assertEquals(" ", rest) 20 | } 21 | 22 | fun testParseBrokenJsonLine() { 23 | val result = parseJson("""{"key": "value" """) 24 | assertNull(result) 25 | } 26 | 27 | fun testParseEmptyString() { 28 | val result = parseJson("") 29 | assertNull(result) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/kotlin/io/github/orangain/prettyjsonlog/logentry/ExtractTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.logentry 2 | 3 | import io.github.orangain.prettyjsonlog.json.parseJson 4 | import junit.framework.TestCase 5 | import java.time.Instant 6 | 7 | private data class ExtractParam( 8 | val description: String, 9 | val json: String, 10 | val expectedTimestamp: Timestamp?, 11 | val expectedLevel: Level?, 12 | val expectedMessage: String?, 13 | val expectedStackTrace: String? 14 | ) 15 | 16 | private val params = listOf( 17 | // https://github.com/GoogleCloudPlatform/spring-cloud-gcp/blob/main/spring-cloud-gcp-logging/src/main/java/com/google/cloud/spring/logging/StackdriverJsonLayout.java 18 | ExtractParam( 19 | "Google Cloud Json Layout from com.google.cloud.spring.logging.StackdriverJsonLayout", 20 | """{"context": "default","logger": "com.example.MyClass","message": "Hello, world!","severity": "INFO","thread": "main","timestampNanos": 69022000,"timestampSeconds": 1729071565}""", 21 | Timestamp.Parsed(Instant.parse("2024-10-16T09:39:25.069022Z")), 22 | Level.INFO, 23 | "Hello, world!", 24 | null, 25 | ), 26 | // https://cloud.google.com/logging/docs/structured-logging 27 | ExtractParam( 28 | "Cloud Logging", 29 | """{"severity":"ERROR", "message":"There was an error in the application.", "httpRequest":{"requestMethod":"GET"},"time":"2020-10-12T07:20:50.52Z"}""", 30 | Timestamp.Parsed(Instant.parse("2020-10-12T07:20:50.52Z")), 31 | Level.ERROR, 32 | "There was an error in the application.", 33 | null, 34 | ), 35 | // https://cloud.google.com/error-reporting/docs/formatting-error-messages#log-error 36 | ExtractParam( 37 | "Cloud Logging with stack trace in the stack_trace field for Error Reporting", 38 | """{"severity":"ERROR", "message":"There was an error in the application.","stack_trace": "com.example.shop.TemplateCartDiv retrieveCart: Error\njava.lang.IndexOutOfBoundsException: Index: 4, Size: 4\n\tat java.util.ArrayList.rangeCheck(ArrayList.java:635)\n","time":"2020-10-12T07:20:50.52Z"}""", 39 | Timestamp.Parsed(Instant.parse("2020-10-12T07:20:50.52Z")), 40 | Level.ERROR, 41 | "There was an error in the application.", 42 | "com.example.shop.TemplateCartDiv retrieveCart: Error\n" 43 | + "java.lang.IndexOutOfBoundsException: Index: 4, Size: 4\n" 44 | + "\tat java.util.ArrayList.rangeCheck(ArrayList.java:635)\n", 45 | ), 46 | // https://cloud.google.com/error-reporting/docs/formatting-error-messages#log-error 47 | ExtractParam( 48 | "Cloud Logging with stack trace in the exception field for Error Reporting", 49 | """{"severity":"ERROR", "message":"There was an error in the application.","exception": "com.example.shop.TemplateCartDiv retrieveCart: Error\njava.lang.IndexOutOfBoundsException: Index: 4, Size: 4\n\tat java.util.ArrayList.rangeCheck(ArrayList.java:635)\n","time":"2020-10-12T07:20:50.52Z"}""", 50 | Timestamp.Parsed(Instant.parse("2020-10-12T07:20:50.52Z")), 51 | Level.ERROR, 52 | "There was an error in the application.", 53 | "com.example.shop.TemplateCartDiv retrieveCart: Error\n" 54 | + "java.lang.IndexOutOfBoundsException: Index: 4, Size: 4\n" 55 | + "\tat java.util.ArrayList.rangeCheck(ArrayList.java:635)\n", 56 | ), 57 | // https://pkg.go.dev/log/slog 58 | ExtractParam( 59 | "log/slog in Go", 60 | """{"time":"2022-11-08T15:28:26.000000000-05:00","level":"INFO","msg":"hello","count":3}""", 61 | Timestamp.Parsed(Instant.parse("2022-11-08T20:28:26Z")), 62 | Level.INFO, 63 | "hello", 64 | null, 65 | ), 66 | // https://github.com/trentm/node-bunyan 67 | ExtractParam( 68 | "Bunyan", 69 | """{"name":"myapp","hostname":"banana.local","pid":40161,"level":30,"msg":"hi","time":"2013-01-04T18:46:23.851Z","v":0,"err":{"message":"boom","name":"TypeError","stack":"TypeError: boom\n at Object. ..."}}""", 70 | Timestamp.Parsed(Instant.parse("2013-01-04T18:46:23.851Z")), 71 | Level.INFO, 72 | "hi", 73 | "TypeError: boom\n at Object. ...", 74 | ), 75 | // https://github.com/pinojs/pino 76 | ExtractParam( 77 | "Pino", 78 | """{"level":30,"time":1531171074631,"msg":"hello world","pid":657,"hostname":"Davids-MBP-3.fritz.box"}""", 79 | Timestamp.Parsed(Instant.parse("2018-07-09T21:17:54.631Z")), 80 | Level.INFO, 81 | "hello world", 82 | null, 83 | ), 84 | // https://github.com/logfellow/logstash-logback-encoder 85 | ExtractParam( 86 | "Logstash Logback Encoder", 87 | """{"@timestamp":"2019-11-03T10:15:30.123+01:00","@version":"1","message":"My message","logger_name":"org.company.stack.Sample","thread_name":"main","level":"INFO","level_value":20000}""", 88 | Timestamp.Parsed(Instant.parse("2019-11-03T09:15:30.123Z")), 89 | Level.INFO, 90 | "My message", 91 | null, 92 | ), 93 | // https://logging.apache.org/log4j/2.x/manual/json-template-layout.html 94 | ExtractParam( 95 | "Log4j2 with EcsLayout.json", 96 | """{"@timestamp":"2024-07-15T03:36:52.899Z","ecs.version":"1.2.0","error.message":null,"error.stack_trace":"javax.transaction.xa.XAException\n\tat org.apache.activemq.artemis.core.protocol.core.impl.ActiveMQSessionContext.xaCommit(ActiveMQSessionContext.java:495)\n\tat org.apache.activemq.artemis.core.client.impl.ClientSessionImpl.commit(ClientSessionImpl.java:1624)\n\tat com.atomikos.datasource.xa.XAResourceTransaction.commit(XAResourceTransaction.java:557)\n\tat com.atomikos.icatch.imp.CommitMessage.send(CommitMessage.java:52)\n\tat com.atomikos.icatch.imp.CommitMessage.send(CommitMessage.java:23)\n\tat com.atomikos.icatch.imp.PropagationMessage.submit(PropagationMessage.java:67)\n\tat com.atomikos.icatch.imp.Propagator${'$'}PropagatorThread.run(Propagator.java:63)\n\tat com.atomikos.icatch.imp.Propagator.submitPropagationMessage(Propagator.java:42)\n\tat com.atomikos.icatch.imp.HeurHazardStateHandler.onTimeout(HeurHazardStateHandler.java:83)\n\tat com.atomikos.icatch.imp.CoordinatorImp.alarm(CoordinatorImp.java:650)\n\tat com.atomikos.timing.PooledAlarmTimer.notifyListeners(PooledAlarmTimer.java:95)\n\tat com.atomikos.timing.PooledAlarmTimer.run(PooledAlarmTimer.java:82)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor${'$'}Worker.run(ThreadPoolExecutor.java:642)\n\tat java.base/java.lang.Thread.run(Thread.java:1583)\n","error.type":"javax.transaction.xa.XAException","log.level":"WARN","log.logger":"com.atomikos.datasource.xa.XAResourceTransaction","message": "XA resource 'jms': commit for XID '27726F6F742D6170706C69636174696F6E27313732303738363038353131323031303130:27726F6F742D6170706C69636174696F6E27393136' raised -4: the supplied XID is invalid for this XA resource","process.thread.name":"Atomikos:5"}""", 97 | Timestamp.Parsed(Instant.parse("2024-07-15T03:36:52.899Z")), 98 | Level.WARN, 99 | "XA resource 'jms': commit for XID '27726F6F742D6170706C69636174696F6E27313732303738363038353131323031303130:27726F6F742D6170706C69636174696F6E27393136' raised -4: the supplied XID is invalid for this XA resource", 100 | "javax.transaction.xa.XAException\n" + 101 | "\tat org.apache.activemq.artemis.core.protocol.core.impl.ActiveMQSessionContext.xaCommit(ActiveMQSessionContext.java:495)\n" + 102 | "\tat org.apache.activemq.artemis.core.client.impl.ClientSessionImpl.commit(ClientSessionImpl.java:1624)\n" + 103 | "\tat com.atomikos.datasource.xa.XAResourceTransaction.commit(XAResourceTransaction.java:557)\n" + 104 | "\tat com.atomikos.icatch.imp.CommitMessage.send(CommitMessage.java:52)\n" + 105 | "\tat com.atomikos.icatch.imp.CommitMessage.send(CommitMessage.java:23)\n" + 106 | "\tat com.atomikos.icatch.imp.PropagationMessage.submit(PropagationMessage.java:67)\n" + 107 | "\tat com.atomikos.icatch.imp.Propagator\$PropagatorThread.run(Propagator.java:63)\n" + 108 | "\tat com.atomikos.icatch.imp.Propagator.submitPropagationMessage(Propagator.java:42)\n" + 109 | "\tat com.atomikos.icatch.imp.HeurHazardStateHandler.onTimeout(HeurHazardStateHandler.java:83)\n" + 110 | "\tat com.atomikos.icatch.imp.CoordinatorImp.alarm(CoordinatorImp.java:650)\n" + 111 | "\tat com.atomikos.timing.PooledAlarmTimer.notifyListeners(PooledAlarmTimer.java:95)\n" + 112 | "\tat com.atomikos.timing.PooledAlarmTimer.run(PooledAlarmTimer.java:82)\n" + 113 | "\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)\n" + 114 | "\tat java.base/java.util.concurrent.ThreadPoolExecutor\$Worker.run(ThreadPoolExecutor.java:642)\n" + 115 | "\tat java.base/java.lang.Thread.run(Thread.java:1583)\n", 116 | ), 117 | // https://pkg.go.dev/go.uber.org/zap 118 | ExtractParam( 119 | "Zap Logger Production Default", 120 | """{"caller": "devorer/main.go:60", "level": "info", "msg": "application starting...", "ts": 1.7235729053485353E9}""", 121 | Timestamp.Parsed(Instant.parse("2024-08-13T18:15:05Z")), 122 | Level.INFO, 123 | "application starting...", 124 | null, 125 | ), 126 | // https://github.com/serilog/serilog-formatting-compact?tab=readme-ov-file#format-details 127 | ExtractParam( 128 | "Serilog Rendered Compact JSON", 129 | """{"@t":"2024-09-07T03:50:13.7292340Z","@m":"Unhandled exception","@i":"f80f533c","@l":"Error","@x":"System.InvalidOperationException: Oops...\n at Program.
${'$'}(String[] args) in /Users/orange/RiderProjects/ConsoleApp1/ConsoleApp1/Program.cs:line 19"}""", 130 | Timestamp.Parsed(Instant.parse("2024-09-07T03:50:13.7292340Z")), 131 | Level.ERROR, 132 | "Unhandled exception", 133 | """ 134 | System.InvalidOperationException: Oops... 135 | at Program.
${'$'}(String[] args) in /Users/orange/RiderProjects/ConsoleApp1/ConsoleApp1/Program.cs:line 19 136 | """.trimIndent(), 137 | ), 138 | // https://github.com/serilog/serilog/blob/main/src/Serilog/Formatting/Json/JsonFormatter.cs 139 | ExtractParam( 140 | "Serilog Rendered JSON", 141 | """{"Timestamp":"2024-09-07T15:48:19.6174980+09:00","Level":"Error","MessageTemplate":"Unhandled exception","RenderedMessage":"Unhandled exception","Exception":"System.InvalidOperationException: Oops...\n at Program.
${'$'}(String[] args) in /Users/orange/RiderProjects/ConsoleApp1/ConsoleApp1/Program.cs:line 19"}""", 142 | Timestamp.Parsed(Instant.parse("2024-09-07T06:48:19.617498Z")), 143 | Level.ERROR, 144 | "Unhandled exception", 145 | """ 146 | System.InvalidOperationException: Oops... 147 | at Program.
${'$'}(String[] args) in /Users/orange/RiderProjects/ConsoleApp1/ConsoleApp1/Program.cs:line 19 148 | """.trimIndent(), 149 | ), 150 | ) 151 | 152 | class ExtractTest : TestCase() { 153 | fun testExtractTimestamp() { 154 | params.forEach { param -> 155 | val (node, _) = parseJson(param.json)!! 156 | val actual = extractTimestamp(node) 157 | assertEquals(param.description, param.expectedTimestamp, actual) 158 | } 159 | } 160 | 161 | fun testExtractLevel() { 162 | params.forEach { param -> 163 | val (node, _) = parseJson(param.json)!! 164 | val actual = extractLevel(node) 165 | assertEquals(param.description, param.expectedLevel, actual) 166 | } 167 | } 168 | 169 | fun testExtractMessage() { 170 | params.forEach { param -> 171 | val (node, _) = parseJson(param.json)!! 172 | val actual = extractMessage(node) 173 | assertEquals(param.description, param.expectedMessage, actual) 174 | } 175 | } 176 | 177 | fun testStackTrace() { 178 | params.forEach { param -> 179 | val (node, _) = parseJson(param.json)!! 180 | val actual = extractStackTrace(node) 181 | assertEquals(param.description, param.expectedStackTrace, actual) 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/test/kotlin/io/github/orangain/prettyjsonlog/logentry/TimestampTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.orangain.prettyjsonlog.logentry 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.databind.node.ObjectNode 5 | import junit.framework.TestCase 6 | import java.time.Instant 7 | 8 | private val testCases: List> = listOf( 9 | // seconds 10 | 0L to "1970-01-01T00:00:00Z", 11 | 1L to "1970-01-01T00:00:01Z", 12 | 1000L to "1970-01-01T00:16:40Z", 13 | 1000000000L to "2001-09-09T01:46:40Z", 14 | 1000000001L to "2001-09-09T01:46:41Z", 15 | 1723644284L to "2024-08-14T14:04:44Z", 16 | 9999999999L to "2286-11-20T17:46:39Z", 17 | // milliseconds 18 | 10000000000L to "1970-04-26T17:46:40Z", 19 | 10000000001L to "1970-04-26T17:46:40.001Z", 20 | 1723644284001L to "2024-08-14T14:04:44.001Z", 21 | 9999999999999L to "2286-11-20T17:46:39.999Z", 22 | // microseconds 23 | 10000000000000L to "1970-04-26T17:46:40Z", 24 | 10000000000001L to "1970-04-26T17:46:40.000001Z", 25 | 1723644284000001L to "2024-08-14T14:04:44.000001Z", 26 | 9999999999999999L to "2286-11-20T17:46:39.999999Z", 27 | // nanoseconds 28 | 10000000000000000L to "1970-04-26T17:46:40Z", 29 | 10000000000000001L to "1970-04-26T17:46:40.000000001Z", 30 | 1723644284000000001L to "2024-08-14T14:04:44.000000001Z", 31 | 9223372036854775807L to "2262-04-11T23:47:16.854775807Z", // Long.MAX_VALUE 32 | ) 33 | 34 | class TimestampTest : TestCase() { 35 | fun testFromEpoch() { 36 | testCases.forEach { (input, expected) -> 37 | val actual = Timestamp.fromEpoch(input) 38 | assertEquals("Timestamp.fromEpoch($input)", Timestamp.Parsed(Instant.parse(expected)), actual) 39 | } 40 | } 41 | 42 | fun testExtractGCPTimestampWithSecondsAndNanos() { 43 | val testCases = listOf( 44 | Pair(Instant.ofEpochSecond(1000, 500), """{"timestampSeconds": 1000, "timestampNanos": 500}"""), 45 | Pair(Instant.ofEpochSecond(0, 0), """{"timestampSeconds": 0, "timestampNanos": 0}"""), 46 | Pair(Instant.ofEpochSecond(1000000000, 123456789), """{"timestampSeconds": 1000000000, "timestampNanos": 123456789}""") 47 | ) 48 | val objectMapper = ObjectMapper() 49 | testCases.forEach { (expectedInstant, jsonString) -> 50 | val jsonNode = objectMapper.readTree(jsonString) as ObjectNode 51 | val actual = extractTimestampWithSecondsAndNanos(jsonNode) 52 | assertEquals("extractTimestampWithSecondsAndNanos($jsonString)", Timestamp.Parsed(expectedInstant), actual) 53 | } 54 | } 55 | } 56 | --------------------------------------------------------------------------------