├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── BUG.md │ └── FEATURE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── no-response.yml └── workflows │ ├── build.yaml │ ├── release-pull-request.yaml │ ├── release.yaml │ └── stale-marker-bot.yml ├── .gitignore ├── .mailmap ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── .zappr.yaml ├── CHANGELOG.md ├── LICENSE ├── MAINTAINERS ├── README.md ├── SECURITY.md ├── build.sh ├── docs ├── logbook.jpg └── scalyr.md ├── logbook-api ├── pom.xml └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── zalando │ │ │ └── logbook │ │ │ ├── ApplyHttpHeaders.java │ │ │ ├── BodyFilter.java │ │ │ ├── BodyReplacer.java │ │ │ ├── ContentType.java │ │ │ ├── Correlation.java │ │ │ ├── CorrelationId.java │ │ │ ├── DefaultHttpHeaders.java │ │ │ ├── DeleteHttpHeaders.java │ │ │ ├── Fold.java │ │ │ ├── ForwardingHttpMessage.java │ │ │ ├── ForwardingHttpRequest.java │ │ │ ├── ForwardingHttpResponse.java │ │ │ ├── HeaderFilter.java │ │ │ ├── HttpHeaders.java │ │ │ ├── HttpLogFormatter.java │ │ │ ├── HttpLogWriter.java │ │ │ ├── HttpMessage.java │ │ │ ├── HttpRequest.java │ │ │ ├── HttpResponse.java │ │ │ ├── HttpStatus.java │ │ │ ├── Logbook.java │ │ │ ├── LogbookCreator.java │ │ │ ├── LogbookFactory.java │ │ │ ├── NonMergeableBodyFilterPair.java │ │ │ ├── NoneBodyFilter.java │ │ │ ├── Origin.java │ │ │ ├── PathFilter.java │ │ │ ├── Precorrelation.java │ │ │ ├── QueryFilter.java │ │ │ ├── RequestFilter.java │ │ │ ├── RequestURI.java │ │ │ ├── ResponseFilter.java │ │ │ ├── Sink.java │ │ │ ├── Strategy.java │ │ │ ├── StructuredHttpLogFormatter.java │ │ │ ├── UpdateHttpHeaders.java │ │ │ ├── attributes │ │ │ ├── AttributeExtractor.java │ │ │ ├── HttpAttributes.java │ │ │ └── NoOpAttributeExtractor.java │ │ │ ├── internal │ │ │ ├── ExceptionThrowingLogbook.java │ │ │ └── ExceptionThrowingLogbookFactory.java │ │ │ └── package-info.java │ └── resources │ │ └── META-INF │ │ └── services │ │ └── org.zalando.logbook.LogbookFactory │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ ├── BodyFilterMergeTest.java │ ├── BodyFilterTest.java │ ├── BodyReplacerTest.java │ ├── ContentTypeTest.java │ ├── CorrelationTest.java │ ├── DefaultHttpHeadersTest.java │ ├── EnforceCoverageTest.java │ ├── FoldTest.java │ ├── ForwardingTest.java │ ├── HeaderFilterTest.java │ ├── HttpHeadersApplyTest.java │ ├── HttpHeadersImmutabilityTest.java │ ├── HttpHeadersTest.java │ ├── HttpHeadersUpdateTest.java │ ├── HttpLogWriterTest.java │ ├── HttpMessageTest.java │ ├── HttpRequestTest.java │ ├── HttpResponseTest.java │ ├── LogbookFactoryTest.java │ ├── PathFilterTest.java │ ├── QueryFilterTest.java │ ├── RequestFilterTest.java │ ├── RequestURITest.java │ ├── ResponseFilterTest.java │ ├── SinkTest.java │ ├── StrategyTest.java │ ├── StructuredHttpLogFormatterTest.java │ ├── attributes │ ├── AttributeExtractorTest.java │ ├── HttpAttributesTest.java │ └── NoOpAttributeExtractorTest.java │ └── internal │ └── LogbookTest.java ├── logbook-bom └── pom.xml ├── logbook-common ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── common │ │ ├── Glob.java │ │ ├── MediaTypeQuery.java │ │ └── PatternLike.java │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ └── common │ ├── GlobTest.java │ └── MediaTypeQueryTest.java ├── logbook-core ├── pom.xml └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── zalando │ │ │ └── logbook │ │ │ └── core │ │ │ ├── BodyFilters.java │ │ │ ├── BodyOnlyIfStatusAtLeastStrategy.java │ │ │ ├── BodyReplacementHttpRequest.java │ │ │ ├── BodyReplacementHttpResponse.java │ │ │ ├── BodyReplacers.java │ │ │ ├── Cache.java │ │ │ ├── CachingHttpRequest.java │ │ │ ├── CachingHttpResponse.java │ │ │ ├── ChunkingSink.java │ │ │ ├── ChunkingSpliterator.java │ │ │ ├── CommonsLogFormatSink.java │ │ │ ├── CompactingXmlBodyFilter.java │ │ │ ├── CompositeSink.java │ │ │ ├── Conditions.java │ │ │ ├── CookieHeaderFilter.java │ │ │ ├── CurlHttpLogFormatter.java │ │ │ ├── DefaultCorrelationId.java │ │ │ ├── DefaultFilters.java │ │ │ ├── DefaultHttpLogFormatter.java │ │ │ ├── DefaultHttpLogWriter.java │ │ │ ├── DefaultLogbook.java │ │ │ ├── DefaultLogbookFactory.java │ │ │ ├── DefaultPathFilter.java │ │ │ ├── DefaultSink.java │ │ │ ├── DefaultStrategy.java │ │ │ ├── DynamicPathFilter.java │ │ │ ├── ExtendedLogFormatSink.java │ │ │ ├── FilteredHttpRequest.java │ │ │ ├── FilteredHttpResponse.java │ │ │ ├── HeaderFilters.java │ │ │ ├── PathFilters.java │ │ │ ├── QueryFilters.java │ │ │ ├── RequestFilters.java │ │ │ ├── ResponseFilters.java │ │ │ ├── SecurityStrategy.java │ │ │ ├── SplunkHttpLogFormatter.java │ │ │ ├── Stages.java │ │ │ ├── StatusAtLeastStrategy.java │ │ │ ├── StreamHttpLogWriter.java │ │ │ ├── WithoutBodyStrategy.java │ │ │ ├── attributes │ │ │ ├── AllAttributesExtractor.java │ │ │ ├── CompositeAttributeExtractor.java │ │ │ ├── JwtAllMatchingClaimsExtractor.java │ │ │ ├── JwtClaimsExtractor.java │ │ │ └── JwtFirstMatchingClaimExtractor.java │ │ │ └── package-info.java │ └── resources │ │ └── META-INF │ │ └── services │ │ └── org.zalando.logbook.LogbookFactory │ └── test │ ├── java │ └── org │ │ └── zalando │ │ └── logbook │ │ └── core │ │ ├── BodyFiltersTest.java │ │ ├── BodyOnlyIfStatusAtLeastStrategyTest.java │ │ ├── BodyReplacersTest.java │ │ ├── CachingHttpRequestTest.java │ │ ├── CachingHttpResponseTest.java │ │ ├── ChunkingSinkTest.java │ │ ├── ChunkingSpliteratorTest.java │ │ ├── CommonsLogFormatSinkTest.java │ │ ├── CompactingXmlBodyFilterTest.java │ │ ├── CompositeSinkTest.java │ │ ├── ConditionsTest.java │ │ ├── CookieHeaderFilterTest.java │ │ ├── CurlHttpLogFormatterTest.java │ │ ├── DefaultHttpLogFormatterTest.java │ │ ├── DefaultHttpLogWriterTest.java │ │ ├── DefaultLogbookFactoryTest.java │ │ ├── DefaultLogbookTest.java │ │ ├── DefaultPathFilterTest.java │ │ ├── DefaultSinkTest.java │ │ ├── DynamicPathFilterTest.java │ │ ├── ExtendedLogFormatSinkTest.java │ │ ├── FilteredHttpRequestTest.java │ │ ├── FilteredHttpResponseTest.java │ │ ├── HeaderFiltersTest.java │ │ ├── QueryFiltersTest.java │ │ ├── RequestFiltersTest.java │ │ ├── ResponseFiltersTest.java │ │ ├── SecurityStrategyTest.java │ │ ├── SimplePrecorrelationTest.java │ │ ├── SplunkHttpLogFormatterTest.java │ │ ├── StatusAtLeastStrategyTest.java │ │ ├── StreamHttpLogWriterTest.java │ │ ├── WithoutBodyStrategyTest.java │ │ └── attributes │ │ ├── AllAttributesExtractorTest.java │ │ ├── CompositeAttributeExtractorTest.java │ │ ├── JwtAllMatchingClaimsExtractorTest.java │ │ ├── JwtClaimsExtractorTest.java │ │ └── JwtFirstMatchingClaimExtractorTest.java │ └── resources │ ├── request.json │ └── response.json ├── logbook-httpclient ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── httpclient │ │ ├── Attributes.java │ │ ├── ForwardingHttpAsyncResponseConsumer.java │ │ ├── HttpEntities.java │ │ ├── LocalRequest.java │ │ ├── LogbookHttpAsyncResponseConsumer.java │ │ ├── LogbookHttpRequestInterceptor.java │ │ ├── LogbookHttpResponseInterceptor.java │ │ ├── RemoteResponse.java │ │ └── package-info.java │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ └── httpclient │ ├── AbstractHttpTest.java │ ├── ForwardingHttpAsyncResponseConsumerTest.java │ ├── LocalRequestTest.java │ ├── LogbookHttpAsyncResponseConsumerTest.java │ ├── LogbookHttpInterceptorsGzipTest.java │ ├── LogbookHttpInterceptorsPutTest.java │ ├── LogbookHttpInterceptorsTest.java │ └── RemoteResponseTest.java ├── logbook-httpclient5 ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── httpclient5 │ │ ├── Attributes.java │ │ ├── BufferingFixedSizeDataStreamChannel.java │ │ ├── ByteBufferUtils.java │ │ ├── ForwardingHttpAsyncResponseConsumer.java │ │ ├── HttpEntities.java │ │ ├── LocalRequest.java │ │ ├── LogbookHttpAsyncResponseConsumer.java │ │ ├── LogbookHttpExecHandler.java │ │ ├── LogbookHttpRequestInterceptor.java │ │ ├── LogbookHttpResponseInterceptor.java │ │ ├── RemoteResponse.java │ │ └── package-info.java │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ └── httpclient5 │ ├── AbstractHttpTest.java │ ├── BufferingFixedSizeDataStreamChannelTest.java │ ├── LocalRequestTest.java │ ├── LogbookHttpAsyncResponseConsumerTest.java │ ├── LogbookHttpExecHandlerTest.java │ ├── LogbookHttpInterceptorsPutTest.java │ ├── LogbookHttpInterceptorsTest.java │ └── RemoteResponseTest.java ├── logbook-jaxrs ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── jaxrs │ │ ├── ByteStreams.java │ │ ├── HttpMessages.java │ │ ├── LocalRequest.java │ │ ├── LocalResponse.java │ │ ├── LogbookClientFilter.java │ │ ├── LogbookServerFilter.java │ │ ├── RemoteRequest.java │ │ ├── RemoteResponse.java │ │ ├── TeeOutputStream.java │ │ └── package-info.java │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ └── jaxrs │ ├── ClientAndServerIgnoringBodyTest.java │ ├── ClientAndServerTest.java │ ├── ClientAndServerWithoutBodyTest.java │ ├── ClientWithApacheConnectorTest.java │ ├── HttpMessagesTest.java │ ├── LogbookClientFilterTest.java │ ├── LogbookServerFilterTest.java │ ├── RemoteResponseTest.java │ ├── RoundTrip.java │ ├── TeeOutputStreamTest.java │ └── testing │ └── support │ ├── TestModel.java │ └── TestWebService.java ├── logbook-jdkserver ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── jdkserver │ │ ├── ByteStreams.java │ │ ├── ForwardingHttpExchange.java │ │ ├── LogbookFilter.java │ │ ├── Request.java │ │ ├── Response.java │ │ └── package-info.java │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ └── jdkserver │ ├── ByteStreamsTest.java │ ├── ForwardingHttpExchangeTest.java │ ├── LogbookFilterTest.java │ ├── MockHttpExchange.java │ ├── RequestTest.java │ └── ResponseTest.java ├── logbook-jmh ├── README.md ├── pom.xml └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── zalando │ │ │ └── logbook │ │ │ └── benchmark │ │ │ ├── HeaderBenchmark.java │ │ │ ├── HeaderState.java │ │ │ ├── HttpLogFormatterBenchmark.java │ │ │ ├── HttpLogFormatterState.java │ │ │ ├── LogbookBenchmark.java │ │ │ ├── LogbookState.java │ │ │ ├── NoopHttpLogFormatter.java │ │ │ ├── RequestResponseState.java │ │ │ ├── jmh │ │ │ ├── DefaultCorrelation.java │ │ │ ├── DefaultPrecorrelation.java │ │ │ └── NOPAppender.java │ │ │ └── json │ │ │ ├── JsonMediaTypeBenchmark.java │ │ │ └── JsonPathBodyFilterBenchmark.java │ └── resources │ │ └── logback.xml │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ └── benchmark │ └── jmh │ └── TestDefaultCorrelation.java ├── logbook-json ├── pom.xml └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── zalando │ │ │ └── logbook │ │ │ └── json │ │ │ ├── AccessTokenBodyFilter.java │ │ │ ├── CompactingJsonBodyFilter.java │ │ │ ├── DefaultJsonGeneratorWrapper.java │ │ │ ├── FastCompactingJsonBodyFilter.java │ │ │ ├── FastJsonHttpLogFormatter.java │ │ │ ├── JacksonJsonFieldBodyFilter.java │ │ │ ├── JsonBodyFilters.java │ │ │ ├── JsonCompactor.java │ │ │ ├── JsonFieldWriter.java │ │ │ ├── JsonGeneratorWrapper.java │ │ │ ├── JsonHttpLogFormatter.java │ │ │ ├── JsonPathBodyFilters.java │ │ │ ├── NumberAsStringJsonGeneratorWrapper.java │ │ │ ├── ParsingJsonCompactor.java │ │ │ ├── PreciseFloatJsonGeneratorWrapper.java │ │ │ ├── PrettyPrintingJsonBodyFilter.java │ │ │ ├── PrimitiveJsonPropertyBodyFilter.java │ │ │ ├── StringReplaceJsonCompactor.java │ │ │ └── package-info.java │ └── resources │ │ └── META-INF │ │ └── services │ │ └── org.zalando.logbook.BodyFilter │ └── test │ ├── java │ └── org │ │ └── zalando │ │ └── logbook │ │ └── json │ │ ├── CompactingJsonBodyFilterTest.java │ │ ├── DefaultBodyFilterTest.java │ │ ├── FastCompactingJsonBodyFilterTest.java │ │ ├── FastJsonHttpLogFormatterTest.java │ │ ├── JacksonJsonFieldBodyFilterTest.java │ │ ├── JsonBodyFilterMergeTest.java │ │ ├── JsonBodyFiltersTest.java │ │ ├── JsonHttpLogFormatterTest.java │ │ ├── JsonPathBodyFiltersTest.java │ │ └── PrettyPrintingJsonBodyFilterTest.java │ └── resources │ ├── cars-array.json │ ├── cars-object.json │ ├── cars-unwrapped-array.json │ ├── huge-json-value.json │ ├── huge-sample.json │ ├── huge-value.json │ ├── many-quotes.json │ ├── student.json │ └── user.json ├── logbook-ktor-client ├── pom.xml └── src │ ├── main │ └── kotlin │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── client │ │ ├── ClientRequest.kt │ │ ├── ClientResponse.kt │ │ └── LogbookClient.kt │ └── test │ └── kotlin │ └── org │ └── zalando │ └── logbook │ └── client │ ├── ClientRequestUnitTest.kt │ ├── ClientResponseUnitTest.kt │ ├── LogbookClientTest.kt │ └── LogbookClientWithSinkTest.kt ├── logbook-ktor-common ├── pom.xml └── src │ ├── main │ └── kotlin │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── common │ │ ├── ContentUtils.kt │ │ ├── ExperimentalLogbookKtorApi.kt │ │ └── State.kt │ └── test │ └── kotlin │ └── org │ └── zalando │ └── logbook │ └── ktor │ └── common │ ├── ContentUtilsUnitTest.kt │ └── StateUnitTest.kt ├── logbook-ktor-server ├── pom.xml └── src │ ├── main │ └── kotlin │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── server │ │ ├── LogbookServer.kt │ │ ├── ServerRequest.kt │ │ └── ServerResponse.kt │ └── test │ └── kotlin │ └── org │ └── zalando │ └── logbook │ └── server │ ├── LogbookServerTest.kt │ ├── ServerRequestTest.kt │ └── ServerResponseUnitTest.kt ├── logbook-ktor └── pom.xml ├── logbook-logstash ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── logstash │ │ ├── AutodetectPrettyPrintingMarker.java │ │ ├── DefaultPrettyPrinterDecorator.java │ │ ├── LogstashLogbackSink.java │ │ └── package-info.java │ └── test │ ├── java │ └── org │ │ └── zalando │ │ └── logbook │ │ └── logstash │ │ ├── LogbackLogstashSinkTest.java │ │ ├── PrettyPrintingStaticAppender.java │ │ └── StaticAppender.java │ └── resources │ └── logback-test.xml ├── logbook-netty ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── netty │ │ ├── Buffering.java │ │ ├── Conditionals.java │ │ ├── HeaderSupport.java │ │ ├── Ignoring.java │ │ ├── LogbookClientHandler.java │ │ ├── LogbookServerHandler.java │ │ ├── Offering.java │ │ ├── Request.java │ │ ├── Response.java │ │ ├── Sequence.java │ │ ├── State.java │ │ ├── Unbuffered.java │ │ └── package-info.java │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ └── netty │ ├── IgnoringTest.java │ ├── LogbookClientHandlerTest.java │ ├── LogbookServerHandlerTest.java │ ├── RequestUnitTest.java │ └── SequenceTest.java ├── logbook-okhttp ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── okhttp │ │ ├── GzipInterceptor.java │ │ ├── LocalRequest.java │ │ ├── LogbookInterceptor.java │ │ ├── RemoteResponse.java │ │ └── package-info.java │ └── test │ ├── java │ └── org │ │ └── zalando │ │ └── logbook │ │ └── okhttp │ │ ├── GzipInterceptorTest.java │ │ ├── LocalRequestTest.java │ │ └── LogbookInterceptorTest.java │ └── resources │ └── response.txt.gz ├── logbook-okhttp2 ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── okhttp2 │ │ ├── GzipInterceptor.java │ │ ├── LocalRequest.java │ │ ├── LogbookInterceptor.java │ │ ├── RemoteResponse.java │ │ └── package-info.java │ └── test │ ├── java │ └── org │ │ └── zalando │ │ └── logbook │ │ └── okhttp2 │ │ ├── GzipInterceptorTest.java │ │ ├── LocalRequestTest.java │ │ └── LogbookInterceptorTest.java │ └── resources │ └── response.txt.gz ├── logbook-openfeign ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── openfeign │ │ ├── ByteStreams.java │ │ ├── FeignLogbookLogger.java │ │ ├── HeaderUtils.java │ │ ├── LocalRequest.java │ │ ├── RemoteResponse.java │ │ └── Utils.java │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ └── openfeign │ ├── FeignClient.java │ ├── FeignHttpServerRunner.java │ ├── FeignLogbookLoggerExceptionTest.java │ ├── FeignLogbookLoggerTest.java │ ├── FeignLogbookLoggerUnitTest.java │ ├── LocalRequestTest.java │ ├── RemoteResponseTest.java │ └── UtilsTest.java ├── logbook-parent └── pom.xml ├── logbook-servlet ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── servlet │ │ ├── AsyncOnCompleteListener.java │ │ ├── ByteStreams.java │ │ ├── FormRequestMode.java │ │ ├── HttpFilter.java │ │ ├── LocalResponse.java │ │ ├── LogbookAsyncListener.java │ │ ├── LogbookFilter.java │ │ ├── NullOutputStream.java │ │ ├── RemoteRequest.java │ │ ├── SecureLogbookFilter.java │ │ ├── ServletInputStreamAdapter.java │ │ └── package-info.java │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ └── servlet │ ├── AsyncDispatchTest.java │ ├── ByteStreamsTest.java │ ├── ExampleController.java │ ├── FormRequestModeTest.java │ ├── FormRequestTest.java │ ├── FormattingTest.java │ ├── ForwardingHttpLogFormatter.java │ ├── HttpSupportTest.java │ ├── LocalResponseTest.java │ ├── LogbookAsyncListenerTest.java │ ├── LogbookFilterTest.java │ ├── Message.java │ ├── MultiFilterSecurityTest.java │ ├── MultiFilterTest.java │ ├── NullOutputStreamTest.java │ ├── RemoteRequestTest.java │ ├── RequestBuilders.java │ ├── SecurityFilter.java │ ├── ServletInputStreamAdapterTest.java │ ├── SkipTest.java │ ├── SpyableFilter.java │ ├── TeeTest.java │ ├── WritingTest.java │ └── junit │ ├── RestoreSystemProperties.java │ └── SystemPropertiesExtension.java ├── logbook-spring-boot-autoconfigure ├── pom.xml └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── zalando │ │ │ └── logbook │ │ │ └── autoconfigure │ │ │ ├── LogbookAutoConfiguration.java │ │ │ ├── LogbookProperties.java │ │ │ └── package-info.java │ └── resources │ │ └── META-INF │ │ ├── spring-configuration-metadata.json │ │ ├── spring.factories │ │ └── spring │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ └── test │ ├── java │ └── org │ │ └── zalando │ │ └── logbook │ │ └── autoconfigure │ │ ├── AttributeExtractorTest.java │ │ ├── BodyOnlyIfStatusAtLeastStrategyTest.java │ │ ├── DeprecatedOnlyExcludeTest.java │ │ ├── DeprecatedOnlyIncludeTest.java │ │ ├── ExcludeTest.java │ │ ├── FeignTest.java │ │ ├── FilterDisabledTest.java │ │ ├── FilterTest.java │ │ ├── FormatStyleCurlTest.java │ │ ├── FormatStyleDefaultTest.java │ │ ├── FormatStyleHttpTest.java │ │ ├── FormatStyleSplunkTest.java │ │ ├── IncludeTest.java │ │ ├── JavaxFilterTest.java │ │ ├── JsonBodyFieldsTest.java │ │ ├── LogbookTest.java │ │ ├── ObfuscateBodyCustomTest.java │ │ ├── ObfuscateBodyDefaultTest.java │ │ ├── ObfuscateHeadersCustomTest.java │ │ ├── ObfuscateHeadersDefaultTest.java │ │ ├── ObfuscateParametersCustomTest.java │ │ ├── ObfuscateParametersDefaultTest.java │ │ ├── ObfuscatePathCustomTest.java │ │ ├── ObfuscatePathDefaultTest.java │ │ ├── ObfuscateReplacementTest.java │ │ ├── ObfuscateRequestCustomTest.java │ │ ├── ObfuscateResponseCustomTest.java │ │ ├── SecurityFilterTest.java │ │ ├── StandardTest.java │ │ ├── StatusAtLeastStrategyTest.java │ │ ├── WithoutBodyStrategyTest.java │ │ ├── WriteBodyMaxSizeTest.java │ │ ├── WriteChunkingTest.java │ │ ├── WriteCustomTest.java │ │ └── WriteNoBodyMaxSizeTest.java │ └── resources │ ├── application-body_fields.yml │ ├── application-claim-extractor-all-matching.yaml │ ├── application-claim-extractor-composite.yaml │ ├── application-claim-extractor-first-matching.yaml │ ├── application-deprecated-exclude.yml │ ├── application-deprecated-include.yml │ ├── application-exclude.yml │ ├── application-headers.yml │ ├── application-include.yml │ ├── application-parameters.yml │ ├── application-paths.yml │ ├── application-replacement.yaml │ ├── application-truncation_order.yml │ ├── application.yml │ └── simplelogger.properties ├── logbook-spring-boot-starter └── pom.xml ├── logbook-spring-boot-webflux-autoconfigure ├── pom.xml └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── zalando │ │ │ └── logbook │ │ │ └── autoconfigure │ │ │ └── webflux │ │ │ └── LogbookWebFluxAutoConfiguration.java │ └── resources │ │ └── META-INF │ │ ├── spring.factories │ │ └── spring │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ └── autoconfigure │ └── webflux │ └── LogbookWebFluxAutoConfigurationTest.java ├── logbook-spring-webflux ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── spring │ │ └── webflux │ │ ├── BufferingClientHttpRequest.java │ │ ├── BufferingServerHttpRequest.java │ │ ├── BufferingServerHttpResponse.java │ │ ├── ClientRequest.java │ │ ├── ClientResponse.java │ │ ├── DataBufferCopyUtils.java │ │ ├── LogbookExchangeFilterFunction.java │ │ ├── LogbookWebFilter.java │ │ ├── ServerRequest.java │ │ ├── ServerResponse.java │ │ └── State.java │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ └── spring │ └── webflux │ ├── BufferingClientHttpRequestUnitTest.java │ ├── BufferingServerHttpResponseUnitTest.java │ ├── ClientRequestUnitTest.java │ ├── LogbookExchangeFilterFunctionTest.java │ ├── LogbookWebFilterTest.java │ ├── ServerRequestUnitTest.java │ ├── StateUnitTest.java │ └── TestApplication.java ├── logbook-spring ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── spring │ │ ├── BufferingClientHttpResponseWrapper.java │ │ ├── ByteStreams.java │ │ ├── LocalRequest.java │ │ ├── LogbookClientHttpRequestInterceptor.java │ │ └── RemoteResponse.java │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ └── spring │ ├── BufferingClientHttpResponseWrapperTest.java │ ├── LocalRequestTest.java │ ├── LogbookClientHttpRequestInterceptorTest.java │ └── RemoteResponseTest.java ├── logbook-test ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── zalando │ │ └── logbook │ │ └── test │ │ ├── MockHttpRequest.java │ │ ├── MockHttpResponse.java │ │ ├── TestStrategy.java │ │ └── package-info.java │ └── test │ └── java │ └── org │ └── zalando │ └── logbook │ └── test │ ├── MockHttpMessageTester.java │ ├── MockHttpRequestTest.java │ └── MockHttpResponseTest.java ├── lombok.config ├── mvnw ├── mvnw.cmd ├── pom.xml └── release.sh /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @lukasniemeier-zalando @kasmarian @msdousti @ChristianLohmann 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Pull requests only 4 | 5 | **DON'T** push to the main branch directly. Always use feature branches and let people discuss changes in pull requests. 6 | Pull requests should only be merged after all discussions have been concluded and at least 1 reviewer has given their 7 | **approval**. 8 | 9 | ## Guidelines 10 | 11 | - **every change** needs a test 12 | - 100% code coverage 13 | - keep the current code style 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41EBug report" 3 | about: Create a report to help us improve 4 | labels: Bug 5 | --- 6 | 7 | 8 | 9 | ## Description 10 | 11 | 12 | ## Expected Behavior 13 | 14 | 15 | ## Actual Behavior 16 | 17 | 18 | ## Possible Fix 19 | 20 | 21 | ## Steps to Reproduce 22 | 23 | 24 | 1. 25 | 2. 26 | 3. 27 | 4. 28 | 29 | ## Context 30 | 31 | 32 | ## Your Environment 33 | 34 | * Version used: 35 | * Link to your project: 36 | 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: Suggest an idea for this project 4 | labels: Feature 5 | --- 6 | 7 | 8 | 9 | ## Detailed Description 10 | 11 | 12 | ## Context 13 | 14 | 15 | 16 | ## Possible Implementation 17 | 18 | 19 | ## Your Environment 20 | 21 | * Version used: 22 | * Link to your project: 23 | 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## Types of changes 11 | 12 | - [ ] Bug fix (non-breaking change which fixes an issue) 13 | - [ ] New feature (non-breaking change which adds functionality) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 15 | 16 | ## Checklist: 17 | 18 | 19 | - [ ] My change requires a change to the documentation. 20 | - [ ] I have updated the documentation accordingly. 21 | - [ ] I have added tests to cover my changes. 22 | - [ ] All commits are [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: org.glassfish.jersey:jersey-bom 11 | versions: 12 | - ">= 3.a, < 4" 13 | - dependency-name: io.netty:netty-codec-http 14 | versions: 15 | - 4.1.62.Final 16 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | responseRequiredLabel: More information needed 4 | 5 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: "0 6 * * *" 12 | 13 | env: 14 | # https://github.com/actions/virtual-environments/issues/1499#issuecomment-689467080 15 | MAVEN_OPTS: >- 16 | -Dhttp.keepAlive=false 17 | -Dmaven.wagon.http.pool=false 18 | -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | profile: [''] 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v3 29 | - name: Dependency Review 30 | uses: actions/dependency-review-action@v4 31 | with: 32 | base-ref: ${{ github.event.pull_request.base.sha || '0.1.0' }} 33 | head-ref: ${{ github.event.pull_request.head.sha || github.ref }} 34 | vulnerability-check: true 35 | license-check: false 36 | comment-summary-in-pr: on-failure 37 | - name: Set up JDK 38 | uses: actions/setup-java@v3 39 | with: 40 | distribution: temurin 41 | java-version: 17 42 | cache: 'maven' 43 | - name: Compile & test 44 | run: ./build.sh 45 | - name: Coverage 46 | if: github.event_name != 'pull_request' 47 | run: ./mvnw -P coverage coveralls:report -B -D repoToken=${{ secrets.COVERALLS_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/release-pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: Release Pull Request 2 | 3 | on: 4 | push: 5 | branches: 6 | - release/* 7 | 8 | jobs: 9 | pull-request: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: pull-request-action 13 | uses: vsoch/pull-request-action@master 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | PULL_REQUEST_TITLE: Release ${{ github.ref }} 17 | PULL_REQUEST_BODY: Bump version 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Extract Release Notes 15 | uses: ffurrer2/extract-release-notes@v1.16.0 16 | id: notes 17 | - name: Create Release 18 | uses: softprops/action-gh-release@v1 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | draft: false 23 | prerelease: ${{ contains(github.ref, 'RC') }} 24 | body: ${{ steps.notes.outputs.release_notes }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### IntelliJ ### 2 | *.iml 3 | .idea/ 4 | 5 | ### Maven ### 6 | target/ 7 | /.mvn/wrapper/*.jar 8 | 9 | *.log 10 | 11 | ### Eclipse ### 12 | .settings/ 13 | .classpath 14 | .project 15 | .factorypath 16 | 17 | ### various ### 18 | jmh-result.json 19 | dependency-reduced-pom.xml 20 | pom.xml.versionsBackup 21 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Willi Schönborn 2 | Lukas Niemeier lukasniemeier-zalando 3 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.1/apache-maven-3.9.1-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 19 | -------------------------------------------------------------------------------- /.zappr.yaml: -------------------------------------------------------------------------------- 1 | approvals: 2 | pattern: "^(:\\+1:|👍|\\+1|:thumbsup:|[Ll][Gg][Tt][Mm])$" 3 | minimum: 1 4 | ignore: pr_opener 5 | from: 6 | orgs: 7 | - zalando 8 | collaborators: true 9 | X-Zalando-Team: zcpe 10 | X-Zalando-Type: code 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Zalando SE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Lukas Niemeier 2 | Willi Schönborn 3 | Karen Asmarian 4 | Sadeq Dousti <3616518+msdousti@users.noreply.github.com> 5 | Christian Lohmann 6 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | We acknowledge that every line of code that we write may potentially contain security issues. 2 | 3 | We are trying to deal with it responsibly and provide patches as quickly as possible. If you have anything to report to 4 | us please use any of the following channels: 5 | 6 | - open an [issue](../../issues) 7 | - use our [security form](https://corporate.zalando.com/en/services-and-contact#security-form) 8 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | # Define default commands 5 | COMPILE_CMD="./mvnw compile" 6 | PACKAGE_CMD="./mvnw package -DskipTests -pl logbook-servlet -am" 7 | VERIFY_CMD="./mvnw verify -B" 8 | INSTALL_CMD="./mvnw install -DskipTests -Djacoco.skip=true" 9 | 10 | # Flags to track selected options 11 | COMPILE=false 12 | NO_TEST_INSTALL=false 13 | PACKAGE=false 14 | 15 | # Parse options 16 | while [[ "$#" -gt 0 ]]; do 17 | case $1 in 18 | --package|-p) PACKAGE=true ;; 19 | --compile|-c) COMPILE=true ;; 20 | --no-test-install|-i) NO_TEST_INSTALL=true ;; 21 | -ci|-ic) COMPILE=true; NO_TEST_INSTALL=true ;; 22 | -cp|-pc) PACKAGE=true; COMPILE=true ;; 23 | -ip|-pi) PACKAGE=true; NO_TEST_INSTALL=true ;; 24 | *) echo "Unknown option: $1"; exit 1 ;; 25 | esac 26 | shift 27 | done 28 | 29 | # Execute commands based on the flags 30 | if $PACKAGE; then 31 | echo "Running package..." 32 | eval "$PACKAGE_CMD" 33 | fi 34 | 35 | if $COMPILE; then 36 | echo "Running compile..." 37 | eval "$COMPILE_CMD" 38 | fi 39 | 40 | if $NO_TEST_INSTALL; then 41 | echo "Running install without tests..." 42 | eval "$INSTALL_CMD" 43 | fi 44 | 45 | if ! $COMPILE && ! $NO_TEST_INSTALL && ! $PACKAGE; then 46 | echo "Running default commands..." 47 | eval "$PACKAGE_CMD" 48 | eval "$VERIFY_CMD" 49 | fi 50 | -------------------------------------------------------------------------------- /docs/logbook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zalando/logbook/ca81f780ce4a238b8652fdeb6dcb48d8245ba6ae/docs/logbook.jpg -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/BodyFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import javax.annotation.Nullable; 6 | 7 | import static org.apiguardian.api.API.Status.STABLE; 8 | 9 | @API(status = STABLE) 10 | @FunctionalInterface 11 | public interface BodyFilter { 12 | 13 | String filter(@Nullable final String contentType, final String body); 14 | 15 | @Nullable 16 | default BodyFilter tryMerge(final BodyFilter next) { 17 | return null; 18 | } 19 | 20 | static BodyFilter none() { 21 | return NoneBodyFilter.NONE; 22 | } 23 | 24 | static BodyFilter merge(final BodyFilter left, final BodyFilter right) { 25 | @Nullable final BodyFilter merged = left.tryMerge(right); 26 | 27 | if (merged == null) { 28 | return new NonMergeableBodyFilterPair(left, right); 29 | } else { 30 | return merged; 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/BodyReplacer.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import javax.annotation.Nullable; 6 | import java.util.Arrays; 7 | import java.util.Objects; 8 | 9 | import static org.apiguardian.api.API.Status.STABLE; 10 | 11 | @API(status = STABLE) 12 | @FunctionalInterface 13 | public interface BodyReplacer { 14 | 15 | @Nullable 16 | String replace(final T message); 17 | 18 | @SafeVarargs 19 | static BodyReplacer composite(final BodyReplacer... replacers) { 20 | // TODO shouldn't this be a composite rather than first one wins? 21 | return message -> Arrays.stream(replacers) 22 | .map(replacer -> replacer.replace(message)) 23 | .filter(Objects::nonNull) 24 | .findFirst() 25 | .orElse(null); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/Correlation.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.time.Duration; 6 | import java.time.Instant; 7 | 8 | import static org.apiguardian.api.API.Status.STABLE; 9 | 10 | @API(status = STABLE) 11 | public interface Correlation extends Precorrelation { 12 | 13 | Instant getEnd(); 14 | 15 | Duration getDuration(); 16 | 17 | @Override 18 | default Correlation correlate() { 19 | return this; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/CorrelationId.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | public interface CorrelationId { 4 | String generate(HttpRequest request); 5 | } 6 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/DeleteHttpHeaders.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.function.BiPredicate; 6 | 7 | interface DeleteHttpHeaders extends HttpHeaders { 8 | 9 | @Override 10 | default HttpHeaders delete(final String... names) { 11 | return delete(Arrays.asList(names)); 12 | } 13 | 14 | @Override 15 | default HttpHeaders delete( 16 | final BiPredicate> predicate) { 17 | return apply(predicate, (name, previous) -> null); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/Fold.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | import java.util.Collection; 6 | import java.util.function.BiFunction; 7 | import java.util.function.BinaryOperator; 8 | 9 | @UtilityClass 10 | final class Fold { 11 | 12 | static R fold( 13 | final Collection collection, 14 | final R seed, 15 | final BiFunction accumulator) { 16 | 17 | return collection.stream() 18 | .reduce(seed, accumulator, throwingCombiner()); 19 | } 20 | 21 | @SuppressWarnings("unchecked") 22 | private static BinaryOperator throwingCombiner() { 23 | return (BinaryOperator) NoCombiner.NONE; 24 | } 25 | 26 | // visible for testing 27 | enum NoCombiner implements BinaryOperator { 28 | NONE; 29 | 30 | @Override 31 | public Object apply(final Object left, final Object right) { 32 | throw new UnsupportedOperationException(); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/ForwardingHttpMessage.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.io.IOException; 6 | import java.nio.charset.Charset; 7 | 8 | import static org.apiguardian.api.API.Status.STABLE; 9 | 10 | @API(status = STABLE) 11 | public interface ForwardingHttpMessage extends HttpMessage { 12 | 13 | HttpMessage delegate(); 14 | 15 | @Override 16 | default String getProtocolVersion() { 17 | return delegate().getProtocolVersion(); 18 | } 19 | 20 | @Override 21 | default Origin getOrigin() { 22 | return delegate().getOrigin(); 23 | } 24 | 25 | @Override 26 | default HttpHeaders getHeaders() { 27 | return delegate().getHeaders(); 28 | } 29 | 30 | @Override 31 | default String getContentType() { 32 | return delegate().getContentType(); 33 | } 34 | 35 | @Override 36 | default Charset getCharset() { 37 | return delegate().getCharset(); 38 | } 39 | 40 | @Override 41 | default byte[] getBody() throws IOException { 42 | return delegate().getBody(); 43 | } 44 | 45 | @Override 46 | default String getBodyAsString() throws IOException { 47 | return delegate().getBodyAsString(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/ForwardingHttpResponse.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | import org.zalando.logbook.attributes.HttpAttributes; 5 | 6 | import java.io.IOException; 7 | 8 | import static org.apiguardian.api.API.Status.STABLE; 9 | 10 | @API(status = STABLE) 11 | public interface ForwardingHttpResponse extends ForwardingHttpMessage, HttpResponse { 12 | 13 | @Override 14 | HttpResponse delegate(); 15 | 16 | @Override 17 | default int getStatus() { 18 | return delegate().getStatus(); 19 | } 20 | 21 | @Override 22 | default HttpResponse withBody() throws IOException { 23 | return delegate().withBody(); 24 | } 25 | 26 | @Override 27 | default HttpResponse withoutBody() { 28 | return delegate().withoutBody(); 29 | } 30 | 31 | @Override 32 | default String getReasonPhrase() { 33 | return delegate().getReasonPhrase(); 34 | } 35 | 36 | @Override 37 | default HttpAttributes getAttributes() { 38 | return delegate().getAttributes(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/HeaderFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import static org.apiguardian.api.API.Status.STABLE; 6 | 7 | @API(status = STABLE) 8 | @FunctionalInterface 9 | public interface HeaderFilter { 10 | 11 | HttpHeaders filter(final HttpHeaders headers); 12 | 13 | static HeaderFilter none() { 14 | return headers -> headers; 15 | } 16 | 17 | static HeaderFilter merge(final HeaderFilter left, final HeaderFilter right) { 18 | return headers -> 19 | left.filter(right.filter(headers)); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/HttpLogFormatter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.io.IOException; 6 | 7 | import static org.apiguardian.api.API.Status.STABLE; 8 | 9 | @API(status = STABLE) 10 | public interface HttpLogFormatter { 11 | 12 | String format(Precorrelation precorrelation, HttpRequest request) throws IOException; 13 | 14 | String format(Correlation correlation, HttpResponse response) throws IOException; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/HttpLogWriter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.io.IOException; 6 | 7 | import static org.apiguardian.api.API.Status.STABLE; 8 | 9 | @API(status = STABLE) 10 | public interface HttpLogWriter { 11 | 12 | default boolean isActive() { 13 | return true; 14 | } 15 | 16 | void write(Precorrelation precorrelation, String request) throws IOException; 17 | 18 | void write(Correlation correlation, String response) throws IOException; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/HttpMessage.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import javax.annotation.Nullable; 6 | import java.io.IOException; 7 | import java.nio.charset.Charset; 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.Optional; 10 | 11 | import static org.apiguardian.api.API.Status.STABLE; 12 | 13 | @API(status = STABLE) 14 | public interface HttpMessage { 15 | 16 | default String getProtocolVersion() { 17 | return "HTTP/1.1"; 18 | } 19 | 20 | Origin getOrigin(); 21 | 22 | HttpHeaders getHeaders(); 23 | 24 | @Nullable 25 | default String getContentType() { 26 | return Optional 27 | .ofNullable(getHeaders()) 28 | .map(headers -> headers.getFirst(ContentType.CONTENT_TYPE_HEADER)) 29 | .map(ContentType::parseMimeType) 30 | .orElse(null); 31 | } 32 | 33 | default Charset getCharset() { 34 | return Optional 35 | .ofNullable(getHeaders()) 36 | .map(headers -> headers.getFirst(ContentType.CONTENT_TYPE_HEADER)) 37 | .map(ContentType::parseCharset) 38 | .orElse(StandardCharsets.UTF_8); 39 | } 40 | 41 | byte[] getBody() throws IOException; 42 | 43 | default String getBodyAsString() throws IOException { 44 | return new String(getBody(), getCharset()); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/HttpRequest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | import org.zalando.logbook.attributes.HttpAttributes; 5 | 6 | import java.io.IOException; 7 | import java.util.Optional; 8 | 9 | import static org.apiguardian.api.API.Status.STABLE; 10 | 11 | @API(status = STABLE) 12 | public interface HttpRequest extends HttpMessage { 13 | 14 | String getRemote(); 15 | 16 | String getMethod(); 17 | 18 | /** 19 | * Absolute Request URI including scheme, host, port (unless http/80 or https/443), path and query string. 20 | * 21 | *

Note that the URI may be invalid if the client issued an HTTP request using a malformed URL.

22 | * 23 | * @return the requested URI 24 | */ 25 | default String getRequestUri() { 26 | return RequestURI.reconstruct(this); 27 | } 28 | 29 | String getScheme(); 30 | 31 | String getHost(); 32 | 33 | Optional getPort(); 34 | 35 | String getPath(); 36 | 37 | String getQuery(); 38 | 39 | default HttpAttributes getAttributes() { 40 | return HttpAttributes.EMPTY; 41 | } 42 | 43 | // TODO don't throw! 44 | // TODO void vs pseudo-function (mutable) 45 | HttpRequest withBody() throws IOException; 46 | 47 | // TODO require all implementations to be without-body by default 48 | HttpRequest withoutBody(); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/HttpResponse.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | import org.zalando.logbook.attributes.HttpAttributes; 5 | 6 | import java.io.IOException; 7 | 8 | import static org.apiguardian.api.API.Status.STABLE; 9 | 10 | @API(status = STABLE) 11 | public interface HttpResponse extends HttpMessage { 12 | 13 | int getStatus(); 14 | 15 | // TODO void vs pseudo-function (mutable) 16 | HttpResponse withBody() throws IOException; 17 | 18 | HttpResponse withoutBody(); 19 | 20 | default HttpAttributes getAttributes() { 21 | return HttpAttributes.EMPTY; 22 | } 23 | 24 | default String getReasonPhrase() { 25 | return HttpStatus.getReasonPhraseByCode(getStatus()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/Logbook.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.io.IOException; 6 | 7 | import static org.apiguardian.api.API.Status.STABLE; 8 | 9 | @API(status = STABLE) 10 | public interface Logbook { 11 | 12 | RequestWritingStage process(HttpRequest request) throws IOException; 13 | 14 | RequestWritingStage process(HttpRequest request, Strategy strategy) throws IOException; 15 | 16 | interface RequestWritingStage extends ResponseProcessingStage { 17 | ResponseProcessingStage write() throws IOException; 18 | } 19 | 20 | interface ResponseProcessingStage { 21 | ResponseWritingStage process(HttpResponse response) throws IOException; 22 | } 23 | 24 | interface ResponseWritingStage { 25 | void write() throws IOException; 26 | } 27 | 28 | static Logbook create() { 29 | return builder().build(); 30 | } 31 | 32 | static LogbookCreator.Builder builder() { 33 | return LogbookCreator.builder(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/LogbookFactory.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | import org.zalando.logbook.attributes.AttributeExtractor; 5 | 6 | import javax.annotation.Nullable; 7 | import java.util.Comparator; 8 | import java.util.ServiceLoader; 9 | import java.util.function.Predicate; 10 | import java.util.stream.StreamSupport; 11 | 12 | import static org.apiguardian.api.API.Status.STABLE; 13 | 14 | @API(status = STABLE) 15 | public interface LogbookFactory { 16 | 17 | default int getPriority() { 18 | return 0; 19 | } 20 | 21 | LogbookFactory INSTANCE = StreamSupport.stream(ServiceLoader.load(LogbookFactory.class).spliterator(), false) 22 | .max(Comparator.comparingInt(LogbookFactory::getPriority)) 23 | .orElse(null); 24 | 25 | Logbook create( 26 | @Nullable final Predicate condition, 27 | @Nullable final CorrelationId correlationId, 28 | @Nullable final QueryFilter queryFilter, 29 | @Nullable final PathFilter pathFilter, 30 | @Nullable final HeaderFilter headerFilter, 31 | @Nullable final BodyFilter bodyFilter, 32 | @Nullable final RequestFilter requestFilter, 33 | @Nullable final ResponseFilter responseFilter, 34 | @Nullable final Strategy strategy, 35 | @Nullable final AttributeExtractor attributeExtractor, 36 | @Nullable final Sink sink); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/NonMergeableBodyFilterPair.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | import javax.annotation.Nullable; 7 | 8 | import static lombok.AccessLevel.PACKAGE; 9 | import static org.zalando.logbook.BodyFilter.merge; 10 | 11 | @AllArgsConstructor 12 | @Getter(PACKAGE) 13 | final class NonMergeableBodyFilterPair implements BodyFilter { 14 | 15 | private final BodyFilter left; 16 | private final BodyFilter right; 17 | 18 | @Override 19 | public String filter( 20 | @Nullable final String contentType, final String body) { 21 | return right.filter(contentType, left.filter(contentType, body)); 22 | } 23 | 24 | @Nullable 25 | @Override 26 | public BodyFilter tryMerge(final BodyFilter next) { 27 | @Nullable final BodyFilter filter = right.tryMerge(next); 28 | 29 | if (filter == null) { 30 | return null; 31 | } 32 | 33 | return merge(left, filter); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/NoneBodyFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | enum NoneBodyFilter implements BodyFilter { 6 | 7 | NONE; 8 | 9 | @Override 10 | public String filter( 11 | @Nullable final String contentType, final String body) { 12 | return body; 13 | } 14 | 15 | @Override 16 | public BodyFilter tryMerge(final BodyFilter next) { 17 | return next; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/Origin.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import static org.apiguardian.api.API.Status.STABLE; 6 | 7 | @API(status = STABLE) 8 | public enum Origin { 9 | 10 | LOCAL, REMOTE 11 | 12 | } 13 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/PathFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import static org.apiguardian.api.API.Status.EXPERIMENTAL; 6 | 7 | @API(status = EXPERIMENTAL) 8 | @FunctionalInterface 9 | public interface PathFilter { 10 | 11 | String filter(final String path); 12 | 13 | static PathFilter none() { 14 | return path -> path; 15 | } 16 | 17 | static PathFilter merge(final PathFilter left, final PathFilter right) { 18 | return path -> 19 | left.filter(right.filter(path)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/Precorrelation.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.time.Instant; 6 | 7 | import static org.apiguardian.api.API.Status.STABLE; 8 | 9 | @API(status = STABLE) 10 | public interface Precorrelation { 11 | 12 | String getId(); 13 | 14 | Instant getStart(); 15 | 16 | Correlation correlate(); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/QueryFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import static org.apiguardian.api.API.Status.STABLE; 6 | 7 | @API(status = STABLE) 8 | @FunctionalInterface 9 | public interface QueryFilter { 10 | 11 | String filter(final String query); 12 | 13 | static QueryFilter none() { 14 | return query -> query; 15 | } 16 | 17 | static QueryFilter merge(final QueryFilter left, final QueryFilter right) { 18 | return query -> 19 | left.filter(right.filter(query)); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/RequestFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import static org.apiguardian.api.API.Status.STABLE; 6 | 7 | @API(status = STABLE) 8 | @FunctionalInterface 9 | public interface RequestFilter { 10 | 11 | HttpRequest filter(final HttpRequest request); 12 | 13 | static RequestFilter none() { 14 | return request -> request; 15 | } 16 | 17 | static RequestFilter merge(final RequestFilter left, final RequestFilter right) { 18 | return request -> 19 | left.filter(right.filter(request)); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/ResponseFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import static org.apiguardian.api.API.Status.STABLE; 6 | 7 | @API(status = STABLE) 8 | @FunctionalInterface 9 | public interface ResponseFilter { 10 | 11 | HttpResponse filter(final HttpResponse response); 12 | 13 | static ResponseFilter none() { 14 | return response -> response; 15 | } 16 | 17 | static ResponseFilter merge(final ResponseFilter left, final ResponseFilter right) { 18 | return response -> 19 | left.filter(right.filter(response)); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/Sink.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import java.io.IOException; 4 | 5 | public interface Sink { 6 | 7 | default boolean isActive() { 8 | return true; 9 | } 10 | 11 | void write(Precorrelation precorrelation, HttpRequest request) throws IOException; 12 | 13 | void write(Correlation correlation, HttpRequest request, HttpResponse response) throws IOException; 14 | 15 | default void writeBoth(final Correlation correlation, final HttpRequest request, final HttpResponse response) 16 | throws IOException { 17 | write(correlation, request); 18 | write(correlation, request, response); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/UpdateHttpHeaders.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | import static org.zalando.logbook.Fold.fold; 8 | 9 | interface UpdateHttpHeaders extends HttpHeaders { 10 | 11 | @Override 12 | default HttpHeaders update( 13 | final String name, 14 | final String... values) { 15 | 16 | return update(name, Arrays.asList(values)); 17 | } 18 | 19 | @Override 20 | default HttpHeaders update( 21 | final Map> headers) { 22 | 23 | final HttpHeaders self = this; 24 | return fold(headers.entrySet(), self, (result, entry) -> { 25 | final String name = entry.getKey(); 26 | final List values = entry.getValue(); 27 | return result.update(name, values); 28 | }); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/attributes/AttributeExtractor.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.attributes; 2 | 3 | import org.apiguardian.api.API; 4 | import org.zalando.logbook.HttpRequest; 5 | import org.zalando.logbook.HttpResponse; 6 | 7 | import javax.annotation.Nonnull; 8 | 9 | import static org.apiguardian.api.API.Status.EXPERIMENTAL; 10 | 11 | @API(status = EXPERIMENTAL) 12 | public interface AttributeExtractor { 13 | 14 | @Nonnull 15 | default HttpAttributes extract(final HttpRequest request) { 16 | return HttpAttributes.EMPTY; 17 | } 18 | 19 | @Nonnull 20 | default HttpAttributes extract(final HttpRequest request, final HttpResponse response) { 21 | return HttpAttributes.EMPTY; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/attributes/NoOpAttributeExtractor.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.attributes; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import org.apiguardian.api.API; 5 | 6 | import static org.apiguardian.api.API.Status.EXPERIMENTAL; 7 | 8 | @API(status = EXPERIMENTAL) 9 | @EqualsAndHashCode 10 | public final class NoOpAttributeExtractor implements AttributeExtractor { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /logbook-api/src/main/java/org/zalando/logbook/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; 5 | -------------------------------------------------------------------------------- /logbook-api/src/main/resources/META-INF/services/org.zalando.logbook.LogbookFactory: -------------------------------------------------------------------------------- 1 | org.zalando.logbook.internal.ExceptionThrowingLogbookFactory 2 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/BodyFilterTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | final class BodyFilterTest { 8 | 9 | @Test 10 | void noneShouldDefaultToNoOp() { 11 | final BodyFilter unit = BodyFilter.none(); 12 | 13 | assertThat(unit.filter("text/plain", "Hello, world!")).isEqualTo("Hello, world!"); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/BodyReplacerTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.IOException; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | import static org.mockito.Mockito.mock; 9 | import static org.mockito.Mockito.when; 10 | 11 | final class BodyReplacerTest { 12 | 13 | @Test 14 | void shouldStopOnFirstReplacerThatReplaced() throws IOException { 15 | final BodyReplacer unit = BodyReplacer.composite( 16 | m -> m.getContentType().startsWith("text/") ? "" : null, 17 | m -> m.getContentType().endsWith("plain") ? "" : null); 18 | final HttpMessage message = mock(HttpMessage.class); 19 | when(message.getContentType()).thenReturn("text/plain"); 20 | 21 | final String body = unit.replace(message); 22 | 23 | assertThat(body).isEqualTo(""); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/CorrelationTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.mockito.invocation.InvocationOnMock; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | import static org.mockito.Mockito.mock; 8 | 9 | class CorrelationTest { 10 | 11 | private final Correlation unit = mock(Correlation.class, InvocationOnMock::callRealMethod); 12 | 13 | @Test 14 | void correlateNoOp() { 15 | assertThat(unit).isSameAs(unit.correlate()); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/FoldTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertThrows; 6 | 7 | class FoldTest { 8 | 9 | @Test 10 | void combinerIsNotSupported() { 11 | final Fold.NoCombiner unit = Fold.NoCombiner.NONE; 12 | 13 | assertThrows(UnsupportedOperationException.class, () -> 14 | unit.apply(null, null)); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/ForwardingTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import lombok.SneakyThrows; 4 | import org.junit.jupiter.api.Test; 5 | import org.mockito.Mockito; 6 | 7 | import java.lang.reflect.Method; 8 | import java.util.function.UnaryOperator; 9 | 10 | import static org.mockito.Mockito.verify; 11 | 12 | final class ForwardingTest { 13 | 14 | @Test 15 | void shouldForwardHttpRequests() { 16 | test(HttpRequest.class, request -> (ForwardingHttpRequest) () -> request); 17 | } 18 | 19 | @Test 20 | void shouldForwardHttpResponses() { 21 | test(HttpResponse.class, response -> (ForwardingHttpResponse) () -> response); 22 | } 23 | 24 | @SneakyThrows 25 | static void test(final Class type, final UnaryOperator forwarder) { 26 | final T delegate = Mockito.mock(type); 27 | final T forwarded = forwarder.apply(delegate); 28 | 29 | for (final Method method : type.getMethods()) { 30 | method.invoke(forwarded); 31 | method.invoke(verify(delegate)); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/HeaderFilterTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | final class HeaderFilterTest { 8 | 9 | @Test 10 | void noneShouldDefaultToNoOp() { 11 | final HeaderFilter unit = HeaderFilter.none(); 12 | 13 | final HttpHeaders headers = HttpHeaders.of( 14 | "Authorization", "Bearer s3cr3t"); 15 | 16 | assertThat(unit.filter(headers)).isSameAs(headers); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/HttpHeadersImmutabilityTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static java.util.Collections.emptyList; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | 9 | @SuppressWarnings("ResultOfMethodCallIgnored") 10 | class HttpHeadersImmutabilityTest { 11 | 12 | @Test 13 | void isImmutable() { 14 | final HttpHeaders unit = HttpHeaders.empty(); 15 | unit.update("Content-Type", "application/json"); 16 | assertEquals(HttpHeaders.empty(), unit); 17 | } 18 | 19 | @Test 20 | void isDeeplyImmutable() { 21 | final HttpHeaders unit = HttpHeaders.empty() 22 | .update("Content-Type", "application/json"); 23 | 24 | assertThrows(UnsupportedOperationException.class, () -> 25 | unit.apply("Content-Type", previous -> { 26 | previous.clear(); 27 | return emptyList(); 28 | })); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/HttpHeadersUpdateTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | 7 | import static java.util.Collections.singletonList; 8 | import static java.util.Collections.singletonMap; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | class HttpHeadersUpdateTest { 12 | 13 | @Test 14 | void updatesMap() { 15 | final HttpHeaders unit = HttpHeaders.empty() 16 | .update("Content-Type", "application/json") 17 | .update(singletonMap("Cookie", list("user=me"))); 18 | 19 | assertThat(unit) 20 | .containsEntry("Content-Type", list("application/json")) 21 | .containsEntry("Cookie", list("user=me")); 22 | } 23 | 24 | private static List list(final T s) { 25 | return singletonList(s); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/HttpLogWriterTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.mockito.Mockito.spy; 7 | 8 | final class HttpLogWriterTest { 9 | 10 | @Test 11 | void shouldBeActiveByDefault() { 12 | final HttpLogWriter unit = spy(HttpLogWriter.class); 13 | 14 | assertThat(unit.isActive()).isTrue(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/HttpRequestTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.zalando.logbook.attributes.HttpAttributes; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | import static org.mockito.Mockito.CALLS_REAL_METHODS; 8 | import static org.mockito.Mockito.mock; 9 | 10 | final class HttpRequestTest { 11 | 12 | @Test 13 | void httpRequestShouldReturnEmptyAttributesByDefault() { 14 | final HttpRequest httpRequest = mock(HttpRequest.class, CALLS_REAL_METHODS); 15 | assertThat(httpRequest.getAttributes()).isEqualTo(HttpAttributes.EMPTY); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/LogbookFactoryTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.zalando.logbook.internal.ExceptionThrowingLogbookFactory; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | final class LogbookFactoryTest { 9 | 10 | @Test 11 | void shouldLoadInstanceUsingSPI() { 12 | final LogbookFactory factory = LogbookFactory.INSTANCE; 13 | 14 | assertThat(factory).isInstanceOf(ExceptionThrowingLogbookFactory.class); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/PathFilterTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | final class PathFilterTest { 8 | 9 | @Test 10 | void noneShouldDefaultToNoOp() { 11 | final PathFilter unit = PathFilter.none(); 12 | 13 | assertThat(unit.filter("/a/b/c")).isEqualTo("/a/b/c"); 14 | } 15 | 16 | @Test 17 | void merge() { 18 | final PathFilter unit = PathFilter.merge(a -> "a", b -> "b"); 19 | 20 | assertThat(unit.filter("/a/b/c")).isEqualTo("a"); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/QueryFilterTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | final class QueryFilterTest { 8 | 9 | @Test 10 | void noneShouldDefaultToNoOp() { 11 | final QueryFilter unit = QueryFilter.none(); 12 | 13 | assertThat(unit.filter("a=b&c=d&f=e")).isEqualTo("a=b&c=d&f=e"); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/RequestFilterTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.mockito.Mockito.mock; 7 | 8 | final class RequestFilterTest { 9 | 10 | @Test 11 | void noneShouldDefaultToNoOp() { 12 | final RequestFilter unit = RequestFilter.none(); 13 | final HttpRequest request = mock(HttpRequest.class); 14 | 15 | assertThat(unit.filter(request)).isSameAs(request); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/ResponseFilterTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.mockito.Mockito.mock; 7 | 8 | final class ResponseFilterTest { 9 | 10 | @Test 11 | void noneShouldDefaultToNoOp() { 12 | final ResponseFilter unit = ResponseFilter.none(); 13 | final HttpResponse response = mock(HttpResponse.class); 14 | 15 | assertThat(unit.filter(response)).isSameAs(response); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/SinkTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.IOException; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertTrue; 8 | import static org.mockito.ArgumentMatchers.any; 9 | import static org.mockito.Mockito.doCallRealMethod; 10 | import static org.mockito.Mockito.mock; 11 | import static org.mockito.Mockito.verify; 12 | import static org.mockito.Mockito.when; 13 | 14 | class SinkTest { 15 | 16 | private final Correlation correlation = mock(Correlation.class); 17 | private final HttpRequest request = mock(HttpRequest.class); 18 | private final HttpResponse response = mock(HttpResponse.class); 19 | 20 | private final Sink unit = mock(Sink.class); 21 | 22 | @Test 23 | void shouldBeActiveByDefault() { 24 | when(unit.isActive()).thenCallRealMethod(); 25 | 26 | assertTrue(unit.isActive()); 27 | } 28 | 29 | @Test 30 | void shouldDelegateToWriteRequestAndWriteResponseByDefault() throws IOException { 31 | doCallRealMethod().when(unit).writeBoth(any(), any(), any()); 32 | 33 | unit.writeBoth(correlation, request, response); 34 | 35 | verify(unit).write(correlation, request); 36 | verify(unit).write(correlation, request, response); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/attributes/AttributeExtractorTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.attributes; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.mockito.Mockito.CALLS_REAL_METHODS; 7 | import static org.mockito.Mockito.mock; 8 | 9 | final class AttributeExtractorTest { 10 | 11 | @Test 12 | void testAttributeExtractorDefaultMethods() { 13 | final AttributeExtractor attributeExtractor = mock( 14 | AttributeExtractor.class, 15 | CALLS_REAL_METHODS 16 | ); 17 | 18 | assertThat(attributeExtractor.extract(mock())).isEqualTo(HttpAttributes.EMPTY); 19 | assertThat(attributeExtractor.extract(mock(), mock())).isEqualTo(HttpAttributes.EMPTY); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /logbook-api/src/test/java/org/zalando/logbook/attributes/NoOpAttributeExtractorTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.attributes; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.zalando.logbook.HttpRequest; 5 | import org.zalando.logbook.HttpResponse; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | import static org.mockito.Mockito.mock; 9 | 10 | final class NoOpAttributeExtractorTest { 11 | 12 | @Test 13 | void shouldHaveNoExtractedAttributesFromRequest() { 14 | final HttpRequest httpRequest = mock(HttpRequest.class); 15 | final AttributeExtractor attributesExtractor = new NoOpAttributeExtractor(); 16 | assertThat(attributesExtractor.extract(httpRequest)).isEqualTo(HttpAttributes.EMPTY); 17 | } 18 | 19 | @Test 20 | void shouldHaveNoExtractedAttributesFromRequestAndResponse() { 21 | final HttpRequest httpRequest = mock(HttpRequest.class); 22 | final HttpResponse httpResponse = mock(HttpResponse.class); 23 | final AttributeExtractor attributesExtractor = new NoOpAttributeExtractor(); 24 | assertThat(attributesExtractor.extract(httpRequest, httpResponse)).isEqualTo(HttpAttributes.EMPTY); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /logbook-common/src/main/java/org/zalando/logbook/common/Glob.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.common; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.function.Predicate; 7 | import java.util.regex.Pattern; 8 | 9 | public final class Glob { 10 | 11 | private static final Pattern GLOB = Pattern.compile("\\?|(/\\*{2}$)|\\*{2}|(/\\*$)|\\*"); 12 | 13 | private static final Map REPLACEMENTS; 14 | 15 | static { 16 | final Map replacements = new HashMap<>(); 17 | 18 | replacements.put("?", "."); 19 | replacements.put("/**", "(/.*)?$"); 20 | replacements.put("**", ".*?"); 21 | replacements.put("/*", "/[^/]*$"); 22 | 23 | REPLACEMENTS = Collections.unmodifiableMap(replacements); 24 | } 25 | 26 | private Glob() { 27 | 28 | } 29 | 30 | public static Predicate compile(final String glob) { 31 | return PatternLike.compile(GLOB, glob, Glob::translate); 32 | } 33 | 34 | private static String translate(final String match) { 35 | return REPLACEMENTS.getOrDefault(match, "[^/]*?"); 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /logbook-common/src/main/java/org/zalando/logbook/common/MediaTypeQuery.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.common; 2 | 3 | import java.util.Arrays; 4 | import java.util.function.Predicate; 5 | import java.util.regex.Pattern; 6 | 7 | public final class MediaTypeQuery { 8 | 9 | private static final Pattern WILDCARD = Pattern.compile("\\*"); 10 | 11 | private MediaTypeQuery() { 12 | 13 | } 14 | 15 | public static Predicate compile(final String query, final String... queries) { 16 | return Arrays.stream(queries) 17 | .map(MediaTypeQuery::compile) 18 | .reduce(compile(query), Predicate::or); 19 | } 20 | 21 | private static Predicate compile(final String query) { 22 | final int slash = query.indexOf('/'); 23 | final int semicolon = query.indexOf(';'); 24 | final int end = semicolon == -1 ? query.length() : semicolon; 25 | 26 | final String type = query.substring(0, slash).trim(); 27 | final String subType = query.substring(slash + 1, end).trim(); 28 | 29 | final String first = PatternLike.toPattern(WILDCARD, type, ".*?"); 30 | final String second = PatternLike.toPattern(WILDCARD, subType, ".*?"); 31 | 32 | // TODO support real matching on parameters 33 | final Pattern pattern = Pattern.compile(first + '/' + second + "(;.*)?"); 34 | 35 | return input -> input != null && pattern.matcher(input).matches(); 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /logbook-common/src/test/java/org/zalando/logbook/common/MediaTypeQueryTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.common; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import javax.annotation.Nullable; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | import static org.zalando.logbook.common.MediaTypeQuery.compile; 9 | 10 | final class MediaTypeQueryTest { 11 | 12 | @Test 13 | void shouldMatchAllMatch() { 14 | deny("*/*", null); 15 | deny("*/*", ""); 16 | deny("text/*", "application/json"); 17 | deny("text/plain", "application/json"); 18 | allow("*/*", "text/plain"); 19 | allow("text/*", "text/plain"); 20 | allow("*/plain", "text/plain"); 21 | allow("text/plain", "text/plain"); 22 | allow("text/plain", "text/plain;charset=UTF-8"); 23 | allow("text/plain;charset=UTF-8", "text/plain"); // TODO should deny 24 | } 25 | 26 | private void allow(final String pattern, @Nullable final String mediaType) { 27 | assertThat(compile(pattern).test(mediaType)) 28 | .as("Media type query %s match media type %s", pattern, mediaType) 29 | .isTrue(); 30 | } 31 | 32 | private void deny(final String pattern, @Nullable final String mediaType) { 33 | assertThat(compile(pattern).test(mediaType)) 34 | .as("Media type query %s match media type %s", pattern, mediaType) 35 | .isFalse(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/BodyOnlyIfStatusAtLeastStrategy.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.apiguardian.api.API; 5 | import org.zalando.logbook.Correlation; 6 | import org.zalando.logbook.HttpRequest; 7 | import org.zalando.logbook.HttpResponse; 8 | import org.zalando.logbook.Precorrelation; 9 | import org.zalando.logbook.Sink; 10 | import org.zalando.logbook.Strategy; 11 | 12 | import java.io.IOException; 13 | 14 | import static org.apiguardian.api.API.Status.EXPERIMENTAL; 15 | 16 | @API(status = EXPERIMENTAL) 17 | @AllArgsConstructor 18 | public final class BodyOnlyIfStatusAtLeastStrategy implements Strategy { 19 | 20 | private final int status; 21 | 22 | @Override 23 | public void write(final Precorrelation precorrelation, final HttpRequest request, final Sink sink) { 24 | // defer decision until response is available 25 | } 26 | 27 | @Override 28 | public void write(final Correlation correlation, final HttpRequest request, final HttpResponse response, 29 | final Sink sink) throws IOException { 30 | 31 | if (response.getStatus() >= status) { 32 | sink.writeBoth(correlation, request, response); 33 | } else { 34 | sink.writeBoth(correlation, request.withoutBody(), response.withoutBody()); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/BodyReplacementHttpRequest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.zalando.logbook.ForwardingHttpRequest; 5 | import org.zalando.logbook.HttpRequest; 6 | 7 | import static java.nio.charset.StandardCharsets.UTF_8; 8 | 9 | @AllArgsConstructor 10 | final class BodyReplacementHttpRequest implements ForwardingHttpRequest { 11 | 12 | private final HttpRequest request; 13 | private final String replacement; 14 | 15 | @Override 16 | public HttpRequest delegate() { 17 | return request; 18 | } 19 | 20 | @Override 21 | public HttpRequest withBody() { 22 | return withoutBody(); 23 | } 24 | 25 | @Override 26 | public HttpRequest withoutBody() { 27 | return new BodyReplacementHttpRequest(request.withoutBody(), replacement); 28 | } 29 | 30 | @Override 31 | public byte[] getBody() { 32 | return replacement.getBytes(UTF_8); 33 | } 34 | 35 | @Override 36 | public String getBodyAsString() { 37 | return replacement; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/BodyReplacementHttpResponse.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.zalando.logbook.ForwardingHttpResponse; 5 | import org.zalando.logbook.HttpResponse; 6 | 7 | import static java.nio.charset.StandardCharsets.UTF_8; 8 | 9 | @AllArgsConstructor 10 | final class BodyReplacementHttpResponse implements ForwardingHttpResponse, HttpResponse { 11 | 12 | private final HttpResponse response; 13 | private final String replacement; 14 | 15 | @Override 16 | public HttpResponse delegate() { 17 | return response; 18 | } 19 | 20 | @Override 21 | public HttpResponse withBody() { 22 | return withoutBody(); 23 | } 24 | 25 | @Override 26 | public HttpResponse withoutBody() { 27 | return new BodyReplacementHttpResponse(response.withoutBody(), replacement); 28 | } 29 | 30 | @Override 31 | public byte[] getBody() { 32 | return replacement.getBytes(UTF_8); 33 | } 34 | 35 | @Override 36 | public String getBodyAsString() { 37 | return replacement; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/Cache.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import lombok.AllArgsConstructor; 4 | 5 | import java.util.concurrent.atomic.AtomicReference; 6 | import java.util.function.Supplier; 7 | 8 | @AllArgsConstructor 9 | final class Cache { 10 | 11 | private final AtomicReference cache = new AtomicReference<>(); 12 | private final Supplier supplier; 13 | 14 | public T get() { 15 | return cache.updateAndGet(cached -> 16 | cached == null ? supplier.get() : cached); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/CachingHttpRequest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.zalando.logbook.ForwardingHttpRequest; 4 | import org.zalando.logbook.attributes.HttpAttributes; 5 | import org.zalando.logbook.HttpHeaders; 6 | import org.zalando.logbook.HttpRequest; 7 | 8 | final class CachingHttpRequest implements ForwardingHttpRequest { 9 | 10 | private final HttpRequest request; 11 | private final Cache headers; 12 | private final HttpAttributes httpAttributes; 13 | 14 | CachingHttpRequest(final HttpRequest request) { 15 | this(request, HttpAttributes.EMPTY); 16 | } 17 | 18 | CachingHttpRequest(final HttpRequest request, final HttpAttributes httpAttributes) { 19 | this.request = request; 20 | this.headers = new Cache<>(request::getHeaders); 21 | this.httpAttributes = httpAttributes; 22 | } 23 | 24 | @Override 25 | public HttpRequest delegate() { 26 | return request; 27 | } 28 | 29 | @Override 30 | public HttpHeaders getHeaders() { 31 | return headers.get(); 32 | } 33 | 34 | @Override 35 | public HttpAttributes getAttributes() { 36 | return httpAttributes; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/CachingHttpResponse.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.zalando.logbook.ForwardingHttpResponse; 4 | import org.zalando.logbook.HttpHeaders; 5 | import org.zalando.logbook.HttpResponse; 6 | import org.zalando.logbook.attributes.HttpAttributes; 7 | 8 | final class CachingHttpResponse implements ForwardingHttpResponse { 9 | 10 | private final HttpResponse response; 11 | private final Cache headers; 12 | private final HttpAttributes httpAttributes; 13 | 14 | CachingHttpResponse(final HttpResponse response) { 15 | this(response, HttpAttributes.EMPTY); 16 | } 17 | 18 | CachingHttpResponse(final HttpResponse response, final HttpAttributes httpAttributes) { 19 | this.response = response; 20 | this.headers = new Cache<>(response::getHeaders); 21 | this.httpAttributes = httpAttributes; 22 | } 23 | 24 | @Override 25 | public HttpResponse delegate() { 26 | return response; 27 | } 28 | 29 | @Override 30 | public HttpHeaders getHeaders() { 31 | return headers.get(); 32 | } 33 | 34 | @Override 35 | public HttpAttributes getAttributes() { 36 | return httpAttributes; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/CompositeSink.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.zalando.fauxpas.ThrowingConsumer; 5 | import org.zalando.logbook.Correlation; 6 | import org.zalando.logbook.HttpRequest; 7 | import org.zalando.logbook.HttpResponse; 8 | import org.zalando.logbook.Precorrelation; 9 | import org.zalando.logbook.Sink; 10 | 11 | import java.io.IOException; 12 | import java.util.Collection; 13 | 14 | @AllArgsConstructor 15 | public final class CompositeSink implements Sink { 16 | 17 | private final Collection sinks; 18 | 19 | @Override 20 | public boolean isActive() { 21 | return sinks.stream().anyMatch(Sink::isActive); 22 | } 23 | 24 | @Override 25 | public void write(final Precorrelation precorrelation, final HttpRequest request) { 26 | each(sink -> sink.write(precorrelation, request)); 27 | } 28 | 29 | @Override 30 | public void write(final Correlation correlation, final HttpRequest request, final HttpResponse response) { 31 | each(sink -> sink.write(correlation, request, response)); 32 | } 33 | 34 | @Override 35 | public void writeBoth(final Correlation correlation, final HttpRequest request, final HttpResponse response) { 36 | each(sink -> sink.writeBoth(correlation, request, response)); 37 | } 38 | 39 | private void each(final ThrowingConsumer consumer) { 40 | sinks.stream().filter(Sink::isActive).forEach(consumer); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/DefaultCorrelationId.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.apiguardian.api.API; 4 | import org.zalando.logbook.CorrelationId; 5 | import org.zalando.logbook.HttpRequest; 6 | 7 | import java.util.Random; 8 | import java.util.concurrent.ThreadLocalRandom; 9 | 10 | import static org.apiguardian.api.API.Status.MAINTAINED; 11 | 12 | @API(status = MAINTAINED) 13 | public final class DefaultCorrelationId implements CorrelationId { 14 | 15 | @Override 16 | public String generate(final HttpRequest request) { 17 | final Random random = ThreadLocalRandom.current(); 18 | // set most significant bit to produce fixed length string 19 | return Long.toHexString(random.nextLong() | Long.MIN_VALUE); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/DefaultFilters.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import java.util.Collection; 4 | 5 | import static java.util.ServiceLoader.load; 6 | import static java.util.stream.Collectors.toList; 7 | import static java.util.stream.StreamSupport.stream; 8 | 9 | final class DefaultFilters { 10 | 11 | private DefaultFilters() { 12 | 13 | } 14 | 15 | static Collection defaultValues(final Class defaultType) { 16 | return stream(load(defaultType).spliterator(), false).collect(toList()); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/DefaultHttpLogWriter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.apiguardian.api.API; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.zalando.logbook.Correlation; 7 | import org.zalando.logbook.HttpLogWriter; 8 | import org.zalando.logbook.Logbook; 9 | import org.zalando.logbook.Precorrelation; 10 | 11 | import static org.apiguardian.api.API.Status.STABLE; 12 | 13 | @API(status = STABLE) 14 | public final class DefaultHttpLogWriter implements HttpLogWriter { 15 | 16 | private final Logger log = LoggerFactory.getLogger(Logbook.class); 17 | 18 | @Override 19 | public boolean isActive() { 20 | return log.isTraceEnabled(); 21 | } 22 | 23 | @Override 24 | public void write(final Precorrelation precorrelation, final String request) { 25 | log.trace(request); 26 | } 27 | 28 | @Override 29 | public void write(final Correlation correlation, final String response) { 30 | log.trace(response); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/DefaultSink.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.zalando.logbook.Correlation; 5 | import org.zalando.logbook.HttpLogFormatter; 6 | import org.zalando.logbook.HttpLogWriter; 7 | import org.zalando.logbook.HttpRequest; 8 | import org.zalando.logbook.HttpResponse; 9 | import org.zalando.logbook.Precorrelation; 10 | import org.zalando.logbook.Sink; 11 | 12 | import java.io.IOException; 13 | 14 | @AllArgsConstructor 15 | public final class DefaultSink implements Sink { 16 | 17 | private final HttpLogFormatter formatter; 18 | private final HttpLogWriter writer; 19 | 20 | @Override 21 | public boolean isActive() { 22 | return writer.isActive(); 23 | } 24 | 25 | @Override 26 | public void write(final Precorrelation precorrelation, final HttpRequest request) throws IOException { 27 | writer.write(precorrelation, formatter.format(precorrelation, request)); 28 | } 29 | 30 | @Override 31 | public void write(final Correlation correlation, final HttpRequest request, final HttpResponse response) 32 | throws IOException { 33 | writer.write(correlation, formatter.format(correlation, response)); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/DefaultStrategy.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.zalando.logbook.Strategy; 5 | 6 | @AllArgsConstructor 7 | public final class DefaultStrategy implements Strategy { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/RequestFilters.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.apiguardian.api.API; 4 | import org.zalando.logbook.BodyReplacer; 5 | import org.zalando.logbook.HttpRequest; 6 | import org.zalando.logbook.RequestFilter; 7 | 8 | import javax.annotation.Nullable; 9 | 10 | import static org.apiguardian.api.API.Status.MAINTAINED; 11 | import static org.apiguardian.api.API.Status.STABLE; 12 | import static org.zalando.logbook.core.DefaultFilters.defaultValues; 13 | 14 | @API(status = STABLE) 15 | public final class RequestFilters { 16 | 17 | private RequestFilters() { 18 | 19 | } 20 | 21 | @API(status = MAINTAINED) 22 | public static RequestFilter defaultValue() { 23 | return defaultValues(RequestFilter.class).stream() 24 | .reduce(replaceBody(BodyReplacers.defaultValue()), RequestFilter::merge); 25 | } 26 | 27 | public static RequestFilter replaceBody(final BodyReplacer replacer) { 28 | return request -> { 29 | @Nullable final String replacement = replacer.replace(request); 30 | if (replacement == null) { 31 | return request; 32 | } 33 | return new BodyReplacementHttpRequest(request, replacement); 34 | }; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/ResponseFilters.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.apiguardian.api.API; 4 | import org.zalando.logbook.BodyReplacer; 5 | import org.zalando.logbook.HttpResponse; 6 | import org.zalando.logbook.ResponseFilter; 7 | 8 | import javax.annotation.Nullable; 9 | 10 | import static org.apiguardian.api.API.Status.MAINTAINED; 11 | import static org.apiguardian.api.API.Status.STABLE; 12 | import static org.zalando.logbook.core.DefaultFilters.defaultValues; 13 | 14 | @API(status = STABLE) 15 | public final class ResponseFilters { 16 | 17 | private ResponseFilters() { 18 | 19 | } 20 | 21 | @API(status = MAINTAINED) 22 | public static ResponseFilter defaultValue() { 23 | return defaultValues(ResponseFilter.class).stream() 24 | .reduce(replaceBody(BodyReplacers.defaultValue()), ResponseFilter::merge); 25 | } 26 | 27 | public static ResponseFilter replaceBody(final BodyReplacer replacer) { 28 | return response -> { 29 | @Nullable final String replacement = replacer.replace(response); 30 | if (replacement == null) { 31 | return response; 32 | } 33 | return new BodyReplacementHttpResponse(response, replacement); 34 | }; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/SecurityStrategy.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.apiguardian.api.API; 4 | import org.zalando.logbook.Correlation; 5 | import org.zalando.logbook.HttpRequest; 6 | import org.zalando.logbook.HttpResponse; 7 | import org.zalando.logbook.Precorrelation; 8 | import org.zalando.logbook.Sink; 9 | import org.zalando.logbook.Strategy; 10 | 11 | import java.io.IOException; 12 | 13 | import static org.apiguardian.api.API.Status.MAINTAINED; 14 | 15 | /** 16 | * A {@link SecurityStrategy} is a {@link Strategy strategy} which is meant to be used in server-side environments to 17 | * give the best possible compromise between security and observability. 18 | *

19 | * This strategy discards requests bodies. 20 | */ 21 | @API(status = MAINTAINED) 22 | public final class SecurityStrategy implements Strategy { 23 | 24 | @Override 25 | public HttpRequest process(final HttpRequest request) { 26 | return request.withoutBody(); 27 | } 28 | 29 | @Override 30 | public void write(final Precorrelation precorrelation, final HttpRequest request, final Sink sink) { 31 | // defer decision until response is available 32 | } 33 | 34 | @Override 35 | public void write(final Correlation correlation, final HttpRequest request, final HttpResponse response, 36 | final Sink sink) throws IOException { 37 | 38 | final int status = response.getStatus(); 39 | if (status == 401 || status == 403) { 40 | sink.writeBoth(correlation, request, response); 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/SplunkHttpLogFormatter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.zalando.logbook.StructuredHttpLogFormatter; 4 | 5 | import java.util.Map; 6 | import java.util.stream.Collectors; 7 | 8 | public class SplunkHttpLogFormatter implements StructuredHttpLogFormatter { 9 | 10 | @Override 11 | public String format(final Map content) { 12 | return content.entrySet().stream() 13 | .map(entry -> String.format("%s=%s", entry.getKey(), entry.getValue())) 14 | .collect(Collectors.joining(" ")); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/Stages.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.zalando.logbook.HttpResponse; 4 | import org.zalando.logbook.Logbook.RequestWritingStage; 5 | 6 | import static org.zalando.logbook.Logbook.ResponseProcessingStage; 7 | import static org.zalando.logbook.Logbook.ResponseWritingStage; 8 | 9 | final class Stages { 10 | 11 | private Stages() { 12 | 13 | } 14 | 15 | static RequestWritingStage noop() { 16 | return NoopRequestWriting.INSTANCE; 17 | } 18 | 19 | private enum NoopRequestWriting implements RequestWritingStage, Noop { 20 | 21 | INSTANCE; 22 | 23 | @Override 24 | public ResponseProcessingStage write() { 25 | return NoopResponseProcessing.INSTANCE; 26 | } 27 | 28 | } 29 | 30 | private enum NoopResponseProcessing implements Noop { 31 | INSTANCE 32 | } 33 | 34 | private interface Noop extends ResponseProcessingStage { 35 | 36 | @Override 37 | default ResponseWritingStage process(final HttpResponse response) { 38 | return NoopResponseWriting.INSTANCE; 39 | } 40 | 41 | } 42 | 43 | private enum NoopResponseWriting implements ResponseWritingStage { 44 | 45 | INSTANCE; 46 | 47 | @Override 48 | public void write() { 49 | // nothing to do here 50 | } 51 | 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/StatusAtLeastStrategy.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.apiguardian.api.API; 5 | import org.zalando.logbook.Correlation; 6 | import org.zalando.logbook.HttpRequest; 7 | import org.zalando.logbook.HttpResponse; 8 | import org.zalando.logbook.Precorrelation; 9 | import org.zalando.logbook.Sink; 10 | import org.zalando.logbook.Strategy; 11 | 12 | import java.io.IOException; 13 | 14 | import static org.apiguardian.api.API.Status.EXPERIMENTAL; 15 | 16 | @API(status = EXPERIMENTAL) 17 | @AllArgsConstructor 18 | public final class StatusAtLeastStrategy implements Strategy { 19 | 20 | private final int status; 21 | 22 | @Override 23 | public void write(final Precorrelation precorrelation, final HttpRequest request, final Sink sink) { 24 | // defer decision until response is available 25 | } 26 | 27 | @Override 28 | public void write(final Correlation correlation, final HttpRequest request, final HttpResponse response, 29 | final Sink sink) throws IOException { 30 | 31 | if (response.getStatus() >= status) { 32 | sink.writeBoth(correlation, request, response); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/StreamHttpLogWriter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.apiguardian.api.API; 4 | import org.zalando.logbook.Correlation; 5 | import org.zalando.logbook.HttpLogWriter; 6 | import org.zalando.logbook.Precorrelation; 7 | 8 | import java.io.IOException; 9 | import java.io.PrintStream; 10 | 11 | import static org.apiguardian.api.API.Status.STABLE; 12 | 13 | @API(status = STABLE) 14 | public final class StreamHttpLogWriter implements HttpLogWriter { 15 | 16 | private final PrintStream stream; 17 | 18 | public StreamHttpLogWriter() { 19 | this(System.out); 20 | } 21 | 22 | public StreamHttpLogWriter(final PrintStream stream) { 23 | this.stream = stream; 24 | } 25 | 26 | @Override 27 | public void write(final Precorrelation precorrelation, final String request) throws IOException { 28 | stream.println(request); 29 | } 30 | 31 | @Override 32 | public void write(final Correlation correlation, final String response) throws IOException { 33 | stream.println(response); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/WithoutBodyStrategy.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.apiguardian.api.API; 4 | import org.zalando.logbook.HttpRequest; 5 | import org.zalando.logbook.HttpResponse; 6 | import org.zalando.logbook.Strategy; 7 | 8 | import static org.apiguardian.api.API.Status.EXPERIMENTAL; 9 | 10 | @API(status = EXPERIMENTAL) 11 | public final class WithoutBodyStrategy implements Strategy { 12 | 13 | @Override 14 | public HttpRequest process(final HttpRequest request) { 15 | return request.withoutBody(); 16 | } 17 | 18 | @Override 19 | public HttpResponse process(final HttpRequest request, final HttpResponse response) { 20 | return response.withoutBody(); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/attributes/AllAttributesExtractor.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core.attributes; 2 | 3 | import org.apiguardian.api.API; 4 | import org.zalando.logbook.HttpRequest; 5 | import org.zalando.logbook.HttpResponse; 6 | import org.zalando.logbook.attributes.AttributeExtractor; 7 | import org.zalando.logbook.attributes.HttpAttributes; 8 | 9 | import javax.annotation.Nonnull; 10 | import java.util.HashMap; 11 | 12 | import static org.apiguardian.api.API.Status.EXPERIMENTAL; 13 | 14 | @API(status = EXPERIMENTAL) 15 | public final class AllAttributesExtractor implements AttributeExtractor { 16 | @Nonnull 17 | @Override 18 | public HttpAttributes extract(HttpRequest request) { 19 | return request.getAttributes(); 20 | } 21 | 22 | @Nonnull 23 | @Override 24 | public HttpAttributes extract(HttpRequest request, HttpResponse response) { 25 | HttpAttributes requestAttributes = extract(request); 26 | HttpAttributes responseAttributes = response.getAttributes(); 27 | HashMap allAttributes = new HashMap<>(requestAttributes); 28 | allAttributes.putAll(responseAttributes); 29 | 30 | return new HttpAttributes(allAttributes); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /logbook-core/src/main/java/org/zalando/logbook/core/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook.core; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; 5 | -------------------------------------------------------------------------------- /logbook-core/src/main/resources/META-INF/services/org.zalando.logbook.LogbookFactory: -------------------------------------------------------------------------------- 1 | org.zalando.logbook.core.DefaultLogbookFactory 2 | -------------------------------------------------------------------------------- /logbook-core/src/test/java/org/zalando/logbook/core/BodyReplacersTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.zalando.logbook.BodyReplacer; 5 | import org.zalando.logbook.HttpMessage; 6 | 7 | import java.io.IOException; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.mockito.Mockito.mock; 11 | import static org.mockito.Mockito.when; 12 | import static org.zalando.logbook.core.BodyReplacers.replaceBody; 13 | 14 | final class BodyReplacersTest { 15 | 16 | @Test 17 | void shouldReplaceWith() { 18 | final BodyReplacer unit = replaceBody(m -> m.getContentType().startsWith("image/"), ""); 19 | final HttpMessage message = mock(HttpMessage.class); 20 | when(message.getContentType()).thenReturn("image/png"); 21 | 22 | final String body = unit.replace(message); 23 | 24 | assertThat(body).isEqualTo(""); 25 | } 26 | 27 | @Test 28 | void shouldNotReplaceWith() throws IOException { 29 | final BodyReplacer unit = replaceBody(m -> m.getContentType().startsWith("image/"), ""); 30 | final HttpMessage message = mock(HttpMessage.class); 31 | when(message.getContentType()).thenReturn("text/plain"); 32 | when(message.getBodyAsString()).thenReturn("Hello, world!"); 33 | 34 | final String body = unit.replace(message); 35 | 36 | assertThat(body).isNull(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /logbook-core/src/test/java/org/zalando/logbook/core/DefaultLogbookFactoryTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.zalando.logbook.HttpRequest; 5 | import org.zalando.logbook.HttpResponse; 6 | import org.zalando.logbook.Logbook; 7 | import org.zalando.logbook.test.MockHttpRequest; 8 | import org.zalando.logbook.test.MockHttpResponse; 9 | 10 | import java.io.IOException; 11 | 12 | class DefaultLogbookFactoryTest { 13 | 14 | private final Logbook logbook = Logbook.create(); 15 | private final HttpRequest request = MockHttpRequest.create(); 16 | private final HttpResponse response = MockHttpResponse.create(); 17 | 18 | @Test 19 | void shouldCreateWithDefaults() throws IOException { 20 | logbook.process(request).write().process(response).write(); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /logbook-core/src/test/java/org/zalando/logbook/core/SimplePrecorrelationTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.core; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.zalando.logbook.Correlation; 5 | import org.zalando.logbook.Precorrelation; 6 | import org.zalando.logbook.core.DefaultLogbook.SimplePrecorrelation; 7 | 8 | import java.time.Clock; 9 | 10 | import static java.time.Duration.between; 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertNotNull; 13 | 14 | class SimplePrecorrelationTest { 15 | 16 | private final Precorrelation unit = new SimplePrecorrelation( 17 | "", Clock.systemUTC()); 18 | 19 | @Test 20 | void getId() { 21 | assertNotNull(unit.getId()); 22 | } 23 | 24 | @Test 25 | void getStart() { 26 | assertNotNull(unit.getStart()); 27 | } 28 | 29 | @Test 30 | void correlate() { 31 | final Correlation correlation = unit.correlate(); 32 | 33 | assertEquals(unit.getId(), correlation.getId()); 34 | assertEquals(unit.getStart(), correlation.getStart()); 35 | assertNotNull(correlation.getEnd()); 36 | assertEquals(between(correlation.getStart(), correlation.getEnd()), correlation.getDuration()); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /logbook-core/src/test/resources/request.json: -------------------------------------------------------------------------------- 1 | { 2 | "sender": "127.0.0.1", 3 | "method": "GET", 4 | "path": "http://localhost:8080/test", 5 | "params": { 6 | "param1": "1000" 7 | }, 8 | "headers": { 9 | "Accept": "application/json", 10 | "Content-Type": "text/plain" 11 | }, 12 | "body": "Test request body" 13 | } -------------------------------------------------------------------------------- /logbook-core/src/test/resources/response.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": 200, 3 | "headers": { 4 | "Content-Type": "text/plain" 5 | }, 6 | "body": "Test response body" 7 | } -------------------------------------------------------------------------------- /logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/Attributes.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.httpclient; 2 | 3 | import lombok.experimental.UtilityClass; 4 | import org.zalando.logbook.Logbook; 5 | 6 | @UtilityClass 7 | final class Attributes { 8 | 9 | static final String STAGE = Logbook.class.getName() + ".STAGE"; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/ForwardingHttpAsyncResponseConsumer.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.httpclient; 2 | 3 | import lombok.experimental.Delegate; 4 | import org.apache.http.nio.protocol.HttpAsyncResponseConsumer; 5 | 6 | abstract class ForwardingHttpAsyncResponseConsumer implements HttpAsyncResponseConsumer { 7 | 8 | @Delegate 9 | protected abstract HttpAsyncResponseConsumer delegate(); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/HttpEntities.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.httpclient; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.experimental.Delegate; 6 | import lombok.experimental.UtilityClass; 7 | import org.apache.http.HttpEntity; 8 | import org.apache.http.entity.ByteArrayEntity; 9 | 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.IOException; 12 | 13 | 14 | @UtilityClass 15 | final class HttpEntities { 16 | 17 | interface Copy extends HttpEntity { 18 | byte[] getBody(); 19 | } 20 | 21 | Copy copy(final HttpEntity entity) throws IOException { 22 | final ByteArrayOutputStream os = new ByteArrayOutputStream(); 23 | entity.writeTo(os); 24 | final byte[] body = os.toByteArray(); 25 | 26 | final ByteArrayEntity copy = new ByteArrayEntity(body); 27 | copy.setChunked(entity.isChunked()); 28 | copy.setContentEncoding(entity.getContentEncoding()); 29 | copy.setContentType(entity.getContentType()); 30 | 31 | return new DefaultCopy(copy, body); 32 | } 33 | 34 | @RequiredArgsConstructor 35 | private static final class DefaultCopy implements Copy { 36 | 37 | @Delegate 38 | private final HttpEntity entity; 39 | 40 | @Getter 41 | private final byte[] body; 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/LogbookHttpRequestInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.httpclient; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.apache.http.HttpRequest; 5 | import org.apache.http.HttpRequestInterceptor; 6 | import org.apache.http.protocol.HttpContext; 7 | import org.apiguardian.api.API; 8 | import org.zalando.logbook.Logbook; 9 | import org.zalando.logbook.Logbook.ResponseProcessingStage; 10 | 11 | import java.io.IOException; 12 | 13 | import static org.apiguardian.api.API.Status.STABLE; 14 | 15 | @API(status = STABLE) 16 | @Slf4j 17 | public final class LogbookHttpRequestInterceptor implements HttpRequestInterceptor { 18 | 19 | private final Logbook logbook; 20 | 21 | public LogbookHttpRequestInterceptor(final Logbook logbook) { 22 | this.logbook = logbook; 23 | } 24 | 25 | @Override 26 | public void process(final HttpRequest httpRequest, final HttpContext context) throws IOException { 27 | try { 28 | final LocalRequest request = new LocalRequest(httpRequest); 29 | final ResponseProcessingStage stage = logbook.process(request).write(); 30 | context.setAttribute(Attributes.STAGE, stage); 31 | } catch (Exception e) { 32 | log.warn("Unable to log request. Will skip the request & response logging step.", e); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook.httpclient; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; -------------------------------------------------------------------------------- /logbook-httpclient5/src/main/java/org/zalando/logbook/httpclient5/Attributes.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.httpclient5; 2 | 3 | import lombok.experimental.UtilityClass; 4 | import org.zalando.logbook.Logbook; 5 | 6 | @UtilityClass 7 | final class Attributes { 8 | static final String STAGE = Logbook.class.getName() + ".STAGE"; 9 | } 10 | -------------------------------------------------------------------------------- /logbook-httpclient5/src/main/java/org/zalando/logbook/httpclient5/BufferingFixedSizeDataStreamChannel.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.httpclient5; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.apache.hc.core5.http.Header; 5 | import org.apache.hc.core5.http.nio.DataStreamChannel; 6 | 7 | import java.nio.ByteBuffer; 8 | import java.util.List; 9 | 10 | import static lombok.AccessLevel.PACKAGE; 11 | 12 | @RequiredArgsConstructor(access = PACKAGE) 13 | final class BufferingFixedSizeDataStreamChannel implements DataStreamChannel { 14 | private final byte[] buffer; 15 | 16 | public byte[] getBuffer() { 17 | return buffer; 18 | } 19 | 20 | @Override 21 | public void requestOutput() { 22 | } 23 | 24 | @Override 25 | public int write(ByteBuffer src) { 26 | ByteBufferUtils.fixedSizeCopy(src, buffer); 27 | return 0; 28 | } 29 | 30 | @Override 31 | public void endStream() { 32 | } 33 | 34 | @Override 35 | public void endStream(List trailers) { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /logbook-httpclient5/src/main/java/org/zalando/logbook/httpclient5/ByteBufferUtils.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.httpclient5; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | import java.nio.ByteBuffer; 6 | 7 | @UtilityClass 8 | class ByteBufferUtils { 9 | 10 | static void fixedSizeCopy(ByteBuffer src, byte[] dest) { 11 | if (src.hasArray()) { 12 | byte[] array = src.array(); 13 | System.arraycopy(array, 0, dest, 0, dest.length); 14 | } else { 15 | src.get(dest, 0, dest.length); 16 | src.flip(); 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /logbook-httpclient5/src/main/java/org/zalando/logbook/httpclient5/ForwardingHttpAsyncResponseConsumer.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.httpclient5; 2 | 3 | import lombok.experimental.Delegate; 4 | import org.apache.hc.core5.http.nio.AsyncResponseConsumer; 5 | 6 | abstract class ForwardingHttpAsyncResponseConsumer implements AsyncResponseConsumer { 7 | 8 | @Delegate 9 | protected abstract AsyncResponseConsumer delegate(); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /logbook-httpclient5/src/main/java/org/zalando/logbook/httpclient5/HttpEntities.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.httpclient5; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.experimental.Delegate; 6 | import lombok.experimental.UtilityClass; 7 | import org.apache.hc.core5.http.ContentType; 8 | import org.apache.hc.core5.http.HttpEntity; 9 | import org.apache.hc.core5.http.io.entity.ByteArrayEntity; 10 | 11 | import java.io.ByteArrayOutputStream; 12 | import java.io.IOException; 13 | 14 | 15 | @UtilityClass 16 | final class HttpEntities { 17 | 18 | interface Copy extends HttpEntity { 19 | byte[] getBody(); 20 | } 21 | 22 | Copy copy(final HttpEntity entity) throws IOException { 23 | final ByteArrayOutputStream os = new ByteArrayOutputStream(); 24 | entity.writeTo(os); 25 | final byte[] body = os.toByteArray(); 26 | ContentType contentType = ContentType.parse(entity.getContentType()); 27 | boolean chunked = entity.isChunked(); 28 | String contentEncoding = entity.getContentEncoding(); 29 | 30 | final ByteArrayEntity copy = new ByteArrayEntity(body, contentType, contentEncoding, chunked); 31 | 32 | return new DefaultCopy(copy, body); 33 | } 34 | 35 | @RequiredArgsConstructor 36 | private static final class DefaultCopy implements Copy { 37 | 38 | @Delegate 39 | private final HttpEntity entity; 40 | 41 | @Getter 42 | private final byte[] body; 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /logbook-httpclient5/src/main/java/org/zalando/logbook/httpclient5/LogbookHttpRequestInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.httpclient5; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.apache.hc.core5.http.EntityDetails; 5 | import org.apache.hc.core5.http.HttpException; 6 | import org.apache.hc.core5.http.HttpRequest; 7 | import org.apache.hc.core5.http.HttpRequestInterceptor; 8 | import org.apache.hc.core5.http.protocol.HttpContext; 9 | import org.apiguardian.api.API; 10 | import org.zalando.logbook.Logbook; 11 | import org.zalando.logbook.Logbook.ResponseProcessingStage; 12 | 13 | import java.io.IOException; 14 | 15 | import static org.apiguardian.api.API.Status.EXPERIMENTAL; 16 | 17 | @API(status = EXPERIMENTAL) 18 | @Slf4j 19 | public final class LogbookHttpRequestInterceptor implements HttpRequestInterceptor { 20 | 21 | private final Logbook logbook; 22 | 23 | public LogbookHttpRequestInterceptor(final Logbook logbook) { 24 | this.logbook = logbook; 25 | } 26 | 27 | @Override 28 | public void process(HttpRequest httpRequest, EntityDetails entity, HttpContext context) throws HttpException, IOException { 29 | try { 30 | LocalRequest request = new LocalRequest(httpRequest, entity); 31 | final ResponseProcessingStage stage = logbook.process(request).write(); 32 | context.setAttribute(Attributes.STAGE, stage); 33 | } catch (Exception e) { 34 | log.warn("Unable to log request. Will skip the request & response logging step.", e); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /logbook-httpclient5/src/main/java/org/zalando/logbook/httpclient5/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook.httpclient5; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; -------------------------------------------------------------------------------- /logbook-httpclient5/src/test/java/org/zalando/logbook/httpclient5/BufferingFixedSizeDataStreamChannelTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.httpclient5; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.nio.ByteBuffer; 8 | 9 | import static java.nio.charset.StandardCharsets.UTF_8; 10 | import static java.util.Collections.emptyList; 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | final class BufferingFixedSizeDataStreamChannelTest { 14 | 15 | private ByteBuffer buffer; 16 | private BufferingFixedSizeDataStreamChannel channel; 17 | private final byte[] result = "b".getBytes(UTF_8); 18 | private final byte[] data = "a".getBytes(UTF_8); 19 | 20 | @BeforeEach 21 | void setUp() { 22 | channel = new BufferingFixedSizeDataStreamChannel(result); 23 | } 24 | 25 | @AfterEach 26 | void tearDown() { 27 | if (buffer != null) buffer.clear(); 28 | } 29 | 30 | @Test 31 | void testHeapByteBuffer() { 32 | buffer = ByteBuffer.wrap(data); 33 | channel.write(buffer); 34 | assertThat(result).isEqualTo(data); 35 | } 36 | 37 | @Test 38 | void testOffHeapByteBuffer() { 39 | buffer = ByteBuffer.allocateDirect(data.length); 40 | buffer.put(data); 41 | buffer.flip(); 42 | channel.write(buffer); 43 | assertThat(result).isEqualTo(data); 44 | } 45 | 46 | @Test 47 | void dummyUnitTest() { 48 | channel.requestOutput(); 49 | channel.endStream(); 50 | channel.endStream(emptyList()); 51 | } 52 | } -------------------------------------------------------------------------------- /logbook-jaxrs/src/main/java/org/zalando/logbook/jaxrs/ByteStreams.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.jaxrs; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.util.Objects; 8 | 9 | final class ByteStreams { 10 | 11 | private ByteStreams() { 12 | 13 | } 14 | 15 | static byte[] toByteArray(final InputStream in) throws IOException { 16 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 17 | copy(in, out); 18 | return out.toByteArray(); 19 | } 20 | 21 | static void copy(final InputStream from, final OutputStream to) throws IOException { 22 | Objects.requireNonNull(from); 23 | Objects.requireNonNull(to); 24 | final byte[] buf = new byte[4096]; 25 | int bytesRead; 26 | 27 | while ((bytesRead = from.read(buf)) != -1) { 28 | to.write(buf, 0, bytesRead); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /logbook-jaxrs/src/main/java/org/zalando/logbook/jaxrs/HttpMessages.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.jaxrs; 2 | 3 | import jakarta.ws.rs.core.MediaType; 4 | 5 | import javax.annotation.Nullable; 6 | import java.net.URI; 7 | import java.nio.charset.Charset; 8 | import java.util.Optional; 9 | 10 | import static jakarta.ws.rs.core.MediaType.CHARSET_PARAMETER; 11 | import static java.nio.charset.StandardCharsets.UTF_8; 12 | import static java.util.Optional.ofNullable; 13 | 14 | final class HttpMessages { 15 | 16 | private HttpMessages() { 17 | } 18 | 19 | static Charset getCharset(@Nullable final MediaType mediaType) { 20 | return ofNullable(mediaType) 21 | .map(MediaType::getParameters) 22 | .map(parameters -> parameters.get(CHARSET_PARAMETER)) 23 | .map(Charset::forName) 24 | .orElse(UTF_8); 25 | } 26 | 27 | static Optional getPort(final URI uri) { 28 | final int port = uri.getPort(); 29 | return port == -1 ? Optional.empty() : Optional.of(port); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /logbook-jaxrs/src/main/java/org/zalando/logbook/jaxrs/TeeOutputStream.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.jaxrs; 2 | 3 | import javax.annotation.Nullable; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.IOException; 6 | import java.io.OutputStream; 7 | 8 | /** 9 | * Copies any bytes written to a stream in an internal buffer for later retrieval. 10 | */ 11 | final class TeeOutputStream extends OutputStream { 12 | 13 | private final OutputStream original; 14 | private final ByteArrayOutputStream copy = new ByteArrayOutputStream(); 15 | 16 | TeeOutputStream(@Nullable final OutputStream original) { 17 | this.original = original; 18 | } 19 | 20 | @Override 21 | public void write(final int b) throws IOException { 22 | if (original != null) 23 | original.write(b); 24 | copy.write(b); 25 | } 26 | 27 | @Override 28 | public void write(final byte[] b, final int off, final int len) throws IOException { 29 | if (original != null) 30 | original.write(b, off, len); 31 | copy.write(b, off, len); 32 | } 33 | 34 | @Override 35 | public void flush() throws IOException { 36 | if (original != null) 37 | original.flush(); 38 | copy.flush(); 39 | } 40 | 41 | @Override 42 | public void close() throws IOException { 43 | if (original != null) 44 | original.close(); 45 | copy.close(); 46 | } 47 | 48 | byte[] toByteArray() { 49 | return copy.toByteArray(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /logbook-jaxrs/src/main/java/org/zalando/logbook/jaxrs/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook.jaxrs; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; 5 | -------------------------------------------------------------------------------- /logbook-jaxrs/src/test/java/org/zalando/logbook/jaxrs/HttpMessagesTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.jaxrs; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.net.URI; 6 | import java.util.Optional; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.zalando.logbook.jaxrs.HttpMessages.getPort; 10 | 11 | class HttpMessagesTest { 12 | 13 | @Test 14 | void shouldReturnPort() throws Exception { 15 | final URI uri = new URI("http://localhost:99999"); 16 | assertEquals(Optional.of(99999), getPort(uri)); 17 | } 18 | 19 | @Test 20 | void shouldReturnEmptyForAbsentPort() throws Exception { 21 | final URI uri = new URI("http://localhost"); 22 | assertEquals(Optional.empty(), getPort(uri)); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /logbook-jaxrs/src/test/java/org/zalando/logbook/jaxrs/LogbookClientFilterTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.jaxrs; 2 | 3 | import jakarta.ws.rs.client.ClientRequestContext; 4 | import jakarta.ws.rs.client.ClientResponseContext; 5 | import org.junit.jupiter.api.Test; 6 | import org.zalando.logbook.Logbook; 7 | 8 | import static org.mockito.Mockito.mock; 9 | import static org.mockito.Mockito.verifyNoInteractions; 10 | 11 | final class LogbookClientFilterTest { 12 | 13 | private final ClientRequestContext request = mock(ClientRequestContext.class); 14 | private final ClientResponseContext response = mock(ClientResponseContext.class); 15 | private final Logbook logbook = mock(Logbook.class); 16 | 17 | private final LogbookClientFilter unit = new LogbookClientFilter(logbook); 18 | 19 | @Test 20 | void filterShouldDoNothingIfStageIsNotPresent() { 21 | unit.filter(request, response); 22 | verifyNoInteractions(logbook); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /logbook-jaxrs/src/test/java/org/zalando/logbook/jaxrs/LogbookServerFilterTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.jaxrs; 2 | 3 | import jakarta.ws.rs.container.ContainerRequestContext; 4 | import jakarta.ws.rs.container.ContainerResponseContext; 5 | import jakarta.ws.rs.container.ContainerResponseFilter; 6 | import org.junit.jupiter.api.Test; 7 | import org.zalando.logbook.Logbook; 8 | 9 | import java.io.IOException; 10 | 11 | import static org.mockito.Mockito.mock; 12 | import static org.mockito.Mockito.verifyNoInteractions; 13 | 14 | final class LogbookServerFilterTest { 15 | 16 | private final ContainerRequestContext request = mock(ContainerRequestContext.class); 17 | private final ContainerResponseContext response = mock(ContainerResponseContext.class); 18 | private final Logbook logbook = mock(Logbook.class); 19 | 20 | private final ContainerResponseFilter unit = new LogbookServerFilter(logbook); 21 | 22 | @Test 23 | void filterShouldDoNothingIfCorrelatorIsNotPresent() throws IOException { 24 | unit.filter(request, response); 25 | verifyNoInteractions(logbook); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /logbook-jaxrs/src/test/java/org/zalando/logbook/jaxrs/RemoteResponseTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.jaxrs; 2 | 3 | import jakarta.ws.rs.client.ClientResponseContext; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.concurrent.atomic.AtomicBoolean; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.when; 15 | 16 | class RemoteResponseTest { 17 | 18 | @Test 19 | void shouldCloseOriginalResponseContext() { 20 | ClientResponseContext context = mock(ClientResponseContext.class); 21 | final AtomicBoolean isStreamClosed = new AtomicBoolean(); 22 | InputStream entityStream = new ByteArrayInputStream("response-body".getBytes(StandardCharsets.UTF_8)) { 23 | @Override 24 | public void close() throws IOException { 25 | super.close(); 26 | isStreamClosed.getAndSet(true); 27 | } 28 | }; 29 | when(context.getEntityStream()).thenReturn(entityStream); 30 | 31 | RemoteResponse remoteResponse = new RemoteResponse(context); 32 | ((RemoteResponse) remoteResponse.withBody()).expose(); 33 | 34 | assertTrue(isStreamClosed.get()); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /logbook-jaxrs/src/test/java/org/zalando/logbook/jaxrs/RoundTrip.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.jaxrs; 2 | 3 | import lombok.Value; 4 | import org.zalando.logbook.HttpRequest; 5 | import org.zalando.logbook.HttpResponse; 6 | 7 | @Value 8 | class RoundTrip { 9 | HttpRequest clientRequest; 10 | HttpResponse clientResponse; 11 | HttpRequest serverRequest; 12 | HttpResponse serverResponse; 13 | } 14 | -------------------------------------------------------------------------------- /logbook-jaxrs/src/test/java/org/zalando/logbook/jaxrs/testing/support/TestModel.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.jaxrs.testing.support; 2 | 3 | public class TestModel { 4 | 5 | private String property1; 6 | private String property2; 7 | 8 | public String getProperty1() { 9 | return property1; 10 | } 11 | 12 | public TestModel setProperty1(final String property1) { 13 | this.property1 = property1; 14 | return this; 15 | } 16 | 17 | public String getProperty2() { 18 | return property2; 19 | } 20 | 21 | public TestModel setProperty2(final String property2) { 22 | this.property2 = property2; 23 | return this; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /logbook-jdkserver/src/main/java/org/zalando/logbook/jdkserver/ByteStreams.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.jdkserver; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.util.Objects; 8 | 9 | final class ByteStreams { 10 | 11 | private ByteStreams() { 12 | 13 | } 14 | 15 | static byte[] toByteArray(final InputStream in) throws IOException { 16 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 17 | copy(in, out); 18 | return out.toByteArray(); 19 | } 20 | 21 | static void copy(final InputStream from, final OutputStream to) throws IOException { 22 | Objects.requireNonNull(from); 23 | Objects.requireNonNull(to); 24 | final byte[] buf = new byte[4096]; 25 | int bytesRead; 26 | 27 | while ((bytesRead = from.read(buf)) != -1) { 28 | to.write(buf, 0, bytesRead); 29 | } 30 | 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /logbook-jdkserver/src/main/java/org/zalando/logbook/jdkserver/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook.jdkserver; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; -------------------------------------------------------------------------------- /logbook-jdkserver/src/test/java/org/zalando/logbook/jdkserver/ByteStreamsTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.jdkserver; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.IOException; 8 | 9 | import static java.nio.charset.StandardCharsets.UTF_8; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | final class ByteStreamsTest { 13 | 14 | @Test 15 | void shouldCollectStreamToByteArray() throws IOException { 16 | final byte[] bytes = ByteStreams.toByteArray(new ByteArrayInputStream("Hello World!".getBytes(UTF_8))); 17 | 18 | assertThat(new String(bytes, UTF_8)).isEqualTo("Hello World!"); 19 | } 20 | 21 | @Test 22 | void shouldCopyStreams() throws IOException { 23 | final ByteArrayOutputStream to = new ByteArrayOutputStream(); 24 | ByteStreams.copy(new ByteArrayInputStream("Hello World!".getBytes(UTF_8)), to); 25 | 26 | assertThat(new String(to.toByteArray(), UTF_8)).isEqualTo("Hello World!"); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /logbook-jmh/src/main/java/org/zalando/logbook/benchmark/HttpLogFormatterState.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.benchmark; 2 | 3 | import org.openjdk.jmh.annotations.Scope; 4 | import org.openjdk.jmh.annotations.State; 5 | import org.zalando.logbook.HttpLogFormatter; 6 | import org.zalando.logbook.core.DefaultHttpLogFormatter; 7 | import org.zalando.logbook.json.FastJsonHttpLogFormatter; 8 | import org.zalando.logbook.json.JsonHttpLogFormatter; 9 | 10 | @State(Scope.Benchmark) 11 | public class HttpLogFormatterState { 12 | 13 | private NoopHttpLogFormatter noopHttpLogFormatter = new NoopHttpLogFormatter(); 14 | 15 | private JsonHttpLogFormatter jsonHttpLogFormatter = new JsonHttpLogFormatter(); 16 | private FastJsonHttpLogFormatter fastJsonHttpLogFormatter = new FastJsonHttpLogFormatter(); 17 | private HttpLogFormatter defaultHttpLogFormatter = new DefaultHttpLogFormatter(); 18 | 19 | public JsonHttpLogFormatter getJsonHttpLogFormatter() { 20 | return jsonHttpLogFormatter; 21 | } 22 | 23 | public FastJsonHttpLogFormatter getFastJsonHttpLogFormatter() { 24 | return fastJsonHttpLogFormatter; 25 | } 26 | 27 | public HttpLogFormatter getDefaultHttpLogFormatter() { 28 | return defaultHttpLogFormatter; 29 | } 30 | 31 | public NoopHttpLogFormatter getNoopHttpLogFormatter() { 32 | return noopHttpLogFormatter; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /logbook-jmh/src/main/java/org/zalando/logbook/benchmark/NoopHttpLogFormatter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.benchmark; 2 | 3 | import org.zalando.logbook.Correlation; 4 | import org.zalando.logbook.HttpLogFormatter; 5 | import org.zalando.logbook.HttpRequest; 6 | import org.zalando.logbook.HttpResponse; 7 | import org.zalando.logbook.Precorrelation; 8 | 9 | import java.io.IOException; 10 | 11 | public class NoopHttpLogFormatter implements HttpLogFormatter { 12 | 13 | private static final String EMPTY = "{}"; 14 | 15 | @Override 16 | public String format(final Precorrelation precorrelation, final HttpRequest request) throws IOException { 17 | return EMPTY; 18 | } 19 | 20 | @Override 21 | public String format(final Correlation correlation, final HttpResponse response) throws IOException { 22 | return EMPTY; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /logbook-jmh/src/main/java/org/zalando/logbook/benchmark/jmh/DefaultCorrelation.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.benchmark.jmh; 2 | 3 | import org.zalando.logbook.Correlation; 4 | 5 | import java.time.Duration; 6 | import java.time.Instant; 7 | 8 | public class DefaultCorrelation implements Correlation { 9 | 10 | private String id; 11 | private Instant start; 12 | private Instant end; 13 | private Duration duration; 14 | 15 | public DefaultCorrelation(final String id, final Instant start, final Instant end, final Duration duration) { 16 | super(); 17 | this.id = id; 18 | this.start = start; 19 | this.end = end; 20 | this.duration = duration; 21 | } 22 | 23 | @Override 24 | public String getId() { 25 | return id; 26 | } 27 | 28 | @Override 29 | public Instant getStart() { 30 | return start; 31 | } 32 | 33 | @Override 34 | public Instant getEnd() { 35 | return end; 36 | } 37 | 38 | @Override 39 | public Duration getDuration() { 40 | return duration; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /logbook-jmh/src/main/java/org/zalando/logbook/benchmark/jmh/DefaultPrecorrelation.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.benchmark.jmh; 2 | 3 | import org.zalando.logbook.Correlation; 4 | import org.zalando.logbook.Precorrelation; 5 | 6 | import java.time.Instant; 7 | 8 | public class DefaultPrecorrelation implements Precorrelation { 9 | 10 | private String id; 11 | private Correlation correlation; 12 | 13 | public DefaultPrecorrelation(final String id, final Correlation correlation) { 14 | super(); 15 | this.id = id; 16 | this.correlation = correlation; 17 | } 18 | 19 | @Override 20 | public String getId() { 21 | return id; 22 | } 23 | 24 | @Override 25 | public Instant getStart() { 26 | return correlation.getStart(); 27 | } 28 | 29 | @Override 30 | public Correlation correlate() { 31 | return correlation; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /logbook-jmh/src/main/java/org/zalando/logbook/benchmark/jmh/NOPAppender.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.benchmark.jmh; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import ch.qos.logback.core.AppenderBase; 5 | 6 | final public class NOPAppender extends AppenderBase { 7 | 8 | @Override 9 | protected void append(final ILoggingEvent eventObject) { 10 | // make sure the event call cannot be discarded by the JIT compiler 11 | if (eventObject == null || eventObject.getMessage() == null) { 12 | throw new IllegalArgumentException(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /logbook-jmh/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /logbook-jmh/src/test/java/org/zalando/logbook/benchmark/jmh/TestDefaultCorrelation.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.benchmark.jmh; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | public class TestDefaultCorrelation { 8 | 9 | @Test 10 | public void testGetters() { 11 | final DefaultPrecorrelation precorrelation = new DefaultPrecorrelation("id", null); 12 | assertThat(precorrelation.getId()).isEqualTo("id"); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /logbook-json/src/main/java/org/zalando/logbook/json/AccessTokenBodyFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.json; 2 | 3 | import lombok.experimental.Delegate; 4 | import org.apiguardian.api.API; 5 | import org.zalando.logbook.BodyFilter; 6 | 7 | import static org.apiguardian.api.API.Status.INTERNAL; 8 | import static org.zalando.logbook.json.JsonBodyFilters.accessToken; 9 | 10 | @API(status = INTERNAL) 11 | public final class AccessTokenBodyFilter implements BodyFilter { 12 | 13 | @Delegate 14 | private final BodyFilter delegate = accessToken(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /logbook-json/src/main/java/org/zalando/logbook/json/CompactingJsonBodyFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.json; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apiguardian.api.API; 6 | import org.zalando.logbook.BodyFilter; 7 | import org.zalando.logbook.ContentType; 8 | 9 | import javax.annotation.Nullable; 10 | import java.io.IOException; 11 | 12 | import static org.apiguardian.api.API.Status.MAINTAINED; 13 | 14 | /** 15 | * @see FastCompactingJsonBodyFilter 16 | */ 17 | @API(status = MAINTAINED) 18 | @Slf4j 19 | @AllArgsConstructor 20 | public final class CompactingJsonBodyFilter implements BodyFilter { 21 | 22 | private final JsonCompactor compactor; 23 | 24 | public CompactingJsonBodyFilter(final JsonGeneratorWrapper jsonGeneratorWrapper) { 25 | this(new ParsingJsonCompactor(jsonGeneratorWrapper)); 26 | } 27 | 28 | public CompactingJsonBodyFilter() { 29 | this(new ParsingJsonCompactor()); 30 | } 31 | 32 | @Override 33 | public String filter(@Nullable final String contentType, final String body) { 34 | if (!ContentType.isJsonMediaType(contentType)) { 35 | return body; 36 | } 37 | 38 | try { 39 | return compactor.compact(body); 40 | } catch (final IOException e) { 41 | log.trace("Unable to compact body, is it a JSON?. Keep it as-is: `{}`", e.getMessage()); 42 | return body; 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /logbook-json/src/main/java/org/zalando/logbook/json/DefaultJsonGeneratorWrapper.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.json; 2 | 3 | 4 | final class DefaultJsonGeneratorWrapper implements JsonGeneratorWrapper { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /logbook-json/src/main/java/org/zalando/logbook/json/FastCompactingJsonBodyFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.json; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.apiguardian.api.API; 5 | import org.zalando.logbook.BodyFilter; 6 | import org.zalando.logbook.ContentType; 7 | 8 | import javax.annotation.Nullable; 9 | 10 | import static org.apiguardian.api.API.Status.EXPERIMENTAL; 11 | 12 | /** 13 | * Fast alternative to {@link CompactingJsonBodyFilter} which just replaces all new lines, rather than 14 | * parsing and rewriting the JSON body. 15 | * 16 | * @see CompactingJsonBodyFilter 17 | */ 18 | @API(status = EXPERIMENTAL) 19 | @Slf4j 20 | public final class FastCompactingJsonBodyFilter implements BodyFilter { 21 | 22 | private final StringReplaceJsonCompactor compactor = new StringReplaceJsonCompactor(); 23 | 24 | @Override 25 | public String filter(@Nullable final String contentType, final String body) { 26 | if (!ContentType.isJsonMediaType(contentType)) { 27 | return body; 28 | } 29 | 30 | return compactor.compact(body); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /logbook-json/src/main/java/org/zalando/logbook/json/JsonCompactor.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.json; 2 | 3 | import java.io.IOException; 4 | 5 | interface JsonCompactor { 6 | 7 | String compact(String json) throws IOException; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /logbook-json/src/main/java/org/zalando/logbook/json/JsonGeneratorWrapper.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.json; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.core.JsonParser; 5 | 6 | import java.io.IOException; 7 | 8 | public interface JsonGeneratorWrapper { 9 | 10 | default void copyCurrentEvent(final JsonGenerator delegate, final JsonParser parser) throws IOException { 11 | delegate.copyCurrentEvent(parser); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /logbook-json/src/main/java/org/zalando/logbook/json/NumberAsStringJsonGeneratorWrapper.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.json; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.core.JsonParser; 5 | import com.fasterxml.jackson.core.JsonToken; 6 | 7 | import java.io.IOException; 8 | 9 | final class NumberAsStringJsonGeneratorWrapper implements JsonGeneratorWrapper { 10 | 11 | public void copyCurrentEvent(JsonGenerator delegate, JsonParser parser) throws IOException { 12 | if (parser.getCurrentToken() == JsonToken.VALUE_NUMBER_FLOAT) { 13 | delegate.writeString(parser.getValueAsString()); 14 | } else { 15 | delegate.copyCurrentEvent(parser); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /logbook-json/src/main/java/org/zalando/logbook/json/PreciseFloatJsonGeneratorWrapper.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.json; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.core.JsonParser; 5 | 6 | import java.io.IOException; 7 | 8 | final class PreciseFloatJsonGeneratorWrapper implements JsonGeneratorWrapper { 9 | 10 | @Override 11 | public void copyCurrentEvent(JsonGenerator delegate, JsonParser parser) throws IOException { 12 | delegate.copyCurrentEventExact(parser); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /logbook-json/src/main/java/org/zalando/logbook/json/StringReplaceJsonCompactor.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.json; 2 | 3 | final class StringReplaceJsonCompactor implements JsonCompactor { 4 | 5 | @Override 6 | public String compact(final String json) { 7 | return json.replace("\n", ""); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /logbook-json/src/main/java/org/zalando/logbook/json/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook.json; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; 5 | -------------------------------------------------------------------------------- /logbook-json/src/main/resources/META-INF/services/org.zalando.logbook.BodyFilter: -------------------------------------------------------------------------------- 1 | org.zalando.logbook.json.CompactingJsonBodyFilter 2 | org.zalando.logbook.json.AccessTokenBodyFilter 3 | -------------------------------------------------------------------------------- /logbook-json/src/test/java/org/zalando/logbook/json/DefaultBodyFilterTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.json; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.zalando.logbook.BodyFilter; 5 | 6 | import static java.util.ServiceLoader.load; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | import static org.assertj.core.data.Index.atIndex; 9 | import static org.assertj.core.util.Lists.newArrayList; 10 | 11 | final class DefaultBodyFilterTest { 12 | 13 | @Test 14 | void shouldDeclareCompactingJsonBodyFilterByDefault() { 15 | assertThat(newArrayList(load(BodyFilter.class))) 16 | .satisfies(filter -> 17 | assertThat(filter).isInstanceOf(CompactingJsonBodyFilter.class), atIndex(0)) 18 | .satisfies(filter -> 19 | assertThat(filter).isInstanceOf(AccessTokenBodyFilter.class), atIndex(1)); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /logbook-json/src/test/resources/cars-array.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"John", 3 | "age":30, 4 | "cars": [ 5 | {"car1":"Ford"}, 6 | {"car2":"BMW"}, 7 | {"car3":"Fiat"} 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /logbook-json/src/test/resources/cars-object.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"John", 3 | "age":30, 4 | "cars": { 5 | "car1":"Ford", 6 | "car2":"BMW", 7 | "car3":"Fiat" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /logbook-json/src/test/resources/cars-unwrapped-array.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | {"id":"car1", "name":"Ford"}, 4 | {"id":"car2", "name":"BMW"}, 5 | {"id":"car3", "name":"Fiat"} 6 | ] -------------------------------------------------------------------------------- /logbook-json/src/test/resources/student.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "name": "Alice", 4 | "password": "s3cr3t", 5 | "active": true, 6 | "address": "Anhalter Straße 17 13, 67278 Bockenheim an der Weinstraße", 7 | "friends": [ 8 | { 9 | "id": 2, 10 | "name": "Bob" 11 | }, 12 | { 13 | "id": 3, 14 | "name": "Charlie" 15 | } 16 | ], 17 | "grades": { 18 | "Math": 1.0, 19 | "English": 2.2, 20 | "Science": 1.9, 21 | "PE": 4.0 22 | }, 23 | "nickname": null, 24 | "debt": 123450.40000000000000002, 25 | "balance": 0.40000000000000002 26 | } 27 | -------------------------------------------------------------------------------- /logbook-json/src/test/resources/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "customerNumber": 1234567, 4 | "profileType": "S", 5 | "status": "A", 6 | "firstName": "My", 7 | "surname": "Name", 8 | "email": "someone@zalando.org", 9 | "languagePreference": "NO", 10 | "uuid": "658DE1D4BB4F3AC4E053020011AC1EC3" 11 | } -------------------------------------------------------------------------------- /logbook-ktor-client/src/test/kotlin/org/zalando/logbook/client/ClientResponseUnitTest.kt: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.client 2 | 3 | import io.ktor.client.statement.HttpResponse 4 | import io.ktor.http.headersOf 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.junit.jupiter.api.Test 7 | import org.mockito.Mockito.mock 8 | import org.mockito.Mockito.`when` 9 | import kotlin.text.Charsets.UTF_8 10 | 11 | internal class ClientResponseUnitTest { 12 | 13 | @Test 14 | fun `ClientResponse unit test`() { 15 | val resp = mock(HttpResponse::class.java) 16 | val response = ClientResponse(resp) 17 | `when`(resp.headers).thenReturn(headersOf()) 18 | assertThat(response.contentType).isNull() 19 | assertThat(response.charset).isEqualTo(UTF_8) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /logbook-ktor-common/src/main/kotlin/org/zalando/logbook/common/ContentUtils.kt: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.common 2 | 3 | import io.ktor.http.content.OutgoingContent 4 | import io.ktor.utils.io.ByteReadChannel 5 | import io.ktor.utils.io.toByteArray 6 | import io.ktor.utils.io.writer 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlin.coroutines.coroutineContext 9 | 10 | @JvmField 11 | internal val EMPTY_BODY = ByteArray(0) 12 | 13 | suspend fun OutgoingContent.readBytes(scope: CoroutineScope): ByteArray = runCatching { 14 | when (this) { 15 | is OutgoingContent.NoContent -> EMPTY_BODY 16 | is OutgoingContent.ProtocolUpgrade -> EMPTY_BODY 17 | is OutgoingContent.ByteArrayContent -> bytes() 18 | is OutgoingContent.ReadChannelContent -> readFrom().readBytes() 19 | is OutgoingContent.WriteChannelContent -> scope.writer(coroutineContext) { writeTo(channel) }.channel.readBytes() 20 | is OutgoingContent.ContentWrapper -> delegate().readBytes(scope) 21 | } 22 | }.getOrElse { 23 | EMPTY_BODY 24 | } 25 | 26 | suspend fun ByteReadChannel.readBytes(): ByteArray = runCatching { 27 | toByteArray() 28 | }.getOrElse { 29 | EMPTY_BODY 30 | } 31 | -------------------------------------------------------------------------------- /logbook-ktor-common/src/main/kotlin/org/zalando/logbook/common/State.kt: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.common 2 | 3 | sealed class State { 4 | var body: ByteArray = EMPTY_BODY 5 | open fun with(): State = this 6 | open fun without(): State = this 7 | open fun buffer(content: ByteArray): State = this 8 | 9 | class Buffering : State() { 10 | override fun without(): State = Ignoring(this) 11 | override fun buffer(content: ByteArray): State = apply { body = content } 12 | } 13 | 14 | class Unbuffered : State() { 15 | override fun with(): State = Offering() 16 | } 17 | 18 | class Offering : State() { 19 | override fun without(): State = Unbuffered() 20 | override fun buffer(content: ByteArray): State = Buffering().buffer(content) 21 | } 22 | 23 | class Ignoring(private val delegate: Buffering) : State() { 24 | override fun with(): State = delegate 25 | override fun buffer(content: ByteArray): State = apply { delegate.buffer(content) } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /logbook-ktor-common/src/test/kotlin/org/zalando/logbook/ktor/common/StateUnitTest.kt: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.ktor.common 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.api.Test 5 | import org.zalando.logbook.common.EMPTY_BODY 6 | import org.zalando.logbook.common.State 7 | import java.util.concurrent.atomic.AtomicReference 8 | import kotlin.text.Charsets.UTF_8 9 | 10 | internal class StateUnitTest { 11 | 12 | @Test 13 | fun `Should keep buffering when ignoring`() { 14 | val state: AtomicReference = AtomicReference(State.Offering()) 15 | state.updateAndGet { it.without() } 16 | state.updateAndGet { it.with() } 17 | state.updateAndGet { it.buffer(EMPTY_BODY) } 18 | state.updateAndGet { it.without() } 19 | state.updateAndGet { it.buffer("foo".toByteArray()) } 20 | state.updateAndGet { it.with() } 21 | state.updateAndGet { it.with() } 22 | state.updateAndGet { it.without() } 23 | state.updateAndGet { it.with() } 24 | val body = state.get().body.toString(UTF_8) 25 | assertEquals("foo", body) 26 | } 27 | 28 | @Test 29 | fun `Should not buffer when unbuffered`() { 30 | val state: AtomicReference = AtomicReference(State.Unbuffered()) 31 | state.updateAndGet { it.with() } 32 | state.updateAndGet { it.without() } 33 | state.updateAndGet { it.buffer("foo".toByteArray()) } 34 | state.updateAndGet { it.without() } 35 | val body = state.get().body 36 | assertEquals(EMPTY_BODY, body) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /logbook-ktor-server/src/test/kotlin/org/zalando/logbook/server/ServerRequestTest.kt: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.server 2 | 3 | import io.ktor.http.HeadersImpl 4 | import io.ktor.http.HttpHeaders 5 | import io.ktor.server.request.ApplicationRequest 6 | import org.assertj.core.api.Assertions 7 | import org.junit.jupiter.api.Test 8 | import org.mockito.Mockito.mock 9 | import org.mockito.Mockito.`when` 10 | 11 | class ServerRequestTest { 12 | 13 | @Test 14 | fun `Should return content type`() { 15 | val applicationRequest = mock(ApplicationRequest::class.java) 16 | `when`(applicationRequest.headers) 17 | .thenReturn(HeadersImpl(mapOf(HttpHeaders.ContentType to listOf("application/json")))) 18 | 19 | val req = ServerRequest(applicationRequest) 20 | 21 | Assertions.assertThat(req.contentType) 22 | .isEqualTo("application/json") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /logbook-ktor/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.zalando 9 | logbook-parent 10 | 3.13.0-SNAPSHOT 11 | ../logbook-parent/pom.xml 12 | 13 | 14 | logbook-ktor 15 | 16 | 17 | true 18 | 19 | 20 | 21 | 22 | org.zalando 23 | logbook-ktor-client 24 | 25 | 26 | org.zalando 27 | logbook-ktor-server 28 | 29 | 30 | -------------------------------------------------------------------------------- /logbook-logstash/src/main/java/org/zalando/logbook/logstash/DefaultPrettyPrinterDecorator.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.logstash; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import net.logstash.logback.decorate.JsonGeneratorDecorator; 5 | 6 | public final class DefaultPrettyPrinterDecorator implements JsonGeneratorDecorator { 7 | 8 | @Override 9 | public JsonGenerator decorate(final JsonGenerator generator) { 10 | return generator.useDefaultPrettyPrinter(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /logbook-logstash/src/main/java/org/zalando/logbook/logstash/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook.logstash; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; 5 | -------------------------------------------------------------------------------- /logbook-logstash/src/test/java/org/zalando/logbook/logstash/PrettyPrintingStaticAppender.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.logstash; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import ch.qos.logback.core.ConsoleAppender; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.OutputStream; 8 | import java.nio.charset.StandardCharsets; 9 | 10 | /** 11 | * Simple utility which works together with the logback-test.xml configuration for 12 | * capturing pretty-printed log output in serialized (byte) form. 13 | */ 14 | public final class PrettyPrintingStaticAppender extends ConsoleAppender { 15 | 16 | private static final ByteArrayOutputStream stream = new ByteArrayOutputStream(); 17 | 18 | static void reset() { 19 | stream.reset(); 20 | } 21 | 22 | static String getLastStatement() { 23 | final String content = new String(stream.toByteArray(), StandardCharsets.UTF_8); 24 | return content.substring(content.lastIndexOf("\n {\n", content.length()) + 1); 25 | } 26 | 27 | @Override 28 | public void setOutputStream(final OutputStream ignored) { 29 | super.setOutputStream(stream); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /logbook-logstash/src/test/java/org/zalando/logbook/logstash/StaticAppender.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.logstash; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import ch.qos.logback.core.ConsoleAppender; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.OutputStream; 8 | import java.nio.charset.StandardCharsets; 9 | 10 | /** 11 | * Simple utility which works together with the logback-test.xml configuration for 12 | * capturing log output in serialized (byte) form. 13 | */ 14 | public final class StaticAppender extends ConsoleAppender { 15 | 16 | private static final ByteArrayOutputStream stream = new ByteArrayOutputStream(); 17 | 18 | static void reset() { 19 | stream.reset(); 20 | } 21 | 22 | static String getLastStatement() { 23 | final String content = new String(stream.toByteArray(), StandardCharsets.UTF_8); 24 | return content.substring(content.lastIndexOf('\n', content.length() - 2) + 1); 25 | } 26 | 27 | @Override 28 | public void setOutputStream(final OutputStream ignored) { 29 | super.setOutputStream(stream); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /logbook-logstash/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /logbook-netty/src/main/java/org/zalando/logbook/netty/Buffering.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.netty; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.Unpooled; 5 | import io.netty.handler.codec.http.HttpMessage; 6 | 7 | final class Buffering implements State { 8 | 9 | private final ByteBuf buffer; 10 | 11 | Buffering(final int initialCapacity) { 12 | this.buffer = Unpooled.buffer(initialCapacity); 13 | } 14 | 15 | @Override 16 | public State without() { 17 | return new Ignoring(this); 18 | } 19 | 20 | @Override 21 | public State buffer(final HttpMessage message, final ByteBuf content) { 22 | final int index = content.readerIndex(); 23 | buffer.ensureWritable(content.readableBytes()); 24 | content.readBytes(buffer, content.readableBytes()); 25 | content.readerIndex(index); 26 | return this; 27 | } 28 | 29 | @Override 30 | public byte[] getBody() { 31 | final int length = buffer.readableBytes(); 32 | 33 | if (length == buffer.capacity()) { 34 | return buffer.array(); 35 | } 36 | 37 | final byte[] target = new byte[length]; 38 | System.arraycopy(buffer.array(), 0, target, 0, length); 39 | return target; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /logbook-netty/src/main/java/org/zalando/logbook/netty/Conditionals.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.netty; 2 | 3 | import org.zalando.fauxpas.ThrowingConsumer; 4 | 5 | final class Conditionals { 6 | 7 | private Conditionals() { 8 | // nothing to do 9 | } 10 | 11 | static void runIf( 12 | final Object object, 13 | final Class type, 14 | final ThrowingConsumer consumer) { 15 | 16 | if (type.isInstance(object)) { 17 | consumer.accept(type.cast(object)); 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /logbook-netty/src/main/java/org/zalando/logbook/netty/HeaderSupport.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.netty; 2 | 3 | import com.google.gag.annotation.remark.ThisWouldBeOneLineIn; 4 | import org.zalando.logbook.HttpHeaders; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Map.Entry; 9 | 10 | import static java.util.Collections.singletonList; 11 | 12 | interface HeaderSupport { 13 | 14 | default HttpHeaders copyOf(final Iterable> entries) { 15 | HttpHeaders headers = HttpHeaders.empty(); 16 | 17 | for (final Entry entry : entries) { 18 | headers = append(headers, entry); 19 | } 20 | 21 | return headers; 22 | } 23 | 24 | // Effectively package-private because this interface is and so are all 25 | // implementations of it. Ideally it would be private 26 | @ThisWouldBeOneLineIn(language = "Java 9", toWit = "private") 27 | default HttpHeaders append( 28 | final HttpHeaders headers, final Entry entry) { 29 | 30 | return headers.apply(entry.getKey(), previous -> { 31 | if (previous == null) { 32 | return singletonList(entry.getValue()); 33 | } 34 | 35 | final List result = new ArrayList<>(previous); 36 | result.add(entry.getValue()); 37 | return result; 38 | }); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /logbook-netty/src/main/java/org/zalando/logbook/netty/Ignoring.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.netty; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.handler.codec.http.HttpMessage; 5 | import lombok.AllArgsConstructor; 6 | 7 | @AllArgsConstructor 8 | final class Ignoring implements State { 9 | 10 | private final Buffering buffering; 11 | 12 | @Override 13 | public State with() { 14 | return buffering; 15 | } 16 | 17 | @Override 18 | public State buffer(final HttpMessage message, final ByteBuf content) { 19 | buffering.buffer(message, content); 20 | return this; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /logbook-netty/src/main/java/org/zalando/logbook/netty/Offering.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.netty; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.Unpooled; 5 | import io.netty.handler.codec.http.HttpMessage; 6 | 7 | import static io.netty.handler.codec.http.HttpUtil.getContentLength; 8 | 9 | final class Offering implements State { 10 | 11 | @Override 12 | public State without() { 13 | return new Unbuffered(); 14 | } 15 | 16 | @Override 17 | public State buffer(final HttpMessage message, final ByteBuf content) { 18 | if (content.equals(Unpooled.EMPTY_BUFFER)) { 19 | // saves us from allocating an unnecessary buffer 20 | return this; 21 | } 22 | 23 | final int contentLength = getContentLength(message, 2048); 24 | return new Buffering(contentLength).buffer(message, content); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /logbook-netty/src/main/java/org/zalando/logbook/netty/Sequence.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.netty; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.LinkedList; 5 | import java.util.List; 6 | 7 | import static java.util.Collections.nCopies; 8 | 9 | final class Sequence { 10 | 11 | private final List tasks; 12 | private int next; 13 | 14 | public Sequence(final int length) { 15 | this.tasks = new LinkedList<>(nCopies(length, null)); 16 | } 17 | 18 | synchronized void set(final int index, final Runnable task) { 19 | tasks.set(index, task); 20 | 21 | if (index == next) { 22 | runEagerly(); 23 | } 24 | } 25 | 26 | private void runEagerly() { 27 | final int end = tasks.size(); 28 | 29 | for (@Nullable final Runnable task : tasks.subList(next, end)) { 30 | if (task == null) { 31 | return; 32 | } 33 | 34 | task.run(); 35 | tasks.set(next, null); 36 | next++; 37 | } 38 | 39 | // Reset next to 0, we've exhausted the tasks and this instance will live on in the thread 40 | // that it was created in, so we need to reset next back to 0 if we want to see any more output 41 | 42 | next = 0; 43 | } 44 | 45 | boolean hasSecondTask() { 46 | return tasks.get(1) != null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /logbook-netty/src/main/java/org/zalando/logbook/netty/State.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.netty; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.handler.codec.http.HttpMessage; 5 | 6 | interface State { 7 | 8 | default State with() { 9 | return this; 10 | } 11 | 12 | default State without() { 13 | return this; 14 | } 15 | 16 | default State buffer( 17 | final HttpMessage message, final ByteBuf content) { 18 | return this; 19 | } 20 | 21 | default byte[] getBody() { 22 | return new byte[0]; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /logbook-netty/src/main/java/org/zalando/logbook/netty/Unbuffered.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.netty; 2 | 3 | final class Unbuffered implements State { 4 | 5 | @Override 6 | public State with() { 7 | return new Offering(); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /logbook-netty/src/main/java/org/zalando/logbook/netty/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook.netty; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; 5 | -------------------------------------------------------------------------------- /logbook-netty/src/test/java/org/zalando/logbook/netty/IgnoringTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.netty; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.handler.codec.http.DefaultHttpRequest; 5 | import io.netty.handler.codec.http.HttpRequest; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.concurrent.atomic.AtomicReference; 10 | 11 | import static io.netty.buffer.Unpooled.wrappedBuffer; 12 | import static io.netty.handler.codec.http.HttpMethod.GET; 13 | import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; 14 | import static java.nio.charset.StandardCharsets.UTF_8; 15 | 16 | final class IgnoringTest { 17 | 18 | final AtomicReference state = 19 | new AtomicReference<>(new Unbuffered()); 20 | 21 | final HttpRequest request = new DefaultHttpRequest(HTTP_1_1, GET, "/"); 22 | 23 | @Test 24 | void shouldKeepBufferingWhenIgnoring() { 25 | state.updateAndGet(State::with); 26 | state.updateAndGet(state -> state.buffer(request, content("foo"))); 27 | state.updateAndGet(State::without); 28 | state.updateAndGet(state -> state.buffer(request, content("bar"))); 29 | // this transition is almost impossible to test under normal 30 | // circumstances because the test would be racing with IO 31 | state.updateAndGet(State::with); 32 | 33 | final String body = new String(state.get().getBody(), UTF_8); 34 | Assertions.assertEquals("foobar", body); 35 | } 36 | 37 | private ByteBuf content(final String s) { 38 | return wrappedBuffer(s.getBytes(UTF_8)); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /logbook-okhttp/src/main/java/org/zalando/logbook/okhttp/GzipInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.okhttp; 2 | 3 | import okhttp3.Interceptor; 4 | import okhttp3.Response; 5 | import okhttp3.internal.http.RealResponseBody; 6 | import okio.GzipSource; 7 | import org.apiguardian.api.API; 8 | 9 | import javax.annotation.Nonnull; 10 | import java.io.IOException; 11 | 12 | import static java.util.Objects.requireNonNull; 13 | import static okio.Okio.buffer; 14 | import static org.apiguardian.api.API.Status.EXPERIMENTAL; 15 | 16 | @API(status = EXPERIMENTAL) 17 | public final class GzipInterceptor implements Interceptor { 18 | 19 | @Nonnull 20 | @Override 21 | public Response intercept(final Chain chain) throws IOException { 22 | final Response response = chain.proceed(chain.request()); 23 | 24 | if (isContentEncodingGzip(response)) { 25 | return response.newBuilder() 26 | .headers(response.headers() 27 | .newBuilder() 28 | .removeAll("Content-Encoding") 29 | .removeAll("Content-Length") 30 | .build()) 31 | .body(new RealResponseBody( 32 | response.header("Content-Type"), -1L, 33 | buffer(new GzipSource(requireNonNull(response.body(), "body").source())))) 34 | .build(); 35 | } 36 | 37 | return response; 38 | } 39 | 40 | private boolean isContentEncodingGzip(final Response response) { 41 | return "gzip".equalsIgnoreCase(response.header("Content-Encoding")); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /logbook-okhttp/src/main/java/org/zalando/logbook/okhttp/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook.okhttp; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; 5 | -------------------------------------------------------------------------------- /logbook-okhttp/src/test/java/org/zalando/logbook/okhttp/LocalRequestTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.okhttp; 2 | 3 | import okhttp3.Request; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | final class LocalRequestTest { 9 | 10 | private LocalRequest unit(final Request request) { 11 | return new LocalRequest(request); 12 | } 13 | 14 | @Test 15 | void shouldResolveLocalhost() { 16 | final LocalRequest unit = unit(get("http://localhost/")); 17 | 18 | assertThat(unit.getRemote()).isEqualTo("localhost"); 19 | } 20 | 21 | @Test 22 | void shouldIgnoreDefaultHttpPort() { 23 | final LocalRequest unit = unit(get("http://localhost/")); 24 | 25 | assertThat(unit.getPort()).isEmpty(); 26 | } 27 | 28 | @Test 29 | void shouldIgnoreDefaultHttpsPort() { 30 | final LocalRequest unit = unit(get("https://localhost/")); 31 | 32 | assertThat(unit.getPort()).isEmpty(); 33 | } 34 | 35 | private Request get(final String uri) { 36 | return new Request.Builder() 37 | .url(uri) 38 | .get() 39 | .build(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /logbook-okhttp/src/test/resources/response.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zalando/logbook/ca81f780ce4a238b8652fdeb6dcb48d8245ba6ae/logbook-okhttp/src/test/resources/response.txt.gz -------------------------------------------------------------------------------- /logbook-okhttp2/src/main/java/org/zalando/logbook/okhttp2/GzipInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.okhttp2; 2 | 3 | import com.squareup.okhttp.Headers; 4 | import com.squareup.okhttp.Interceptor; 5 | import com.squareup.okhttp.Response; 6 | import com.squareup.okhttp.internal.http.RealResponseBody; 7 | import okio.GzipSource; 8 | import org.apiguardian.api.API; 9 | 10 | import java.io.IOException; 11 | 12 | import static java.util.Objects.requireNonNull; 13 | import static okio.Okio.buffer; 14 | import static org.apiguardian.api.API.Status.EXPERIMENTAL; 15 | 16 | @API(status = EXPERIMENTAL) 17 | public final class GzipInterceptor implements Interceptor { 18 | 19 | @Override 20 | public Response intercept(final Chain chain) throws IOException { 21 | final Response response = chain.proceed(chain.request()); 22 | 23 | if (isContentEncodingGzip(response)) { 24 | final Headers headers = response.headers() 25 | .newBuilder() 26 | .removeAll("Content-Encoding") 27 | .removeAll("Content-Length") 28 | .build(); 29 | 30 | return response.newBuilder() 31 | .headers(headers) 32 | .body(new RealResponseBody( 33 | headers, 34 | buffer(new GzipSource(requireNonNull(response.body(), "body").source())))) 35 | .build(); 36 | } 37 | 38 | return response; 39 | } 40 | 41 | private boolean isContentEncodingGzip(final Response response) { 42 | return "gzip".equalsIgnoreCase(response.header("Content-Encoding")); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /logbook-okhttp2/src/main/java/org/zalando/logbook/okhttp2/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook.okhttp2; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; 5 | -------------------------------------------------------------------------------- /logbook-okhttp2/src/test/java/org/zalando/logbook/okhttp2/LocalRequestTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.okhttp2; 2 | 3 | import com.squareup.okhttp.Request; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | final class LocalRequestTest { 9 | 10 | private LocalRequest unit(final Request request) { 11 | return new LocalRequest(request); 12 | } 13 | 14 | @Test 15 | void shouldResolveLocalhost() { 16 | final LocalRequest unit = unit(get("http://localhost/")); 17 | 18 | assertThat(unit.getRemote()).isEqualTo("localhost"); 19 | } 20 | 21 | @Test 22 | void shouldIgnoreDefaultHttpPort() { 23 | final LocalRequest unit = unit(get("http://localhost/")); 24 | 25 | assertThat(unit.getPort()).isEmpty(); 26 | } 27 | 28 | @Test 29 | void shouldIgnoreDefaultHttpsPort() { 30 | final LocalRequest unit = unit(get("https://localhost/")); 31 | 32 | assertThat(unit.getPort()).isEmpty(); 33 | } 34 | 35 | private Request get(final String uri) { 36 | return new Request.Builder() 37 | .url(uri) 38 | .get() 39 | .build(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /logbook-okhttp2/src/test/resources/response.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zalando/logbook/ca81f780ce4a238b8652fdeb6dcb48d8245ba6ae/logbook-okhttp2/src/test/resources/response.txt.gz -------------------------------------------------------------------------------- /logbook-openfeign/src/main/java/org/zalando/logbook/openfeign/ByteStreams.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.openfeign; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.util.Objects; 8 | 9 | final class ByteStreams { 10 | private ByteStreams() { 11 | } 12 | 13 | static byte[] toByteArray(final InputStream in) throws IOException { 14 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 15 | copy(in, out); 16 | return out.toByteArray(); 17 | } 18 | 19 | static void copy(final InputStream from, final OutputStream to) throws IOException { 20 | Objects.requireNonNull(from); 21 | Objects.requireNonNull(to); 22 | final byte[] buf = new byte[4096]; 23 | int bytesRead; 24 | 25 | while ((bytesRead = from.read(buf)) != -1) { 26 | to.write(buf, 0, bytesRead); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /logbook-openfeign/src/main/java/org/zalando/logbook/openfeign/HeaderUtils.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.openfeign; 2 | 3 | import org.zalando.logbook.HttpHeaders; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | class HeaderUtils { 12 | private HeaderUtils() { 13 | } 14 | 15 | /** 16 | * Convert Feign headers to Logbook-compatible format 17 | * 18 | * @param feignHeaders original headers 19 | * @return Logbook headers 20 | */ 21 | static HttpHeaders toLogbookHeaders(Map> feignHeaders) { 22 | Map> convertedHeaders = new HashMap<>(); 23 | for (Map.Entry> header : feignHeaders.entrySet()) { 24 | convertedHeaders.put(header.getKey(), new ArrayList<>(header.getValue())); 25 | } 26 | return HttpHeaders.of(convertedHeaders); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /logbook-openfeign/src/main/java/org/zalando/logbook/openfeign/Utils.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.openfeign; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | 6 | public class Utils { 7 | private Utils() { 8 | } 9 | 10 | public static void ensureClosed(Closeable closeable) { 11 | if (closeable != null) { 12 | try { 13 | closeable.close(); 14 | } catch (IOException ignored) { 15 | // ignore 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /logbook-openfeign/src/test/java/org/zalando/logbook/openfeign/FeignClient.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.openfeign; 2 | 3 | import feign.RequestLine; 4 | 5 | public interface FeignClient { 6 | @RequestLine("GET /get/string") 7 | String getString(); 8 | 9 | @RequestLine("GET /get/void") 10 | void getVoid(); 11 | 12 | @RequestLine("POST /post/bad-request") 13 | String postBadRequest(String request); 14 | } 15 | -------------------------------------------------------------------------------- /logbook-openfeign/src/test/java/org/zalando/logbook/openfeign/UtilsTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.openfeign; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.Closeable; 6 | import java.io.IOException; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | import static org.mockito.Mockito.doThrow; 10 | import static org.mockito.Mockito.mock; 11 | import static org.mockito.Mockito.when; 12 | 13 | class UtilsTest { 14 | 15 | @Test 16 | void ensureClosedShouldIgnoreCloseIoException() throws IOException { 17 | Closeable closeable = mock(Closeable.class); 18 | doThrow(new IOException()).when(closeable).close(); 19 | 20 | Utils.ensureClosed(closeable); 21 | } 22 | 23 | @Test 24 | void ensureClosedShouldIgnoreNull() { 25 | Utils.ensureClosed(null); 26 | } 27 | } -------------------------------------------------------------------------------- /logbook-servlet/src/main/java/org/zalando/logbook/servlet/AsyncOnCompleteListener.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import jakarta.servlet.AsyncEvent; 4 | 5 | import java.io.IOException; 6 | 7 | @FunctionalInterface 8 | interface AsyncOnCompleteListener { 9 | void onComplete(AsyncEvent event) throws IOException; 10 | } 11 | -------------------------------------------------------------------------------- /logbook-servlet/src/main/java/org/zalando/logbook/servlet/ByteStreams.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.util.Objects; 8 | 9 | final class ByteStreams { 10 | 11 | private ByteStreams() { 12 | 13 | } 14 | 15 | static byte[] toByteArray(final InputStream in) throws IOException { 16 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 17 | copy(in, out); 18 | return out.toByteArray(); 19 | } 20 | 21 | static void copy(final InputStream from, final OutputStream to) throws IOException { 22 | Objects.requireNonNull(from); 23 | Objects.requireNonNull(to); 24 | final byte[] buf = new byte[4096]; 25 | int bytesRead; 26 | 27 | while ((bytesRead = from.read(buf)) != -1) { 28 | to.write(buf, 0, bytesRead); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /logbook-servlet/src/main/java/org/zalando/logbook/servlet/FormRequestMode.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | import static java.util.Locale.ROOT; 6 | 7 | public enum FormRequestMode { 8 | 9 | BODY, PARAMETER, OFF; 10 | 11 | public static FormRequestMode fromProperties() { 12 | @Nullable final String property = System.getProperty("logbook.servlet.form-request"); 13 | 14 | if (property == null) { 15 | return BODY; 16 | } 17 | 18 | return FormRequestMode.valueOf(property.toUpperCase(ROOT)); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /logbook-servlet/src/main/java/org/zalando/logbook/servlet/HttpFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import jakarta.servlet.Filter; 4 | import jakarta.servlet.FilterChain; 5 | import jakarta.servlet.FilterConfig; 6 | import jakarta.servlet.ServletException; 7 | import jakarta.servlet.ServletRequest; 8 | import jakarta.servlet.ServletResponse; 9 | import jakarta.servlet.http.HttpServletRequest; 10 | import jakarta.servlet.http.HttpServletResponse; 11 | 12 | import java.io.IOException; 13 | 14 | interface HttpFilter extends Filter { 15 | 16 | @Override 17 | default void init(final FilterConfig filterConfig) { 18 | // no initialization needed by default 19 | } 20 | 21 | @Override 22 | default void doFilter(final ServletRequest request, final ServletResponse response, 23 | final FilterChain chain) throws ServletException, IOException { 24 | 25 | if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { 26 | throw new IllegalArgumentException(getClass().getSimpleName() + " only supports HTTP"); 27 | } 28 | 29 | final HttpServletRequest httpRequest = (HttpServletRequest) request; 30 | final HttpServletResponse httpResponse = (HttpServletResponse) response; 31 | 32 | doFilter(httpRequest, httpResponse, chain); 33 | } 34 | 35 | void doFilter(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse, 36 | final FilterChain chain) throws ServletException, IOException; 37 | 38 | @Override 39 | default void destroy() { 40 | // no deconstruction needed by default 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /logbook-servlet/src/main/java/org/zalando/logbook/servlet/LogbookAsyncListener.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import jakarta.servlet.AsyncEvent; 4 | import jakarta.servlet.AsyncListener; 5 | 6 | import java.io.IOException; 7 | 8 | class LogbookAsyncListener implements AsyncListener { 9 | 10 | private final AsyncOnCompleteListener onCompleteListener; 11 | 12 | public LogbookAsyncListener(AsyncOnCompleteListener onCompleteListener) { 13 | this.onCompleteListener = onCompleteListener; 14 | } 15 | 16 | @Override 17 | public void onComplete(AsyncEvent event) throws IOException { 18 | onCompleteListener.onComplete(event); 19 | } 20 | 21 | @Override 22 | public void onTimeout(AsyncEvent event) { 23 | } 24 | 25 | @Override 26 | public void onError(AsyncEvent event) { 27 | } 28 | 29 | @Override 30 | public void onStartAsync(AsyncEvent event) { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /logbook-servlet/src/main/java/org/zalando/logbook/servlet/NullOutputStream.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | 6 | final class NullOutputStream extends OutputStream { 7 | 8 | static final OutputStream NULL = new NullOutputStream(); 9 | 10 | private NullOutputStream() { 11 | 12 | } 13 | 14 | @Override 15 | public void write(final int b) throws IOException { 16 | // ignore 17 | } 18 | 19 | @Override 20 | public void write(final byte[] b) throws IOException { 21 | // ignore 22 | } 23 | 24 | @Override 25 | public void write(final byte[] b, final int off, final int len) throws IOException { 26 | // ignore 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /logbook-servlet/src/main/java/org/zalando/logbook/servlet/SecureLogbookFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import org.apiguardian.api.API; 8 | import org.zalando.logbook.Logbook; 9 | import org.zalando.logbook.core.SecurityStrategy; 10 | 11 | import java.io.IOException; 12 | 13 | import static org.apiguardian.api.API.Status.STABLE; 14 | 15 | @API(status = STABLE) 16 | public final class SecureLogbookFilter implements HttpFilter { 17 | 18 | private final HttpFilter filter; 19 | 20 | public SecureLogbookFilter() { 21 | this(Logbook.create()); 22 | } 23 | 24 | public SecureLogbookFilter(final Logbook logbook) { 25 | this.filter = new LogbookFilter(logbook, new SecurityStrategy()); 26 | } 27 | 28 | @Override 29 | public void doFilter(final HttpServletRequest request, final HttpServletResponse response, 30 | final FilterChain chain) throws ServletException, IOException { 31 | 32 | filter.doFilter(request, response, chain); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /logbook-servlet/src/main/java/org/zalando/logbook/servlet/ServletInputStreamAdapter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import jakarta.servlet.ReadListener; 4 | import jakarta.servlet.ServletInputStream; 5 | import lombok.AllArgsConstructor; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.io.IOException; 9 | 10 | @AllArgsConstructor 11 | final class ServletInputStreamAdapter extends ServletInputStream { 12 | 13 | private final ByteArrayInputStream stream; 14 | 15 | @Override 16 | public int read() { 17 | return stream.read(); 18 | } 19 | 20 | @Override 21 | public int read(final byte[] b) throws IOException { 22 | return stream.read(b); 23 | } 24 | 25 | @Override 26 | public int read(final byte[] b, final int off, final int len) { 27 | return stream.read(b, off, len); 28 | } 29 | 30 | @Override 31 | public boolean isFinished() { 32 | return stream.available() == 0; 33 | } 34 | 35 | @Override 36 | public boolean isReady() { 37 | return true; 38 | } 39 | 40 | @Override 41 | public void setReadListener(final ReadListener readListener) { 42 | throw new UnsupportedOperationException(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /logbook-servlet/src/main/java/org/zalando/logbook/servlet/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook.servlet; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; -------------------------------------------------------------------------------- /logbook-servlet/src/test/java/org/zalando/logbook/servlet/ByteStreamsTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.IOException; 8 | 9 | import static java.nio.charset.StandardCharsets.UTF_8; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | final class ByteStreamsTest { 13 | 14 | @Test 15 | void shouldCollectStreamToByteArray() throws IOException { 16 | final byte[] bytes = ByteStreams.toByteArray(new ByteArrayInputStream("Hello World!".getBytes(UTF_8))); 17 | 18 | assertThat(new String(bytes, UTF_8)).isEqualTo("Hello World!"); 19 | } 20 | 21 | @Test 22 | void shouldCopyStreams() throws IOException { 23 | final ByteArrayOutputStream to = new ByteArrayOutputStream(); 24 | ByteStreams.copy(new ByteArrayInputStream("Hello World!".getBytes(UTF_8)), to); 25 | 26 | assertThat(new String(to.toByteArray(), UTF_8)).isEqualTo("Hello World!"); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /logbook-servlet/src/test/java/org/zalando/logbook/servlet/FormRequestModeTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.provider.CsvSource; 6 | import org.zalando.logbook.servlet.junit.RestoreSystemProperties; 7 | 8 | import javax.annotation.concurrent.NotThreadSafe; 9 | 10 | import static java.util.Locale.ROOT; 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.zalando.logbook.servlet.FormRequestMode.BODY; 13 | 14 | @NotThreadSafe 15 | @RestoreSystemProperties 16 | class FormRequestModeTest { 17 | 18 | @ParameterizedTest 19 | @CsvSource({"off", "body", "parameter"}) 20 | void test(final String value) { 21 | System.setProperty("logbook.servlet.form-request", value); 22 | final FormRequestMode unit = FormRequestMode.fromProperties(); 23 | assertEquals(value, unit.name().toLowerCase(ROOT)); 24 | } 25 | 26 | @Test 27 | void defaultsToBody() { 28 | assertEquals(BODY, FormRequestMode.fromProperties()); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /logbook-servlet/src/test/java/org/zalando/logbook/servlet/ForwardingHttpLogFormatter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import org.zalando.logbook.Correlation; 4 | import org.zalando.logbook.HttpLogFormatter; 5 | import org.zalando.logbook.HttpRequest; 6 | import org.zalando.logbook.HttpResponse; 7 | import org.zalando.logbook.Precorrelation; 8 | 9 | import java.io.IOException; 10 | 11 | // non final so we can wrap a spy around it 12 | public class ForwardingHttpLogFormatter implements HttpLogFormatter { 13 | 14 | private final HttpLogFormatter formatter; 15 | 16 | public ForwardingHttpLogFormatter(final HttpLogFormatter formatter) { 17 | this.formatter = formatter; 18 | } 19 | 20 | private HttpLogFormatter delegate() { 21 | return formatter; 22 | } 23 | 24 | @Override 25 | public String format(final Precorrelation precorrelation, final HttpRequest request) throws IOException { 26 | return delegate().format(precorrelation, request); 27 | } 28 | 29 | @Override 30 | public String format(final Correlation correlation, final HttpResponse response) 31 | throws IOException { 32 | return delegate().format(correlation, response); 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return delegate().toString(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /logbook-servlet/src/test/java/org/zalando/logbook/servlet/LogbookAsyncListenerTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import jakarta.servlet.AsyncEvent; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.IOException; 7 | 8 | import static org.mockito.Mockito.mock; 9 | import static org.mockito.Mockito.verify; 10 | 11 | class LogbookAsyncListenerTest { 12 | 13 | private final AsyncOnCompleteListener asyncOnCompleteListener = mock(AsyncOnCompleteListener.class); 14 | private final AsyncEvent asyncEvent = mock(AsyncEvent.class); 15 | 16 | private final LogbookAsyncListener logbookAsyncListener = new LogbookAsyncListener(asyncOnCompleteListener); 17 | 18 | @Test 19 | void onComplete() throws IOException { 20 | logbookAsyncListener.onComplete(asyncEvent); 21 | 22 | verify(asyncOnCompleteListener).onComplete(asyncEvent); 23 | } 24 | 25 | @Test 26 | void onTimeout() { 27 | logbookAsyncListener.onTimeout(asyncEvent); 28 | } 29 | 30 | @Test 31 | void onError() { 32 | logbookAsyncListener.onError(asyncEvent); 33 | } 34 | 35 | @Test 36 | void onStartAsync() { 37 | logbookAsyncListener.onStartAsync(asyncEvent); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /logbook-servlet/src/test/java/org/zalando/logbook/servlet/Message.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | class Message { 7 | String value; 8 | } 9 | -------------------------------------------------------------------------------- /logbook-servlet/src/test/java/org/zalando/logbook/servlet/NullOutputStreamTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.zalando.logbook.servlet.NullOutputStream.NULL; 6 | 7 | final class NullOutputStreamTest { 8 | 9 | @Test 10 | void shouldIgnoreByte() throws Exception { 11 | NULL.write(0); 12 | } 13 | 14 | @Test 15 | void shouldIgnoreBytes() throws Exception { 16 | NULL.write(new byte[0]); 17 | } 18 | 19 | @Test 20 | void shouldIgnoreBytesOffset() throws Exception { 21 | NULL.write(new byte[0], 0, 0); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /logbook-servlet/src/test/java/org/zalando/logbook/servlet/RequestBuilders.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import jakarta.servlet.DispatcherType; 4 | import org.springframework.mock.web.MockHttpServletRequest; 5 | import org.springframework.test.web.servlet.MvcResult; 6 | import org.springframework.test.web.servlet.RequestBuilder; 7 | 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; 9 | 10 | final class RequestBuilders { 11 | 12 | static RequestBuilder async(final MvcResult result) { 13 | final RequestBuilder builder = asyncDispatch(result); 14 | 15 | return context -> { 16 | final MockHttpServletRequest request = builder.buildRequest(context); 17 | // this is missing in MockMvcRequestBuilders#asyncDispatch 18 | request.setDispatcherType(DispatcherType.ASYNC); 19 | return request; 20 | }; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /logbook-servlet/src/test/java/org/zalando/logbook/servlet/SecurityFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | 8 | import javax.annotation.Nullable; 9 | import java.io.IOException; 10 | 11 | class SecurityFilter implements HttpFilter { 12 | 13 | @Nullable 14 | private Integer status; 15 | 16 | void setStatus(final Integer status) { 17 | this.status = status; 18 | } 19 | 20 | @Override 21 | public void doFilter(final HttpServletRequest request, final HttpServletResponse response, 22 | final FilterChain chain) throws ServletException, IOException { 23 | 24 | if (status == null) { 25 | chain.doFilter(request, response); 26 | } else { 27 | response.setStatus(status); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /logbook-servlet/src/test/java/org/zalando/logbook/servlet/ServletInputStreamAdapterTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import jakarta.servlet.ReadListener; 4 | import jakarta.servlet.ServletInputStream; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mockito; 7 | 8 | import java.io.ByteArrayInputStream; 9 | import java.io.IOException; 10 | 11 | import static java.nio.charset.StandardCharsets.UTF_8; 12 | import static org.junit.jupiter.api.Assertions.assertFalse; 13 | import static org.junit.jupiter.api.Assertions.assertThrows; 14 | import static org.junit.jupiter.api.Assertions.assertTrue; 15 | 16 | class ServletInputStreamAdapterTest { 17 | 18 | private final ServletInputStream unit = new ServletInputStreamAdapter(new ByteArrayInputStream( 19 | "Hello, world".getBytes(UTF_8))); 20 | 21 | @Test 22 | void shouldBeReady() { 23 | assertTrue(unit.isReady()); 24 | } 25 | 26 | @Test 27 | void shouldBeFinishedWhenDone() throws IOException { 28 | assertFalse(unit.isFinished()); 29 | ByteStreams.copy(unit, NullOutputStream.NULL); 30 | assertTrue(unit.isFinished()); 31 | } 32 | 33 | @Test 34 | void shouldNotSupportReadListener() { 35 | assertThrows(UnsupportedOperationException.class, () -> 36 | unit.setReadListener(Mockito.mock(ReadListener.class))); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /logbook-servlet/src/test/java/org/zalando/logbook/servlet/SpyableFilter.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet; 2 | 3 | import jakarta.servlet.Filter; 4 | import jakarta.servlet.FilterChain; 5 | import jakarta.servlet.FilterConfig; 6 | import jakarta.servlet.ServletException; 7 | import jakarta.servlet.ServletRequest; 8 | import jakarta.servlet.ServletResponse; 9 | 10 | import java.io.IOException; 11 | 12 | // non-final so we can wrap a spy around it 13 | class SpyableFilter implements Filter { 14 | 15 | private final Filter filter; 16 | 17 | SpyableFilter(final Filter filter) { 18 | this.filter = filter; 19 | } 20 | 21 | @Override 22 | public void init(final FilterConfig filterConfig) throws ServletException { 23 | filter.init(filterConfig); 24 | } 25 | 26 | @Override 27 | public void doFilter(final ServletRequest request, final ServletResponse response, 28 | final FilterChain chain) throws IOException, ServletException { 29 | 30 | filter.doFilter(request, response, chain); 31 | } 32 | 33 | @Override 34 | public void destroy() { 35 | filter.destroy(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /logbook-servlet/src/test/java/org/zalando/logbook/servlet/junit/RestoreSystemProperties.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet.junit; 2 | 3 | import org.junit.jupiter.api.extension.ExtendWith; 4 | 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.Target; 7 | 8 | import static java.lang.annotation.ElementType.METHOD; 9 | import static java.lang.annotation.ElementType.TYPE; 10 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 11 | 12 | @Target({TYPE, METHOD}) 13 | @Retention(RUNTIME) 14 | @ExtendWith(SystemPropertiesExtension.class) 15 | public @interface RestoreSystemProperties { 16 | } 17 | -------------------------------------------------------------------------------- /logbook-servlet/src/test/java/org/zalando/logbook/servlet/junit/SystemPropertiesExtension.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.servlet.junit; 2 | 3 | import org.junit.jupiter.api.extension.AfterEachCallback; 4 | import org.junit.jupiter.api.extension.BeforeEachCallback; 5 | import org.junit.jupiter.api.extension.Extension; 6 | import org.junit.jupiter.api.extension.ExtensionContext; 7 | 8 | import java.util.Properties; 9 | 10 | final class SystemPropertiesExtension 11 | implements Extension, BeforeEachCallback, AfterEachCallback { 12 | 13 | private final Properties original = new Properties(); 14 | 15 | @Override 16 | public void beforeEach(final ExtensionContext context) throws Exception { 17 | original.putAll(System.getProperties()); 18 | } 19 | 20 | @Override 21 | public void afterEach(final ExtensionContext context) throws Exception { 22 | final Properties properties = System.getProperties(); 23 | properties.clear(); 24 | properties.putAll(original); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/main/java/org/zalando/logbook/autoconfigure/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook.autoconfigure; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; 5 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration = org.zalando.logbook.autoconfigure.LogbookAutoConfiguration 2 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | org.zalando.logbook.autoconfigure.LogbookAutoConfiguration 2 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/BodyOnlyIfStatusAtLeastStrategyTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.zalando.logbook.Strategy; 6 | import org.zalando.logbook.core.BodyOnlyIfStatusAtLeastStrategy; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | @LogbookTest(properties = "logbook.strategy = body-only-if-status-at-least") 11 | class BodyOnlyIfStatusAtLeastStrategyTest { 12 | 13 | @Autowired 14 | private Strategy strategy; 15 | 16 | @Test 17 | void shouldUseCorrectStrategy() { 18 | assertThat(strategy).isInstanceOf(BodyOnlyIfStatusAtLeastStrategy.class); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/FeignTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.runner.WebApplicationContextRunner; 5 | import org.zalando.logbook.Logbook; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | class FeignTest { 10 | 11 | private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner(); 12 | 13 | @Test 14 | void shouldInitializeFeignLogbookLogger() { 15 | this.contextRunner 16 | .withBean("logbook", Logbook.class, Logbook::create) 17 | .withBean("logbookProperties", LogbookProperties.class, LogbookProperties::new) 18 | .withUserConfiguration(LogbookAutoConfiguration.FeignLogbookLoggerConfiguration.class) 19 | .run(context -> { 20 | assertThat(context).hasBean("feignLogbookLogger"); 21 | }); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/FilterDisabledTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Qualifier; 6 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | @LogbookTest(properties = "logbook.filter.enabled = false") 11 | class FilterDisabledTest { 12 | 13 | @Autowired(required = false) 14 | @Qualifier("authorizedLogbookFilter") 15 | private FilterRegistrationBean authorizedLogbookFilter; 16 | 17 | @Test 18 | void shouldInitializeFilter() { 19 | assertThat(authorizedLogbookFilter).isNull(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/FilterTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Qualifier; 6 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | @LogbookTest 11 | class FilterTest { 12 | 13 | @Autowired 14 | @Qualifier("logbookFilter") 15 | private FilterRegistrationBean logbookFilter; 16 | 17 | @Test 18 | void shouldInitializeFilter() { 19 | assertThat(logbookFilter).isNotNull(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/FormatStyleCurlTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.mockito.ArgumentCaptor; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.test.context.bean.override.mockito.MockitoBean; 8 | import org.zalando.logbook.HttpLogWriter; 9 | import org.zalando.logbook.Logbook; 10 | import org.zalando.logbook.Precorrelation; 11 | import org.zalando.logbook.test.MockHttpRequest; 12 | 13 | import java.io.IOException; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.mockito.ArgumentMatchers.any; 17 | import static org.mockito.Mockito.doReturn; 18 | import static org.mockito.Mockito.verify; 19 | 20 | @LogbookTest(properties = "logbook.format.style = curl") 21 | class FormatStyleCurlTest { 22 | 23 | @Autowired 24 | private Logbook logbook; 25 | 26 | @MockitoBean 27 | private HttpLogWriter writer; 28 | 29 | @BeforeEach 30 | void setUp() throws IOException { 31 | doReturn(true).when(writer).isActive(); 32 | } 33 | 34 | @Test 35 | void shouldUseHttpFormatter() throws IOException { 36 | logbook.process(MockHttpRequest.create()).write(); 37 | 38 | final ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); 39 | verify(writer).write(any(Precorrelation.class), captor.capture()); 40 | assertThat(captor.getValue()).contains("curl -v -X GET 'http://localhost/'"); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/FormatStyleDefaultTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.mockito.ArgumentCaptor; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.test.context.bean.override.mockito.MockitoBean; 8 | import org.zalando.logbook.HttpLogWriter; 9 | import org.zalando.logbook.Logbook; 10 | import org.zalando.logbook.Precorrelation; 11 | import org.zalando.logbook.test.MockHttpRequest; 12 | 13 | import java.io.IOException; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.mockito.ArgumentMatchers.any; 17 | import static org.mockito.Mockito.doReturn; 18 | import static org.mockito.Mockito.verify; 19 | 20 | @LogbookTest 21 | class FormatStyleDefaultTest { 22 | 23 | @Autowired 24 | private Logbook logbook; 25 | 26 | @MockitoBean 27 | private HttpLogWriter writer; 28 | 29 | @BeforeEach 30 | void setUp() throws IOException { 31 | doReturn(true).when(writer).isActive(); 32 | } 33 | 34 | @Test 35 | void shouldUseJsonFormatter() throws IOException { 36 | logbook.process(MockHttpRequest.create()).write(); 37 | 38 | final ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); 39 | verify(writer).write(any(Precorrelation.class), captor.capture()); 40 | assertThat(captor.getValue()).startsWith("{").endsWith("}"); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/FormatStyleHttpTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.mockito.ArgumentCaptor; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.test.context.bean.override.mockito.MockitoBean; 8 | import org.zalando.logbook.HttpLogWriter; 9 | import org.zalando.logbook.Logbook; 10 | import org.zalando.logbook.Precorrelation; 11 | import org.zalando.logbook.test.MockHttpRequest; 12 | 13 | import java.io.IOException; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.mockito.ArgumentMatchers.any; 17 | import static org.mockito.Mockito.doReturn; 18 | import static org.mockito.Mockito.verify; 19 | 20 | @LogbookTest(properties = "logbook.format.style = http") 21 | class FormatStyleHttpTest { 22 | 23 | @Autowired 24 | private Logbook logbook; 25 | 26 | @MockitoBean 27 | private HttpLogWriter writer; 28 | 29 | @BeforeEach 30 | void setUp() throws IOException { 31 | doReturn(true).when(writer).isActive(); 32 | } 33 | 34 | @Test 35 | void shouldUseHttpFormatter() throws IOException { 36 | logbook.process(MockHttpRequest.create()).write(); 37 | 38 | 39 | final ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); 40 | verify(writer).write(any(Precorrelation.class), captor.capture()); 41 | assertThat(captor.getValue()).contains("GET http://localhost/ HTTP/1.1"); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/ObfuscateBodyDefaultTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.json.JSONException; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Qualifier; 7 | import org.zalando.logbook.BodyFilter; 8 | 9 | import java.io.IOException; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | @LogbookTest 14 | class ObfuscateBodyDefaultTest { 15 | 16 | @Autowired 17 | @Qualifier("jsonBodyFieldsFilter") 18 | private BodyFilter jsonBodyFieldsFilter; 19 | 20 | @Test 21 | void shouldNotFilterJsonBodiesIfEmptyObfuscateJsonBodyFieldNames() throws IOException, JSONException { 22 | assertThat(jsonBodyFieldsFilter).isSameAs(BodyFilter.none()); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/SecurityFilterTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Qualifier; 6 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | @LogbookTest 11 | class SecurityFilterTest { 12 | 13 | @Autowired 14 | @Qualifier("secureLogbookFilter") 15 | private FilterRegistrationBean secureLogbookFilter; 16 | 17 | @Autowired 18 | @Qualifier("logbookFilter") 19 | private FilterRegistrationBean logbookFilter; 20 | 21 | @Test 22 | void shouldInitializeFilters() { 23 | assertThat(secureLogbookFilter).isNotNull(); 24 | assertThat(logbookFilter).isNotNull(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/StandardTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.zalando.logbook.Logbook; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | @LogbookTest 10 | class StandardTest { 11 | 12 | @Autowired 13 | private Logbook logbook; 14 | 15 | @Test 16 | void shouldBeAutowired() { 17 | assertThat(logbook).isNotNull(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/StatusAtLeastStrategyTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.zalando.logbook.Strategy; 6 | import org.zalando.logbook.core.StatusAtLeastStrategy; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | @LogbookTest(properties = "logbook.strategy = status-at-least") 11 | class StatusAtLeastStrategyTest { 12 | 13 | @Autowired 14 | private Strategy strategy; 15 | 16 | @Test 17 | void shouldUseCorrectStrategy() { 18 | assertThat(strategy).isInstanceOf(StatusAtLeastStrategy.class); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/WithoutBodyStrategyTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.zalando.logbook.Strategy; 6 | import org.zalando.logbook.core.WithoutBodyStrategy; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | @LogbookTest(properties = "logbook.strategy = without-body") 11 | class WithoutBodyStrategyTest { 12 | 13 | @Autowired 14 | private Strategy strategy; 15 | 16 | @Test 17 | void shouldUseCorrectStrategy() { 18 | assertThat(strategy).isInstanceOf(WithoutBodyStrategy.class); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/WriteChunkingTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.zalando.logbook.Sink; 6 | import org.zalando.logbook.core.ChunkingSink; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | @LogbookTest(properties = "logbook.write.chunk-size = 100") 11 | class WriteChunkingTest { 12 | 13 | @Autowired 14 | private Sink sink; 15 | 16 | @Test 17 | void shouldUseChunkingSink() { 18 | assertThat(sink).isInstanceOf(ChunkingSink.class); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/WriteCustomTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.test.context.bean.override.mockito.MockitoBean; 6 | import org.zalando.logbook.HttpLogWriter; 7 | import org.zalando.logbook.HttpRequest; 8 | import org.zalando.logbook.Logbook; 9 | import org.zalando.logbook.test.MockHttpRequest; 10 | 11 | import java.io.IOException; 12 | 13 | import static org.mockito.Mockito.verify; 14 | 15 | @LogbookTest 16 | class WriteCustomTest { 17 | 18 | @Autowired 19 | private Logbook logbook; 20 | 21 | @MockitoBean 22 | private HttpLogWriter writer; 23 | 24 | @Test 25 | void shouldUseCustomWriter() throws IOException { 26 | final HttpRequest request = MockHttpRequest.create(); 27 | 28 | logbook.process(request).write(); 29 | 30 | verify(writer).isActive(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/java/org/zalando/logbook/autoconfigure/WriteNoBodyMaxSizeTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.autoconfigure; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Qualifier; 6 | import org.zalando.logbook.BodyFilter; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | @LogbookTest 11 | class WriteNoBodyMaxSizeTest { 12 | 13 | @Autowired 14 | @Qualifier("bodyFilter") 15 | private BodyFilter bodyFilter; 16 | 17 | @Test 18 | void shouldNotUseBodyMaxSizeFilter() { 19 | final String body = "{\"open_id\":\"someLongSecret\",\"foo\":\"bar\"}"; 20 | final String filtered = bodyFilter.filter("application/json", body); 21 | assertThat(filtered).isEqualTo("{\"open_id\":\"XXX\",\"foo\":\"bar\"}"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application-body_fields.yml: -------------------------------------------------------------------------------- 1 | logbook: 2 | obfuscate: 3 | json-body-fields: 4 | - first_name 5 | - last_name 6 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application-claim-extractor-all-matching.yaml: -------------------------------------------------------------------------------- 1 | logbook: 2 | attribute-extractors: 3 | - type: JwtAllMatchingClaimsExtractor 4 | claim-names: [ "iss", "iat" ] 5 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application-claim-extractor-composite.yaml: -------------------------------------------------------------------------------- 1 | logbook: 2 | # some keys are omitted to test default values 3 | attribute-extractors: 4 | - type: JwtFirstMatchingClaimExtractor 5 | - type: JwtAllMatchingClaimsExtractor 6 | claim-names: [ "sub", "iat" ] 7 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application-claim-extractor-first-matching.yaml: -------------------------------------------------------------------------------- 1 | logbook: 2 | attribute-extractors: 3 | - type: JwtFirstMatchingClaimExtractor 4 | claim-names: [ "sub", "subject" ] 5 | claim-key: Principal 6 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application-deprecated-exclude.yml: -------------------------------------------------------------------------------- 1 | logbook: 2 | exclude: 3 | - /admin/** 4 | - /another-api 5 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application-deprecated-include.yml: -------------------------------------------------------------------------------- 1 | logbook: 2 | include: 3 | - /api/** 4 | - /another-api 5 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application-exclude.yml: -------------------------------------------------------------------------------- 1 | logbook: 2 | exclude: 3 | - /admin/** 4 | predicate: 5 | exclude: 6 | - path: /another-api 7 | methods: 8 | - PUT 9 | - path: /yet-another-api 10 | - methods: 11 | - DELETE 12 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application-headers.yml: -------------------------------------------------------------------------------- 1 | logbook: 2 | format: 3 | style: http 4 | obfuscate: 5 | headers: 6 | - Authorization 7 | - X-Access-Token 8 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application-include.yml: -------------------------------------------------------------------------------- 1 | logbook: 2 | include: 3 | - /api/** 4 | predicate: 5 | include: 6 | - path: /another-api 7 | - path: /yet-another-api 8 | methods: 9 | - DELETE 10 | - methods: 11 | - PUT 12 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application-parameters.yml: -------------------------------------------------------------------------------- 1 | logbook: 2 | obfuscate: 3 | parameters: 4 | - q 5 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application-paths.yml: -------------------------------------------------------------------------------- 1 | logbook: 2 | obfuscate: 3 | paths: 4 | - /a/{secret}/c 5 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application-replacement.yaml: -------------------------------------------------------------------------------- 1 | logbook: 2 | obfuscate: 3 | json-body-fields: 4 | - name 5 | replacement: ZZZ 6 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application-truncation_order.yml: -------------------------------------------------------------------------------- 1 | logbook: 2 | obfuscate: 3 | json-body-fields: 4 | - first_name 5 | - last_name 6 | write: 7 | max-body-size: 20 8 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring.main.banner-mode: "off" 2 | -------------------------------------------------------------------------------- /logbook-spring-boot-autoconfigure/src/test/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | ### 2 | # #%L 3 | # Logbook: Spring 4 | # %% 5 | # Copyright (C) 2015 Zalando SE 6 | # %% 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # #L% 19 | ### 20 | org.slf4j.simpleLogger.defaultLogLevel = error -------------------------------------------------------------------------------- /logbook-spring-boot-webflux-autoconfigure/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration = org.zalando.logbook.autoconfigure.webflux.LogbookWebFluxAutoConfiguration -------------------------------------------------------------------------------- /logbook-spring-boot-webflux-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | org.zalando.logbook.autoconfigure.webflux.LogbookWebFluxAutoConfiguration 2 | -------------------------------------------------------------------------------- /logbook-spring-webflux/src/main/java/org/zalando/logbook/spring/webflux/BufferingClientHttpRequest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.spring.webflux; 2 | 3 | import org.reactivestreams.Publisher; 4 | import org.springframework.core.io.buffer.DataBuffer; 5 | import org.springframework.http.client.reactive.ClientHttpRequest; 6 | import org.springframework.http.client.reactive.ClientHttpRequestDecorator; 7 | import reactor.core.publisher.Mono; 8 | 9 | @SuppressWarnings({"NullableProblems"}) 10 | final class BufferingClientHttpRequest extends ClientHttpRequestDecorator { 11 | private final ClientRequest clientRequest; 12 | 13 | BufferingClientHttpRequest(ClientHttpRequest delegate, ClientRequest clientRequest) { 14 | super(delegate); 15 | this.clientRequest = clientRequest; 16 | } 17 | 18 | @Override 19 | public Mono writeWith(Publisher body) { 20 | return super.writeWith(bufferingWrap(body)); 21 | } 22 | 23 | private Publisher bufferingWrap(Publisher body) { 24 | if (clientRequest.shouldBuffer()) { 25 | return DataBufferCopyUtils.wrapAndBuffer(body, clientRequest::buffer); 26 | } else { 27 | return body; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /logbook-spring-webflux/src/main/java/org/zalando/logbook/spring/webflux/BufferingServerHttpResponse.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.spring.webflux; 2 | 3 | import org.reactivestreams.Publisher; 4 | import org.springframework.core.io.buffer.DataBuffer; 5 | import org.springframework.http.server.reactive.ServerHttpResponse; 6 | import org.springframework.http.server.reactive.ServerHttpResponseDecorator; 7 | import reactor.core.publisher.Mono; 8 | 9 | 10 | @SuppressWarnings({"NullableProblems"}) 11 | class BufferingServerHttpResponse extends ServerHttpResponseDecorator { 12 | private final ServerResponse serverResponse; 13 | 14 | BufferingServerHttpResponse(ServerHttpResponse delegate, ServerResponse serverResponse, Runnable writeHook) { 15 | super(delegate); 16 | this.serverResponse = serverResponse; 17 | beforeCommit(() -> { 18 | writeHook.run(); 19 | return Mono.empty(); 20 | }); 21 | } 22 | 23 | @Override 24 | public Mono writeWith(Publisher body) { 25 | return super.writeWith(bufferingWrap(body)); 26 | } 27 | 28 | private Publisher bufferingWrap(Publisher body) { 29 | if (serverResponse.shouldBuffer()) { 30 | return DataBufferCopyUtils.wrapAndBuffer(body, serverResponse::buffer); 31 | } else { 32 | return body; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /logbook-spring-webflux/src/main/java/org/zalando/logbook/spring/webflux/DataBufferCopyUtils.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.spring.webflux; 2 | 3 | import lombok.experimental.UtilityClass; 4 | import org.reactivestreams.Publisher; 5 | import org.springframework.core.io.buffer.DataBuffer; 6 | import org.springframework.core.io.buffer.DataBufferUtils; 7 | import org.springframework.core.io.buffer.DefaultDataBuffer; 8 | import org.springframework.core.io.buffer.DefaultDataBufferFactory; 9 | 10 | import java.util.function.Consumer; 11 | 12 | @UtilityClass 13 | class DataBufferCopyUtils { 14 | 15 | Publisher wrapAndBuffer(Publisher body, Consumer copyConsumer) { 16 | return DataBufferUtils 17 | .join(body) 18 | .defaultIfEmpty(DefaultDataBufferFactory.sharedInstance.wrap(new byte[0])) 19 | .map(dataBuffer -> { 20 | byte[] bytes = new byte[dataBuffer.readableByteCount()]; 21 | dataBuffer.read(bytes); 22 | DataBufferUtils.release(dataBuffer); 23 | DefaultDataBuffer wrappedDataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(bytes); 24 | copyConsumer.accept(bytes); 25 | return wrappedDataBuffer; 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /logbook-spring-webflux/src/test/java/org/zalando/logbook/spring/webflux/BufferingClientHttpRequestUnitTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.spring.webflux; 2 | 3 | 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.http.client.reactive.ClientHttpRequest; 6 | import reactor.core.publisher.Mono; 7 | 8 | import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; 9 | import static org.mockito.ArgumentMatchers.any; 10 | import static org.mockito.Mockito.mock; 11 | import static org.mockito.Mockito.when; 12 | 13 | public class BufferingClientHttpRequestUnitTest { 14 | 15 | @Test 16 | void shouldNotBufferIfNotNeeded() { 17 | ClientHttpRequest clientHttpRequest = mock(ClientHttpRequest.class); 18 | org.springframework.web.reactive.function.client.ClientRequest springClientRequest = mock(org.springframework.web.reactive.function.client.ClientRequest.class); 19 | ClientRequest clientRequest = new ClientRequest(springClientRequest); 20 | clientRequest.withoutBody(); 21 | 22 | when(clientHttpRequest.writeWith(any())).thenReturn(Mono.empty()); 23 | 24 | BufferingClientHttpRequest request = new BufferingClientHttpRequest(clientHttpRequest, clientRequest); 25 | Exception ex = null; 26 | try { 27 | request.writeWith(Mono.empty()).block(); 28 | } catch (Exception e) { 29 | ex = e; 30 | } 31 | 32 | assertThat(ex).isNull(); 33 | } 34 | } -------------------------------------------------------------------------------- /logbook-spring-webflux/src/test/java/org/zalando/logbook/spring/webflux/BufferingServerHttpResponseUnitTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.spring.webflux; 2 | 3 | 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.http.server.reactive.ServerHttpResponse; 6 | import reactor.core.publisher.Mono; 7 | 8 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException; 9 | import static org.mockito.ArgumentMatchers.any; 10 | import static org.mockito.Mockito.mock; 11 | import static org.mockito.Mockito.when; 12 | 13 | public class BufferingServerHttpResponseUnitTest { 14 | 15 | @Test 16 | void shouldNotBufferIfNotNeeded() { 17 | ServerHttpResponse serverHttpResponse = mock(ServerHttpResponse.class); 18 | ServerResponse serverResponse = new ServerResponse(serverHttpResponse); 19 | serverResponse.withoutBody(); 20 | 21 | when(serverHttpResponse.writeWith(any())).thenReturn(Mono.empty()); 22 | 23 | BufferingServerHttpResponse response = new BufferingServerHttpResponse(serverHttpResponse, serverResponse, () -> {}); 24 | response.writeWith(Mono.empty()).block(); 25 | assertThatNoException(); 26 | } 27 | } -------------------------------------------------------------------------------- /logbook-spring-webflux/src/test/java/org/zalando/logbook/spring/webflux/ClientRequestUnitTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.spring.webflux; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.net.URI; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.mockito.Mockito.mock; 12 | import static org.mockito.Mockito.when; 13 | 14 | 15 | public class ClientRequestUnitTest { 16 | 17 | @Test 18 | void shouldBeEmptyIfPortIsNegative() { 19 | org.springframework.web.reactive.function.client.ClientRequest mock = mock(org.springframework.web.reactive.function.client.ClientRequest.class); 20 | when(mock.url()).thenReturn(URI.create("https://example.com:-1")); 21 | 22 | ClientRequest request = new ClientRequest(mock); 23 | assertThat(request.getPort()).isEmpty(); 24 | } 25 | 26 | @Test 27 | void shouldReturnAttributesIfPresent() { 28 | Map expectedAttributes = new HashMap<>(); 29 | expectedAttributes.put("foo", "bar"); 30 | org.springframework.web.reactive.function.client.ClientRequest mock = mock(org.springframework.web.reactive.function.client.ClientRequest.class); 31 | when(mock.attributes()).thenReturn(expectedAttributes); 32 | 33 | ClientRequest request = new ClientRequest(mock); 34 | assertEquals(expectedAttributes, request.getAttributes()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /logbook-spring-webflux/src/test/java/org/zalando/logbook/spring/webflux/ServerRequestUnitTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.spring.webflux; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.http.server.reactive.ServerHttpRequest; 5 | 6 | import java.net.URI; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.mockito.Mockito.mock; 10 | import static org.mockito.Mockito.when; 11 | 12 | 13 | public class ServerRequestUnitTest { 14 | 15 | @Test 16 | void shouldBeEmptyIfPortIsNegative() { 17 | ServerHttpRequest mock = mock(ServerHttpRequest.class); 18 | when(mock.getURI()).thenReturn(URI.create("https://example.com:-1")); 19 | 20 | ServerRequest request = new ServerRequest(mock); 21 | assertThat(request.getPort()).isEmpty(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /logbook-spring-webflux/src/test/java/org/zalando/logbook/spring/webflux/StateUnitTest.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.spring.webflux; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.nio.charset.StandardCharsets; 6 | import java.util.concurrent.atomic.AtomicReference; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | 11 | public class StateUnitTest { 12 | 13 | @Test 14 | void shouldBufferWhenIgnoring() { 15 | AtomicReference state = new AtomicReference<>(new State.Offering()); 16 | state.updateAndGet(State::without); 17 | state.updateAndGet(s -> s.buffer(new byte[0])); 18 | state.updateAndGet(State::with); 19 | state.updateAndGet(s -> s.buffer(new byte[0])); 20 | state.updateAndGet(State::without); 21 | state.updateAndGet(s -> s.buffer("Hello, world!".getBytes(StandardCharsets.UTF_8))); 22 | assertThat(new String(state.get().getBody(), StandardCharsets.UTF_8)).isEqualTo("Hello, world!"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /logbook-spring/src/main/java/org/zalando/logbook/spring/ByteStreams.java: -------------------------------------------------------------------------------- 1 | package org.zalando.logbook.spring; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.util.Objects; 8 | 9 | final class ByteStreams { 10 | 11 | private ByteStreams() { 12 | 13 | } 14 | 15 | static byte[] toByteArray(final InputStream in) throws IOException { 16 | final ByteArrayOutputStream out = new ByteArrayOutputStream(); 17 | copy(in, out); 18 | return out.toByteArray(); 19 | } 20 | 21 | static void copy(final InputStream from, final OutputStream to) throws IOException { 22 | Objects.requireNonNull(from); 23 | Objects.requireNonNull(to); 24 | 25 | final byte[] buf = new byte[4096]; 26 | int bytesRead; 27 | 28 | while ((bytesRead = from.read(buf)) != -1) { 29 | to.write(buf, 0, bytesRead); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /logbook-test/src/main/java/org/zalando/logbook/test/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNonnullByDefault 2 | package org.zalando.logbook.test; 3 | 4 | import javax.annotation.ParametersAreNonnullByDefault; 5 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation = true 2 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | 5 | : "${1?"Usage: $0 <[pre]major|[pre]minor|[pre]patch|prerelease>"}" 6 | : "${CHANGELOG_GITHUB_TOKEN?"Needs CHANGELOG_GITHUB_TOKEN env var (access token with repo scopes)"}" 7 | 8 | ./mvnw scm:check-local-modification 9 | 10 | [ "$1" == "prerelease" ] && versionsuffix="" || versionsuffix="-" 11 | current=$({ echo 0.0.0; git -c "versionsort.suffix=${versionsuffix}" tag --list --sort=version:refname; } | tail -n1) 12 | release=$(semver "${current}" -i "$1" --preid RC) 13 | next=$(semver "${release}" -i minor) 14 | 15 | ./mvnw versions:set -D newVersion="${release}" 16 | 17 | docker run -it --rm -e CHANGELOG_GITHUB_TOKEN -v "$(pwd)":/usr/local/src/your-app \ 18 | githubchangeloggenerator/github-changelog-generator \ 19 | -u zalando -p logbook \ 20 | --future-release ${release} \ 21 | --exclude-labels "duplicate,question,invalid,wontfix,stale,not-a-bug" 22 | 23 | git commit -am "Release ${release}" 24 | 25 | ./mvnw clean deploy scm:tag -P release -D tag="${release}" -D pushChanges=false -D skipTests -D dependency-check.skip 26 | 27 | ./mvnw versions:set -D newVersion="${next}-SNAPSHOT" 28 | 29 | git commit -am "Development ${next}-SNAPSHOT" 30 | 31 | git push --atomic origin main "${release}" 32 | --------------------------------------------------------------------------------