From 28a603f83539deec9489267f8462744133b1700c Mon Sep 17 00:00:00 2001 From: Juan Vanecek Date: Tue, 2 Jul 2024 18:43:51 -0300 Subject: [PATCH] Add support to log incoming and outgoing http requests --- .../StargateApplicationTest.class.st | 196 +++++++++++++++++- .../StargateApplication.class.st | 78 ++++--- 2 files changed, 237 insertions(+), 37 deletions(-) diff --git a/source/Stargate-API-Skeleton-Tests/StargateApplicationTest.class.st b/source/Stargate-API-Skeleton-Tests/StargateApplicationTest.class.st index 0672be2..9a07e8a 100644 --- a/source/Stargate-API-Skeleton-Tests/StargateApplicationTest.class.st +++ b/source/Stargate-API-Skeleton-Tests/StargateApplicationTest.class.st @@ -6,7 +6,10 @@ Class { #superclass : 'LaunchpadTest', #instVars : [ 'port', - 'baseUrl' + 'baseUrl', + 'logIncomingHTTPRequests', + 'logOutgoingHTTPRequests', + 'logger' ], #category : 'Stargate-API-Skeleton-Tests', #package : 'Stargate-API-Skeleton-Tests' @@ -18,6 +21,22 @@ StargateApplicationTest class >> defaultTimeLimit [ ^5 minute ] +{ #category : 'private' } +StargateApplicationTest >> assert: aString isExpectedIncomingSummaryWith: aRequestShortDescription [ + + self assert: ( aString matchesRegex: + ( '\d{4}-\d{2}-\d{2} \d{2}\:\d{2}\:\d{2} ((\d+) (\d+))? Request Handled a\s{0,1}ZnRequest\(<1s>\) (\d+)ms' + expandMacrosWith: aRequestShortDescription ) ) +] + +{ #category : 'private' } +StargateApplicationTest >> assert: aString isExpectedOutgoingSummaryWith: aRequestShortDescription [ + + self assert: ( aString matchesRegex: + ( '\d{4}-\d{2}-\d{2} \d{2}\:\d{2}\:\d{2} ((\d+) (\d+))? <1s> (\d+) (\d+)B (\d+)ms' + expandMacrosWith: aRequestShortDescription ) ) +] + { #category : 'private' } StargateApplicationTest >> assert: string isLineEndingInsensitiveEqualsTo: anotherString [ @@ -85,8 +104,12 @@ StargateApplicationTest >> secret [ StargateApplicationTest >> setUp [ super setUp. + logger := MemoryLogger new. port := self freeListeningTCPPort. - StargateApplication logsDirectory ensureCreateDirectory + StargateApplication logsDirectory ensureCreateDirectory. + + logIncomingHTTPRequests := false. + logOutgoingHTTPRequests := false. ] { #category : 'private' } @@ -103,11 +126,16 @@ StargateApplicationTest >> startConcurrentConnectionsApp [ { #category : 'private' } StargateApplicationTest >> startPetStore [ - self start: PetStoreApplication withAll: { - '--pet-store.stargate.public-url=http://localhost:<1p>' expandMacrosWith: port. - '--pet-store.stargate.port=<1p>' expandMacrosWith: port. - '--pet-store.stargate.operations-secret=<1s>' expandMacrosWith: self secret }. - baseUrl := runningApplication configuration petStore stargate publicURL + self + start: PetStoreApplication + withAll: { ( '--pet-store.stargate.public-url=http://localhost:<1p>' expandMacrosWith: port ) . + ( '--pet-store.stargate.port=<1p>' expandMacrosWith: port ) . + ( '--pet-store.stargate.operations-secret=<1s>' expandMacrosWith: self secret ) . + ( '--pet-store.stargate.log-incoming-http-requests=<1p>' expandMacrosWith: + logIncomingHTTPRequests ) . + ( '--pet-store.stargate.log-outgoing-http-requests=<1p>' expandMacrosWith: + logOutgoingHTTPRequests ) }. + baseUrl := runningApplication configuration petStore stargate publicURL ] { #category : 'private' } @@ -264,6 +292,150 @@ StargateApplicationTest >> testGetPets [ assert: json links size equals: 1 ] +{ #category : 'tests - logs' } +StargateApplicationTest >> testLogIncomingRequestsDuringSuccessfulGet [ + + | logRecord | + logger runDuring: [ + logIncomingHTTPRequests := true. + self testGetPets + ]. + + logRecord := NeoJSONObject fromString: ( String streamContents: [ :stream | + ( logger recordings at: logger recordings size - 1 ) printOneLineJsonOn: + stream ] ). + + self + assert: logRecord level equals: 'DEBUG'; + assert: logRecord message equals: 'Incoming HTTP request responded'; + assert: logRecord process equals: 'ZnManagingMultiThreadedServer HTTP worker'; + assert: logRecord summary isExpectedIncomingSummaryWith: 'GET \/pets'; + assert: logRecord request method equals: 'GET'; + assert: logRecord request uri equals: ( 'http://localhost:<1p>/pets' expandMacrosWith: port ); + assert: logRecord response code equals: 200; + assert: logRecord response totalSize equals: 59 +] + +{ #category : 'tests - logs' } +StargateApplicationTest >> testLogIncomingRequestsDuringSucessfulPost [ + + | logRecord | + logger runDuring: [ + logIncomingHTTPRequests := true. + self testCreatePet + ]. + + logRecord := NeoJSONObject fromString: ( String streamContents: [ :stream | + ( logger recordings at: logger recordings size - 1 ) printOneLineJsonOn: + stream ] ). + + self + assert: logRecord level equals: 'DEBUG'; + assert: logRecord message equals: 'Incoming HTTP request responded'; + assert: logRecord process equals: 'ZnManagingMultiThreadedServer HTTP worker'; + assert: logRecord summary isExpectedIncomingSummaryWith: 'POST \/pets'; + assert: logRecord request method equals: 'POST'; + assert: logRecord request uri equals: ( 'http://localhost:<1p>/pets' expandMacrosWith: port ); + assert: logRecord response code equals: 201; + assert: logRecord response totalSize equals: 96 +] + +{ #category : 'tests - logs' } +StargateApplicationTest >> testLogIncomingRequestsDuringUnsucessfulPost [ + + | logRecord | + logger runDuring: [ + logIncomingHTTPRequests := true. + self testUnsupportedMediaType + ]. + + logRecord := NeoJSONObject fromString: ( String streamContents: [ :stream | + ( logger recordings at: logger recordings size - 1 ) printOneLineJsonOn: + stream ] ). + + self + assert: logRecord level equals: 'DEBUG'; + assert: logRecord message equals: 'Incoming HTTP request responded'; + assert: logRecord process equals: 'ZnManagingMultiThreadedServer HTTP worker'; + assert: logRecord summary isExpectedIncomingSummaryWith: 'POST \/pets'; + assert: logRecord request method equals: 'POST'; + assert: logRecord request uri equals: ( 'http://localhost:<1p>/pets' expandMacrosWith: port ); + assert: logRecord response code equals: 415; + assert: logRecord response totalSize equals: 72 +] + +{ #category : 'tests - logs' } +StargateApplicationTest >> testLogOutgoingRequestsDuringSucessfulGet [ + + | logRecord | + logger runDuring: [ + logOutgoingHTTPRequests := true. + self testGetPets + ]. + + logRecord := NeoJSONObject fromString: + ( String streamContents: [ :stream | + logger recordings last printOneLineJsonOn: stream ] ). + + self + assert: logRecord level equals: 'DEBUG'; + assert: logRecord message equals: 'Outgoing HTTP request responded'; + assert: logRecord process equals: 'Launchpad CLI'; + assert: logRecord summary isExpectedOutgoingSummaryWith: 'GET \/pets'; + assert: logRecord request method equals: 'GET'; + assert: logRecord request uri equals: ( 'http://localhost:<1p>/pets' expandMacrosWith: port ); + assert: logRecord response code equals: 200; + assert: logRecord response totalSize equals: 59 +] + +{ #category : 'tests - logs' } +StargateApplicationTest >> testLogOutgoingRequestsDuringSucessfulPost [ + + | logRecord | + logger runDuring: [ + logOutgoingHTTPRequests := true. + self testCreatePet + ]. + + logRecord := NeoJSONObject fromString: + ( String streamContents: [ :stream | + logger recordings last printOneLineJsonOn: stream ] ). + + self + assert: logRecord level equals: 'DEBUG'; + assert: logRecord message equals: 'Outgoing HTTP request responded'; + assert: logRecord process equals: 'Launchpad CLI'; + assert: logRecord summary isExpectedOutgoingSummaryWith: 'POST \/pets'; + assert: logRecord request method equals: 'POST'; + assert: logRecord request uri equals: ( 'http://localhost:<1p>/pets' expandMacrosWith: port ); + assert: logRecord response code equals: 201; + assert: logRecord response totalSize equals: 96 +] + +{ #category : 'tests - logs' } +StargateApplicationTest >> testLogOutgoingRequestsDuringUnsucessfulPost [ + + | logRecord | + logger runDuring: [ + logOutgoingHTTPRequests := true. + self testUnsupportedMediaType + ]. + + logRecord := NeoJSONObject fromString: + ( String streamContents: [ :stream | + logger recordings last printOneLineJsonOn: stream ] ). + + self + assert: logRecord level equals: 'DEBUG'; + assert: logRecord message equals: 'Outgoing HTTP request responded'; + assert: logRecord process equals: 'Launchpad CLI'; + assert: logRecord summary isExpectedOutgoingSummaryWith: 'POST \/pets'; + assert: logRecord request method equals: 'POST'; + assert: logRecord request uri equals: ( 'http://localhost:<1p>/pets' expandMacrosWith: port ); + assert: logRecord response code equals: 415; + assert: logRecord response totalSize equals: 72 +] + { #category : 'tests - api' } StargateApplicationTest >> testMethodNotAllowed [ @@ -320,7 +492,7 @@ StargateApplicationTest >> testPrintHelpOn [ self assert: help isLineEndingInsensitiveEqualsTo: ('NAME pet-store [<1s>] - A RESTful API for Pet stores SYNOPSYS - pet-store --pet-store.stargate.public-url=% --pet-store.stargate.port=% --pet-store.stargate.operations-secret=% [--pet-store.stargate.concurrent-connections-threshold=%] + pet-store --pet-store.stargate.public-url=% --pet-store.stargate.port=% --pet-store.stargate.operations-secret=% [--pet-store.stargate.log-incoming-http-requests=%] [--pet-store.stargate.log-outgoing-http-requests=%] [--pet-store.stargate.concurrent-connections-threshold=%] PARAMETERS --pet-store.stargate.public-url=% Public URL where the API is deployed. Used to create hypermedia links. @@ -328,6 +500,10 @@ PARAMETERS Listening port. --pet-store.stargate.operations-secret=% Secret key for checking JWT signatures. + --pet-store.stargate.log-incoming-http-requests=% + Boolean that indicates whether to log all the Incoming HTTP Requests. Defaults to false. + --pet-store.stargate.log-outgoing-http-requests=% + Boolean that indicates whether to log all the Outgoing HTTP Requests. Defaults to false. --pet-store.stargate.concurrent-connections-threshold=% Set the maximum number of concurrent connections that I will accept. When this threshold is reached, a 503 Service Unavailable response will be sent and the connection will be closed. Defaults to 32. ENVIRONMENT @@ -337,6 +513,10 @@ ENVIRONMENT Listening port. PET_STORE__STARGATE__OPERATIONS_SECRET Secret key for checking JWT signatures. + PET_STORE__STARGATE__LOG_INCOMING_HTTP_REQUESTS + Boolean that indicates whether to log all the Incoming HTTP Requests. Defaults to false. + PET_STORE__STARGATE__LOG_OUTGOING_HTTP_REQUESTS + Boolean that indicates whether to log all the Outgoing HTTP Requests. Defaults to false. PET_STORE__STARGATE__CONCURRENT_CONNECTIONS_THRESHOLD Set the maximum number of concurrent connections that I will accept. When this threshold is reached, a 503 Service Unavailable response will be sent and the connection will be closed. Defaults to 32. ' expandMacrosWith: PetStoreApplication version) diff --git a/source/Stargate-API-Skeleton/StargateApplication.class.st b/source/Stargate-API-Skeleton/StargateApplication.class.st index 2faa02f..dadb510 100644 --- a/source/Stargate-API-Skeleton/StargateApplication.class.st +++ b/source/Stargate-API-Skeleton/StargateApplication.class.st @@ -2,7 +2,8 @@ Class { #name : 'StargateApplication', #superclass : 'LaunchpadApplication', #instVars : [ - 'apiOptional' + 'apiOptional', + 'znEventToLogRecordAdapter' ], #classInstVars : [ 'Version' @@ -87,29 +88,42 @@ StargateApplication class >> stackTraceDumpExtension [ { #category : 'private' } StargateApplication class >> stargateConfigurationParameters [ - ^ Array - with: ( MandatoryConfigurationParameter - named: 'Public URL' - describedBy: 'Public URL where the API is deployed. Used to create hypermedia links' - inside: self sectionsForStargateConfiguration - convertingWith: #asUrl ) - with: ( MandatoryConfigurationParameter - named: 'Port' - describedBy: 'Listening port' - inside: self sectionsForStargateConfiguration - convertingWith: #asNumber ) - with: ( MandatoryConfigurationParameter - named: 'Operations Secret' - describedBy: 'Secret key for checking JWT signatures' - inside: self sectionsForStargateConfiguration - convertingWith: #asByteArray ) asSensitive - with: ( OptionalConfigurationParameter - named: 'Concurrent Connections Threshold' - describedBy: - 'Set the maximum number of concurrent connections that I will accept. When this threshold is reached, a 503 Service Unavailable response will be sent and the connection will be closed' - inside: self sectionsForStargateConfiguration - defaultingTo: 32 - convertingWith: #asNumber ) + ^ OrderedCollection new + add: ( MandatoryConfigurationParameter + named: 'Public URL' + describedBy: 'Public URL where the API is deployed. Used to create hypermedia links' + inside: self sectionsForStargateConfiguration + convertingWith: #asUrl ); + add: ( MandatoryConfigurationParameter + named: 'Port' + describedBy: 'Listening port' + inside: self sectionsForStargateConfiguration + convertingWith: #asNumber ); + add: ( MandatoryConfigurationParameter + named: 'Operations Secret' + describedBy: 'Secret key for checking JWT signatures' + inside: self sectionsForStargateConfiguration + convertingWith: #asByteArray ) asSensitive; + add: ( OptionalConfigurationParameter + named: 'Log Incoming HTTP Requests' + describedBy: 'Boolean that indicates whether to log all the Incoming HTTP Requests' + inside: self sectionsForStargateConfiguration + defaultingTo: false + convertingWith: #asBoolean ); + add: ( OptionalConfigurationParameter + named: 'Log Outgoing HTTP Requests' + describedBy: 'Boolean that indicates whether to log all the Outgoing HTTP Requests' + inside: self sectionsForStargateConfiguration + defaultingTo: false + convertingWith: #asBoolean ); + add: ( OptionalConfigurationParameter + named: 'Concurrent Connections Threshold' + describedBy: + 'Set the maximum number of concurrent connections that I will accept. When this threshold is reached, a 503 Service Unavailable response will be sent and the connection will be closed' + inside: self sectionsForStargateConfiguration + defaultingTo: 32 + convertingWith: #asNumber ); + yourself ] { #category : 'accessing' } @@ -177,8 +191,9 @@ StargateApplication >> authAlgorithm [ StargateApplication >> basicStartWithin: context [ | api | - + self logAPIVersion. + self configureHTTPRequestsLogging. api := self createAPI. self configureGlobalErrorHandlerIn: api; @@ -190,6 +205,7 @@ StargateApplication >> basicStartWithin: context [ StargateApplication >> basicStop [ apiOptional withContentDo: [ :api | api stop ]. + znEventToLogRecordAdapter stopListeners. super basicStop ] @@ -224,10 +240,13 @@ StargateApplication >> configureGlobalErrorHandlerIn: api [ ] unless: self isDebugModeEnabled ] -{ #category : 'private - accessing' } -StargateApplication >> controllersToInstall [ +{ #category : 'private - activation/deactivation' } +StargateApplication >> configureHTTPRequestsLogging [ - ^ self subclassResponsibility + znEventToLogRecordAdapter logOutgoingRequests: self stargateConfiguration logOutgoingHTTPRequests. + znEventToLogRecordAdapter logIncomingRequests: self stargateConfiguration logIncomingHTTPRequests. + + znEventToLogRecordAdapter startUpListeners ] { #category : 'private - activation/deactivation' } @@ -245,7 +264,8 @@ StargateApplication >> createAPI [ StargateApplication >> initialize [ super initialize. - apiOptional := Optional unused + apiOptional := Optional unused. + znEventToLogRecordAdapter := ZnEventToLogRecordAdapter new ] { #category : 'private - activation/deactivation' }