-
Notifications
You must be signed in to change notification settings - Fork 41
Add cloud logging service feature #1847
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
7220532
bb7fc04
4a2a63a
a42c170
9558829
7c9bf29
8e157bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package org.cloudfoundry.multiapps.controller.core.auditlogging; | ||
|
|
||
| import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; | ||
|
|
||
| public interface CloudLoggingServiceConfigurationAuditLog { | ||
|
|
||
| void logCreateLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); | ||
|
|
||
|
|
||
| void logUpdateLoggingConfiguration(String username, String spaceId, LoggingConfiguration newConfiguration); | ||
|
|
||
|
|
||
| void logDeleteLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); | ||
|
|
||
|
|
||
| void logGetLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package org.cloudfoundry.multiapps.controller.core.auditlogging.impl; | ||
|
|
||
| import org.cloudfoundry.multiapps.controller.core.Messages; | ||
| import org.cloudfoundry.multiapps.controller.core.auditlogging.AuditLoggingFacade; | ||
| import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; | ||
| import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; | ||
| import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions; | ||
| import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; | ||
|
|
||
| import static org.apache.commons.lang3.StringUtils.EMPTY; | ||
|
|
||
| public class DefaultCloudLoggingServiceConfigurationAuditLog implements CloudLoggingServiceConfigurationAuditLog { | ||
|
|
||
| private final AuditLoggingFacade auditLoggingFacade; | ||
|
|
||
| public DefaultCloudLoggingServiceConfigurationAuditLog(AuditLoggingFacade auditLoggingFacade) { | ||
| this.auditLoggingFacade = auditLoggingFacade; | ||
| } | ||
|
|
||
| @Override | ||
| public void logCreateLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration) { | ||
| auditLoggingFacade.logConfigurationChangeAuditLog( | ||
| createAuditLogConfiguration(Messages.LOGGING_CONFIGURATION_CREATE_AUDIT_LOG_CONFIG), | ||
| ConfigurationChangeActions.CONFIGURATION_CREATE); | ||
| } | ||
|
|
||
| @Override | ||
| public void logUpdateLoggingConfiguration(String username, String spaceId, LoggingConfiguration newConfiguration) { | ||
| auditLoggingFacade.logConfigurationChangeAuditLog( | ||
| createAuditLogConfiguration(Messages.LOGGING_CONFIGURATION_UPDATE_AUDIT_LOG_CONFIG), | ||
| ConfigurationChangeActions.CONFIGURATION_UPDATE); | ||
| } | ||
|
|
||
| @Override | ||
| public void logDeleteLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration) { | ||
| auditLoggingFacade.logConfigurationChangeAuditLog( | ||
| createAuditLogConfiguration(Messages.LOGGING_CONFIGURATION_DELETE_AUDIT_LOG_CONFIG), | ||
| ConfigurationChangeActions.CONFIGURATION_DELETE); | ||
| } | ||
|
|
||
| @Override | ||
| public void logGetLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration) { | ||
| auditLoggingFacade.logDataAccessAuditLog(createAuditLogConfiguration(Messages.LOGGING_CONFIGURATION_GET_AUDIT_LOG_CONFIG)); | ||
| } | ||
|
|
||
| private AuditLogConfiguration createAuditLogConfiguration(String message) { | ||
| return new AuditLogConfiguration(EMPTY, | ||
| EMPTY, | ||
| message, | ||
| Messages.LOGGING_CONFIGURATION_GET_AUDIT_LOG_CONFIG); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| package org.cloudfoundry.multiapps.controller.core.cloudlogging; | ||
|
|
||
| import java.io.IOException; | ||
| import java.text.MessageFormat; | ||
| import java.time.Duration; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import java.util.concurrent.TimeoutException; | ||
|
|
||
| import jakarta.inject.Inject; | ||
| import jakarta.inject.Named; | ||
| import org.cloudfoundry.multiapps.common.util.JsonUtil; | ||
| import org.cloudfoundry.multiapps.controller.persistence.Messages; | ||
| import org.cloudfoundry.multiapps.controller.persistence.model.ExternalOperationLogEntry; | ||
| import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; | ||
| import org.cloudfoundry.multiapps.controller.persistence.util.CloudLoggingServiceUtil; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.reactive.function.client.WebClient; | ||
| import org.springframework.web.reactive.function.client.WebClientException; | ||
| import org.springframework.web.reactive.function.client.WebClientRequestException; | ||
| import org.springframework.web.reactive.function.client.WebClientResponseException; | ||
| import reactor.core.Exceptions; | ||
| import reactor.netty.http.client.PrematureCloseException; | ||
| import reactor.util.retry.Retry; | ||
|
|
||
| import static org.springframework.http.HttpHeaders.CONTENT_TYPE; | ||
| import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; | ||
|
|
||
| @Named("cloudLoggingServiceHttpClient") | ||
| public class CloudLoggingServiceHttpClient { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This class is a HttpClient and some methods expect passing a webClient. Consider more encapsulation. |
||
|
|
||
| private static final Logger LOGGER = LoggerFactory.getLogger(CloudLoggingServiceHttpClient.class); | ||
| private static final int MAX_RETRY_ATTEMPTS = 4; | ||
| private static final Duration INITIAL_RETRY_BACKOFF = Duration.ofMillis(500); | ||
| private static final Duration MAX_RETRY_BACKOFF = Duration.ofSeconds(10); | ||
| private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(30); | ||
| private static final Set<Integer> RETRYABLE_STATUS_CODES = Set.of(408, 425, 429, 500, 502, 503, 504); | ||
|
|
||
| private final CloudLoggingServiceWebClientFactory webClientFactory; | ||
| private final CloudLoggingServiceWebClientCache webClientCache; | ||
|
|
||
| public CloudLoggingServiceHttpClient() { | ||
| this(new CloudLoggingServiceWebClientFactory(), new CloudLoggingServiceWebClientCache()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these classes are beans and still created with new?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is for the unit tests |
||
| } | ||
|
|
||
| @Inject | ||
| public CloudLoggingServiceHttpClient(CloudLoggingServiceWebClientFactory webClientFactory, | ||
| CloudLoggingServiceWebClientCache webClientCache) { | ||
| this.webClientFactory = webClientFactory; | ||
| this.webClientCache = webClientCache; | ||
| } | ||
|
|
||
| public void sendLogs(LoggingConfiguration loggingConfiguration, List<ExternalOperationLogEntry> logEntryBatch) { | ||
| WebClient webClient = webClientCache.getOrCreate(loggingConfiguration, this::createWebClientWithMtls); | ||
| sendLogsToCloudLoggingService(loggingConfiguration, webClient, logEntryBatch); | ||
| } | ||
|
|
||
| public void removeClientFromCache(String operationId) { | ||
| webClientCache.remove(operationId); | ||
| } | ||
|
|
||
| public void sendLogsToCloudLoggingService(LoggingConfiguration loggingConfiguration, WebClient webClient, | ||
| List<ExternalOperationLogEntry> logEntryBatch) { | ||
| try { | ||
| ResponseEntity<Void> response = executeSendLogHttpRequest(webClient, logEntryBatch); | ||
| if (hasRequestFailed(response)) { | ||
| CloudLoggingServiceUtil.logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, LOGGER, | ||
| Messages.FAILED_TO_SEND_LOG_MESSAGE_TO_CLS); | ||
| } | ||
| } catch (WebClientException e) { | ||
| if (!isTransportFailure(Exceptions.unwrap(e))) { | ||
| throw e; | ||
| } | ||
| handleSendLogFailure(loggingConfiguration, e); | ||
| } | ||
| } | ||
|
|
||
| private boolean isTransportFailure(Throwable throwable) { | ||
| return throwable instanceof IOException || throwable instanceof TimeoutException; | ||
| } | ||
|
|
||
| private void handleSendLogFailure(LoggingConfiguration loggingConfiguration, Throwable failure) { | ||
| CloudLoggingServiceUtil.logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, LOGGER, | ||
| Messages.FAILED_TO_SEND_LOG_MESSAGE_TO_CLS + ": " | ||
| + describeFailure(failure)); | ||
| } | ||
|
|
||
| public WebClient createWebClientWithMtls(LoggingConfiguration loggingConfiguration) { | ||
| return webClientFactory.createWebClientWithMtls(loggingConfiguration); | ||
| } | ||
|
|
||
| private ResponseEntity<Void> executeSendLogHttpRequest(WebClient webClient, List<ExternalOperationLogEntry> logEntryBatch) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this webClient be part of the class itself? |
||
| return webClient.post() | ||
| .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) | ||
| .bodyValue(JsonUtil.toJson(logEntryBatch)) | ||
| .retrieve() | ||
| .toBodilessEntity() | ||
| .timeout(REQUEST_TIMEOUT) | ||
| .retryWhen(buildRetrySpec()) | ||
| .block(); | ||
| } | ||
|
|
||
| private Retry buildRetrySpec() { | ||
| return Retry.backoff(MAX_RETRY_ATTEMPTS, INITIAL_RETRY_BACKOFF) | ||
| .maxBackoff(MAX_RETRY_BACKOFF) | ||
| .jitter(0.5d) | ||
| .filter(this::isRetryableError) | ||
| .doBeforeRetry(retrySignal -> LOGGER.warn(MessageFormat.format(Messages.RETRYING_SEND_LOGS_TO_CLS, | ||
| describeFailure(retrySignal.failure())))) | ||
| .onRetryExhaustedThrow((spec, retrySignal) -> retrySignal.failure()); | ||
| } | ||
|
|
||
| private boolean isRetryableError(Throwable throwable) { | ||
| if (throwable instanceof WebClientResponseException responseException) { | ||
| return RETRYABLE_STATUS_CODES.contains(responseException.getStatusCode() | ||
| .value()); | ||
| } | ||
| if (throwable instanceof WebClientRequestException) { | ||
| return true; | ||
| } | ||
| return throwable instanceof PrematureCloseException || throwable instanceof IOException; | ||
| } | ||
|
|
||
| private String describeFailure(Throwable throwable) { | ||
| if (throwable instanceof WebClientResponseException responseException) { | ||
| String retryAfter = responseException.getHeaders() | ||
| .getFirst(HttpHeaders.RETRY_AFTER); | ||
| return MessageFormat.format("HTTP {0} {1}{2}", responseException.getStatusCode() | ||
| .value(), | ||
| responseException.getStatusText(), | ||
| retryAfter != null ? " (Retry-After=" + retryAfter + ")" : ""); | ||
| } | ||
| return throwable.getClass() | ||
| .getSimpleName() + ": " + throwable.getMessage(); | ||
| } | ||
|
|
||
| private boolean hasRequestFailed(ResponseEntity<Void> response) { | ||
| if (response == null) { | ||
| return true; | ||
| } | ||
| int statusCode = response.getStatusCode() | ||
| .value(); | ||
| return statusCode < 200 || statusCode > 299; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it will be better if we move this class in the internal project (to be extended in the internal project)