Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions multiapps-controller-core/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
exports org.cloudfoundry.multiapps.controller.core.cf.util;
exports org.cloudfoundry.multiapps.controller.core.cf.v2;
exports org.cloudfoundry.multiapps.controller.core.cf.v3;
exports org.cloudfoundry.multiapps.controller.core.cloudlogging;
exports org.cloudfoundry.multiapps.controller.core.configuration;
exports org.cloudfoundry.multiapps.controller.core.health;
exports org.cloudfoundry.multiapps.controller.core.health.model;
Expand Down Expand Up @@ -81,5 +82,7 @@

requires static java.compiler;
requires static org.immutables.value;
requires reactor.netty.http;
requires reactor.netty.core;

}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public final class Messages {
public static final String UNSUPPORTED_FILE_FORMAT = "Unsupported file format! \"{0}\" detected";
public static final String ENCRYPTION_HAS_FAILED = "Encryption has failed! Errored with \"{0}\"";
public static final String DECRYPTION_HAS_FAILED = "Decryption has failed! Errored with \"{0}\"";
public static final String COULD_NOT_CREATE_SSL_CONTEXT_FOR_CLOUD_LOGGING_SERVICE = "Could not create SSL context for Cloud Logging service";

// Warning messages
public static final String ENVIRONMENT_VARIABLE_IS_NOT_SET_USING_DEFAULT = "Environment variable \"{0}\" is not set. Using default \"{1}\"...";
Expand Down Expand Up @@ -287,6 +288,17 @@ public final class Messages {
public static final String ENTRY_CREATE_AUDIT_LOG_CONFIG = "Configuration entry create";
public static final String ENTRY_UPDATE_AUDIT_LOG_CONFIG = "Configuration entry update";

public static final String LOGGING_CONFIGURATION_CREATE = "Create cloud-logging-configuration in space with id: {0}";
public static final String LOGGING_CONFIGURATION_UPDATE = "Update cloud-logging-configuration in space with id: {0}";
public static final String LOGGING_CONFIGURATION_DELETE = "Delete cloud-logging-configuration in space with id: {0}";
public static final String LOGGING_CONFIGURATION_GET = "Get cloud-logging-configuration in space with id: {0}";
public static final String LOGGING_CONFIGURATION_LIST = "List cloud-logging-configurations in space with id: {0}";
public static final String LOGGING_CONFIGURATION_CREATE_AUDIT_LOG_CONFIG = "Cloud logging configuration create";
public static final String LOGGING_CONFIGURATION_UPDATE_AUDIT_LOG_CONFIG = "Cloud logging configuration update";
public static final String LOGGING_CONFIGURATION_DELETE_AUDIT_LOG_CONFIG = "Cloud logging configuration delete";
public static final String LOGGING_CONFIGURATION_GET_AUDIT_LOG_CONFIG = "Cloud logging configuration get";
public static final String LOGGING_CONFIGURATION_LIST_AUDIT_LOG_CONFIG = "Cloud logging configuration list";

public static final String API_INFO_AUDIT_LOG_CONFIG = "Api info";
public static final String IGNORING_NAMESPACE_PARAMETERS = "Ignoring parameter \"{0}\" , as the MTA is not deployed with namespace!";
public static final String NAMESPACE_PARSING_ERROR_MESSAGE = "Cannot parse \"{0}\" flag - expected a boolean format.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import javax.sql.DataSource;

import org.cloudfoundry.multiapps.controller.core.auditlogging.impl.AuditLoggingFacadeSLImpl;
import org.cloudfoundry.multiapps.controller.core.auditlogging.impl.DefaultCloudLoggingServiceConfigurationAuditLog;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

Expand Down Expand Up @@ -58,4 +59,9 @@ public ConfigurationSubscriptionServiceAuditLog buildAConfigurationSubscriptionS
public ConfigurationEntryServiceAuditLog buildAConfigurationEntryServiceAuditLog(AuditLoggingFacade auditLoggingFacade) {
return new ConfigurationEntryServiceAuditLog(auditLoggingFacade);
}

@Bean
public CloudLoggingServiceConfigurationAuditLog buildCloudLoggingServiceConfigurationAuditLog(AuditLoggingFacade auditLoggingFacade) {
return new DefaultCloudLoggingServiceConfigurationAuditLog(auditLoggingFacade);
}
}
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;

Copy link
Copy Markdown
Contributor

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)


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
Expand Up @@ -9,7 +9,8 @@

public enum ResourceType {
MANAGED_SERVICE("managed-service", SupportedParameters.SERVICE, SupportedParameters.SERVICE_PLAN), USER_PROVIDED_SERVICE(
"user-provided-service"), EXISTING_SERVICE("existing-service"), EXISTING_SERVICE_KEY("existing-service-key");
"user-provided-service"), EXISTING_SERVICE("existing-service"), EXISTING_SERVICE_KEY("existing-service-key"),
CLOUD_LOGGING_SERVICE("cloud-logging-service");

private final String name;
private final Set<String> requiredParameters = new HashSet<>();
Expand All @@ -33,7 +34,7 @@ public static ResourceType get(String value) {
}

public static Set<ResourceType> getServiceTypes() {
return EnumSet.of(MANAGED_SERVICE, USER_PROVIDED_SERVICE, EXISTING_SERVICE);
return EnumSet.of(MANAGED_SERVICE, USER_PROVIDED_SERVICE, EXISTING_SERVICE, CLOUD_LOGGING_SERVICE);
}

public Set<String> getRequiredParameters() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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.util.retry.Retry;

import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

@Named("cloudLoggingServiceClient")
public class CloudLoggingServiceClient {

private static final Logger LOGGER = LoggerFactory.getLogger(CloudLoggingServiceClient.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;
private Retry retrySpec;

public CloudLoggingServiceClient() {
this(new DefaultCloudLoggingServiceWebClientFactory(),
new CloudLoggingServiceWebClientCache());
}

@Inject
public CloudLoggingServiceClient(CloudLoggingServiceWebClientFactory webClientFactory,
CloudLoggingServiceWebClientCache webClientCache) {
this.webClientFactory = webClientFactory;
this.webClientCache = webClientCache;
}

public void withRetrySpec(Retry retrySpec) {
this.retrySpec = retrySpec;
}

public void removeClientFromCache(String operationId) {
webClientCache.remove(operationId);
}

public void sendLogsToCloudLoggingService(LoggingConfiguration loggingConfiguration,
List<ExternalOperationLogEntry> logEntryBatch) {
WebClient webClient;
try {
webClient = webClientCache.getOrCreate(loggingConfiguration, webClientFactory::createWebClientWithMtls);
} catch (Exception e) {
handleSendLogFailure(loggingConfiguration, e);
return;
}
try {
ResponseEntity<Void> response = executeSendLogHttpRequest(webClient, logEntryBatch);
if (hasRequestFailed(response)) {
CloudLoggingServiceUtil.logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, LOGGER,
Messages.FAILED_TO_SEND_LOG_MESSAGE_TO_CLS);
}
} catch (IllegalStateException | WebClientResponseException e) {
handleSendLogFailure(loggingConfiguration, e);
} catch (WebClientException e) {
if (isTransportFailure(Exceptions.unwrap(e))) {
handleSendLogFailure(loggingConfiguration, e);
} else {
throw 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));
}

private ResponseEntity<Void> executeSendLogHttpRequest(WebClient webClient, List<ExternalOperationLogEntry> logEntryBatch) {
Comment thread
Yavor16 marked this conversation as resolved.
return webClient.post()
.header(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.bodyValue(JsonUtil.toJson(logEntryBatch))
.retrieve()
.toBodilessEntity()
.timeout(REQUEST_TIMEOUT)
.retryWhen(retrySpec != null ? retrySpec : 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());
}

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 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;
}
}
Loading