diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..b2ad03f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## Describe your changes + +## Issue ticket number and link + +## Checklist before requesting a review + +- [ ] I have resolved any merge conflicts +- [ ] I have run tests locally and they pass +- [ ] I have linted and auto-formatted the code +- [ ] If there is new or changed functionality, I have added/updated the tests +- [ ] If there is new or changed functionality, I have added/updated the documentation diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..0bed0d7 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,39 @@ +name: Main + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + # https://help.github.com/en/articles/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix + strategy: + fail-fast: false + matrix: + # Uncomment ci_node_total and ci_node_index, ONLY if we have multiple tests + # and need to run it in parallel + # [n] - where the n is a number of parallel jobs you want to run your tests on. + # Use a higher number if you have slow tests to split them between more parallel jobs. + # Remember to update the value of the `ci_node_index` below to (0..n-1). + #ci_node_total: [8] + # Indexes for parallel jobs (starting from zero). + # E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc. + #ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7] + java-version: ['11', '17', '21'] + + env: + TZ: "Europe/Ireland" + + steps: + - uses: actions/checkout@v2 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'oracle' + cache: 'gradle' + + - name: Run tests + run: | + ./gradlew test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d47f292 --- /dev/null +++ b/.gitignore @@ -0,0 +1,104 @@ +# General +*.log +*.keystore +.history/ +*.tgz + +# git-secret rules +.secrets/*.* +!.secrets/*.secret +!.secrets/public-keys/ +.gitsecret/keys/random_seed +.gitsecret/keys/pubring.kbx~ +gradle.properties + +.ci/gradle* + +# You can put a script which call `yarn prettier-eslint --write` that prettify the file in-place. +# Keep in mind the hook files are executed by alphabet sequence: so `in_place_prettier_eslint` will be executed before `prettier_eslint` +buildtools/hooks/pre-commit.d/in_place_prettier_eslint + +# Archives / Binaries +*.jar +*.zip +*.tar.gz +*.so +*.xcarchive + +# MacOS +.DS_Store + +# Build Output +build/ +out/ +bin/ +codemr/ + +# JetBrains +.idea/ +*.iml +*.ipr +*.iws +.mvn/ +.project + +# VSCode +.vscode/ +*.code-workspace + +# Eclipse +.settings/ +.classpath + +# Java +*.hprof +.gradle/ +!gradle-wrapper.jar +!gradle-wrapper.properties + + +# Node or NPM or Yarn +npm-debug.log* +yarn-debug.log* +yarn-error.log* +node_modules/ +.eslintcache + +# Jest/Bamboo test results output +jest.json +coverage/ + +# Vagrant +.vagrant/ + +# Xcode +# +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +**/*.framework/ + + +# VIM +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] +version.yml + +#Andorid +local.properties +/examples/src/main/resources/application-local.yml diff --git a/README.md b/README.md index 993305e..b34020e 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ # firetail-java-lib + +## Building the library + +1. In the projects root folder, run `./gradlew build -x test` +2. Run `./gradlew publishToMavenLocal` + +### Springboot configuration +1. In `build.gradle.kts` (if you are using kotlin configuration), add within `dependencies {}`, the firetail library: + +``` +dependencies { + implementation("com.github.firetail-io:firetail-java-lib:$version") +} +``` +2. Also in `build.gradle.kts` add below the `plugins {}`: + +``` +group = "com.github.firetail-io" +version = "0.0.1-SNAPSHOT" +``` + +3. In your springboot project, add `application-local.yml` in `build/resources` + +**NOTE: Ensure there is no FIRETAIL_URL and FIRETAIL_API_KEY variable in your environment,** + **which will override your yaml file configuration** + +```yaml +firetail: + apikey: "PS-02....441b09761c3" + url: "https://your-apiapi.logging.eu-north-west-99.sandbox.firetail.app" + ## Cache control before dispatching logs to API + buffer: + # Millis + interval: 100000 + # Max capacity + capacity: 5 +``` diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..3ae35ca --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,89 @@ +plugins { + `java-library` + // application + `maven-publish` + signing + // Not possible to set the version for a plugin from a variable. + kotlin("plugin.spring") version "1.8.21" + kotlin("jvm") version "1.8.21" + id("io.spring.dependency-management") version "1.1.2" +} + +buildscript { + val kotlinVersion = "1.8.21" + dependencies { + classpath("org.jetbrains.kotlin:kotlin-noarg:$kotlinVersion") + classpath("org.jmailen.gradle:kotlinter-gradle:3.14.0") + } +} + +repositories { + mavenLocal() + maven { + url = uri("https://repo.maven.apache.org/maven2/") + } + mavenCentral() +} + +group = "com.github.firetail-io" +version = "0.0.1-SNAPSHOT" +description = "firetail-java-lib" + +// java.sourceCompatibility = JavaVersion.VERSION_1_8 + +dependencies { + // Dependencies are transitively imported from spring-boot-dependencies + implementation( + platform("org.springframework.boot:spring-boot-dependencies:3.1.5"), + ) + implementation("org.jetbrains.kotlin:kotlin-stdlib") + api("com.fasterxml.jackson.module:jackson-module-kotlin") + api("org.slf4j:slf4j-api") + api("ch.qos.logback:logback-classic") + compileOnly("org.yaml:snakeyaml:2.2") + compileOnly("org.springframework.boot:spring-boot-autoconfigure") + compileOnly("org.springframework:spring-context") + compileOnly("org.springframework:spring-web") + compileOnly("org.springframework:spring-webmvc") + testImplementation(kotlin("test")) + testImplementation("org.springframework.boot:spring-boot-starter-test") + // javax vs. jakarta - Spring boot 3x uses jakarta. Need a version of this for < 3x for + // older SB apps + compileOnly("jakarta.annotation:jakarta.annotation-api") + compileOnly("jakarta.servlet:jakarta.servlet-api:5.0.0") + testImplementation("org.springframework.cloud:spring-cloud-contract-wiremock:4.0.4") + testImplementation("jakarta.servlet:jakarta.servlet-api") + // end javax vs. jakarta + testImplementation("org.springframework:spring-webmvc") + testImplementation("org.assertj:assertj-core") + testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0") +} + +signing { + setRequired { + // signing is only required if the artifacts are to be published + gradle.taskGraph.allTasks.any { it is PublishToMavenRepository } + } + if (project.hasProperty("signJar") && project.property("signJar") == "true") { + val signingKey: String? by project + val signingPassword: String? by project + useInMemoryPgpKeys(signingKey, signingPassword) + sign(configurations["archives"]) + } +} + +publishing { + publications { + create("jar") { + artifact(tasks.named("jar")) + } + } +} + +kotlin { + jvmToolchain(17) +} + +tasks.test { // See 5️⃣ + useJUnitPlatform() // JUnitPlatform for tests. See 6️⃣ +} diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..2dac8f4 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,41 @@ +## Spring Boot Demo + +Requires Java 17 + +You will require an `application-local.yml` file. It will look something like this: + +```yaml +firetail: + apikey: "PS-02....441b09761c3" + url: "https://your-apiapi.logging.eu-north-west-99.sandbox.firetail.app" + ## Cache control before dispatching logs to API + buffer: + # Millis + interval: 100000 + # Max capacity + capacity: 5 + +``` + +Firstly, build the Firetail-Java-Library + +```bash +# Build the firetail library +cd .. +./gradlew build publishToMavenLocal +# Run the example +cd examples +./gradlew bootRun +# By default, you'll want to hit this endpoint 5 times before the logs are dispatched +# Otherwise hit it < 5 and wait for 10 seconds +curl http://localhost:8080/hello +``` + +You can then login to the [FireTail app](https://www.sandbox.firetail.app/) and see your logs + +## Open API documentation + +This example uses OpenAPI v3 + + 1. http://localhost:8080/api-docs + 2. http://localhost:8080/swagger-ui.html \ No newline at end of file diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts new file mode 100644 index 0000000..69464db --- /dev/null +++ b/examples/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("org.springframework.boot") version "3.1.3" + id("io.spring.dependency-management") version "1.1.2" + kotlin("jvm") version "1.6.10" +} + +group = "com.github.firetail-io" +version = "0.0.1-SNAPSHOT" + +repositories { + mavenLocal() + maven { + url = uri("https://repo.maven.apache.org/maven2/") + } + mavenCentral() +} + +dependencies { + implementation( + platform("org.springframework.boot:spring-boot-dependencies:3.1.5"), + ) + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0") + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.github.firetail-io:firetail-java-lib:$version") + testImplementation("org.springframework.boot:spring-boot-starter-test") +} + +tasks.named("bootRun") { + args("--spring.profiles.active=local") +} diff --git a/examples/gradle/wrapper/gradle-wrapper.jar b/examples/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7f93135 Binary files /dev/null and b/examples/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/gradle/wrapper/gradle-wrapper.properties b/examples/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3fa8f86 --- /dev/null +++ b/examples/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/gradlew b/examples/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/examples/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/gradlew.bat b/examples/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/examples/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/settings.gradle.kts b/examples/settings.gradle.kts new file mode 100644 index 0000000..97da9e0 --- /dev/null +++ b/examples/settings.gradle.kts @@ -0,0 +1,10 @@ +pluginManagement { + plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" + } +} +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") +} + +rootProject.name = "firetail-spring-demo" diff --git a/examples/src/main/java/com/example/demo/DemoApplication.java b/examples/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 0000000..6d28f62 --- /dev/null +++ b/examples/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,26 @@ +package com.example.demo; + +import io.firetail.logging.spring.EnableFiretail; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; + +@SpringBootApplication +@RestController +@EnableFiretail +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + + @GetMapping("/hello") + public String hello() { + return String.format("Hello %s, utc: %s!", LocalDateTime.now(), ZonedDateTime.now(Clock.systemUTC())); + } +} diff --git a/examples/src/main/resources/application.yml b/examples/src/main/resources/application.yml new file mode 100644 index 0000000..f366319 --- /dev/null +++ b/examples/src/main/resources/application.yml @@ -0,0 +1,26 @@ +firetail: + apikey: "set-your-ft-api-key-here" + url: "https://your.sandbox.firetail.app" + buffer: + interval: 1000 + capacity: 10 + +logging: + level: + root: debug + io: + firetail: debug + com.example.demo: debug + javax: error + java: debug + #java.net: debug + sun: error + netflix: error + jdk: error + org: + springframework: error + apache: error + +springdoc: + api-docs: + path: /api-docs diff --git a/examples/src/test/java/com/example/demo/DemoApplicationTests.java b/examples/src/test/java/com/example/demo/DemoApplicationTests.java new file mode 100644 index 0000000..2778a6a --- /dev/null +++ b/examples/src/test/java/com/example/demo/DemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.demo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DemoApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7f93135 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3fa8f86 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pom.xml b/pom.xml index 2ac8a70..252c401 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ 0.0.1.SNAPSPOT firetail-java-lib Java Library for Firetail - https://github.com/muhammadn/firetail-java-lib + https://github.com/muhammadn/firetail-java-lib @@ -30,7 +30,7 @@ 2.6 4.0.1 5.3 - 5.2.3.RELEASE + 5.2.21.RELEASE 2.1.4.RELEASE 1.7.26 @@ -74,7 +74,7 @@ org.springframework.boot spring-boot-autoconfigure - ${spring.boot.version} + ${spring.boot.version} provided diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..2c8deb4 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,10 @@ +pluginManagement { + plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" + } +} +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") +} + +rootProject.name = "firetail-java-lib" diff --git a/src/main/java/io/firetail/logging/client/RestTemplateSetHeaderInterceptor.java b/src/main/java/io/firetail/logging/client/RestTemplateSetHeaderInterceptor.java deleted file mode 100644 index 5965953..0000000 --- a/src/main/java/io/firetail/logging/client/RestTemplateSetHeaderInterceptor.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.firetail.logging.client; - -import org.slf4j.MDC; -import org.springframework.http.HttpRequest; -import org.springframework.http.client.ClientHttpRequestExecution; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.http.client.ClientHttpResponse; - -import java.io.IOException; - -public class RestTemplateSetHeaderInterceptor implements ClientHttpRequestInterceptor { - - public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { - request.getHeaders().add("X-Correlation-ID", MDC.get("X-Correlation-ID")); - request.getHeaders().add("X-Request-ID", MDC.get("X-Request-ID")); - return execution.execute(request, body); - } - -} diff --git a/src/main/java/io/firetail/logging/config/SpringLoggerAutoConfiguration.java b/src/main/java/io/firetail/logging/config/SpringLoggerAutoConfiguration.java deleted file mode 100644 index 09d50d8..0000000 --- a/src/main/java/io/firetail/logging/config/SpringLoggerAutoConfiguration.java +++ /dev/null @@ -1,142 +0,0 @@ -package io.firetail.logging.config; - -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.core.net.ssl.KeyStoreFactoryBean; -import ch.qos.logback.core.net.ssl.SSLConfiguration; -//import net.logstash.logback.appender.LogstashTcpSocketAppender; -//import net.logstash.logback.encoder.LogstashEncoder; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.web.client.RestTemplate; - -import io.firetail.logging.client.RestTemplateSetHeaderInterceptor; -import io.firetail.logging.filter.SpringLoggingFilter; -import io.firetail.logging.util.UniqueIDGenerator; - -import javax.annotation.PostConstruct; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -@Configuration -@ConfigurationProperties(prefix = "logging.logstash") -public class SpringLoggingAutoConfiguration { - - private static final String FIRETAIL_APPENDER_NAME = "FIRETAIL"; - - private String url = "localhost:8500"; - private String ignorePatterns; - private boolean logHeaders; - private String trustStoreLocation; - private String trustStorePassword; - @Value("${spring.application.name:-}") - String name; - @Autowired(required = false) - Optional template; - - @Bean - public UniqueIDGenerator generator() { - return new UniqueIDGenerator(); - } - - @Bean - public SpringLoggingFilter loggingFilter() { - return new SpringLoggingFilter(generator(), ignorePatterns, logHeaders); - } - - @Bean - @ConditionalOnMissingBean(RestTemplate.class) - public RestTemplate restTemplate() { - RestTemplate restTemplate = new RestTemplate(); - List interceptorList = new ArrayList(); - interceptorList.add(new RestTemplateSetHeaderInterceptor()); - restTemplate.setInterceptors(interceptorList); - return restTemplate; - } - - /* rewrite this method to send data to firetail backend - @Bean - @ConditionalOnProperty("logging.firetail.enabled") - public FiretailTcpSocketAppender firetailAppender() { - LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); - LogstashTcpSocketAppender logstashTcpSocketAppender = new LogstashTcpSocketAppender(); - logstashTcpSocketAppender.setName(FIRETAIL_APPENDER_NAME); - logstashTcpSocketAppender.setContext(loggerContext); - logstashTcpSocketAppender.addDestination(url); - if (trustStoreLocation != null) { - SSLConfiguration sslConfiguration = new SSLConfiguration(); - KeyStoreFactoryBean factory = new KeyStoreFactoryBean(); - factory.setLocation(trustStoreLocation); - if (trustStorePassword != null) - factory.setPassword(trustStorePassword); - sslConfiguration.setTrustStore(factory); - logstashTcpSocketAppender.setSsl(sslConfiguration); - } - LogstashEncoder encoder = new LogstashEncoder(); - encoder.setContext(loggerContext); - encoder.setIncludeContext(true); - encoder.setCustomFields("{\"appname\":\"" + name + "\"}"); - encoder.start(); - logstashTcpSocketAppender.setEncoder(encoder); - logstashTcpSocketAppender.start(); - loggerContext.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(logstashTcpSocketAppender); - return logstashTcpSocketAppender; - } */ - - @PostConstruct - public void init() { - template.ifPresent(restTemplate -> { - List interceptorList = new ArrayList(); - interceptorList.add(new RestTemplateSetHeaderInterceptor()); - restTemplate.setInterceptors(interceptorList); - }); - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getTrustStoreLocation() { - return trustStoreLocation; - } - - public void setTrustStoreLocation(String trustStoreLocation) { - this.trustStoreLocation = trustStoreLocation; - } - - public String getTrustStorePassword() { - return trustStorePassword; - } - - public void setTrustStorePassword(String trustStorePassword) { - this.trustStorePassword = trustStorePassword; - } - - public String getIgnorePatterns() { - return ignorePatterns; - } - - public void setIgnorePatterns(String ignorePatterns) { - this.ignorePatterns = ignorePatterns; - } - - public boolean isLogHeaders() { - return logHeaders; - } - - public void setLogHeaders(boolean logHeaders) { - this.logHeaders = logHeaders; - } -} diff --git a/src/main/java/io/firetail/logging/filter/SpringLoggerFilter.java b/src/main/java/io/firetail/logging/filter/SpringLoggerFilter.java deleted file mode 100644 index ce11374..0000000 --- a/src/main/java/io/firetail/logging/filter/SpringLoggerFilter.java +++ /dev/null @@ -1,99 +0,0 @@ -package io.firetail.logging.filter; - -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.slf4j.MDC; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.HandlerExecutionChain; -import org.springframework.web.servlet.mvc.method.RequestMappingInfo; -import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; -import io.firetail.logging.util.UniqueIDGenerator; -import io.firetail.logging.wrapper.SpringRequestWrapper; -import io.firetail.logging.wrapper.SpringResponseWrapper; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Map; -import java.util.Objects; - -public class SpringLoggingFilter extends OncePerRequestFilter { - - private static final Logger LOGGER = LoggerFactory.getLogger(SpringLoggingFilter.class); - private UniqueIDGenerator generator; - private String ignorePatterns; - private boolean logHeaders; - - @Autowired - ApplicationContext context; - - public SpringLoggingFilter(UniqueIDGenerator generator, String ignorePatterns, boolean logHeaders) { - this.generator = generator; - this.ignorePatterns = ignorePatterns; - this.logHeaders = logHeaders; - } - - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { - if (ignorePatterns != null && request.getRequestURI().matches(ignorePatterns)) { - chain.doFilter(request, response); - } else { - generator.generateAndSetMDC(request); - try { - getHandlerMethod(request); - } catch (Exception e) { - LOGGER.trace("Cannot get handler method"); - } - final long startTime = System.currentTimeMillis(); - final SpringRequestWrapper wrappedRequest = new SpringRequestWrapper(request); - if (logHeaders) - LOGGER.info("Request: method={}, uri={}, payload={}, headers={}, audit={}", wrappedRequest.getMethod(), - wrappedRequest.getRequestURI(), IOUtils.toString(wrappedRequest.getInputStream(), - wrappedRequest.getCharacterEncoding()), wrappedRequest.getAllHeaders(), value("audit", true)); - else - LOGGER.info("Request: method={}, uri={}, payload={}, audit={}", wrappedRequest.getMethod(), - wrappedRequest.getRequestURI(), IOUtils.toString(wrappedRequest.getInputStream(), - wrappedRequest.getCharacterEncoding()), value("audit", true)); - final SpringResponseWrapper wrappedResponse = new SpringResponseWrapper(response); - wrappedResponse.setHeader("X-Request-ID", MDC.get("X-Request-ID")); - wrappedResponse.setHeader("X-Correlation-ID", MDC.get("X-Correlation-ID")); - - try { - chain.doFilter(wrappedRequest, wrappedResponse); - } catch (Exception e) { - logResponse(startTime, wrappedResponse, 500); - throw e; - } - logResponse(startTime, wrappedResponse, wrappedResponse.getStatus()); - } - } - - private void logResponse(long startTime, SpringResponseWrapper wrappedResponse, int overriddenStatus) throws IOException { - final long duration = System.currentTimeMillis() - startTime; - wrappedResponse.setCharacterEncoding("UTF-8"); - if (logHeaders) - LOGGER.info("Response({} ms): status={}, payload={}, headers={}, audit={}", value("X-Response-Time", duration), - value("X-Response-Status", overriddenStatus), IOUtils.toString(wrappedResponse.getContentAsByteArray(), - wrappedResponse.getCharacterEncoding()), wrappedResponse.getAllHeaders(), value("audit", true)); - else - LOGGER.info("Response({} ms): status={}, payload={}, audit={}", value("X-Response-Time", duration), - value("X-Response-Status", overriddenStatus), - IOUtils.toString(wrappedResponse.getContentAsByteArray(), wrappedResponse.getCharacterEncoding()), value("audit", true)); - } - - private void getHandlerMethod(HttpServletRequest request) throws Exception { - RequestMappingHandlerMapping mappings1 = (RequestMappingHandlerMapping) context.getBean("requestMappingHandlerMapping"); - Map handlerMethods = mappings1.getHandlerMethods(); - HandlerExecutionChain handler = mappings1.getHandler(request); - if (Objects.nonNull(handler)) { - HandlerMethod handler1 = (HandlerMethod) handler.getHandler(); - MDC.put("X-Operation-Name", handler1.getBeanType().getSimpleName() + "." + handler1.getMethod().getName()); - } - } - -} diff --git a/src/main/java/io/firetail/logging/util/UniqueIDGenerator.java b/src/main/java/io/firetail/logging/util/UniqueIDGenerator.java deleted file mode 100644 index 7d95a69..0000000 --- a/src/main/java/io/firetail/logging/util/UniqueIDGenerator.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.firetail.logging.util; - -import org.slf4j.MDC; - -import javax.servlet.http.HttpServletRequest; -import java.util.UUID; - -public class UniqueIDGenerator { - - private static final String REQUEST_ID_HEADER_NAME = "X-Request-ID"; - private static final String CORRELATION_ID_HEADER_NAME = "X-Correlation-ID"; - - public void generateAndSetMDC(HttpServletRequest request) { - MDC.clear(); - String requestId = request.getHeader(REQUEST_ID_HEADER_NAME); - if (requestId == null) - requestId = UUID.randomUUID().toString(); - MDC.put(REQUEST_ID_HEADER_NAME, requestId); - - String correlationId = request.getHeader(CORRELATION_ID_HEADER_NAME); - if (correlationId == null) - correlationId = UUID.randomUUID().toString(); - MDC.put(CORRELATION_ID_HEADER_NAME, correlationId); - } - -} diff --git a/src/main/java/io/firetail/logging/wrapper/ServletOutputStreamWrapper.java b/src/main/java/io/firetail/logging/wrapper/ServletOutputStreamWrapper.java deleted file mode 100644 index c9964dd..0000000 --- a/src/main/java/io/firetail/logging/wrapper/ServletOutputStreamWrapper.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.firetail.logging.wrapper; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -import javax.servlet.ServletOutputStream; -import javax.servlet.WriteListener; - -public class ServletOutputStreamWrapper extends ServletOutputStream { - - private OutputStream outputStream; - private ByteArrayOutputStream copy; - - public ServletOutputStreamWrapper(OutputStream outputStream) { - this.outputStream = outputStream; - this.copy = new ByteArrayOutputStream(); - } - - @Override - public void write(int b) throws IOException { - outputStream.write(b); - copy.write(b); - } - - public byte[] getCopy() { - return copy.toByteArray(); - } - - @Override - public boolean isReady() { - return true; - } - - @Override - public void setWriteListener(WriteListener writeListener) { - - } -} diff --git a/src/main/java/io/firetail/logging/wrapper/SpringRequestWrapper.java b/src/main/java/io/firetail/logging/wrapper/SpringRequestWrapper.java deleted file mode 100644 index 79be944..0000000 --- a/src/main/java/io/firetail/logging/wrapper/SpringRequestWrapper.java +++ /dev/null @@ -1,57 +0,0 @@ -package io.firetail.logging.wrapper; - -import org.apache.commons.io.IOUtils; - -import javax.servlet.ReadListener; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -public class SpringRequestWrapper extends HttpServletRequestWrapper { - - private byte[] body; - - public SpringRequestWrapper(HttpServletRequest request) { - super(request); - try { - body = IOUtils.toByteArray(request.getInputStream()); - } catch (IOException ex) { - body = new byte[0]; - } - } - - @Override - public ServletInputStream getInputStream() throws IOException { - return new ServletInputStream() { - public boolean isFinished() { - return false; - } - - public boolean isReady() { - return true; - } - - public void setReadListener(ReadListener readListener) { - - } - - ByteArrayInputStream byteArray = new ByteArrayInputStream(body); - - @Override - public int read() throws IOException { - return byteArray.read(); - } - }; - } - - public Map getAllHeaders() { - final Map headers = new HashMap<>(); - Collections.list(getHeaderNames()).forEach(it -> headers.put(it, getHeader(it))); - return headers; - } -} diff --git a/src/main/java/io/firetail/logging/wrapper/SpringResponseWrapper.java b/src/main/java/io/firetail/logging/wrapper/SpringResponseWrapper.java deleted file mode 100644 index b8c191c..0000000 --- a/src/main/java/io/firetail/logging/wrapper/SpringResponseWrapper.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.firetail.logging.wrapper; - -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.util.HashMap; -import java.util.Map; - -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; - -public class SpringResponseWrapper extends HttpServletResponseWrapper { - - private ServletOutputStream outputStream; - private PrintWriter writer; - private ServletOutputStreamWrapper copier; - - public SpringResponseWrapper(HttpServletResponse response) throws IOException { - super(response); - } - - @Override - public ServletOutputStream getOutputStream() throws IOException { - if (writer != null) { - throw new IllegalStateException("getWriter() has already been called on this response."); - } - - if (outputStream == null) { - outputStream = getResponse().getOutputStream(); - copier = new ServletOutputStreamWrapper(outputStream); - } - - return copier; - } - - @Override - public PrintWriter getWriter() throws IOException { - if (outputStream != null) { - throw new IllegalStateException("getOutputStream() has already been called on this response."); - } - - if (writer == null) { - copier = new ServletOutputStreamWrapper(getResponse().getOutputStream()); - writer = new PrintWriter(new OutputStreamWriter(copier, getResponse().getCharacterEncoding()), true); - } - - return writer; - } - - @Override - public void flushBuffer() throws IOException { - if (writer != null) { - writer.flush(); - } - else if (outputStream != null) { - copier.flush(); - } - } - - public byte[] getContentAsByteArray() { - if (copier != null) { - return copier.getCopy(); - } - else { - return new byte[0]; - } - } - - public Map getAllHeaders() { - final Map headers = new HashMap<>(); - getHeaderNames().forEach(it -> headers.put(it, getHeader(it))); - return headers; - } - -} - diff --git a/src/main/kotlin/io/firetail/logging/core/Constants.kt b/src/main/kotlin/io/firetail/logging/core/Constants.kt new file mode 100644 index 0000000..80b4e81 --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/core/Constants.kt @@ -0,0 +1,15 @@ +package io.firetail.logging.core + +class Constants { + companion object { + const val REQUEST_ID = "X-Request-ID" + const val CORRELATION_ID = "X-Correlation-ID" + const val OP_NAME = "X-Operation-Name" + const val RESPONSE_TIME = "X-Response-Time" + const val RESPONSE_STATUS = "X-Response-Status" + const val AUDIT = "audit" + + // const val FIRETAIL_APPENDER_NAME = "FIRETAIL" + val empty = ByteArray(0) + } +} diff --git a/src/main/kotlin/io/firetail/logging/core/FiretailBuffer.kt b/src/main/kotlin/io/firetail/logging/core/FiretailBuffer.kt new file mode 100644 index 0000000..c43f14c --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/core/FiretailBuffer.kt @@ -0,0 +1,82 @@ +package io.firetail.logging.core + +import io.firetail.logging.core.FiretailLogger.Companion.LOGGER +import io.firetail.logging.servlet.FiretailMapper +import io.firetail.logging.spring.FiretailConfig +import org.slf4j.LoggerFactory +import java.util.* +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock + +class FiretailBuffer( + private val firetailConfig: FiretailConfig, + private val firetailTemplate: FiretailTemplate, + private val firetailMapper: FiretailMapper = FiretailMapper(), +) { + private val buffer: MutableList = mutableListOf() + private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() + private val flushCallback = mutableListOf() + private val bufferLock = ReentrantLock() + + init { + // Schedule the periodic flush task + scheduler.scheduleAtFixedRate( + object : TimerTask() { + override fun run() { + flush() + } + }, + firetailConfig.flushIntervalMillis, + firetailConfig.flushIntervalMillis, + TimeUnit.MILLISECONDS, + ) + } + + // Caller sycronizes access before calling this function + fun add(item: FiretailData) { + bufferLock.lock() + try { + buffer.add(item) + if (buffer.size >= firetailConfig.capacity) { + flush() + } + } finally { + bufferLock.unlock() + } + } + + // Threadsafe - write and reset the cached data + fun flush(): String { + bufferLock.lock() + try { + if (buffer.isNotEmpty()) { + LOGGER.debug("Buffer flushing ${buffer.size}") + val result = firetailTemplate.send(buffer) + buffer.clear() + return firetailMapper.getResult(result) + } + } finally { + bufferLock.unlock() + } + return "" + } + + fun get(): List { + return buffer.toList() + } + + // Cleanup method to stop the scheduler + fun stop() { + scheduler.shutdown() + } + + fun size(): Int { + return buffer.size + } + + companion object { + val LOGGER = LoggerFactory.getLogger(FiretailBuffer::class.java) + } +} diff --git a/src/main/kotlin/io/firetail/logging/core/FiretailData.kt b/src/main/kotlin/io/firetail/logging/core/FiretailData.kt new file mode 100644 index 0000000..ce082b9 --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/core/FiretailData.kt @@ -0,0 +1,29 @@ +package io.firetail.logging.core + +import org.springframework.http.HttpStatus +import java.time.LocalDateTime +import java.time.ZoneOffset + +data class FiretailData( + val version: String = "1.0.0-alpha", + val dateCreated: Long = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000L, + val executionTime: Int = 0, + val request: FtRequest = FtRequest(), + val response: FtResponse = FtResponse(), +) + +data class FtRequest( + val httpProtocol: String = "HTTP", + val method: String = "GET", + val body: String = "", + val headers: Map> = mapOf(), + val ip: String = "127.0.0.1", + val resource: String? = "", + val uri: String = "/", +) + +data class FtResponse( + val statusCode: Int = HttpStatus.OK.value(), + val body: String = "", + val headers: Map> = mapOf(), +) diff --git a/src/main/kotlin/io/firetail/logging/core/FiretailLogger.kt b/src/main/kotlin/io/firetail/logging/core/FiretailLogger.kt new file mode 100644 index 0000000..bbad951 --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/core/FiretailLogger.kt @@ -0,0 +1,43 @@ +package io.firetail.logging.core + +import io.firetail.logging.spring.FiretailConfig +import io.firetail.logging.servlet.SpringRequestWrapper +import io.firetail.logging.servlet.SpringResponseWrapper +import org.slf4j.LoggerFactory + +class FiretailLogger (val firetailConfig: FiretailConfig) { + fun logRequest(wrappedRequest: SpringRequestWrapper) = + if (firetailConfig.logHeaders) { + logWithHeaders(wrappedRequest) + } else { + logNoHeaders(wrappedRequest) + } + + private fun logNoHeaders(wrappedRequest: SpringRequestWrapper) { + LOGGER.info( + "${FiretailTemplate.logRequestPrefix} method: ${wrappedRequest.method}, uri: ${wrappedRequest.requestURI}", + ) + } + + private fun logWithHeaders(wrappedRequest: SpringRequestWrapper) { + LOGGER.info( + "${FiretailTemplate.logRequestPrefix} " + + "method: ${wrappedRequest.method}, " + + "uri: ${wrappedRequest.requestURI}, " + + "headers: ${wrappedRequest.allHeaders}", + ) + } + + fun logResponse( + wrappedResponse: SpringResponseWrapper, + status: Int = wrappedResponse.status, + duration: Long, + ) { + LOGGER.info( + "${FiretailTemplate.logResponsePrefix} ms: $duration, status: $status, headers: ${wrappedResponse.allHeaders}", + ) + } + companion object { + val LOGGER = LoggerFactory.getLogger(FiretailLogger::class.java) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/firetail/logging/core/FiretailTemplate.kt b/src/main/kotlin/io/firetail/logging/core/FiretailTemplate.kt new file mode 100644 index 0000000..487278d --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/core/FiretailTemplate.kt @@ -0,0 +1,50 @@ +package io.firetail.logging.core + +import io.firetail.logging.servlet.FiretailMapper +import io.firetail.logging.spring.FiretailConfig +import io.firetail.logging.util.StringUtils +import org.slf4j.LoggerFactory +import java.io.OutputStream +import java.net.HttpURLConnection +import java.net.URL + +class FiretailTemplate(private val firetailConfig: FiretailConfig, private val firetailMapper: FiretailMapper) { + + private val stringUtils: StringUtils = StringUtils() + + companion object { + private val LOGGER = LoggerFactory.getLogger(FiretailTemplate::class.java) + const val logRequestPrefix = "Request:" + const val logResponsePrefix = "Response:" + } + + fun send(fireTailData: List): String { + val jsonBody = firetailMapper.from(fireTailData) + val connection = URL("${firetailConfig.url}${firetailConfig.logsBulk}") + .openConnection() as HttpURLConnection + with(connection) { + requestMethod = "POST" + doOutput = true + setRequestProperty(firetailConfig.key, firetailConfig.apikey) + setRequestProperty("CONTENT-TYPE", "application/nd-json") + } + + // Write the JSON body to the request + val outputStream: OutputStream = connection.outputStream + with(outputStream) { + write(jsonBody.toByteArray(Charsets.UTF_8)) + flush() + close() + } + if (connection.responseCode == HttpURLConnection.HTTP_OK) { + LOGGER.info("Wrote ${fireTailData.size} rows to ${firetailConfig.url}") + return connection.inputStream.bufferedReader().readText() + } else { + LOGGER.error("Failed to dispatch request. Status code: ${connection.responseCode}") + throw RuntimeException( + "HTTP POST request failed with status code: ${connection.responseCode}, " + + "message: ${connection.inputStream.bufferedReader().readText()}", + ) + } + } +} diff --git a/src/main/kotlin/io/firetail/logging/servlet/FiretailFilter.kt b/src/main/kotlin/io/firetail/logging/servlet/FiretailFilter.kt new file mode 100644 index 0000000..5d9823f --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/servlet/FiretailFilter.kt @@ -0,0 +1,106 @@ +package io.firetail.logging.servlet + +import io.firetail.logging.core.Constants.Companion.CORRELATION_ID +import io.firetail.logging.core.Constants.Companion.OP_NAME +import io.firetail.logging.core.Constants.Companion.REQUEST_ID +import io.firetail.logging.core.FiretailBuffer +import io.firetail.logging.core.FiretailLogger +import io.firetail.logging.core.FiretailTemplate +import io.firetail.logging.spring.FiretailConfig +import io.firetail.logging.util.FiretailMDC +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.stereotype.Service +import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.method.HandlerMethod +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping +import java.util.* +import java.util.concurrent.CompletableFuture + +@Service +@ConditionalOnClass(FiretailConfig::class) +class FiretailFilter( + private val firetailLogContext: FiretailMDC, + private val firetailLogger: FiretailLogger, + private val firetailConfig: FiretailConfig, + private val firetailMapper: FiretailMapper, +) { + @Autowired + private lateinit var firetailBuffer: FiretailBuffer + + @Autowired + lateinit var context: ApplicationContext + + @Bean + @ConditionalOnClass(FiretailConfig::class) + fun firetailRequestFilter(): OncePerRequestFilter { + return object : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + chain: FilterChain, + ) { + if (firetailConfig.ignorePatterns != null && request.requestURI.matches(firetailConfig.ignorePatterns!!.toRegex())) { + chain.doFilter(request, response) + } else { + firetailLogContext.generateAndSetMDC(request) + try { + getHandlerMethod(request) + } catch (e: Exception) { + LOGGER.trace("Cannot get handler method") + } + val startTime = System.currentTimeMillis() + val wrappedRequest = SpringRequestWrapper(request) + firetailLogger.logRequest(wrappedRequest) + val wrappedResponse = SpringResponseWrapper(response) + try { + with(wrappedResponse) { + setHeader(REQUEST_ID, firetailLogContext.get(REQUEST_ID)) + setHeader(CORRELATION_ID, firetailLogContext.get(CORRELATION_ID)) + } + chain.doFilter(wrappedRequest, wrappedResponse) + val duration = System.currentTimeMillis() - startTime + firetailLogger.logResponse(wrappedResponse, duration = duration) + val firetailLog = + firetailMapper.from( + wrappedRequest, + wrappedResponse, + duration, + ) + CompletableFuture.runAsync { + try { + firetailBuffer.add(firetailLog) + } catch (e: Exception) { + LOGGER.error(e.message) + throw e + } + } + } catch (e: Exception) { + firetailLogger.logResponse(wrappedResponse, 500, startTime) + throw e + } + } + } + } + } + + private fun getHandlerMethod(request: HttpServletRequest) { + val mappings = context.getBean("requestMappingHandlerMapping") + as RequestMappingHandlerMapping + val nullableHandler = mappings.getHandler(request) + if (Objects.nonNull(nullableHandler)) { + val handler = nullableHandler?.handler as HandlerMethod + firetailLogContext.put(OP_NAME, handler.beanType.simpleName + "." + handler.method.name) + } + } + + companion object { + private val LOGGER = LoggerFactory.getLogger(FiretailTemplate::class.java) + } +} diff --git a/src/main/kotlin/io/firetail/logging/servlet/FiretailHeaderInterceptor.kt b/src/main/kotlin/io/firetail/logging/servlet/FiretailHeaderInterceptor.kt new file mode 100644 index 0000000..b871589 --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/servlet/FiretailHeaderInterceptor.kt @@ -0,0 +1,25 @@ +package io.firetail.logging.servlet + +import io.firetail.logging.core.Constants.Companion.CORRELATION_ID +import io.firetail.logging.core.Constants.Companion.REQUEST_ID +import org.slf4j.MDC +import org.springframework.http.HttpRequest +import org.springframework.http.client.ClientHttpRequestExecution +import org.springframework.http.client.ClientHttpRequestInterceptor +import org.springframework.http.client.ClientHttpResponse +import org.springframework.stereotype.Service + +@Service +class FiretailHeaderInterceptor : ClientHttpRequestInterceptor { + override fun intercept( + request: HttpRequest, + body: ByteArray, + execution: ClientHttpRequestExecution, + ): ClientHttpResponse { + with(request) { + headers.add(CORRELATION_ID, MDC.get(CORRELATION_ID)) + headers.add(REQUEST_ID, MDC.get(REQUEST_ID)) + } + return execution.execute(request, body) + } +} diff --git a/src/main/kotlin/io/firetail/logging/servlet/FiretailMapper.kt b/src/main/kotlin/io/firetail/logging/servlet/FiretailMapper.kt new file mode 100644 index 0000000..e19d353 --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/servlet/FiretailMapper.kt @@ -0,0 +1,52 @@ +package io.firetail.logging.servlet + +import com.fasterxml.jackson.databind.ObjectMapper +import io.firetail.logging.core.FiretailData +import io.firetail.logging.core.FtRequest +import io.firetail.logging.core.FtResponse +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import java.util.HashMap + +class FiretailMapper { + private val objectMapper = ObjectMapper() + fun from(request: HttpServletRequest, response: HttpServletResponse, executionTime: Long): FiretailData { + return FiretailData(request = from(request), response = from(response), executionTime = executionTime.toInt()) + } + + fun from(request: HttpServletRequest): FtRequest { + val headers = request.headerNames + .toList() + .mapIndexed { _, value -> value to listOf(request.getHeader(value)) } + .toMap() + + return FtRequest( + httpProtocol = request.protocol, + method = request.method, + headers = headers, + ip = request.remoteAddr, + resource = request.requestURI, + uri = request.requestURL.toString(), // FT calls the defines the URI as URL. + ) + } + + fun from(response: HttpServletResponse): FtResponse { + val headers = response.headerNames + .mapIndexed { _, value -> value to listOf(response.getHeader(value)) } + .toMap() + return FtResponse( + statusCode = response.status, + body = "", + headers = headers, + ) + } + + fun getResult(result: String): String { + return objectMapper.readValue(result, HashMap::class.java) + .get("message") as String + } + + fun from(fireTailData: List): String { + return fireTailData.joinToString("\n") { objectMapper.writeValueAsString(it) } + } +} diff --git a/src/main/kotlin/io/firetail/logging/servlet/ServletOutputStreamWrapper.kt b/src/main/kotlin/io/firetail/logging/servlet/ServletOutputStreamWrapper.kt new file mode 100644 index 0000000..9c9e91f --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/servlet/ServletOutputStreamWrapper.kt @@ -0,0 +1,25 @@ +package io.firetail.logging.servlet + +import jakarta.servlet.ServletOutputStream +import jakarta.servlet.WriteListener +import java.io.ByteArrayOutputStream +import java.io.OutputStream + +class ServletOutputStreamWrapper(private val outputStream: OutputStream) : ServletOutputStream() { + private val copy: ByteArrayOutputStream = ByteArrayOutputStream() + + override fun write(b: Int) { + outputStream.write(b) + copy.write(b) + } + + fun getCopy(): ByteArray { + return copy.toByteArray() + } + + override fun isReady(): Boolean { + return true + } + + override fun setWriteListener(writeListener: WriteListener) {} +} diff --git a/src/main/kotlin/io/firetail/logging/servlet/SpringRequestWrapper.kt b/src/main/kotlin/io/firetail/logging/servlet/SpringRequestWrapper.kt new file mode 100644 index 0000000..bcb347f --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/servlet/SpringRequestWrapper.kt @@ -0,0 +1,50 @@ +package io.firetail.logging.servlet + +import io.firetail.logging.core.Constants.Companion.empty +import jakarta.servlet.ReadListener +import jakarta.servlet.ServletInputStream +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletRequestWrapper +import java.io.ByteArrayInputStream +import java.io.IOException +import java.util.* +import java.util.function.Consumer + +class SpringRequestWrapper(request: HttpServletRequest) : HttpServletRequestWrapper(request) { + private var body: ByteArray + + init { + body = try { + request.inputStream.readBytes() + } catch (ex: IOException) { + empty + } + } + + override fun getInputStream(): ServletInputStream { + return object : ServletInputStream() { + override fun isFinished(): Boolean { + return false + } + + override fun isReady(): Boolean { + return true + } + + override fun setReadListener(readListener: ReadListener) {} + var byteArray = ByteArrayInputStream(body) + + override fun read(): Int { + return byteArray.read() + } + } + } + + val allHeaders: Map + get() { + val headers: MutableMap = HashMap() + Collections.list(headerNames) + .forEach(Consumer { it: String -> headers[it] = getHeader(it) }) + return headers + } +} diff --git a/src/main/kotlin/io/firetail/logging/servlet/SpringResponseWrapper.kt b/src/main/kotlin/io/firetail/logging/servlet/SpringResponseWrapper.kt new file mode 100644 index 0000000..b7a2bb2 --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/servlet/SpringResponseWrapper.kt @@ -0,0 +1,51 @@ +package io.firetail.logging.servlet + +import io.firetail.logging.core.Constants.Companion.empty +import jakarta.servlet.ServletOutputStream +import jakarta.servlet.http.HttpServletResponse +import jakarta.servlet.http.HttpServletResponseWrapper +import java.io.OutputStreamWriter +import java.io.PrintWriter +import java.util.function.Consumer + +class SpringResponseWrapper(response: HttpServletResponse?) : HttpServletResponseWrapper(response) { + private var outputStream: ServletOutputStream? = null + private var writer: PrintWriter? = null + private var copier: ServletOutputStreamWrapper? = null + + override fun getOutputStream(): ServletOutputStream { + check(writer == null) { "getWriter() has already been called on this response." } + copier = ServletOutputStreamWrapper(response.outputStream) + return copier!! + } + + override fun getWriter(): PrintWriter { + check(outputStream == null) { "getOutputStream() has already been called on this response." } + if (writer == null) { + copier = ServletOutputStreamWrapper(response.outputStream) + writer = PrintWriter(OutputStreamWriter(copier!!, response.characterEncoding), true) + } + return writer!! + } + + override fun flushBuffer() { + if (writer != null) { + writer!!.flush() + } else if (outputStream != null) { + copier!!.flush() + } + } + + val contentAsByteArray: ByteArray + get() = if (copier != null) { + copier!!.getCopy() + } else { + empty + } + val allHeaders: Map + get() { + val headers: MutableMap = HashMap() + headerNames.forEach(Consumer { it: String -> headers[it] = getHeader(it) }) + return headers + } +} diff --git a/src/main/kotlin/io/firetail/logging/spring/EnableFiretail.kt b/src/main/kotlin/io/firetail/logging/spring/EnableFiretail.kt new file mode 100644 index 0000000..da6e7aa --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/spring/EnableFiretail.kt @@ -0,0 +1,12 @@ +package io.firetail.logging.spring + +import org.springframework.context.annotation.Import + +/** + * Include this annotation to enable deployment of Firetail support + * classes + */ +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Import(FiretailConfig::class) +annotation class EnableFiretail diff --git a/src/main/kotlin/io/firetail/logging/spring/FiretailBeanFactory.kt b/src/main/kotlin/io/firetail/logging/spring/FiretailBeanFactory.kt new file mode 100644 index 0000000..1fb6d9e --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/spring/FiretailBeanFactory.kt @@ -0,0 +1,40 @@ +package io.firetail.logging.spring + +import io.firetail.logging.core.FiretailBuffer +import io.firetail.logging.core.FiretailTemplate +import io.firetail.logging.servlet.FiretailHeaderInterceptor +import io.firetail.logging.servlet.FiretailMapper +import io.firetail.logging.util.FiretailMDC +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.client.RestTemplate + +@Configuration +@ConditionalOnClass(FiretailConfig::class) +class FiretailBeanFactory { + + @Bean + fun firetailMDC(): FiretailMDC = FiretailMDC() + + @Bean + fun firetailMapper(): FiretailMapper = FiretailMapper() + + @Bean + fun firetailTemplate(firetailConfig: FiretailConfig, firetailMapper: FiretailMapper): FiretailTemplate { + return FiretailTemplate(firetailConfig, firetailMapper) + } + + @Bean + fun firetailBuffer(firetailConfig: FiretailConfig, + firetailTemplate: FiretailTemplate, + firetailMapper: FiretailMapper): FiretailBuffer = + FiretailBuffer(firetailConfig, firetailTemplate, firetailMapper) + + @Bean + fun firetailHeaderInterceptor(restTemplate: RestTemplate): FiretailHeaderInterceptor { + val ftHeader = FiretailHeaderInterceptor() + restTemplate.interceptors.add(ftHeader) + return ftHeader + } +} diff --git a/src/main/kotlin/io/firetail/logging/spring/FiretailConfig.kt b/src/main/kotlin/io/firetail/logging/spring/FiretailConfig.kt new file mode 100644 index 0000000..4646dfd --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/spring/FiretailConfig.kt @@ -0,0 +1,52 @@ +package io.firetail.logging.spring + +import io.firetail.logging.core.FiretailBuffer +import io.firetail.logging.core.FiretailLogger +import io.firetail.logging.servlet.FiretailFilter +import io.firetail.logging.util.StringUtils +import jakarta.annotation.PostConstruct +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.web.client.RestTemplate + +@Configuration +@Import( + StringUtils::class, + FiretailFilter::class, + FiretailBeanFactory::class, + RestTemplate::class, +) +@ConditionalOnClass(EnableFiretail::class) +class FiretailConfig @Autowired constructor( + @Value("\${firetail.ignorePatterns:#null}") + val ignorePatterns: String? = null, + @Value("\${firetail.logHeaders:false}") + val logHeaders: Boolean = false, + @Value("\${firetail.url:http://localhost:8500}") + val url: String, + @Value("\${firetail.apikey:not-defined}") + val apikey: String = "not-defined", + + @Value("\${firetail.buffer.interval:60000}") + val flushIntervalMillis: Long = 60000, + @Value("\${firetail.buffer.capacity:1}") + val capacity: Int = 1, + + ) { + + val key = "x-ft-api-key" + val logsBulk = "/logs/bulk" + + @Bean + fun firetailLogger(): FiretailLogger = FiretailLogger(this) + + @PostConstruct + fun logStatus() { + LoggerFactory.getLogger(FiretailConfig::class.java).info("Firetail Initialized. url: $url") + } +} diff --git a/src/main/kotlin/io/firetail/logging/util/FiretailMDC.kt b/src/main/kotlin/io/firetail/logging/util/FiretailMDC.kt new file mode 100644 index 0000000..d6c8521 --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/util/FiretailMDC.kt @@ -0,0 +1,32 @@ +package io.firetail.logging.util + +import io.firetail.logging.core.Constants.Companion.CORRELATION_ID +import io.firetail.logging.core.Constants.Companion.REQUEST_ID +import jakarta.servlet.http.HttpServletRequest + +class FiretailMDC(private val keyGenerator: KeyGenerator = KeyGenerator()) { + + private val contextData: ThreadLocal> = ThreadLocal.withInitial { mutableMapOf() } + + fun put(key: String, value: String) { + contextData.get()[key] = value + } + + fun get(key: String): String? { + return contextData.get()[key] + } + + fun clear() { + contextData.remove() + } + + fun generateAndSetMDC(request: HttpServletRequest) { + clear() + put(REQUEST_ID, getValue(request, REQUEST_ID)) + put(CORRELATION_ID, getValue(request, CORRELATION_ID)) + } + + private fun getValue(request: HttpServletRequest, key: String): String { + return request.getHeader(key) ?: keyGenerator.generate() + } +} diff --git a/src/main/kotlin/io/firetail/logging/util/KeyGenerator.kt b/src/main/kotlin/io/firetail/logging/util/KeyGenerator.kt new file mode 100644 index 0000000..9e94355 --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/util/KeyGenerator.kt @@ -0,0 +1,11 @@ +package io.firetail.logging.util + +import org.springframework.stereotype.Service +import java.util.* + +@Service +class KeyGenerator { + fun generate(): String { + return UUID.randomUUID().toString() + } +} diff --git a/src/main/kotlin/io/firetail/logging/util/StringUtils.kt b/src/main/kotlin/io/firetail/logging/util/StringUtils.kt new file mode 100644 index 0000000..fdbc211 --- /dev/null +++ b/src/main/kotlin/io/firetail/logging/util/StringUtils.kt @@ -0,0 +1,14 @@ +package io.firetail.logging.util + +import org.springframework.stereotype.Service +import java.nio.charset.Charset +import kotlin.text.Charsets.UTF_8 + +@Service +class StringUtils(private val defaultCharset: Charset = UTF_8) { + fun toString(inputStream: ByteArray, characterEncoding: String = defaultCharset.toString()): String { + return inputStream.toString(Charset.forName(characterEncoding)) + } + + fun charSet() = defaultCharset.toString() +} diff --git a/src/test/kotlin/io/firetail/logging/FiretailBufferTest.kt b/src/test/kotlin/io/firetail/logging/FiretailBufferTest.kt new file mode 100644 index 0000000..d8acdac --- /dev/null +++ b/src/test/kotlin/io/firetail/logging/FiretailBufferTest.kt @@ -0,0 +1,43 @@ +package io.firetail.logging + +import io.firetail.logging.core.FiretailBuffer +import io.firetail.logging.core.FiretailData +import io.firetail.logging.core.FiretailTemplate +import io.firetail.logging.core.FtRequest +import io.firetail.logging.core.FtResponse +import io.firetail.logging.servlet.FiretailMapper +import io.firetail.logging.spring.FiretailConfig +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import kotlin.test.Test + +class FiretailBufferTest { + + @Mock + private lateinit var firetailConfig: FiretailConfig + + @Mock + private lateinit var firetailTemplate: FiretailTemplate + + @BeforeEach + fun setUp() { + MockitoAnnotations.openMocks(this) + } + + @Test + fun buffer() { + val firetailConfig = FiretailConfig(capacity = 10, url = "") + // val firetailTemplate: FiretailTemplate = FiretailTemplate(firetailConfig) + val firetailBuffer = FiretailBuffer(firetailConfig, firetailTemplate) + firetailBuffer.add(FiretailData(request = FtRequest(), response = FtResponse())) + assertThat(firetailBuffer.size() == 1) + Mockito.`when`(firetailTemplate.send(any())) + .thenReturn("{\n \"message\": \"success\"\n}") + firetailBuffer.flush() + assertThat(firetailBuffer.size() == 0) + } +} diff --git a/src/test/kotlin/io/firetail/logging/FiretailDataSerialization.kt b/src/test/kotlin/io/firetail/logging/FiretailDataSerialization.kt new file mode 100644 index 0000000..56849b8 --- /dev/null +++ b/src/test/kotlin/io/firetail/logging/FiretailDataSerialization.kt @@ -0,0 +1,28 @@ +package io.firetail.logging + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.firetail.logging.core.FiretailData +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.core.io.ClassPathResource + +class FiretailDataSerialization { + @Test + fun validateV1Alpha() { + val firetailLog = firetailLog() + assertThat(firetailLog) + .isNotNull + .hasFieldOrPropertyWithValue("version", "1.0.0-alpha") + .hasFieldOrProperty("request") + .hasFieldOrProperty("response") + } + + companion object { + @JvmStatic + fun firetailLog(): FiretailData? { + val objectMapper = jacksonObjectMapper() + val jsonFile = ClassPathResource("/schemaV1Alpha.json").file + return objectMapper.readValue(jsonFile, FiretailData::class.java) + } + } +} diff --git a/src/test/kotlin/io/firetail/logging/FiretailDisabledTest.kt b/src/test/kotlin/io/firetail/logging/FiretailDisabledTest.kt new file mode 100644 index 0000000..6e9bf36 --- /dev/null +++ b/src/test/kotlin/io/firetail/logging/FiretailDisabledTest.kt @@ -0,0 +1,51 @@ +package io.firetail.logging + +import io.firetail.logging.spring.FiretailConfig +import io.firetail.logging.core.FiretailTemplate +import io.firetail.logging.servlet.FiretailFilter +import io.firetail.logging.util.FiretailMDC +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.web.client.RestTemplate +import kotlin.test.assertNotNull + +@ContextConfiguration( + classes = [ + RequestInterceptorTests.SimpleController::class, + RestTemplate::class, + ], +) +@ExtendWith(SpringExtension::class) +class FiretailDisabledTest { + + @Autowired(required = false) + private val firetailConfig: FiretailConfig? = null + + @Autowired(required = false) + private val firetailTemplate: FiretailTemplate? = null + + @Autowired(required = false) + private val firetailFilter: FiretailFilter? = null + + @Autowired(required = false) + private val firetailLogContext: FiretailMDC? = null + + @Autowired + private lateinit var restTemplate: RestTemplate + + @Test + fun assertNotWired() { + assertNull(firetailTemplate) + assertNull(firetailConfig) + assertNull(firetailFilter) + assertNull(firetailLogContext) + assertNotNull(restTemplate) + // Clean interceptor as FT interceptor is disabled + assertThat(restTemplate.interceptors).isEmpty() + } +} diff --git a/src/test/kotlin/io/firetail/logging/FiretailMapperTest.kt b/src/test/kotlin/io/firetail/logging/FiretailMapperTest.kt new file mode 100644 index 0000000..178dc96 --- /dev/null +++ b/src/test/kotlin/io/firetail/logging/FiretailMapperTest.kt @@ -0,0 +1,66 @@ +package io.firetail.logging + +import io.firetail.logging.core.FiretailData +import io.firetail.logging.core.FtRequest +import io.firetail.logging.core.FtResponse +import io.firetail.logging.servlet.FiretailMapper +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import java.util.* + +class FiretailMapperTest { + + private val firetailMapper = FiretailMapper() + + @Test + fun fromResponse() { + val mockResponse: HttpServletResponse = Mockito.mock(HttpServletResponse::class.java) + Mockito.`when`(mockResponse.headerNames).thenReturn(listOf(TEST)) + Mockito.`when`(mockResponse.getHeader(TEST)).thenReturn(TEST_RESULTS) + val result = firetailMapper.from(mockResponse) + Assertions.assertThat(result.headers) + .isNotNull + .hasFieldOrPropertyWithValue(TEST, listOf(TEST_RESULTS)) + } + + @Test + fun fromRequest() { + val mockRequest: HttpServletRequest = Mockito.mock(HttpServletRequest::class.java) + + Mockito.`when`(mockRequest.protocol).thenReturn("HTTP") + Mockito.`when`(mockRequest.method).thenReturn("GET") + Mockito.`when`(mockRequest.requestURI).thenReturn("/") + Mockito.`when`(mockRequest.requestURL).thenReturn(StringBuffer().append("http://blah.com")) + Mockito.`when`(mockRequest.remoteAddr).thenReturn("127.0.0.1") + Mockito.`when`(mockRequest.queryString).thenReturn("123") + Mockito.`when`(mockRequest.getHeader(TEST)).thenReturn(TEST_RESULTS) + Mockito.`when`(mockRequest.headerNames) + .thenReturn(Collections.enumeration(Collections.singletonList(TEST))) + + val result = firetailMapper.from(mockRequest) + + Assertions.assertThat(result.headers) + .isNotNull + .hasFieldOrPropertyWithValue(TEST, listOf(TEST_RESULTS)) + } + + @Test + fun jsonNd() { + val firetailMapper = FiretailMapper() + val rows = listOf( + FiretailData(request = FtRequest(body = "body1"), response = FtResponse()), + FiretailData(request = FtRequest(body = "body2"), response = FtResponse()) + ) + val result = firetailMapper.from(rows) + assertThat(result).contains("body1").contains("body2") + } + + companion object { + private const val TEST = "X-TEST" + private const val TEST_RESULTS = "TEST-RESULTS" + } +} diff --git a/src/test/kotlin/io/firetail/logging/MDCGeneratorTests.kt b/src/test/kotlin/io/firetail/logging/MDCGeneratorTests.kt new file mode 100644 index 0000000..b0afc21 --- /dev/null +++ b/src/test/kotlin/io/firetail/logging/MDCGeneratorTests.kt @@ -0,0 +1,50 @@ +package io.firetail.logging + +import io.firetail.logging.core.Constants.Companion.CORRELATION_ID +import io.firetail.logging.core.Constants.Companion.REQUEST_ID +import io.firetail.logging.util.FiretailMDC +import io.firetail.logging.util.KeyGenerator +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.springframework.mock.web.MockHttpServletRequest + +class MDCGeneratorTests { + @Test + fun mdcIsSetFromHeaderValues() { + val firetailLogContext = FiretailMDC() // test with default generator + val httpRequest = MockHttpServletRequest() + val requestId = "requestId" + val correlationId = "correlationId" + httpRequest.addHeader(REQUEST_ID, requestId) + httpRequest.addHeader(CORRELATION_ID, correlationId) + firetailLogContext.generateAndSetMDC(httpRequest) + + assertThat(firetailLogContext.get(REQUEST_ID)).isEqualTo(requestId) + assertThat(firetailLogContext.get(CORRELATION_ID)).isEqualTo(correlationId) + assertThat(httpRequest.getHeader(REQUEST_ID)).isEqualTo(requestId) + assertThat(httpRequest.getHeader(CORRELATION_ID)).isEqualTo(correlationId) + } + + @Test + fun mdcIsSetWhenNoHeaderValues() { + val keyGenerator = Mockito.mock(KeyGenerator::class.java) + val firetailMdc = FiretailMDC(keyGenerator = keyGenerator) // test with default generator + firetailMdc.clear() + assertThat(firetailMdc.get(CORRELATION_ID)).isNull() + assertThat(firetailMdc.get(REQUEST_ID)).isNull() + val httpRequest = MockHttpServletRequest() + val id = "someValue" + Mockito.`when`(keyGenerator.generate()).thenReturn(id) + firetailMdc.generateAndSetMDC(httpRequest) + assertThat(httpRequest.headerNames.toList()).isEmpty() + assertThat(firetailMdc.get(REQUEST_ID)).isEqualTo(id) + assertThat(firetailMdc.get(CORRELATION_ID)).isEqualTo(id) + } + + @Test + fun generateId() { + val keyGenerator = KeyGenerator() + assertThat(keyGenerator.generate()).isNotNull().isNotEmpty() + } +} diff --git a/src/test/kotlin/io/firetail/logging/RequestInterceptorTests.kt b/src/test/kotlin/io/firetail/logging/RequestInterceptorTests.kt new file mode 100644 index 0000000..469f2c5 --- /dev/null +++ b/src/test/kotlin/io/firetail/logging/RequestInterceptorTests.kt @@ -0,0 +1,126 @@ +package io.firetail.logging + +import io.firetail.logging.core.Constants +import io.firetail.logging.core.FiretailBuffer +import io.firetail.logging.core.FiretailLogger +import io.firetail.logging.core.FiretailTemplate +import io.firetail.logging.servlet.FiretailFilter +import io.firetail.logging.servlet.FiretailHeaderInterceptor +import io.firetail.logging.servlet.FiretailMapper +import io.firetail.logging.spring.EnableFiretail +import io.firetail.logging.util.FiretailMDC +import io.firetail.logging.util.StringUtils +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.verify +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock +import org.springframework.test.context.TestPropertySource +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultMatchers +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.client.RestTemplate + +@SpringBootTest( + classes = [ + RequestInterceptorTests.SimpleController::class, + ], +) +@AutoConfigureMockMvc +@AutoConfigureWireMock(port = 0) +@TestPropertySource( + properties = [ + "firetail.url=http://localhost:\${wiremock.server.port}", + "firetail.buffer.capacity=5", + ], +) +@EnableFiretail +class RequestInterceptorTests { + + @MockBean + private lateinit var firetailLogger: FiretailLogger + + @Autowired + private lateinit var firetailMapper: FiretailMapper + + @Autowired + private lateinit var firetailTemplate: FiretailTemplate + + @Autowired + private lateinit var stringUtils: StringUtils + + @Autowired + private lateinit var firetailHeaderInterceptor: FiretailHeaderInterceptor + + @Autowired + private lateinit var firetailBuffer: FiretailBuffer + + @Autowired + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var restTemplate: RestTemplate + + @Autowired + private lateinit var firetailFilter: FiretailFilter + + @Autowired + private lateinit var firetailMDC: FiretailMDC + + @Test + fun testWiring() { + assertThat(stringUtils).isNotNull + assertThat(firetailTemplate).isNotNull + assertThat(firetailMDC).isNotNull + assertThat(firetailFilter).isNotNull + assertThat(firetailMapper).isNotNull + assertThat(restTemplate).isNotNull + assertThat(firetailBuffer).isNotNull + assertThat(restTemplate.interceptors).isNotEmpty.contains(firetailHeaderInterceptor) + } + + @Test + fun fireTailRequestLoggingAndResponse() { + firetailMDC.clear() + val result = mockMvc.perform(MockMvcRequestBuilders.get("/hello")) + .andExpect(MockMvcResultMatchers.status().isOk) + .andReturn() + + assertThat(result.response.headerNames) + .contains(Constants.REQUEST_ID, Constants.CORRELATION_ID) + + verify(firetailLogger) + .logRequest(any()) // Called once + + verify(firetailLogger) + .logResponse(any(), any(), any()) // Called once + + assertThat(firetailMDC.get(Constants.REQUEST_ID)) + .isNotNull() + .isEqualTo(result.response.getHeaderValue(Constants.REQUEST_ID)) + + assertThat(firetailMDC.get(Constants.CORRELATION_ID)) + .isNotNull() + .isEqualTo(result.response.getHeaderValue(Constants.CORRELATION_ID)) + + assertThat(firetailBuffer.size() == 1) + assertThat(firetailBuffer.flush()).isEqualTo("success") + } + + // Emulates a general MVC controller for which we want to + // assert Firetail calls have been made. + @RestController + internal class SimpleController { + + @GetMapping("/hello") + fun sayHello(): String { + return "hello" + } + } +} diff --git a/src/test/kotlin/io/firetail/logging/StringUtilsTest.kt b/src/test/kotlin/io/firetail/logging/StringUtilsTest.kt new file mode 100644 index 0000000..ac92068 --- /dev/null +++ b/src/test/kotlin/io/firetail/logging/StringUtilsTest.kt @@ -0,0 +1,31 @@ +package io.firetail.logging + +import io.firetail.logging.util.StringUtils +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import kotlin.text.Charsets.US_ASCII + +class StringUtilsTest { + // Assertions around \r\n and unicode. + private val input = "Now is the time \r\n for all good \n people to. 你好世界." + + @Test + fun bytesToStringUsingDefaultEncoder() { + assertThat(StringUtils().toString(input.encodeToByteArray())) + .isEqualTo(input) + } + + @Test + fun bytesToStringUsingAsciiEncoderIsNotEqual() { + assertThat(StringUtils(US_ASCII).toString(input.encodeToByteArray())) + .isNotEqualTo(input) + .startsWith(input.subSequence(0, 18)) + } + + @Test + fun bytesToStringUsingSpecifiedEncoderIsNotEqual() { + assertThat(StringUtils().toString(input.encodeToByteArray(), US_ASCII.toString())) + .isNotEqualTo(input) + .startsWith(input.subSequence(0, 18)) + } +} diff --git a/src/test/resources/mappings/v1alpha.json b/src/test/resources/mappings/v1alpha.json new file mode 100644 index 0000000..fbaece4 --- /dev/null +++ b/src/test/resources/mappings/v1alpha.json @@ -0,0 +1,19 @@ +{ + "request": { + "method": "POST", + "url": "/logs/bulk", + "headers": { + "Content-Type": { + "equalTo": "application/nd-json" + } + } + }, + + "response": { + "status": 200, + "body": "{\n \"message\": \"success\"\n}", + "headers": { + "Content-Type": "application/json" + } + } +} \ No newline at end of file diff --git a/src/test/resources/schemaV1Alpha.json b/src/test/resources/schemaV1Alpha.json new file mode 100644 index 0000000..0b8a6c3 --- /dev/null +++ b/src/test/resources/schemaV1Alpha.json @@ -0,0 +1,27 @@ +{ + "version": "1.0.0-alpha", + "dateCreated": 1700616797, + "executionTime": 10, + "request": { + "headers": { + "key": [ + "value" + ] + }, + "httpProtocol": "2.0", + "method": "GET", + "body": "", + "ip": "127.0.0.1", + "resource": "/hello", + "uri": "http://hello" + }, + "response": { + "statusCode": 200, + "body": "body", + "headers": { + "key": [ + "value" + ] + } + } +} \ No newline at end of file