From 7220532bdf1aacc091aae61c0b23b45d3f27f38b Mon Sep 17 00:00:00 2001 From: Yavor16 Date: Thu, 28 May 2026 09:50:50 +0300 Subject: [PATCH 1/2] Add cloud logging service feature # Conflicts: # multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn # multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn # multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/stop-dependent-modules.bpmn # multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn --- .../multiapps/controller/core/Messages.java | 11 + .../core/auditlogging/AuditLogBean.java | 5 + ...udLoggingServiceConfigurationAuditLog.java | 100 ++++ .../controller/core/cf/v2/ResourceType.java | 5 +- .../core/helpers/MtaConfigurationPurger.java | 21 +- .../ExternalLoggingServiceConfiguration.java | 47 ++ .../core/model/SupportedParameters.java | 5 +- .../termination/DataTerminationService.java | 16 + ...ggingServiceConfigurationAuditLogTest.java | 338 ++++++++++++ .../helpers/MtaConfigurationPurgerTest.java | 86 ++- .../DataTerminationServiceTest.java | 96 ++++ multiapps-controller-persistence/pom.xml | 4 + .../src/main/java/module-info.java | 4 + .../controller/persistence/Messages.java | 3 + .../model/ExternalOperationLogEntry.java | 32 ++ .../persistence/model/LogLevel.java | 36 ++ .../model/LoggingConfiguration.java | 63 +++ ...gingServiceConfigurationQueryProvider.java | 181 +++++++ .../SqlOperationLogQueryProvider.java | 8 +- ...oudLoggingServiceConfigurationService.java | 72 +++ .../services/OperationLogsExporter.java | 350 ++++++++++++ .../services/ProcessLoggerPersister.java | 67 ++- .../db-changelog-2.43.0-persistence.xml | 79 +++ .../persistence/db/changelog/db-changelog.xml | 4 + .../persistence/model/LogLevelTest.java | 87 +++ ...oggingServiceConfigurationServiceTest.java | 173 ++++++ .../services/OperationLogsExporterTest.java | 385 +++++++++++++ .../services/ProcessLoggerPersisterTest.java | 55 +- .../src/main/java/module-info.java | 5 + .../process/flowable/FlowableFacade.java | 11 +- .../AbstractProcessExecutionListener.java | 22 +- .../CreateUpdateServicesListener.java | 18 +- .../DeployAppSubProcessEndListener.java | 13 +- ...ineServiceCreateUpdateActionsListener.java | 8 +- .../DoNotDeleteServicesListener.java | 7 +- .../process/listeners/EndProcessListener.java | 11 +- .../EndProcessStatisticsListener.java | 7 +- .../listeners/EnterTestingPhaseListener.java | 8 +- ...portCloudLoggingConfigurationListener.java | 60 +++ .../listeners/HooksEndProcessListener.java | 9 +- .../listeners/LeaveTestingPhaseListener.java | 7 +- .../ManageAppServiceBindingEndListener.java | 8 +- .../listeners/StartProcessListener.java | 8 +- ...lectCloudLoggingServiceParametersStep.java | 188 +++++++ .../process/steps/ExecuteTaskStep.java | 5 +- .../IncrementalAppInstancesUpdateStep.java | 10 +- .../steps/PollExecuteAppStatusExecution.java | 8 +- .../steps/PollExecuteTaskStatusExecution.java | 8 +- .../steps/PollStageAppStatusExecution.java | 8 +- ...tartAppExecutionWithRollbackExecution.java | 6 +- .../steps/PollStartAppStatusExecution.java | 8 +- .../steps/PollStartLiveAppExecution.java | 6 +- ...erviceBrokerSubscriberStatusExecution.java | 6 +- .../process/steps/ProcessStepHelper.java | 35 +- .../process/steps/RestartAppStep.java | 7 +- .../RestartServiceBrokerSubscriberStep.java | 2 +- .../process/steps/StageAppStep.java | 5 +- .../controller/process/steps/StepsUtil.java | 12 +- .../process/steps/SyncFlowableStep.java | 12 +- .../steps/UploadAppAsyncExecution.java | 45 +- .../process/steps/UploadAppStep.java | 6 +- ...oggingServiceConfigurationsCalculator.java | 251 +++++++++ .../util/OperationInFinalStateHandler.java | 43 +- .../controller/process/util/StepLogger.java | 57 +- .../process/variables/Variables.java | 24 + .../process/backup-existing-app.bpmn | 2 + .../controller/process/delete-services.bpmn | 4 + .../controller/process/deploy-app.bpmn | 4 + .../process/process-batches-sequentially.bpmn | 2 + .../process/recreate-service-keys.bpmn | 2 + .../process/stop-dependent-modules.bpmn | 2 + .../controller/process/undeploy-app.bpmn | 3 + .../controller/process/xs2-bg-deploy.bpmn | 273 +++++----- .../controller/process/xs2-deploy.bpmn | 261 +++++---- .../controller/process/xs2-undeploy.bpmn | 508 +++++++++--------- .../listeners/EndProcessListenerTest.java | 10 +- .../EnterTestingPhaseListenerTest.java | 2 +- ...CloudLoggingConfigurationListenerTest.java | 166 ++++++ ...anageAppServiceBindingEndListenerTest.java | 7 +- .../listeners/StartProcessListenerTest.java | 22 +- .../IncrementalAppInstanceUpdateStepTest.java | 4 +- .../PollExecuteAppStatusExecutionTest.java | 5 +- ...ementalAppInstanceUpdateExecutionTest.java | 4 +- .../PollStageAppStatusExecutionTest.java | 5 +- ...AppExecutionWithRollbackExecutionTest.java | 6 +- .../PollStartAppStatusExecutionTest.java | 5 +- .../steps/PollStartLiveAppExecutionTest.java | 5 +- .../process/steps/ProcessStepHelperTest.java | 9 +- .../process/steps/SyncFlowableStepTest.java | 8 +- .../steps/UploadAppAsyncExecutionTest.java | 96 +++- ...ngServiceConfigurationsCalculatorTest.java | 354 ++++++++++++ .../OperationInFinalStateHandlerTest.java | 269 +++++++++- .../AsyncProcessLoggerConfiguration.java | 15 +- .../ConfigurationEntriesResource.java | 10 +- 94 files changed, 4664 insertions(+), 686 deletions(-) create mode 100644 multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLog.java create mode 100644 multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/ExternalLoggingServiceConfiguration.java create mode 100644 multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLogTest.java create mode 100644 multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/ExternalOperationLogEntry.java create mode 100644 multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevel.java create mode 100644 multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LoggingConfiguration.java create mode 100644 multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/CloudLoggingServiceConfigurationQueryProvider.java create mode 100644 multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationService.java create mode 100644 multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporter.java create mode 100644 multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog-2.43.0-persistence.xml create mode 100644 multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevelTest.java create mode 100644 multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationServiceTest.java create mode 100644 multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporterTest.java create mode 100644 multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListener.java create mode 100644 multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CollectCloudLoggingServiceParametersStep.java create mode 100644 multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/ExternalLoggingServiceConfigurationsCalculator.java create mode 100644 multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListenerTest.java create mode 100644 multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ExternalLoggingServiceConfigurationsCalculatorTest.java diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java index 6efb7e7b3d..f4cf21e959 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java @@ -287,6 +287,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."; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/AuditLogBean.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/AuditLogBean.java index 755f5a13f4..830d3212eb 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/AuditLogBean.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/AuditLogBean.java @@ -58,4 +58,9 @@ public ConfigurationSubscriptionServiceAuditLog buildAConfigurationSubscriptionS public ConfigurationEntryServiceAuditLog buildAConfigurationEntryServiceAuditLog(AuditLoggingFacade auditLoggingFacade) { return new ConfigurationEntryServiceAuditLog(auditLoggingFacade); } + + @Bean + public CloudLoggingServiceConfigurationAuditLog buildCloudLoggingServiceConfigurationAuditLog(AuditLoggingFacade auditLoggingFacade) { + return new CloudLoggingServiceConfigurationAuditLog(auditLoggingFacade); + } } diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLog.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLog.java new file mode 100644 index 0000000000..4310d0c894 --- /dev/null +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLog.java @@ -0,0 +1,100 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.cloudfoundry.multiapps.controller.core.Messages; +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; + +public class CloudLoggingServiceConfigurationAuditLog { + + private static final String ID_PROPERTY_NAME = "id"; + private static final String MTA_ID_PROPERTY_NAME = "mtaId"; + private static final String MTA_SPACE_PROPERTY_NAME = "mtaSpace"; + private static final String MTA_SPACE_ID_PROPERTY_NAME = "mtaSpaceId"; + private static final String MTA_ORG_PROPERTY_NAME = "mtaOrg"; + private static final String NAMESPACE_PROPERTY_NAME = "namespace"; + private static final String TARGET_SPACE_PROPERTY_NAME = "targetSpace"; + private static final String TARGET_ORG_PROPERTY_NAME = "targetOrg"; + private static final String SERVICE_INSTANCE_NAME_PROPERTY_NAME = "serviceInstanceName"; + private static final String SERVICE_KEY_NAME_PROPERTY_NAME = "serviceKeyName"; + private static final String LOG_LEVEL_PROPERTY_NAME = "logLevel"; + private static final String IS_FAILSAFE_PROPERTY_NAME = "isFailSafe"; + + private final AuditLoggingFacade auditLoggingFacade; + + public CloudLoggingServiceConfigurationAuditLog(AuditLoggingFacade auditLoggingFacade) { + this.auditLoggingFacade = auditLoggingFacade; + } + + public void logCreateLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration) { + String performedAction = MessageFormat.format(Messages.LOGGING_CONFIGURATION_CREATE, spaceId); + auditLoggingFacade.logConfigurationChangeAuditLog(new AuditLogConfiguration(username, + spaceId, + performedAction, + Messages.LOGGING_CONFIGURATION_CREATE_AUDIT_LOG_CONFIG, + buildIdentifiers(loggingConfiguration)), + ConfigurationChangeActions.CONFIGURATION_CREATE); + } + + public void logUpdateLoggingConfiguration(String username, String spaceId, LoggingConfiguration newConfiguration) { + String performedAction = MessageFormat.format(Messages.LOGGING_CONFIGURATION_UPDATE, spaceId); + auditLoggingFacade.logConfigurationChangeAuditLog(new AuditLogConfiguration(username, + spaceId, + performedAction, + Messages.LOGGING_CONFIGURATION_UPDATE_AUDIT_LOG_CONFIG, + buildIdentifiers(newConfiguration)), + ConfigurationChangeActions.CONFIGURATION_UPDATE); + } + + public void logDeleteLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration) { + String performedAction = MessageFormat.format(Messages.LOGGING_CONFIGURATION_DELETE, spaceId); + auditLoggingFacade.logConfigurationChangeAuditLog(new AuditLogConfiguration(username, + spaceId, + performedAction, + Messages.LOGGING_CONFIGURATION_DELETE_AUDIT_LOG_CONFIG, + buildIdentifiers(loggingConfiguration)), + ConfigurationChangeActions.CONFIGURATION_DELETE); + } + + public void logGetLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration) { + String performedAction = MessageFormat.format(Messages.LOGGING_CONFIGURATION_GET, spaceId); + Map identifiers = new HashMap<>(); + identifiers.put(MTA_ID_PROPERTY_NAME, loggingConfiguration.getMtaId()); + identifiers.put(NAMESPACE_PROPERTY_NAME, loggingConfiguration.getNamespace()); + auditLoggingFacade.logDataAccessAuditLog(new AuditLogConfiguration(username, + spaceId, + performedAction, + Messages.LOGGING_CONFIGURATION_GET_AUDIT_LOG_CONFIG, + identifiers)); + } + + public void logListLoggingConfigurations(String username, String spaceId) { + String performedAction = MessageFormat.format(Messages.LOGGING_CONFIGURATION_LIST, spaceId); + auditLoggingFacade.logDataAccessAuditLog(new AuditLogConfiguration(username, + spaceId, + performedAction, + Messages.LOGGING_CONFIGURATION_LIST_AUDIT_LOG_CONFIG)); + } + + private Map buildIdentifiers(LoggingConfiguration loggingConfiguration) { + Map identifiers = new HashMap<>(); + identifiers.put(ID_PROPERTY_NAME, loggingConfiguration.getId()); + identifiers.put(MTA_ID_PROPERTY_NAME, loggingConfiguration.getMtaId()); + identifiers.put(MTA_SPACE_PROPERTY_NAME, loggingConfiguration.getMtaSpace()); + identifiers.put(MTA_SPACE_ID_PROPERTY_NAME, loggingConfiguration.getMtaSpaceId()); + identifiers.put(MTA_ORG_PROPERTY_NAME, loggingConfiguration.getMtaOrg()); + identifiers.put(NAMESPACE_PROPERTY_NAME, loggingConfiguration.getNamespace()); + identifiers.put(TARGET_SPACE_PROPERTY_NAME, loggingConfiguration.getTargetSpace()); + identifiers.put(TARGET_ORG_PROPERTY_NAME, loggingConfiguration.getTargetOrg()); + identifiers.put(SERVICE_INSTANCE_NAME_PROPERTY_NAME, loggingConfiguration.getServiceInstanceName()); + identifiers.put(SERVICE_KEY_NAME_PROPERTY_NAME, loggingConfiguration.getServiceKeyName()); + identifiers.put(LOG_LEVEL_PROPERTY_NAME, Objects.toString(loggingConfiguration.getLogLevel())); + identifiers.put(IS_FAILSAFE_PROPERTY_NAME, Objects.toString(loggingConfiguration.isFailSafe())); + return identifiers; + } +} diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ResourceType.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ResourceType.java index 0846c93b82..00ad4686df 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ResourceType.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/cf/v2/ResourceType.java @@ -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 requiredParameters = new HashSet<>(); @@ -33,7 +34,7 @@ public static ResourceType get(String value) { } public static Set 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 getRequiredParameters() { diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurger.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurger.java index 060abf9fd4..b6771e7379 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurger.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurger.java @@ -12,6 +12,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudSpaceClient; import org.cloudfoundry.multiapps.controller.core.Messages; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.auditlogging.MtaConfigurationPurgerAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadata; import org.cloudfoundry.multiapps.controller.core.cf.metadata.processor.MtaMetadataParser; @@ -20,6 +21,8 @@ import org.cloudfoundry.multiapps.controller.persistence.model.CloudTarget; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationSubscription; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationEntryService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationSubscriptionService; import org.slf4j.Logger; @@ -37,17 +40,23 @@ public class MtaConfigurationPurger { private final ConfigurationEntryService configurationEntryService; private final ConfigurationSubscriptionService configurationSubscriptionService; private MtaMetadataParser mtaMetadataParser; + private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; public MtaConfigurationPurger(CloudControllerClient client, CloudSpaceClient spaceClient, ConfigurationEntryService configurationEntryService, ConfigurationSubscriptionService configurationSubscriptionService, MtaMetadataParser mtaMetadataParser, - MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog) { + MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog, + CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService, + CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog) { this.client = client; this.spaceClient = spaceClient; this.configurationEntryService = configurationEntryService; this.configurationSubscriptionService = configurationSubscriptionService; this.mtaMetadataParser = mtaMetadataParser; this.mtaConfigurationPurgerAuditLog = mtaConfigurationPurgerAuditLog; + this.cloudLoggingServiceConfigurationService = cloudLoggingServiceConfigurationService; + this.cloudLoggingServiceConfigurationAuditLog = cloudLoggingServiceConfigurationAuditLog; } public void purge(String org, String space) { @@ -56,6 +65,7 @@ public void purge(String org, String space) { List existingApps = getExistingApps(); purgeConfigurationSubscriptions(targetId, existingApps); purgeConfigurationEntries(targetSpace, existingApps, targetId); + purgeCloudLoggingServiceConfigurations(targetId); } private void purgeConfigurationSubscriptions(String spaceId, List existingApps) { @@ -96,6 +106,15 @@ private void purgeConfigurationEntries(CloudTarget targetSpace, List loggingConfigurations = cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace( + spaceId); + for (LoggingConfiguration loggingConfiguration : loggingConfigurations) { + cloudLoggingServiceConfigurationService.deleteCloudLoggingServiceConfiguration(loggingConfiguration.getId()); + cloudLoggingServiceConfigurationAuditLog.logDeleteLoggingConfiguration("", spaceId, loggingConfiguration); + } + } + private boolean isStillRelevant(List stillRelevantEntries, ConfigurationEntry entry) { return stillRelevantEntries.stream() .anyMatch(currentEntry -> haveSameProviderIdAndVersion(currentEntry, entry)); diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/ExternalLoggingServiceConfiguration.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/ExternalLoggingServiceConfiguration.java new file mode 100644 index 0000000000..eeb98f1267 --- /dev/null +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/ExternalLoggingServiceConfiguration.java @@ -0,0 +1,47 @@ +package org.cloudfoundry.multiapps.controller.core.model; + +import java.util.List; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cloudfoundry.multiapps.common.Nullable; +import org.immutables.value.Value; + +@Value.Immutable +@JsonSerialize(as = ImmutableExternalLoggingServiceConfiguration.class) +@JsonDeserialize(as = ImmutableExternalLoggingServiceConfiguration.class) +public interface ExternalLoggingServiceConfiguration { + + @Nullable + String getServiceInstanceName(); + + @Nullable + String getServiceKeyName(); + + @Nullable + String getTargetOrg(); + + @Nullable + String getTargetSpace(); + + @Nullable + String getOperationId(); + + @Nullable + String getEndpointUrl(); + + @Nullable + String getServerCa(); + + @Nullable + String getClientCert(); + + @Nullable + String getClientKey(); + + @Nullable + List getLogLevels(); + + @Nullable + Boolean isFailSafe(); +} diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java index 1e84c0acd4..7570ea7d6c 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java @@ -158,6 +158,8 @@ public class SupportedParameters { public static final String FAIL_ON_SERVICE_UPDATE = "fail-on-service-update"; public static final String SYSLOG_DRAIN_URL = "syslog-drain-url"; public static final String SERVICE_GUID = "service-guid"; + public static final String LOG_LEVEL = "log-level"; + public static final String DESTINATION = "destination"; // Configuration reference (new syntax): public static final String PROVIDER_NID = "provider-nid"; @@ -210,7 +212,8 @@ public class SupportedParameters { SERVICE_KEY_NAME, SERVICE_NAME, SERVICE_PLAN, SERVICE_TAGS, SERVICE_BROKER, SKIP_SERVICE_UPDATES, TYPE, PROVIDER_ID, PROVIDER_NID, TARGET, SERVICE_CONFIG_PATH, FILTER, MANAGED, VERSION, PATH, MEMORY, - FAIL_ON_SERVICE_UPDATE, SERVICE_PROVIDER, SERVICE_VERSION); + FAIL_ON_SERVICE_UPDATE, SERVICE_PROVIDER, SERVICE_VERSION, LOG_LEVEL, + DESTINATION); public static final Set GLOBAL_PARAMETERS = Set.of(KEEP_EXISTING_ROUTES, APPS_UPLOAD_TIMEOUT, APPS_TASK_EXECUTION_TIMEOUT, APPS_START_TIMEOUT, APPS_STAGE_TIMEOUT, APPLY_NAMESPACE, ENABLE_PARALLEL_DEPLOYMENTS, DEPLOY_MODE, BG_DEPENDENCY_AWARE_STOP_ORDER); diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationService.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationService.java index ffda6a00e0..e95788a1b0 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationService.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationService.java @@ -12,6 +12,7 @@ import org.cloudfoundry.multiapps.controller.api.model.Operation; import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; import org.cloudfoundry.multiapps.controller.core.Messages; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.auditlogging.MtaConfigurationPurgerAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.clients.CFOptimizedEventGetter; import org.cloudfoundry.multiapps.controller.core.cf.clients.WebClientFactory; @@ -21,6 +22,8 @@ import org.cloudfoundry.multiapps.controller.persistence.dto.BackupDescriptor; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationSubscription; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationEntryService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationSubscriptionService; import org.cloudfoundry.multiapps.controller.persistence.services.DescriptorBackupService; @@ -59,6 +62,10 @@ public class DataTerminationService { private MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog; @Inject private DescriptorBackupService descriptorBackupService; + @Inject + private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + @Inject + private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; private static void log(Exception e) { LOGGER.error(format(Messages.ERROR_DURING_DATA_TERMINATION_0, e.getMessage()), e); @@ -72,6 +79,7 @@ public void deleteOrphanUserData() { SAFE_EXECUTOR.execute(() -> deleteConfigurationEntryOrphanData(spaceId)); SAFE_EXECUTOR.execute(() -> deleteUserOperationsOrphanData(spaceId)); SAFE_EXECUTOR.execute(() -> deletedMtaDescriptorsOrphanData(spaceId)); + SAFE_EXECUTOR.execute(() -> deleteExistingCloudLoggingServiceConfiguration(spaceId)); } if (!spaceEventsToBeDeleted.isEmpty()) { SAFE_EXECUTOR.execute(() -> deleteSpaceIdsLeftovers(spaceEventsToBeDeleted)); @@ -161,4 +169,12 @@ private void deleteSpaceIdsLeftovers(List spaceIds) { } } + private void deleteExistingCloudLoggingServiceConfiguration(String spaceId) { + List loggingConfigurations = cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace( + spaceId); + for (LoggingConfiguration loggingConfiguration : loggingConfigurations) { + cloudLoggingServiceConfigurationService.deleteCloudLoggingServiceConfiguration(loggingConfiguration.getId()); + cloudLoggingServiceConfigurationAuditLog.logDeleteLoggingConfiguration("", spaceId, loggingConfiguration); + } + } } \ No newline at end of file diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLogTest.java new file mode 100644 index 0000000000..a407ac08bd --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLogTest.java @@ -0,0 +1,338 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.mta.model.ConfigurationIdentifier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.verify; + +class CloudLoggingServiceConfigurationAuditLogTest { + + private static final String USERNAME = "test-user"; + private static final String SPACE_ID = "space-guid-1"; + private static final String LOGGING_CONFIG_ID = "logging-config-1"; + private static final String MTA_ID = "my-mta"; + private static final String MTA_SPACE = "my-space"; + private static final String MTA_SPACE_ID = "mta-space-guid-1"; + private static final String MTA_ORG = "my-org"; + private static final String NAMESPACE = "dev"; + private static final String TARGET_SPACE = "target-space"; + private static final String TARGET_ORG = "target-org"; + private static final String SERVICE_INSTANCE_NAME = "my-cls-instance"; + private static final String SERVICE_KEY_NAME = "my-cls-key"; + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private CloudLoggingServiceConfigurationAuditLog auditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + auditLog = new CloudLoggingServiceConfigurationAuditLog(auditLoggingFacade); + } + + // --- logCreateLoggingConfiguration --- + + @Test + void testLogCreateLoggingConfiguration_invokesFacadeWithCreateAction() { + auditLog.logCreateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + verify(auditLoggingFacade).logConfigurationChangeAuditLog(any(AuditLogConfiguration.class), + eqAction(ConfigurationChangeActions.CONFIGURATION_CREATE)); + } + + @Test + void testLogCreateLoggingConfiguration_setsUserAndSpace() { + auditLog.logCreateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + AuditLogConfiguration captured = captureCreate(); + assertEquals(USERNAME, captured.getUserId()); + assertEquals(SPACE_ID, captured.getSpaceId()); + } + + @Test + void testLogCreateLoggingConfiguration_setsPerformedActionContainingSpaceId() { + auditLog.logCreateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + AuditLogConfiguration captured = captureCreate(); + assertTrue(captured.getPerformedAction() + .contains(SPACE_ID)); + } + + @Test + void testLogCreateLoggingConfiguration_includesAllIdentifiers() { + auditLog.logCreateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + Map identifiers = identifiersFromCreate(); + assertEquals(LOGGING_CONFIG_ID, identifiers.get("id")); + assertEquals(MTA_ID, identifiers.get("mtaId")); + assertEquals(MTA_SPACE, identifiers.get("mtaSpace")); + assertEquals(MTA_SPACE_ID, identifiers.get("mtaSpaceId")); + assertEquals(MTA_ORG, identifiers.get("mtaOrg")); + assertEquals(NAMESPACE, identifiers.get("namespace")); + assertEquals(TARGET_SPACE, identifiers.get("targetSpace")); + assertEquals(TARGET_ORG, identifiers.get("targetOrg")); + assertEquals(SERVICE_INSTANCE_NAME, identifiers.get("serviceInstanceName")); + assertEquals(SERVICE_KEY_NAME, identifiers.get("serviceKeyName")); + assertEquals("INFO", identifiers.get("logLevel")); + assertEquals("true", identifiers.get("isFailSafe")); + } + + @Test + void testLogCreateLoggingConfiguration_logLevelIsNullStringWhenLogLevelIsNull() { + LoggingConfiguration config = ImmutableLoggingConfiguration.builder() + .from(buildLoggingConfiguration()) + .logLevel(null) + .build(); + + auditLog.logCreateLoggingConfiguration(USERNAME, SPACE_ID, config); + + assertEquals("null", identifiersFromCreate().get("logLevel")); + } + + // --- logUpdateLoggingConfiguration --- + + @Test + void testLogUpdateLoggingConfiguration_invokesFacadeWithUpdateAction() { + auditLog.logUpdateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + verify(auditLoggingFacade).logConfigurationChangeAuditLog(any(AuditLogConfiguration.class), + eqAction(ConfigurationChangeActions.CONFIGURATION_UPDATE)); + } + + @Test + void testLogUpdateLoggingConfiguration_setsUserAndSpace() { + auditLog.logUpdateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + AuditLogConfiguration captured = captureUpdate(); + assertEquals(USERNAME, captured.getUserId()); + assertEquals(SPACE_ID, captured.getSpaceId()); + } + + @Test + void testLogUpdateLoggingConfiguration_includesAllIdentifiers() { + auditLog.logUpdateLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + Map identifiers = identifiersFromUpdate(); + assertEquals(LOGGING_CONFIG_ID, identifiers.get("id")); + assertEquals(MTA_ID, identifiers.get("mtaId")); + assertEquals(NAMESPACE, identifiers.get("namespace")); + } + + // --- logDeleteLoggingConfiguration --- + + @Test + void testLogDeleteLoggingConfiguration_invokesFacadeWithDeleteAction() { + auditLog.logDeleteLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + verify(auditLoggingFacade).logConfigurationChangeAuditLog(any(AuditLogConfiguration.class), + eqAction(ConfigurationChangeActions.CONFIGURATION_DELETE)); + } + + @Test + void testLogDeleteLoggingConfiguration_setsUserAndSpace() { + auditLog.logDeleteLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + AuditLogConfiguration captured = captureDelete(); + assertEquals(USERNAME, captured.getUserId()); + assertEquals(SPACE_ID, captured.getSpaceId()); + } + + @Test + void testLogDeleteLoggingConfiguration_includesAllIdentifiers() { + auditLog.logDeleteLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + Map identifiers = identifiersFromDelete(); + assertEquals(LOGGING_CONFIG_ID, identifiers.get("id")); + assertEquals(MTA_ID, identifiers.get("mtaId")); + assertEquals(NAMESPACE, identifiers.get("namespace")); + } + + @Test + void testLogDeleteLoggingConfiguration_omitsNullValuesFromConfigurationIdentifiers() { + LoggingConfiguration sparseConfig = ImmutableLoggingConfiguration.builder() + .id(LOGGING_CONFIG_ID) + .build(); + + auditLog.logDeleteLoggingConfiguration(USERNAME, SPACE_ID, sparseConfig); + + AuditLogConfiguration captured = captureDelete(); + Map identifiers = identifiersFromConfig(captured); + assertEquals(LOGGING_CONFIG_ID, identifiers.get("id")); + // null fields should not be exposed in getConfigurationIdentifiers + for (ConfigurationIdentifier identifier : captured.getConfigurationIdentifiers()) { + assertNotNull(identifier.getValue(), "Configuration identifier value should not be null"); + } + } + + // --- logGetLoggingConfiguration --- + + @Test + void testLogGetLoggingConfiguration_invokesDataAccessFacade() { + auditLog.logGetLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + verify(auditLoggingFacade).logDataAccessAuditLog(any(AuditLogConfiguration.class)); + } + + @Test + void testLogGetLoggingConfiguration_setsUserAndSpace() { + auditLog.logGetLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + AuditLogConfiguration captured = captureDataAccess(); + assertEquals(USERNAME, captured.getUserId()); + assertEquals(SPACE_ID, captured.getSpaceId()); + } + + @Test + void testLogGetLoggingConfiguration_includesMtaIdAndNamespaceOnly() { + auditLog.logGetLoggingConfiguration(USERNAME, SPACE_ID, buildLoggingConfiguration()); + + Map identifiers = identifiersFromDataAccess(); + assertEquals(MTA_ID, identifiers.get("mtaId")); + assertEquals(NAMESPACE, identifiers.get("namespace")); + // The "get" variant only logs mtaId and namespace + assertEquals(2, countNonReservedIdentifiers(captureDataAccess())); + } + + // --- logListLoggingConfigurations --- + + @Test + void testLogListLoggingConfigurations_invokesDataAccessFacade() { + auditLog.logListLoggingConfigurations(USERNAME, SPACE_ID); + + verify(auditLoggingFacade).logDataAccessAuditLog(any(AuditLogConfiguration.class)); + } + + @Test + void testLogListLoggingConfigurations_setsUserAndSpace() { + auditLog.logListLoggingConfigurations(USERNAME, SPACE_ID); + + AuditLogConfiguration captured = captureDataAccess(); + assertEquals(USERNAME, captured.getUserId()); + assertEquals(SPACE_ID, captured.getSpaceId()); + } + + @Test + void testLogListLoggingConfigurations_hasNoConfigurationParameters() { + auditLog.logListLoggingConfigurations(USERNAME, SPACE_ID); + + // List variant uses 3-argument constructor without parameters, so only base identifiers exist + AuditLogConfiguration captured = captureDataAccess(); + assertEquals(0, countNonReservedIdentifiers(captured)); + } + + @Test + void testLogListLoggingConfigurations_setsPerformedActionContainingSpaceId() { + auditLog.logListLoggingConfigurations(USERNAME, SPACE_ID); + + assertTrue(captureDataAccess().getPerformedAction() + .contains(SPACE_ID)); + } + + // --- Helpers --- + + private AuditLogConfiguration captureCreate() { + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + verify(auditLoggingFacade).logConfigurationChangeAuditLog(captor.capture(), + eqAction(ConfigurationChangeActions.CONFIGURATION_CREATE)); + return captor.getValue(); + } + + private AuditLogConfiguration captureUpdate() { + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + verify(auditLoggingFacade).logConfigurationChangeAuditLog(captor.capture(), + eqAction(ConfigurationChangeActions.CONFIGURATION_UPDATE)); + return captor.getValue(); + } + + private AuditLogConfiguration captureDelete() { + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + verify(auditLoggingFacade).logConfigurationChangeAuditLog(captor.capture(), + eqAction(ConfigurationChangeActions.CONFIGURATION_DELETE)); + return captor.getValue(); + } + + private AuditLogConfiguration captureDataAccess() { + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + verify(auditLoggingFacade).logDataAccessAuditLog(captor.capture()); + return captor.getValue(); + } + + private Map identifiersFromCreate() { + return identifiersFromConfig(captureCreate()); + } + + private Map identifiersFromUpdate() { + return identifiersFromConfig(captureUpdate()); + } + + private Map identifiersFromDelete() { + return identifiersFromConfig(captureDelete()); + } + + private Map identifiersFromDataAccess() { + return identifiersFromConfig(captureDataAccess()); + } + + private Map identifiersFromConfig(AuditLogConfiguration config) { + Map result = new HashMap<>(); + List configurationIdentifiers = config.getConfigurationIdentifiers(); + for (ConfigurationIdentifier identifier : configurationIdentifiers) { + result.put(identifier.getName(), identifier.getValue()); + } + return result; + } + + private int countNonReservedIdentifiers(AuditLogConfiguration config) { + int count = 0; + for (ConfigurationIdentifier identifier : config.getConfigurationIdentifiers()) { + String name = identifier.getName(); + if (!"performed_action".equals(name) && !"time".equals(name) && !"spaceId".equals(name)) { + count++; + } + } + return count; + } + + private static LoggingConfiguration buildLoggingConfiguration() { + return ImmutableLoggingConfiguration.builder() + .id(LOGGING_CONFIG_ID) + .mtaId(MTA_ID) + .mtaSpace(MTA_SPACE) + .mtaSpaceId(MTA_SPACE_ID) + .mtaOrg(MTA_ORG) + .namespace(NAMESPACE) + .targetSpace(TARGET_SPACE) + .targetOrg(TARGET_ORG) + .serviceInstanceName(SERVICE_INSTANCE_NAME) + .serviceKeyName(SERVICE_KEY_NAME) + .logLevel(LogLevel.INFO) + .isFailSafe(true) + .build(); + } + + private static T any(Class clazz) { + return org.mockito.ArgumentMatchers.any(clazz); + } + + private static ConfigurationChangeActions eqAction(ConfigurationChangeActions action) { + return org.mockito.ArgumentMatchers.eq(action); + } +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurgerTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurgerTest.java index bbbedb936c..41241908df 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurgerTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaConfigurationPurgerTest.java @@ -2,14 +2,19 @@ import java.util.ArrayList; import java.util.List; +import java.util.UUID; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudMetadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudApplication; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudSpace; import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableLifecycle; import org.cloudfoundry.multiapps.controller.client.facade.domain.LifecycleType; import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudSpaceClient; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.auditlogging.MtaConfigurationPurgerAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.metadata.processor.MtaMetadataParser; import org.cloudfoundry.multiapps.controller.core.cf.metadata.processor.MtaMetadataValidator; @@ -17,9 +22,12 @@ import org.cloudfoundry.multiapps.controller.persistence.model.CloudTarget; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationSubscription; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; import org.cloudfoundry.multiapps.controller.persistence.query.ConfigurationEntryQuery; import org.cloudfoundry.multiapps.controller.persistence.query.ConfigurationSubscriptionQuery; import org.cloudfoundry.multiapps.controller.persistence.query.Query; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationEntryService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationSubscriptionService; import org.cloudfoundry.multiapps.mta.model.Version; @@ -31,6 +39,8 @@ import org.mockito.MockitoAnnotations; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class MtaConfigurationPurgerTest { @@ -63,6 +73,10 @@ class MtaConfigurationPurgerTest { ConfigurationSubscriptionQuery configurationSubscriptionQuery; @Mock MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog; + @Mock + CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + @Mock + CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; @BeforeEach void setUp() throws Exception { @@ -78,7 +92,8 @@ void testPurge() { MtaConfigurationPurger purger = new MtaConfigurationPurger(client, spaceClient, configurationEntryService, configurationSubscriptionService, new MtaMetadataParser(new MtaMetadataValidator()), - mtaConfigurationPurgerAuditLog); + mtaConfigurationPurgerAuditLog, cloudLoggingServiceConfigurationService, + cloudLoggingServiceConfigurationAuditLog); purger.purge("org", "space"); verifyConfigurationEntriesDeleted(); verifyConfigurationEntriesNotDeleted(); @@ -147,4 +162,73 @@ private ConfigurationEntry createEntry(int id, String providerId) { new CloudTarget(TARGET_ORG, TARGET_SPACE), null, null, null, null); } + @Test + void testPurgeCloudLoggingServiceConfigurations_deletesAllConfigurationsInSpace() { + String spaceId = "00000000-0000-0000-0000-000000000001"; + LoggingConfiguration config1 = createLoggingConfiguration("id-1", spaceId, "mta-1"); + LoggingConfiguration config2 = createLoggingConfiguration("id-2", spaceId, "mta-2"); + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)).thenReturn(List.of(config1, + config2)); + when(spaceClient.getSpace(TARGET_ORG, TARGET_SPACE)).thenReturn(createCloudSpace(spaceId)); + + MtaConfigurationPurger purger = createPurger(); + purger.purge(TARGET_ORG, TARGET_SPACE); + + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-1"); + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-2"); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("", spaceId, config1); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("", spaceId, config2); + } + + @Test + void testPurgeCloudLoggingServiceConfigurations_doesNothingWhenNoConfigurationsExist() { + String spaceId = "00000000-0000-0000-0000-000000000002"; + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)).thenReturn(List.of()); + when(spaceClient.getSpace(TARGET_ORG, TARGET_SPACE)).thenReturn(createCloudSpace(spaceId)); + + MtaConfigurationPurger purger = createPurger(); + purger.purge(TARGET_ORG, TARGET_SPACE); + + verify(cloudLoggingServiceConfigurationService, never()).deleteCloudLoggingServiceConfiguration(Mockito.anyString()); + verify(cloudLoggingServiceConfigurationAuditLog, never()).logDeleteLoggingConfiguration(Mockito.anyString(), Mockito.anyString(), + Mockito.any()); + } + + @Test + void testPurgeCloudLoggingServiceConfigurations_deletesSingleConfiguration() { + String spaceId = "00000000-0000-0000-0000-000000000003"; + LoggingConfiguration config = createLoggingConfiguration("id-1", spaceId, "mta-1"); + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)).thenReturn(List.of(config)); + when(spaceClient.getSpace(TARGET_ORG, TARGET_SPACE)).thenReturn(createCloudSpace(spaceId)); + + MtaConfigurationPurger purger = createPurger(); + purger.purge(TARGET_ORG, TARGET_SPACE); + + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-1"); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("", spaceId, config); + } + + private MtaConfigurationPurger createPurger() { + return new MtaConfigurationPurger(client, spaceClient, configurationEntryService, configurationSubscriptionService, + new MtaMetadataParser(new MtaMetadataValidator()), mtaConfigurationPurgerAuditLog, + cloudLoggingServiceConfigurationService, cloudLoggingServiceConfigurationAuditLog); + } + + private LoggingConfiguration createLoggingConfiguration(String id, String spaceId, String mtaId) { + return ImmutableLoggingConfiguration.builder() + .id(id) + .mtaSpaceId(spaceId) + .mtaId(mtaId) + .build(); + } + + private CloudSpace createCloudSpace(String spaceId) { + return ImmutableCloudSpace.builder() + .metadata(ImmutableCloudMetadata.builder() + .guid(UUID.fromString(spaceId)) + .build()) + .name(TARGET_SPACE) + .build(); + } + } diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationServiceTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationServiceTest.java index 2e9c0656e5..eb906e5adb 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationServiceTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/security/data/termination/DataTerminationServiceTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; @@ -19,6 +20,7 @@ import java.util.stream.Stream; import org.cloudfoundry.multiapps.controller.core.Messages; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.auditlogging.MtaConfigurationPurgerAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.clients.CFOptimizedEventGetter; import org.cloudfoundry.multiapps.controller.core.test.MockBuilder; @@ -26,11 +28,14 @@ import org.cloudfoundry.multiapps.controller.persistence.model.CloudTarget; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationSubscription; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; import org.cloudfoundry.multiapps.controller.persistence.query.ConfigurationEntryQuery; import org.cloudfoundry.multiapps.controller.persistence.query.ConfigurationSubscriptionQuery; import org.cloudfoundry.multiapps.controller.persistence.query.OperationQuery; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationEntryService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationSubscriptionService; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; import org.cloudfoundry.multiapps.controller.persistence.services.FileService; import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; import org.cloudfoundry.multiapps.controller.persistence.services.OperationService; @@ -71,6 +76,10 @@ class DataTerminationServiceTest { private CFOptimizedEventGetter cfOptimizedEventsGetter; @Mock private MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog; + @Mock + private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + @Mock + private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; @InjectMocks private final DataTerminationService dataTerminationService = createDataTerminationService(); @@ -221,4 +230,91 @@ void testDoesNotThrowExceptionOnFailToDeleteSpace() throws FileStorageException assertDoesNotThrow(() -> dataTerminationService.deleteOrphanUserData()); } + @Test + void testDeleteExistingCloudLoggingServiceConfiguration_deletesAllConfigurationsInSpace() { + String spaceId = "space-1"; + LoggingConfiguration config1 = createLoggingConfiguration("id-1", spaceId, "mta-1"); + LoggingConfiguration config2 = createLoggingConfiguration("id-2", spaceId, "mta-2"); + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)).thenReturn( + List.of(config1, config2)); + prepareGlobalAuditorCredentials(); + prepareCfOptimizedEventsGetter(List.of(spaceId)); + when(configurationSubscriptionService.createQuery()).thenReturn(configurationSubscriptionQuery); + when(configurationEntryService.createQuery()).thenReturn(configurationEntryQuery); + when(operationService.createQuery()).thenReturn(operationQuery); + + dataTerminationService.deleteOrphanUserData(); + + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-1"); + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-2"); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("", spaceId, config1); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("", spaceId, config2); + } + + @Test + void testDeleteExistingCloudLoggingServiceConfiguration_doesNothingWhenNoConfigurationsExist() { + String spaceId = "space-2"; + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)).thenReturn(List.of()); + prepareGlobalAuditorCredentials(); + prepareCfOptimizedEventsGetter(List.of(spaceId)); + when(configurationSubscriptionService.createQuery()).thenReturn(configurationSubscriptionQuery); + when(configurationEntryService.createQuery()).thenReturn(configurationEntryQuery); + when(operationService.createQuery()).thenReturn(operationQuery); + + dataTerminationService.deleteOrphanUserData(); + + verify(cloudLoggingServiceConfigurationService, never()).deleteCloudLoggingServiceConfiguration(anyString()); + verify(cloudLoggingServiceConfigurationAuditLog, never()).logDeleteLoggingConfiguration(anyString(), anyString(), any()); + } + + @Test + void testDeleteExistingCloudLoggingServiceConfiguration_deletesSingleConfiguration() { + String spaceId = "space-3"; + LoggingConfiguration config = createLoggingConfiguration("id-1", spaceId, "mta-1"); + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)).thenReturn(List.of(config)); + prepareGlobalAuditorCredentials(); + prepareCfOptimizedEventsGetter(List.of(spaceId)); + when(configurationSubscriptionService.createQuery()).thenReturn(configurationSubscriptionQuery); + when(configurationEntryService.createQuery()).thenReturn(configurationEntryQuery); + when(operationService.createQuery()).thenReturn(operationQuery); + + dataTerminationService.deleteOrphanUserData(); + + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-1"); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("", spaceId, config); + } + + @Test + void testDeleteExistingCloudLoggingServiceConfiguration_deletesAcrossMultipleSpaces() { + String spaceId1 = "space-4"; + String spaceId2 = "space-5"; + LoggingConfiguration config1 = createLoggingConfiguration("id-1", spaceId1, "mta-1"); + LoggingConfiguration config2 = createLoggingConfiguration("id-2", spaceId2, "mta-2"); + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId1)).thenReturn( + List.of(config1)); + when(cloudLoggingServiceConfigurationService.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId2)).thenReturn( + List.of(config2)); + prepareGlobalAuditorCredentials(); + prepareCfOptimizedEventsGetter(List.of(spaceId1, spaceId2)); + when(configurationSubscriptionService.createQuery()).thenReturn(configurationSubscriptionQuery); + when(configurationEntryService.createQuery()).thenReturn(configurationEntryQuery); + when(operationService.createQuery()).thenReturn(operationQuery); + + dataTerminationService.deleteOrphanUserData(); + + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-1"); + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration("id-2"); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("", spaceId1, config1); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration("", spaceId2, config2); + } + + private LoggingConfiguration createLoggingConfiguration(String id, String spaceId, String mtaId) { + return ImmutableLoggingConfiguration.builder() + .id(id) + .mtaSpaceId(spaceId) + .mtaId(mtaId) + .build(); + } + } + diff --git a/multiapps-controller-persistence/pom.xml b/multiapps-controller-persistence/pom.xml index eade772907..70875d56f8 100644 --- a/multiapps-controller-persistence/pom.xml +++ b/multiapps-controller-persistence/pom.xml @@ -231,5 +231,9 @@ javax.xml.bind jaxb-api + + org.springframework + spring-webflux + diff --git a/multiapps-controller-persistence/src/main/java/module-info.java b/multiapps-controller-persistence/src/main/java/module-info.java index 8c3c8226e0..40ade3562f 100644 --- a/multiapps-controller-persistence/src/main/java/module-info.java +++ b/multiapps-controller-persistence/src/main/java/module-info.java @@ -55,6 +55,7 @@ requires org.bouncycastle.fips.core; requires org.bouncycastle.fips.pkix; requires org.cloudfoundry.multiapps.common; + requires spring.webflux; requires org.eclipse.persistence.core; requires org.slf4j; requires spring.beans; @@ -63,4 +64,7 @@ requires static java.compiler; requires static org.immutables.value; + requires io.netty.handler; + requires reactor.netty.http; + requires reactor.netty.core; } diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/Messages.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/Messages.java index 4a2daa7c71..208b10afc0 100644 --- a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/Messages.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/Messages.java @@ -14,6 +14,7 @@ public final class Messages { public static final String ERROR_GETTING_FILES_WITH_SPACE_AND_NAMESPACE = "Error getting files with space {0} and namespace {1}"; public static final String ERROR_GETTING_LOGS_WITH_SPACE_AND_OPERATION_ID = "Error getting logs with space {0} and operation id {1}"; public static final String ERROR_GETTING_LOGS_WITH_SPACE_OPERATION_ID_AND_NAME = "Error getting logs with space {0} operation id {1} and file name {2}"; + public static final String ERROR_UPDATING_LOGS_WITH_SPACE_OPERATION_ID_AND_NAME = "Error updating log with space {0} operation id {1} and file name {2}"; public static final String ERROR_GETTING_ALL_FILES = "Error getting all files"; public static final String ERROR_LOG_FILE_NOT_FOUND = "Log file with name \"{0}\" for operation \"{1}\" in space \"{2}\" was not found"; public static final String ERROR_CORRELATION_ID_OR_ACTIVITY_ID_NULL = "Unable to retrieve correlation id or activity id for process \"{0}\" at activity \"{1}\" and space \"{2}\""; @@ -48,6 +49,7 @@ public final class Messages { public static final String APPLICATION_SHUTDOWN_WITH_APPLICATION_INSTANCE_ID_ALREADY_EXIST = "Application shutdown application instance ID \"{0}\" already exist"; public static final String SECRET_TOKEN_WITH_ID_NOT_EXIST = "Secret token with ID \"{0}\" does not exist"; public static final String DATABASE_HEALTH_CHECK_FAILED = "Database health check failed"; + public static final String FAILED_TO_SEND_LOG_MESSAGE_TO_CLS = "Failed to send log message to Cloud Logging service"; // ERROR log messages: public static final String UPLOAD_STREAM_FAILED_TO_CLOSE = "Cannot close file upload stream"; @@ -77,6 +79,7 @@ public final class Messages { public static final String RETRIEVED_SECRET_TOKEN_WITH_ID_0_FOR_PROCESS_WITH_ID_1 = "Retrieved secret token with id \"{0}\" for process with id \"{1}\""; public static final String DELETED_0_SECRET_TOKENS_FOR_PROCESS_WITH_ID_1 = "Deleted \"{0}\" secret tokens for process with id \"{1}\""; public static final String DELETED_0_SECRET_TOKENS_WITH_EXPIRATION_DATE_1 = "Deleted secret tokens \"{0}\" with an expiration date \"{1}\""; + public static final String CREATING_WEBCLIENT_WITH_MTLS_CONFIGURATION_FOR_ENDPOINT_1 = "Creating WebClient with mTLS configuration for endpoint: {0}"; protected Messages() { } diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/ExternalOperationLogEntry.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/ExternalOperationLogEntry.java new file mode 100644 index 0000000000..f729c5fe63 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/ExternalOperationLogEntry.java @@ -0,0 +1,32 @@ +package org.cloudfoundry.multiapps.controller.persistence.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cloudfoundry.multiapps.common.Nullable; +import org.immutables.value.Value; + +@Value.Immutable +@JsonSerialize(as = ImmutableExternalOperationLogEntry.class) +@JsonDeserialize(as = ImmutableExternalOperationLogEntry.class) +public interface ExternalOperationLogEntry { + + @JsonProperty("msg") + String getMessage(); + + @JsonProperty("date") + String getTimestamp(); + + @JsonProperty("correlation_id") + @Nullable + String getCorrelationId(); + + @JsonProperty("operation_log_name") + @Nullable + String getOperationLogName(); + + @Nullable + String getLevel(); + + String getId(); +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevel.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevel.java new file mode 100644 index 0000000000..30419a7920 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevel.java @@ -0,0 +1,36 @@ +package org.cloudfoundry.multiapps.controller.persistence.model; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public enum LogLevel { + + INFO, WARN, DEBUG, ERROR, TRACE; + + private static final Map> logLevelLoggingType = setupLogLevels(); + + public static LogLevel get(String value) { + for (LogLevel logLevel : values()) { + if (logLevel.name() + .equals(value)) { + return logLevel; + } + } + return null; + } + + public static Map> getLogLevelLoggingType() { + return logLevelLoggingType; + } + + private static Map> setupLogLevels() { + Map> logLevels = new HashMap<>(); + logLevels.put(TRACE, List.of(TRACE, DEBUG, INFO, WARN, ERROR)); + logLevels.put(DEBUG, List.of(DEBUG, INFO, WARN, ERROR)); + logLevels.put(INFO, List.of(INFO, WARN, ERROR)); + logLevels.put(WARN, List.of(WARN, ERROR)); + logLevels.put(ERROR, List.of(ERROR)); + return logLevels; + } +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LoggingConfiguration.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LoggingConfiguration.java new file mode 100644 index 0000000000..f65356e2e2 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/LoggingConfiguration.java @@ -0,0 +1,63 @@ +package org.cloudfoundry.multiapps.controller.persistence.model; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cloudfoundry.multiapps.common.Nullable; +import org.immutables.value.Value; + +@Value.Immutable +@JsonSerialize(as = ImmutableLoggingConfiguration.class) +@JsonDeserialize(as = ImmutableLoggingConfiguration.class) +public interface LoggingConfiguration { + + @Nullable + String getId(); + + @Nullable + String getTargetOrg(); + + @Nullable + String getTargetSpace(); + + @Nullable + String getMtaOrg(); + + @Nullable + String getMtaSpace(); + + @Nullable + String getMtaSpaceId(); + + @Nullable + String getMtaId(); + + @Nullable + String getOperationId(); + + @Nullable + String getEndpointUrl(); + + @Nullable + String getServerCa(); + + @Nullable + String getClientCert(); + + @Nullable + String getClientKey(); + + @Nullable + LogLevel getLogLevel(); + + @Nullable + Boolean isFailSafe(); + + @Nullable + String getServiceInstanceName(); + + @Nullable + String getServiceKeyName(); + + @Nullable + String getNamespace(); +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/CloudLoggingServiceConfigurationQueryProvider.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/CloudLoggingServiceConfigurationQueryProvider.java new file mode 100644 index 0000000000..fed2429419 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/CloudLoggingServiceConfigurationQueryProvider.java @@ -0,0 +1,181 @@ +package org.cloudfoundry.multiapps.controller.persistence.query.providers; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.query.SqlQuery; +import org.cloudfoundry.multiapps.controller.persistence.util.JdbcUtil; + +public class CloudLoggingServiceConfigurationQueryProvider { + + public static final String INSERT_CLOUD_LOGGING_SERVICE_CONFIGURATION = "INSERT INTO %s (ID, TARGET_SPACE, TARGET_ORG, MTA_ID, MTA_ORG, MTA_SPACE, MTA_SPACE_ID, SERVICE_INSTANCE_NAME, SERVICE_KEY_NAME, LOG_LEVEL, IS_FAILSAFE, ADDED_AT, NAMESPACE) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + public static final String GET_CLOUD_LOGGING_CONFIGURATION = "SELECT ID, TARGET_SPACE, TARGET_ORG, MTA_ID, MTA_ORG, MTA_SPACE, MTA_SPACE_ID, SERVICE_INSTANCE_NAME, SERVICE_KEY_NAME, LOG_LEVEL, IS_FAILSAFE, NAMESPACE FROM %s WHERE MTA_SPACE=? AND MTA_ID=? AND NAMESPACE=?"; + public static final String GET_CLOUD_LOGGING_CONFIGURATION_NULL_NAMESPACE = "SELECT ID, TARGET_SPACE, TARGET_ORG, MTA_ID, MTA_ORG, MTA_SPACE, MTA_SPACE_ID, SERVICE_INSTANCE_NAME, SERVICE_KEY_NAME, LOG_LEVEL, IS_FAILSAFE, NAMESPACE FROM %s WHERE MTA_SPACE=? AND MTA_ID=? AND NAMESPACE IS NULL"; + public static final String GET_ALL_CLOUD_LOGGING_CONFIGURATIONS = "SELECT ID, TARGET_SPACE, TARGET_ORG, MTA_ID, MTA_ORG, MTA_SPACE, MTA_SPACE_ID, SERVICE_INSTANCE_NAME, SERVICE_KEY_NAME, LOG_LEVEL, IS_FAILSAFE FROM %s WHERE MTA_SPACE_ID=?"; + public static final String DELETE_CLOUD_LOGGING_CONFIGURATION = "DELETE FROM %s WHERE ID=?"; + public static final String UPDATE_CLOUD_LOGGING_CONFIGURATION = "UPDATE %s SET TARGET_SPACE=?, TARGET_ORG=?, SERVICE_INSTANCE_NAME=?, SERVICE_KEY_NAME=?, LOG_LEVEL=?, IS_FAILSAFE=?, ADDED_AT=? WHERE MTA_SPACE=? AND MTA_ID=? AND NAMESPACE=?"; + public static final String UPDATE_CLOUD_LOGGING_CONFIGURATION_NULL_NAMESPACE = "UPDATE %s SET TARGET_SPACE=?, TARGET_ORG=?, SERVICE_INSTANCE_NAME=?, SERVICE_KEY_NAME=?, LOG_LEVEL=?, IS_FAILSAFE=?, ADDED_AT=? WHERE MTA_SPACE=? AND MTA_ID=? AND NAMESPACE IS NULL"; + private static final String ID_COLUMN_LABEL = "id"; + private static final String TARGET_SPACE_COLUMN_LABEL = "target_space"; + private static final String TARGET_ORG_COLUMN_LABEL = "target_org"; + private static final String MTA_ID_COLUMN_LABEL = "mta_id"; + private static final String MTA_ORG_COLUMN_LABEL = "mta_org"; + private static final String MTA_SPACE_COLUMN_LABEL = "mta_space"; + private static final String MTA_SPACE_ID_COLUMN_LABEL = "mta_space_id"; + private static final String SERVICE_INSTANCE_NAME_COLUMN_LABEL = "service_instance_name"; + private static final String SERVICE_KEY_NAME_COLUMN_LABEL = "service_key_name"; + private static final String LOG_LEVEL_COLUMN_LABEL = "log_level"; + private static final String IS_FAILSAFE_COLUMN_LABEL = "is_failsafe"; + private final String tableName; + + public CloudLoggingServiceConfigurationQueryProvider(String tableName) { + this.tableName = tableName; + } + + public SqlQuery getStoreLoggingConfigurationQuery(LoggingConfiguration loggingConfiguration) { + return (Connection connection) -> { + PreparedStatement statement = null; + try { + statement = connection.prepareStatement(getStoreLoggingConfigurationQueryString()); + statement.setString(1, loggingConfiguration.getId()); + statement.setString(2, loggingConfiguration.getTargetSpace()); + statement.setString(3, loggingConfiguration.getTargetOrg()); + statement.setString(4, loggingConfiguration.getMtaId()); + statement.setString(5, loggingConfiguration.getMtaOrg()); + statement.setString(6, loggingConfiguration.getMtaSpace()); + statement.setString(7, loggingConfiguration.getMtaSpaceId()); + statement.setString(8, loggingConfiguration.getServiceInstanceName()); + statement.setString(9, loggingConfiguration.getServiceKeyName()); + statement.setString(10, loggingConfiguration.getLogLevel() + .name()); + statement.setBoolean(11, loggingConfiguration.isFailSafe() == null ? true : loggingConfiguration.isFailSafe()); + statement.setTimestamp(12, Timestamp.valueOf(LocalDateTime.now())); + statement.setString(13, loggingConfiguration.getNamespace()); + + return statement.executeUpdate(); + } finally { + JdbcUtil.closeQuietly(statement); + } + }; + } + + public SqlQuery getGetLoggingConfigurationQuery(String mtaSpace, String mtaId, String namespace) { + return (Connection connection) -> { + PreparedStatement statement = null; + ResultSet resultSet = null; + try { + if (namespace == null) { + statement = connection.prepareStatement(String.format(GET_CLOUD_LOGGING_CONFIGURATION_NULL_NAMESPACE, tableName)); + } else { + statement = connection.prepareStatement(String.format(GET_CLOUD_LOGGING_CONFIGURATION, tableName)); + statement.setString(3, namespace); + } + statement.setString(1, mtaSpace); + statement.setString(2, mtaId); + resultSet = statement.executeQuery(); + + if (resultSet.next()) { + return getLoggingConfiguration(resultSet); + } + return null; + } finally { + JdbcUtil.closeQuietly(statement); + JdbcUtil.closeQuietly(resultSet); + } + }; + } + + public SqlQuery getDeleteLoggingConfigurationQuery(String id) { + return (Connection connection) -> { + PreparedStatement statement = null; + try { + statement = connection.prepareStatement(getDeleteLoggingConfigurationQueryString()); + statement.setString(1, id); + return statement.executeUpdate(); + } finally { + JdbcUtil.closeQuietly(statement); + } + }; + } + + public SqlQuery getUpdateLoggingConfigurationQuery(LoggingConfiguration loggingConfiguration) { + return (Connection connection) -> { + PreparedStatement statement = null; + try { + String queryTemplate = loggingConfiguration.getNamespace() == null + ? UPDATE_CLOUD_LOGGING_CONFIGURATION_NULL_NAMESPACE + : UPDATE_CLOUD_LOGGING_CONFIGURATION; + statement = connection.prepareStatement(String.format(queryTemplate, tableName)); + statement.setString(1, loggingConfiguration.getTargetSpace()); + statement.setString(2, loggingConfiguration.getTargetOrg()); + statement.setString(3, loggingConfiguration.getServiceInstanceName()); + statement.setString(4, loggingConfiguration.getServiceKeyName()); + statement.setString(5, loggingConfiguration.getLogLevel() + .name()); + statement.setBoolean(6, loggingConfiguration.isFailSafe() == null ? true : loggingConfiguration.isFailSafe()); + statement.setTimestamp(7, Timestamp.valueOf(LocalDateTime.now())); + statement.setString(8, loggingConfiguration.getMtaSpace()); + statement.setString(9, loggingConfiguration.getMtaId()); + if (loggingConfiguration.getNamespace() != null) { + statement.setString(10, loggingConfiguration.getNamespace()); + } + return statement.executeUpdate(); + } finally { + JdbcUtil.closeQuietly(statement); + } + }; + } + + public SqlQuery> getAllCloudLoggingServiceConfigurationsFromSpace(String spaceId) { + return (Connection connection) -> { + PreparedStatement statement = null; + ResultSet resultSet = null; + try { + statement = connection.prepareStatement(String.format(GET_ALL_CLOUD_LOGGING_CONFIGURATIONS, tableName)); + statement.setString(1, spaceId); + resultSet = statement.executeQuery(); + List result = new ArrayList<>(); + while (resultSet.next()) { + result.add(getLoggingConfiguration(resultSet)); + } + return result; + } finally { + JdbcUtil.closeQuietly(statement); + JdbcUtil.closeQuietly(resultSet); + } + }; + } + + private LoggingConfiguration getLoggingConfiguration(ResultSet resultSet) throws SQLException { + return ImmutableLoggingConfiguration.builder() + .id(resultSet.getString(ID_COLUMN_LABEL)) + .targetSpace(resultSet.getString(TARGET_SPACE_COLUMN_LABEL)) + .targetOrg(resultSet.getString(TARGET_ORG_COLUMN_LABEL)) + .mtaId(resultSet.getString(MTA_ID_COLUMN_LABEL)) + .mtaOrg(resultSet.getString(MTA_ORG_COLUMN_LABEL)) + .mtaSpace(resultSet.getString(MTA_SPACE_COLUMN_LABEL)) + .mtaSpaceId(resultSet.getString(MTA_SPACE_ID_COLUMN_LABEL)) + .serviceInstanceName(resultSet.getString(SERVICE_INSTANCE_NAME_COLUMN_LABEL)) + .serviceKeyName(resultSet.getString(SERVICE_KEY_NAME_COLUMN_LABEL)) + .logLevel(LogLevel.get(resultSet.getString(LOG_LEVEL_COLUMN_LABEL))) + .isFailSafe(resultSet.getBoolean(IS_FAILSAFE_COLUMN_LABEL)) + .build(); + } + + private String getStoreLoggingConfigurationQueryString() { + return String.format(INSERT_CLOUD_LOGGING_SERVICE_CONFIGURATION, tableName); + } + + private String getDeleteLoggingConfigurationQueryString() { + return String.format(DELETE_CLOUD_LOGGING_CONFIGURATION, tableName); + } +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/SqlOperationLogQueryProvider.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/SqlOperationLogQueryProvider.java index a438a7e574..594bb2aa9e 100644 --- a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/SqlOperationLogQueryProvider.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/SqlOperationLogQueryProvider.java @@ -20,10 +20,12 @@ public class SqlOperationLogQueryProvider { private static final String ID_COLUMN_LABEL = "id"; private static final String OPERATION_LOG_COLUMN_LABEL = "operation_log"; private static final String OPERATION_LOG_NAME_COLUMN_LABEL = "operation_log_name"; - private static final String SELECT_LOGS_BY_SPACE_ID_OPERATION_ID_AND_OPERATION_LOG_NAME = "SELECT ID, OPERATION_LOG, OPERATION_LOG_NAME FROM %s WHERE SPACE=? AND OPERATION_ID=? AND OPERATION_LOG_NAME=? ORDER BY MODIFIED ASC"; + private static final String OPERATION_LOG_MODIFIED_COLUMN_LABEL = "modified"; + private static final String SELECT_LOGS_BY_SPACE_ID_OPERATION_ID_AND_OPERATION_LOG_NAME = "SELECT ID, OPERATION_LOG, OPERATION_LOG_NAME, MODIFIED FROM %s WHERE SPACE=? AND OPERATION_ID=? AND OPERATION_LOG_NAME=? ORDER BY MODIFIED ASC"; private static final String SELECT_LOGS_BY_SPACE_ID_AND_NAME = "SELECT DISTINCT ID, OPERATION_LOG, OPERATION_LOG_NAME, MODIFIED FROM %s WHERE SPACE=? AND OPERATION_ID=? ORDER BY MODIFIED ASC"; - private final String tableName; + public static final String UPDATE_IS_SEND_TO_CLOUD_LOGGING_SERVICE = "UPDATE %s SET IS_SEND_TO_CLOUD_LOGGING_SERVICE = ? where ID = ?"; + private final String tableName; public SqlOperationLogQueryProvider(String tableName) { this.tableName = tableName; @@ -115,6 +117,8 @@ private OperationLogEntry getOperationLogEntry(ResultSet resultSet) throws SQLEx .id(resultSet.getString(ID_COLUMN_LABEL)) .operationLog(resultSet.getString(OPERATION_LOG_COLUMN_LABEL)) .operationLogName(resultSet.getString(OPERATION_LOG_NAME_COLUMN_LABEL)) + .modified(resultSet.getTimestamp(OPERATION_LOG_MODIFIED_COLUMN_LABEL) + .toLocalDateTime()) .build(); } } diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationService.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationService.java new file mode 100644 index 0000000000..aa839cd58f --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationService.java @@ -0,0 +1,72 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import java.sql.SQLException; +import java.util.List; + +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.persistence.DataSourceWithDialect; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.query.providers.CloudLoggingServiceConfigurationQueryProvider; +import org.cloudfoundry.multiapps.controller.persistence.util.SqlQueryExecutor; + +@Named("cloudLoggingServiceConfigurationService") +public class CloudLoggingServiceConfigurationService { + + public static final String TABLE_NAME = "cloud_logging_service_configuration"; + private final SqlQueryExecutor sqlQueryExecutor; + + private final CloudLoggingServiceConfigurationQueryProvider cloudLoggingServiceConfigurationQueryProvider; + + public CloudLoggingServiceConfigurationService(DataSourceWithDialect dataSourceWithDialect) { + cloudLoggingServiceConfigurationQueryProvider = new CloudLoggingServiceConfigurationQueryProvider(TABLE_NAME); + this.sqlQueryExecutor = new SqlQueryExecutor(dataSourceWithDialect.getDataSource()); + } + + public void storeCloudLoggingServiceConfiguration(LoggingConfiguration loggingConfiguration) { + try { + getSqlQueryExecutor().execute( + cloudLoggingServiceConfigurationQueryProvider.getStoreLoggingConfigurationQuery(loggingConfiguration)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public LoggingConfiguration getCloudLoggingServiceConfiguration(String mtaSpace, String mtaId, String namespace) { + try { + return getSqlQueryExecutor().execute( + cloudLoggingServiceConfigurationQueryProvider.getGetLoggingConfigurationQuery(mtaSpace, mtaId, namespace)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public void deleteCloudLoggingServiceConfiguration(String id) { + try { + getSqlQueryExecutor().execute(cloudLoggingServiceConfigurationQueryProvider.getDeleteLoggingConfigurationQuery(id)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public void updateCloudLoggingServiceConfiguration(LoggingConfiguration loggingConfiguration) { + try { + getSqlQueryExecutor().execute( + cloudLoggingServiceConfigurationQueryProvider.getUpdateLoggingConfigurationQuery(loggingConfiguration)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public List getAllCloudLoggingServiceConfigurationsFromSpace(String spaceId) { + try { + return getSqlQueryExecutor().execute( + cloudLoggingServiceConfigurationQueryProvider.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public SqlQueryExecutor getSqlQueryExecutor() { + return sqlQueryExecutor; + } +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporter.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporter.java new file mode 100644 index 0000000000..6674b0f4f9 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporter.java @@ -0,0 +1,350 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.common.SLException; +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.ImmutableExternalOperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.OperationLogEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +import static com.azure.core.http.ContentType.APPLICATION_JSON; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; + +@Named("operationLogsExporter") +public class OperationLogsExporter { + + private static final Logger LOGGER = LoggerFactory.getLogger(OperationLogsExporter.class); + private static final long MAX_LIMIT_REQUEST_SIZE_BYTES = 3 * 1024 * 1024 + 512 * 1024; // 3.5MB + private static final Map clientCache = new ConcurrentHashMap<>(); + private static final Pattern MESSAGE_LOG_DATE_PATTERN = Pattern.compile("^#([^#\\r\\n]*)#", Pattern.MULTILINE); + private static final Pattern MESSAGE_LOG_LEVEL_PATTERN = Pattern.compile("^#[^#\\r\\n]*#[^#\\r\\n]*#([^#\\r\\n]*)#", Pattern.MULTILINE); + private static final Pattern MESSAGE_LOG_NAME = Pattern.compile("^#[^#\\r\\n]*#[^#\\r\\n]*#[^#\\r\\n]*#([^#\\r\\n]*)#", + Pattern.MULTILINE); + + private static final String MESSAGE_SPLITTING_REGEX = "(?m)^#[^#\\r\\n]*#[^#\\r\\n]*#[^#\\r\\n]*#[^#\\r\\n]*#[^#\\r\\n]*#(?:\\r?\\n)?"; + private final ProcessLogsPersistenceService processLogsPersistenceService; + + public OperationLogsExporter(ProcessLogsPersistenceService processLogsPersistenceService) { + this.processLogsPersistenceService = processLogsPersistenceService; + } + + public void sendLogsToCloudLoggingService(LoggingConfiguration loggingConfiguration, String message) { + List> externalOperationLogEntryBatches = getExternalOperationLogEntryBatches(loggingConfiguration, + message); + + WebClient cloudLogginServiceWebClient = getCloudLogginServiceWebClient(loggingConfiguration); + if (cloudLogginServiceWebClient == null) { + return; + } + + sendLogsToCloudLoggingService(externalOperationLogEntryBatches, cloudLogginServiceWebClient, loggingConfiguration); + } + + public void sendLogsToCloudLoggingService(LoggingConfiguration loggingConfiguration, OperationLogEntry operationLogEntry) { + if (loggingConfiguration == null) { + return; + } + List> externalOperationLogEntryBatches = getExternalOperationLogEntryBatches(loggingConfiguration, + operationLogEntry); + + WebClient cloudLogginServiceWebClient = getCloudLogginServiceWebClient(loggingConfiguration); + if (cloudLogginServiceWebClient == null) { + return; + } + + sendLogsToCloudLoggingService(externalOperationLogEntryBatches, cloudLogginServiceWebClient, loggingConfiguration); + } + + public List getUnsendProcessLogs(LoggingConfiguration loggingConfiguration) { + try { + return processLogsPersistenceService.listOperationLogsBySpaceAndOperationId(loggingConfiguration.getMtaSpaceId(), + loggingConfiguration.getOperationId()); + } catch (FileStorageException e) { + logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, e.getMessage()); + return List.of(); + } + } + + public void removeClientFromCache(String operationId) { + clientCache.remove(operationId); + } + + private List> getExternalOperationLogEntryBatches(LoggingConfiguration loggingConfiguration, + String message) { + Map> operationLogs = getLogsFromOperationLogEntry(message); + Map> filteredOperationLogs = removeLogsWithUnwantedLogLevel(loggingConfiguration, operationLogs); + List externalOperationLogEntries = new ArrayList<>(); + String logName = "asd"; + Matcher matcher = MESSAGE_LOG_NAME.matcher(message); + if (matcher.find()) { + logName = matcher.group(1); + logName = logName.substring(logName.indexOf(".") + 1); + } + + for (Map.Entry> operationLog : filteredOperationLogs.entrySet()) { + for (LogLogLog log : operationLog.getValue()) { + externalOperationLogEntries.add(convertToExternalLogEntry(loggingConfiguration, log, operationLog.getKey(), logName)); + } + } + return getLogEntryBatches(externalOperationLogEntries); + } + + private List> getExternalOperationLogEntryBatches(LoggingConfiguration loggingConfiguration, + OperationLogEntry operationLogEntry) { + Map> operationLogs = getLogsFromOperationLogEntry(operationLogEntry.getOperationLog()); + Map> filteredOperationLogs = removeLogsWithUnwantedLogLevel(loggingConfiguration, operationLogs); + List externalOperationLogEntries = new ArrayList<>(); + + for (Map.Entry> operationLog : filteredOperationLogs.entrySet()) { + for (LogLogLog log : operationLog.getValue()) { + externalOperationLogEntries.add(convertToExternalLogEntry(operationLogEntry, log, operationLog.getKey())); + } + } + return getLogEntryBatches(externalOperationLogEntries); + } + + private Map> removeLogsWithUnwantedLogLevel(LoggingConfiguration loggingConfiguration, + Map> operationLogs) { + List allowedLevelsToLog = LogLevel.getLogLevelLoggingType() + .get(loggingConfiguration.getLogLevel()); + + return operationLogs.entrySet() + .stream() + .filter(operationLog -> allowedLevelsToLog.contains(operationLog.getKey())) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue + )); + } + + private WebClient getCloudLogginServiceWebClient(LoggingConfiguration loggingConfiguration) { + WebClient webClient = null; + + if (!clientCache.containsKey(loggingConfiguration.getOperationId())) { + webClient = createWebClientWithMtls(loggingConfiguration); + clientCache.put(loggingConfiguration.getOperationId(), webClient); + } else { + webClient = clientCache.get(loggingConfiguration.getOperationId()); + } + + LOGGER.debug(MessageFormat.format(Messages.CREATING_WEBCLIENT_WITH_MTLS_CONFIGURATION_FOR_ENDPOINT_1, + loggingConfiguration.getEndpointUrl())); + return webClient; + } + + private void sendLogsToCloudLoggingService(List> externalOperationLogEntryBatches, + WebClient webClient, LoggingConfiguration loggingConfiguration) { + for (List logEntryBatch : externalOperationLogEntryBatches) { + ResponseEntity response = executeSendLongHttpRequest(webClient, logEntryBatch); + if (hasRequestFailed(response)) { + logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, Messages.FAILED_TO_SEND_LOG_MESSAGE_TO_CLS); + } + } + } + + private boolean hasRequestFailed(ResponseEntity response) { + if (response == null) { + return false; + } + int statusCode = response.getStatusCode() + .value(); + return statusCode < 200 || statusCode > 299; + } + + private ResponseEntity executeSendLongHttpRequest(WebClient webClient, List logEntryBatch) { + return webClient.post() + .header(CONTENT_TYPE, APPLICATION_JSON) + .bodyValue(JsonUtil.toJson(logEntryBatch)) + .retrieve() + .toBodilessEntity() + .block(); + } + + private List> getLogEntryBatches(List externalLogEntries) { + List> batches = new ArrayList<>(); + List currentBatch = new ArrayList<>(); + long currentChunkSize = 0L; + + for (ExternalOperationLogEntry entry : externalLogEntries) { + String entryJson = JsonUtil.toJson(entry); + int entrySize = entryJson.getBytes().length; + + if (currentChunkSize + entrySize > MAX_LIMIT_REQUEST_SIZE_BYTES && !currentBatch.isEmpty()) { + batches.add(new ArrayList<>(currentBatch)); + currentBatch.clear(); + currentChunkSize = 0L; + } + + currentBatch.add(entry); + currentChunkSize += entrySize; + } + if (!currentBatch.isEmpty()) { + batches.add(currentBatch); + } + return batches; + } + + private Map> getLogsFromOperationLogEntry(String message) { + Map> logsMap = new HashMap<>(); + getMessagesToLog(message, logsMap); + return logsMap; + } + + private void getMessagesToLog(String log, Map> logsMap) { + String[] messages = log.split(MESSAGE_SPLITTING_REGEX); + + List logLevels = getLogLevels(log); + List dateLevels = getLogDate(log); + if (logLevels.isEmpty()) { + return; + } + + int levelIndex = 0; + for (String message : messages) { + if (message.isBlank()) { + continue; + } + + String cleanedMessage = extractMessage(message); + String level = logLevels.get(levelIndex); + LocalDateTime date = dateLevels.get(levelIndex); + + logsMap.computeIfAbsent(LogLevel.get(level), key -> new ArrayList<>()) + .add(new LogLogLog(cleanedMessage, date)); + levelIndex++; + } + } + + private List getLogLevels(String log) { + Matcher matcher = MESSAGE_LOG_LEVEL_PATTERN.matcher(log); + List logLevels = new ArrayList<>(); + + while (matcher.find()) { + logLevels.add(matcher.group(1)); + } + + return logLevels; + } + + private List getLogDate(String log) { + Matcher matcher = MESSAGE_LOG_DATE_PATTERN.matcher(log); + List logLevels = new ArrayList<>(); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd HH:mm:ss.SSS"); + + while (matcher.find()) { + LocalDateTime dateTime = LocalDateTime.parse(matcher.group(1), formatter); + logLevels.add(dateTime); + } + + return logLevels; + } + + private String extractMessage(String message) { + String trimmed = message.substring(message.indexOf("]") + 1) + .trim(); + return trimmed.substring(0, trimmed.length() - 1); + } + + protected WebClient createWebClientWithMtls(LoggingConfiguration loggingConfiguration) { + SslContext sslContext = getSslContext(loggingConfiguration); + if (sslContext == null) { + return null; + } + HttpClient httpClient = HttpClient.create() + .secure(sslSpec -> sslSpec.sslContext(sslContext)); + + return WebClient.builder() + .baseUrl(loggingConfiguration.getEndpointUrl()) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); + } + + private SslContext getSslContext(LoggingConfiguration loggingConfiguration) { + try { + InputStream serverCaStream = getCredentialInputStream(loggingConfiguration.getServerCa()); + InputStream clientCertStream = getCredentialInputStream(loggingConfiguration.getClientCert()); + InputStream clientKeyStream = getCredentialInputStream(loggingConfiguration.getClientKey()); + return SslContextBuilder.forClient() + .keyManager(clientCertStream, clientKeyStream) + .trustManager(serverCaStream) + .build(); + } catch (IOException e) { + logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, e.getMessage()); + return null; + } + } + + private void logErrorOrThrowExceptionBasedOnFailSafe(LoggingConfiguration loggingConfiguration, String message) { + if (loggingConfiguration.isFailSafe()) { + LOGGER.error(message); + } else { + throw new SLException(message); + } + } + + private InputStream getCredentialInputStream(String credential) { + return new ByteArrayInputStream((credential.getBytes(StandardCharsets.UTF_8))); + } + + private ExternalOperationLogEntry convertToExternalLogEntry(OperationLogEntry operationLogEntry, LogLogLog operationLog, + LogLevel level) { + return ImmutableExternalOperationLogEntry.builder() + .timestamp(String.valueOf(operationLog.dateTime() + .atOffset(ZoneOffset.UTC))) + .message(operationLog.log()) + .id(UUID.randomUUID() + .toString()) + .operationLogName(operationLogEntry.getOperationLogName()) + .correlationId(operationLogEntry.getOperationId()) + .level(level.name()) + .build(); + } + + private ExternalOperationLogEntry convertToExternalLogEntry(LoggingConfiguration loggingConfiguration, LogLogLog operationLog, + LogLevel level, String logName) { + return ImmutableExternalOperationLogEntry.builder() + .timestamp(String.valueOf(operationLog.dateTime() + .atOffset(ZoneOffset.UTC))) + .message(operationLog.log()) + .id(UUID.randomUUID() + .toString()) + .operationLogName(logName) + .correlationId(loggingConfiguration.getOperationId()) + .level(level.name()) + .build(); + } + + public record LogLogLog(String log, LocalDateTime dateTime) { + + } +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersister.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersister.java index 66474371a5..0b78810d5d 100644 --- a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersister.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersister.java @@ -1,17 +1,18 @@ package org.cloudfoundry.multiapps.controller.persistence.services; -import jakarta.inject.Inject; -import jakarta.inject.Named; -import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableOperationLogEntry; -import org.cloudfoundry.multiapps.controller.persistence.model.OperationLogEntry; -import org.springframework.scheduling.annotation.Async; - import java.time.LocalDateTime; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableOperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.OperationLogEntry; +import org.springframework.scheduling.annotation.Async; + @Named("processLoggerPersister") public class ProcessLoggerPersister { @@ -27,13 +28,48 @@ public ProcessLoggerPersister(ProcessLoggerProvider processLoggerProvider, @Async("asyncExecutor") public void persistLogs(String correlationId, String taskId) { + List processLoggers = processLoggerProvider.getExistingLoggers(correlationId, taskId); - Map processLogsMessages = new HashMap<>(); + Map processLogsMessages = getProcessLogsMessages(processLoggers); + if (processLogsMessages.isEmpty()) { + return; + } + + OperationLogEntry operationLogEntryWithExistingData = processLoggers.getFirst() + .getOperationLogEntry(); + + for (var processLogsMessage : processLogsMessages.entrySet()) { + OperationLogEntry operationLogEntry = ImmutableOperationLogEntry.copyOf(operationLogEntryWithExistingData) + .withId(UUID.randomUUID() + .toString()) + .withOperationLogName(processLogsMessage.getKey()) + .withOperationLog(processLogsMessage.getValue() + .toString()) + .withModified(LocalDateTime.now()); + + processLogsPersistenceService.persistLog(operationLogEntry); + } + } + + public List getApplicationProcessLogsMessages(String correlationId, String taskId) { + List processLoggers = processLoggerProvider.getExistingLoggers(correlationId, taskId); + Map processLogsMessages = getProcessLogsMessages(processLoggers); + processLogsMessages.remove("OPERATION.log"); + + return processLogsMessages.values() + .stream() + .map(StringBuilder::toString) + .toList(); + } + + public Map getProcessLogsMessages(List processLoggers) { if (processLoggers.isEmpty()) { - return; + return Collections.emptyMap(); } + Map processLogsMessages = new HashMap<>(); + for (ProcessLogger processLogger : processLoggers) { if (processLogsMessages.containsKey(processLogger.getOperationLogEntry() .getOperationLogName())) { @@ -49,19 +85,6 @@ public void persistLogs(String correlationId, String taskId) { processLoggerProvider.removeProcessLoggerFromCache(processLogger); } - - OperationLogEntry operationLogEntryWithExistingData = processLoggers.get(0) - .getOperationLogEntry(); - - for (var processLogsMessage : processLogsMessages.entrySet()) { - OperationLogEntry operationLogEntry = ImmutableOperationLogEntry.copyOf(operationLogEntryWithExistingData) - .withId(UUID.randomUUID() - .toString()) - .withOperationLogName(processLogsMessage.getKey()) - .withOperationLog(processLogsMessage.getValue() - .toString()) - .withModified(LocalDateTime.now()); - processLogsPersistenceService.persistLog(operationLogEntry); - } + return processLogsMessages; } } diff --git a/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog-2.43.0-persistence.xml b/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog-2.43.0-persistence.xml new file mode 100644 index 0000000000..edc8a2942d --- /dev/null +++ b/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog-2.43.0-persistence.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml b/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml index d4379c650e..0592d75f42 100644 --- a/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml +++ b/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml @@ -44,4 +44,8 @@ + + + diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevelTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevelTest.java new file mode 100644 index 0000000000..c7b0c1ecd8 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/LogLevelTest.java @@ -0,0 +1,87 @@ +package org.cloudfoundry.multiapps.controller.persistence.model; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LogLevelTest { + + static Stream testGet() { + return Stream.of(Arguments.of("INFO", LogLevel.INFO), Arguments.of("WARN", LogLevel.WARN), Arguments.of("DEBUG", LogLevel.DEBUG), + Arguments.of("ERROR", LogLevel.ERROR), Arguments.of("TRACE", LogLevel.TRACE)); + } + + @ParameterizedTest + @MethodSource + void testGet(String value, LogLevel expected) { + assertEquals(expected, LogLevel.get(value)); + } + + @Test + void testGet_returnsNullForUnknownValue() { + assertNull(LogLevel.get("UNKNOWN")); + } + + @Test + void testGet_returnsNullForNull() { + assertNull(LogLevel.get(null)); + } + + @Test + void testGet_isCaseSensitive() { + assertNull(LogLevel.get("info")); + assertNull(LogLevel.get("Info")); + } + + static Stream testLogLevelLoggingType() { + return Stream.of( + Arguments.of(LogLevel.TRACE, List.of(LogLevel.TRACE, LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR)), + Arguments.of(LogLevel.DEBUG, List.of(LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR)), + Arguments.of(LogLevel.INFO, List.of(LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR)), + Arguments.of(LogLevel.WARN, List.of(LogLevel.WARN, LogLevel.ERROR)), + Arguments.of(LogLevel.ERROR, List.of(LogLevel.ERROR))); + } + + @ParameterizedTest + @MethodSource + void testLogLevelLoggingType(LogLevel level, List expectedAllowedLevels) { + Map> logLevelLoggingType = LogLevel.getLogLevelLoggingType(); + assertEquals(expectedAllowedLevels, logLevelLoggingType.get(level)); + } + + @Test + void testGetLogLevelLoggingTypeThatContainsAllLevels() { + Map> logLevelLoggingType = LogLevel.getLogLevelLoggingType(); + assertEquals(LogLevel.values().length, logLevelLoggingType.size()); + for (LogLevel level : LogLevel.values()) { + assertTrue(logLevelLoggingType.containsKey(level)); + } + } + + @Test + void testLogLevelLoggingTypeThatErrorOnlyIncludesError() { + List allowedForError = LogLevel.getLogLevelLoggingType() + .get(LogLevel.ERROR); + assertEquals(1, allowedForError.size()); + assertTrue(allowedForError.contains(LogLevel.ERROR)); + } + + @Test + void testLogLevelLoggingTypeThatTraceIncludesAll() { + List allowedForTrace = LogLevel.getLogLevelLoggingType() + .get(LogLevel.TRACE); + assertEquals(LogLevel.values().length, allowedForTrace.size()); + for (LogLevel level : LogLevel.values()) { + assertTrue(allowedForTrace.contains(level)); + } + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationServiceTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationServiceTest.java new file mode 100644 index 0000000000..277f0d5965 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationServiceTest.java @@ -0,0 +1,173 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import java.util.List; + +import org.cloudfoundry.multiapps.controller.persistence.DataSourceWithDialect; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.test.TestDataSourceProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class CloudLoggingServiceConfigurationServiceTest { + + private static final String LIQUIBASE_CHANGELOG_LOCATION = "org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml"; + + private static final String SPACE_ID_1 = "space-id-1"; + private static final String SPACE_ID_2 = "space-id-2"; + private static final String MTA_SPACE_1 = "mta-space-1"; + private static final String MTA_ID_1 = "mta-id-1"; + private static final String MTA_ID_2 = "mta-id-2"; + private static final String ID_1 = "id-1"; + private static final String ID_2 = "id-2"; + + private CloudLoggingServiceConfigurationService service; + + @BeforeEach + void setUp() throws Exception { + DataSourceWithDialect dataSource = new DataSourceWithDialect(TestDataSourceProvider.getDataSource(LIQUIBASE_CHANGELOG_LOCATION)); + service = new CloudLoggingServiceConfigurationService(dataSource); + } + + @AfterEach + void tearDown() { + service.getAllCloudLoggingServiceConfigurationsFromSpace(SPACE_ID_1) + .forEach(config -> service.deleteCloudLoggingServiceConfiguration(config.getId())); + service.getAllCloudLoggingServiceConfigurationsFromSpace(SPACE_ID_2) + .forEach(config -> service.deleteCloudLoggingServiceConfiguration(config.getId())); + } + + @Test + void testStoreAndGetConfiguration() { + LoggingConfiguration config = buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, "ns-1"); + service.storeCloudLoggingServiceConfiguration(config); + + LoggingConfiguration result = service.getCloudLoggingServiceConfiguration(MTA_SPACE_1, MTA_ID_1, "ns-1"); + + assertNotNull(result); + assertEquals(ID_1, result.getId()); + assertEquals(MTA_ID_1, result.getMtaId()); + assertEquals(MTA_SPACE_1, result.getMtaSpace()); + assertEquals(SPACE_ID_1, result.getMtaSpaceId()); + assertEquals(LogLevel.INFO, result.getLogLevel()); + } + + @Test + void testGetConfiguration_withNullNamespace() { + LoggingConfiguration config = buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, null); + service.storeCloudLoggingServiceConfiguration(config); + + LoggingConfiguration result = service.getCloudLoggingServiceConfiguration(MTA_SPACE_1, MTA_ID_1, null); + + assertNotNull(result); + assertEquals(ID_1, result.getId()); + } + + @Test + void testGetConfiguration_returnsNullWhenNotFound() { + LoggingConfiguration result = service.getCloudLoggingServiceConfiguration("nonexistent-space", "nonexistent-mta", "ns"); + + assertNull(result); + } + + @Test + void testDeleteConfiguration() { + LoggingConfiguration config = buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, "ns-2"); + service.storeCloudLoggingServiceConfiguration(config); + + service.deleteCloudLoggingServiceConfiguration(ID_1); + + assertNull(service.getCloudLoggingServiceConfiguration(MTA_SPACE_1, MTA_ID_1, "ns-2")); + } + + @Test + void testDeleteConfiguration_nonExistentIdDoesNotThrow() { + service.deleteCloudLoggingServiceConfiguration("nonexistent-id"); + } + + @Test + void testUpdateConfiguration() { + LoggingConfiguration config = buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, "ns-3"); + service.storeCloudLoggingServiceConfiguration(config); + + LoggingConfiguration updated = ImmutableLoggingConfiguration.builder() + .from(config) + .logLevel(LogLevel.ERROR) + .serviceInstanceName("updated-instance") + .build(); + service.updateCloudLoggingServiceConfiguration(updated); + + LoggingConfiguration result = service.getCloudLoggingServiceConfiguration(MTA_SPACE_1, MTA_ID_1, "ns-3"); + assertNotNull(result); + assertEquals(LogLevel.ERROR, result.getLogLevel()); + assertEquals("updated-instance", result.getServiceInstanceName()); + } + + @Test + void testUpdateConfiguration_withNullNamespace() { + LoggingConfiguration config = buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, null); + service.storeCloudLoggingServiceConfiguration(config); + + LoggingConfiguration updated = ImmutableLoggingConfiguration.builder() + .from(config) + .logLevel(LogLevel.WARN) + .build(); + service.updateCloudLoggingServiceConfiguration(updated); + + LoggingConfiguration result = service.getCloudLoggingServiceConfiguration(MTA_SPACE_1, MTA_ID_1, null); + assertNotNull(result); + assertEquals(LogLevel.WARN, result.getLogLevel()); + } + + @Test + void testGetAllConfigurationsFromSpace_returnsAllForSpace() { + service.storeCloudLoggingServiceConfiguration(buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, "ns-4")); + service.storeCloudLoggingServiceConfiguration(buildConfiguration(ID_2, SPACE_ID_1, MTA_SPACE_1, MTA_ID_2, "ns-5")); + + List results = service.getAllCloudLoggingServiceConfigurationsFromSpace(SPACE_ID_1); + + assertEquals(2, results.size()); + } + + @Test + void testGetAllConfigurationsFromSpace_returnsEmptyListWhenNoneExist() { + List results = service.getAllCloudLoggingServiceConfigurationsFromSpace("unknown-space"); + + assertEquals(0, results.size()); + } + + @Test + void testGetAllConfigurationsFromSpace_doesNotReturnConfigurationsFromOtherSpaces() { + service.storeCloudLoggingServiceConfiguration(buildConfiguration(ID_1, SPACE_ID_1, MTA_SPACE_1, MTA_ID_1, "ns-6")); + service.storeCloudLoggingServiceConfiguration(buildConfiguration(ID_2, SPACE_ID_2, "mta-space-2", MTA_ID_2, "ns-7")); + + List results = service.getAllCloudLoggingServiceConfigurationsFromSpace(SPACE_ID_1); + + assertEquals(1, results.size()); + assertEquals(ID_1, results.get(0) + .getId()); + } + + private LoggingConfiguration buildConfiguration(String id, String mtaSpaceId, String mtaSpace, String mtaId, String namespace) { + return ImmutableLoggingConfiguration.builder() + .id(id) + .mtaSpaceId(mtaSpaceId) + .mtaSpace(mtaSpace) + .mtaId(mtaId) + .mtaOrg("mta-org") + .targetSpace("target-space") + .targetOrg("target-org") + .serviceInstanceName("my-cls-instance") + .serviceKeyName("my-cls-key") + .logLevel(LogLevel.INFO) + .isFailSafe(true) + .namespace(namespace) + .build(); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporterTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporterTest.java new file mode 100644 index 0000000000..bc9f0aaa19 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporterTest.java @@ -0,0 +1,385 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.common.util.JsonUtil; +import org.cloudfoundry.multiapps.controller.persistence.model.ExternalOperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableOperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.OperationLogEntry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class OperationLogsExporterTest { + + private static final String OPERATION_ID = "op-123"; + private static final String SPACE_ID = "space-1"; + private static final String LOG_DATE = "2024 01 15 10:30:00.000"; + private static final String INFO_LOG = logLine(LOG_DATE, "INFO", "deploy-app.hello-backend", + "[main] Starting deployment"); + private static final String WARN_LOG = logLine(LOG_DATE, "WARN", "deploy-app.hello-backend", + "[main] Low memory"); + private static final String ERROR_LOG = logLine(LOG_DATE, "ERROR", "deploy-app.hello-backend", + "[main] Deployment failed"); + private static final String DEBUG_LOG = logLine(LOG_DATE, "DEBUG", "deploy-app.hello-backend", + "[main] Debug info"); + private static final String TRACE_LOG = logLine(LOG_DATE, "TRACE", "deploy-app.hello-backend", + "[main] Trace info"); + + @Mock + private ProcessLogsPersistenceService processLogsPersistenceService; + + private TestOperationLogsExporter exporter; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + exporter = new TestOperationLogsExporter(processLogsPersistenceService); + exporter.removeClientFromCache(OPERATION_ID); + } + + @Test + void testSendLogs_withNullLoggingConfiguration_doesNothing() { + exporter.sendLogsToCloudLoggingService(null, buildEntry(INFO_LOG)); + + assertTrue(exporter.capturedEntries() + .isEmpty()); + } + + @Test + void testSendLogs_withOperationLogEntry_sendsExpectedNumberOfEntries() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, buildEntry(INFO_LOG + WARN_LOG)); + + assertEquals(2, exporter.capturedEntries() + .size()); + } + + @Test + void testSendLogs_withOperationLogEntry_setsLevelOnEntry() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, buildEntry(WARN_LOG)); + + assertEquals("WARN", exporter.capturedEntries() + .get(0) + .getLevel()); + } + + @Test + void testSendLogs_withOperationLogEntry_setsCorrelationId() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, buildEntry(INFO_LOG)); + + assertEquals(OPERATION_ID, exporter.capturedEntries() + .get(0) + .getCorrelationId()); + } + + @Test + void testSendLogs_withOperationLogEntry_setsOperationLogName() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + OperationLogEntry entry = ImmutableOperationLogEntry.builder() + .operationId(OPERATION_ID) + .operationLog(INFO_LOG) + .operationLogName("my-log") + .build(); + + exporter.sendLogsToCloudLoggingService(config, entry); + + assertEquals("my-log", exporter.capturedEntries() + .get(0) + .getOperationLogName()); + } + + @Test + void testSendLogs_withMessageString_extractsLogNameSuffix() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + + assertEquals("hello-backend", exporter.capturedEntries() + .get(0) + .getOperationLogName()); + } + + @Test + void testSendLogs_withMessageString_setsCorrelationId() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + + assertEquals(OPERATION_ID, exporter.capturedEntries() + .get(0) + .getCorrelationId()); + } + + @Test + void testSendLogs_withMessageString_setsLevel() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, ERROR_LOG); + + assertEquals("ERROR", exporter.capturedEntries() + .get(0) + .getLevel()); + } + + @Test + void testSendLogs_withMessageString_producesNoBatchesWhenAllFilteredOut() { + LoggingConfiguration config = buildConfig(LogLevel.ERROR); + + exporter.sendLogsToCloudLoggingService(config, INFO_LOG + DEBUG_LOG); + + assertTrue(exporter.capturedEntries() + .isEmpty()); + } + + static Stream testLogLevelFiltering() { + String allLevels = INFO_LOG + WARN_LOG + ERROR_LOG + DEBUG_LOG + TRACE_LOG; + return Stream.of( + Arguments.of(LogLevel.ERROR, allLevels, 1), + Arguments.of(LogLevel.WARN, allLevels, 2), + Arguments.of(LogLevel.INFO, allLevels, 3), + Arguments.of(LogLevel.DEBUG, allLevels, 4), + Arguments.of(LogLevel.TRACE, allLevels, 5)); + } + + @ParameterizedTest + @MethodSource + void testLogLevelFiltering(LogLevel configuredLevel, String logMessage, int expectedCount) { + LoggingConfiguration config = buildConfig(configuredLevel); + + exporter.sendLogsToCloudLoggingService(config, buildEntry(logMessage)); + + assertEquals(expectedCount, exporter.capturedEntries() + .size()); + } + + @Test + void testSendLogs_multipleEntriesAreSentInOneBatch() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, buildEntry(INFO_LOG + WARN_LOG + ERROR_LOG)); + + assertEquals(1, exporter.capturedBatches.size()); + assertEquals(3, exporter.capturedEntries() + .size()); + } + + @Test + void testSendLogs_emptyLogProducesNoBatches() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + + exporter.sendLogsToCloudLoggingService(config, buildEntry("")); + + assertTrue(exporter.capturedBatches.isEmpty()); + } + + @Test + void testSendLogs_largeBatchIsSplitWhenOverSizeLimit() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + // Build a log entry whose JSON representation exceeds 3.5 MB + String largeText = "x".repeat(1024 * 1024); + String log1 = logLine(LOG_DATE, "INFO", "deploy-app.svc", "[t] " + largeText); + String log2 = logLine(LOG_DATE, "INFO", "deploy-app.svc", "[t] " + largeText); + String log3 = logLine(LOG_DATE, "INFO", "deploy-app.svc", "[t] " + largeText); + String log4 = logLine(LOG_DATE, "INFO", "deploy-app.svc", "[t] " + largeText); + + exporter.sendLogsToCloudLoggingService(config, buildEntry(log1 + log2 + log3 + log4)); + + assertTrue(exporter.capturedBatches.size() > 1); + assertEquals(4, exporter.capturedEntries() + .size()); + } + + // --- failSafe behavior --- + + @Test + void testSendLogs_failSafeTrue_doesNotThrowOnHttpError() { + LoggingConfiguration config = buildConfig(LogLevel.INFO, true); + exporter.responseStatus = HttpStatus.INTERNAL_SERVER_ERROR; + + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + // no exception + } + + @Test + void testSendLogs_failSafeFalse_throwsOnHttpError() { + LoggingConfiguration config = buildConfig(LogLevel.INFO, false); + exporter.responseStatus = HttpStatus.INTERNAL_SERVER_ERROR; + + assertThrows(SLException.class, () -> exporter.sendLogsToCloudLoggingService(config, INFO_LOG)); + } + + @Test + void testSendLogs_nullResponseDoesNotThrow() { + LoggingConfiguration config = buildConfig(LogLevel.INFO, false); + exporter.returnNullResponse = true; + + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + // null response is treated as success + } + + @Test + void testGetUnsendProcessLogs_returnsLogsFromService() throws FileStorageException { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + OperationLogEntry entry = buildEntry(INFO_LOG); + when(processLogsPersistenceService.listOperationLogsBySpaceAndOperationId(SPACE_ID, OPERATION_ID)).thenReturn(List.of(entry)); + + List result = exporter.getUnsendProcessLogs(config); + + assertEquals(1, result.size()); + assertEquals(entry, result.get(0)); + } + + @Test + void testGetUnsendProcessLogs_failSafeTrue_returnsEmptyListOnStorageException() throws FileStorageException { + LoggingConfiguration config = buildConfig(LogLevel.INFO, true); + when(processLogsPersistenceService.listOperationLogsBySpaceAndOperationId(anyString(), + anyString())).thenThrow( + new FileStorageException("db error")); + + List result = exporter.getUnsendProcessLogs(config); + + assertTrue(result.isEmpty()); + } + + @Test + void testGetUnsendProcessLogs_failSafeFalse_throwsOnStorageException() throws FileStorageException { + LoggingConfiguration config = buildConfig(LogLevel.INFO, false); + when(processLogsPersistenceService.listOperationLogsBySpaceAndOperationId(anyString(), + anyString())).thenThrow( + new FileStorageException("db error")); + + assertThrows(SLException.class, () -> exporter.getUnsendProcessLogs(config)); + } + + @Test + void testRemoveClientFromCache_newClientCreatedOnNextSend() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + int clientCreationsAfterFirst = exporter.clientCreations; + + exporter.removeClientFromCache(OPERATION_ID); + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + + assertEquals(clientCreationsAfterFirst + 1, exporter.clientCreations); + } + + @Test + void testSendLogs_cachedClientReusedOnSubsequentCalls() { + LoggingConfiguration config = buildConfig(LogLevel.INFO); + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + int clientCreationsAfterFirst = exporter.clientCreations; + + exporter.sendLogsToCloudLoggingService(config, INFO_LOG); + + assertEquals(clientCreationsAfterFirst, exporter.clientCreations); + } + + private static String logLine(String date, String level, String logName, String text) { + return "#" + date + "#org.example.Logger#" + level + "#" + logName + "#main#\n" + text + "\n"; + } + + private static LoggingConfiguration buildConfig(LogLevel logLevel) { + return buildConfig(logLevel, true); + } + + private static LoggingConfiguration buildConfig(LogLevel logLevel, boolean failSafe) { + return ImmutableLoggingConfiguration.builder() + .operationId(OPERATION_ID) + .mtaSpaceId(SPACE_ID) + .logLevel(logLevel) + .isFailSafe(failSafe) + .endpointUrl("https://cls.example.com") + .serverCa("server-ca") + .clientCert("client-cert") + .clientKey("client-key") + .build(); + } + + private static OperationLogEntry buildEntry(String log) { + return ImmutableOperationLogEntry.builder() + .operationId(OPERATION_ID) + .operationLog(log) + .operationLogName("test-log") + .build(); + } + + private class TestOperationLogsExporter extends OperationLogsExporter { + + final List> capturedBatches = new ArrayList<>(); + HttpStatus responseStatus = HttpStatus.OK; + boolean returnNullResponse = false; + int clientCreations = 0; + + TestOperationLogsExporter(ProcessLogsPersistenceService processLogsPersistenceService) { + super(processLogsPersistenceService); + } + + List capturedEntries() { + return capturedBatches.stream() + .flatMap(List::stream) + .toList(); + } + + @Override + @SuppressWarnings("unchecked") + protected WebClient createWebClientWithMtls(LoggingConfiguration loggingConfiguration) { + clientCreations++; + WebClient webClient = mock(WebClient.class); + WebClient.RequestBodyUriSpec uriSpec = mock(WebClient.RequestBodyUriSpec.class); + WebClient.RequestHeadersSpec headersSpec = mock(WebClient.RequestHeadersSpec.class); + WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class); + + when(webClient.post()).thenReturn(uriSpec); + when(uriSpec.header(anyString(), anyString())).thenReturn(uriSpec); + when(uriSpec.bodyValue(any())).thenAnswer(invocation -> { + String json = invocation.getArgument(0); + List entries = JsonUtil.convertJsonToList(json, + new TypeReference>() { + }); + capturedBatches.add(entries); + return headersSpec; + }); + when(headersSpec.retrieve()).thenReturn(responseSpec); + + if (returnNullResponse) { + when(responseSpec.toBodilessEntity()).thenReturn(Mono.empty()); + } else { + ResponseEntity response = ResponseEntity.status(responseStatus) + .build(); + when(responseSpec.toBodilessEntity()).thenReturn(Mono.just(response)); + } + + return webClient; + } + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersisterTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersisterTest.java index 72756c5382..9be28602e9 100644 --- a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersisterTest.java +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/ProcessLoggerPersisterTest.java @@ -1,23 +1,19 @@ package org.cloudfoundry.multiapps.controller.persistence.services; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.when; - import org.cloudfoundry.multiapps.controller.persistence.Constants; -import org.cloudfoundry.multiapps.controller.persistence.DataSourceWithDialect; -import org.cloudfoundry.multiapps.controller.persistence.test.TestDataSourceProvider; import org.flowable.engine.delegate.DelegateExecution; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + class ProcessLoggerPersisterTest { private final static String TEST_CORRELATION_ID = "test-correlation-id"; @@ -54,12 +50,17 @@ void testPersistLog() { processLoggerPersister.persistLogs(TEST_CORRELATION_ID, TEST_TASK_ID); - Mockito.verify(processLoggerProvider).getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID); - Mockito.verify(processLoggerProvider).removeProcessLoggerFromCache(processLogger); - Mockito.verify(processLoggerProvider).removeProcessLoggerFromCache(processLoggerSecond); - Mockito.verify(processLogsPersistenceService, times(2)).persistLog(any()); - - Assertions.assertEquals(processLoggerProvider.getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID).size(), 0); + Mockito.verify(processLoggerProvider) + .getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID); + Mockito.verify(processLoggerProvider) + .removeProcessLoggerFromCache(processLogger); + Mockito.verify(processLoggerProvider) + .removeProcessLoggerFromCache(processLoggerSecond); + Mockito.verify(processLogsPersistenceService, times(2)) + .persistLog(any()); + + Assertions.assertEquals(processLoggerProvider.getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID) + .size(), 0); } @Test @@ -70,20 +71,28 @@ void testPersistLogWithTwoLogsWithTheSameOperationLogName() { processLoggerPersister.persistLogs(TEST_CORRELATION_ID, TEST_TASK_ID); - Mockito.verify(processLoggerProvider).getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID); - Mockito.verify(processLoggerProvider).removeProcessLoggerFromCache(processLogger); - Mockito.verify(processLoggerProvider).removeProcessLoggerFromCache(processLoggerSecond); - Mockito.verify(processLoggerProvider).removeProcessLoggerFromCache(processLoggerThird); - Mockito.verify(processLogsPersistenceService, times(2)).persistLog(any()); - - Assertions.assertEquals(processLoggerProvider.getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID).size(), 0); + Mockito.verify(processLoggerProvider) + .getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID); + Mockito.verify(processLoggerProvider) + .removeProcessLoggerFromCache(processLogger); + Mockito.verify(processLoggerProvider) + .removeProcessLoggerFromCache(processLoggerSecond); + Mockito.verify(processLoggerProvider) + .removeProcessLoggerFromCache(processLoggerThird); + Mockito.verify(processLogsPersistenceService, times(2)) + .persistLog(any()); + + Assertions.assertEquals(processLoggerProvider.getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID) + .size(), 0); } @Test void testPersistLogWithoutLogs() { processLoggerPersister.persistLogs(TEST_CORRELATION_ID, TEST_TASK_ID); - Mockito.verify(processLoggerProvider).getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID); - Mockito.verify(processLogsPersistenceService, times(0)).persistLog(any()); + Mockito.verify(processLoggerProvider) + .getExistingLoggers(TEST_CORRELATION_ID, TEST_TASK_ID); + Mockito.verify(processLogsPersistenceService, times(0)) + .persistLog(any()); } } diff --git a/multiapps-controller-process/src/main/java/module-info.java b/multiapps-controller-process/src/main/java/module-info.java index 35a6a74387..7fb5765585 100644 --- a/multiapps-controller-process/src/main/java/module-info.java +++ b/multiapps-controller-process/src/main/java/module-info.java @@ -63,6 +63,11 @@ requires static java.compiler; requires static org.immutables.value; + requires spring.webflux; + requires annotations; + requires reactor.netty.core; + requires reactor.netty.http; requires org.cloudfoundry.multiapps.controller.shutdown.client; + requires com.google.errorprone.annotations; } \ No newline at end of file diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/flowable/FlowableFacade.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/flowable/FlowableFacade.java index f32f4fbdce..55c7efe65d 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/flowable/FlowableFacade.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/flowable/FlowableFacade.java @@ -1,7 +1,5 @@ package org.cloudfoundry.multiapps.controller.process.flowable; -import static java.text.MessageFormat.format; - import java.text.MessageFormat; import java.time.LocalDateTime; import java.time.ZoneId; @@ -12,9 +10,9 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.persistence.Constants; import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.variables.VariableHandling; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.flowable.common.engine.api.FlowableObjectNotFoundException; import org.flowable.common.engine.api.FlowableOptimisticLockingException; @@ -32,6 +30,8 @@ import org.slf4j.LoggerFactory; import org.springframework.context.annotation.DependsOn; +import static java.text.MessageFormat.format; + @Named @DependsOn("processEngine") public class FlowableFacade { @@ -299,4 +299,9 @@ public void setVariableInParentProcess(DelegateExecution execution, String varia .setVariable(parentProcessId, variableName, value); } + public void setVariableInParentProcessXSA(DelegateExecution execution, String variableName, Object value) { + String parentProcessInstanceId = VariableHandling.get(execution, Variables.PARENT_PROCESS_INSTANCE_ID); + processEngine.getRuntimeService() + .setVariable(parentProcessInstanceId, variableName, value); + } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/AbstractProcessExecutionListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/AbstractProcessExecutionListener.java index 846b6f8b5f..8ef844fa44 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/AbstractProcessExecutionListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/AbstractProcessExecutionListener.java @@ -1,10 +1,10 @@ package org.cloudfoundry.multiapps.controller.process.listeners; import jakarta.inject.Inject; - import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLogger; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; @@ -32,6 +32,7 @@ public abstract class AbstractProcessExecutionListener implements ExecutionListe private final FlowableFacade flowableFacade; protected final ApplicationConfiguration configuration; private final ProcessLoggerPersister processLoggerPersister; + private final OperationLogsExporter operationLogsExporter; private StepLogger stepLogger; @@ -39,7 +40,7 @@ public abstract class AbstractProcessExecutionListener implements ExecutionListe protected AbstractProcessExecutionListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { this.progressMessageService = progressMessageService; this.stepLoggerFactory = stepLoggerFactory; this.processLoggerProvider = processLoggerProvider; @@ -47,6 +48,7 @@ protected AbstractProcessExecutionListener(ProgressMessageService progressMessag this.historicOperationEventService = historicOperationEventService; this.flowableFacade = flowableFacade; this.configuration = configuration; + this.operationLogsExporter = operationLogsExporter; } @Override @@ -56,8 +58,8 @@ public void notify(DelegateExecution execution) { this.stepLogger = createStepLogger(execution); notifyInternal(execution); } catch (Exception e) { - logException(e, Messages.EXECUTION_OF_PROCESS_LISTENER_HAS_FAILED); - throw new SLException(e, Messages.EXECUTION_OF_PROCESS_LISTENER_HAS_FAILED); + logException(e, Messages.EXECUTION_OF_PROCESS_LISTENER_HAS_FAILED + e.getMessage()); + throw new SLException(e, Messages.EXECUTION_OF_PROCESS_LISTENER_HAS_FAILED + e.getMessage()); } finally { finalizeLogs(execution); } @@ -114,7 +116,7 @@ protected HistoricOperationEventService getHistoricOperationEventService() { } private StepLogger createStepLogger(DelegateExecution execution) { - return stepLoggerFactory.create(execution, progressMessageService, processLoggerProvider, getLogger()); + return stepLoggerFactory.create(execution, progressMessageService, processLoggerProvider, getLogger(), operationLogsExporter); } protected boolean isRootProcess(DelegateExecution execution) { @@ -127,6 +129,16 @@ protected void setVariableInParentProcess(DelegateExecution execution, String va flowableFacade.setVariableInParentProcess(execution, variableName, value); } + protected void setVariableInParentProcessXSA(DelegateExecution execution, String variableName, Object value) { + flowableFacade.setVariableInParentProcessXSA(execution, variableName, value); + } + + protected boolean hasSuperExecution(DelegateExecution execution) { + return execution.getParentId() != null + && flowableFacade.getParentExecution(execution.getParentId()) + .getSuperExecutionId() != null; + } + protected abstract void notifyInternal(DelegateExecution execution) throws Exception; protected Logger getLogger() { diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/CreateUpdateServicesListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/CreateUpdateServicesListener.java index 05a98f8790..44d1d9db39 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/CreateUpdateServicesListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/CreateUpdateServicesListener.java @@ -8,12 +8,12 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.core.helpers.DynamicResolvableParametersHelper; import org.cloudfoundry.multiapps.controller.core.model.DynamicResolvableParameter; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -34,14 +34,15 @@ public class CreateUpdateServicesListener extends AbstractProcessExecutionListen protected CreateUpdateServicesListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); } @Override @@ -59,8 +60,9 @@ private void setCloudServiceInstancesInParentProcess(DelegateExecution execution } private void setDynamicResolvableParametersInParentProcess(DelegateExecution execution, List services) { - Set dynamicResolvableParametersFromSubProcesses = getDynamicResolvableParametersFromSubProcesses(execution, - services); + Set dynamicResolvableParametersFromSubProcesses = getDynamicResolvableParametersFromSubProcesses( + execution, + services); Set resolvedParameters = new HashSet<>(VariableHandling.get(execution, Variables.DYNAMIC_RESOLVABLE_PARAMETERS)); @@ -71,7 +73,8 @@ private void setDynamicResolvableParametersInParentProcess(DelegateExecution exe Variables.DYNAMIC_RESOLVABLE_PARAMETERS.getSerializer() .serialize(resolvedParameters)); execution.setVariable(Variables.DYNAMIC_RESOLVABLE_PARAMETERS.getName(), Variables.DYNAMIC_RESOLVABLE_PARAMETERS.getSerializer() - .serialize(resolvedParameters)); + .serialize( + resolvedParameters)); } private Set getDynamicResolvableParametersFromSubProcesses(DelegateExecution execution, @@ -82,7 +85,8 @@ private Set getDynamicResolvableParametersFromSubPro .map(serviceGuidConstant -> StepsUtil.getObject(execution, serviceGuidConstant)) .filter(Objects::nonNull) .map(dynamicResolvableParameterObject -> Variables.DYNAMIC_RESOLVABLE_PARAMETER.getSerializer() - .deserialize(dynamicResolvableParameterObject)) + .deserialize( + dynamicResolvableParameterObject)) .collect(Collectors.toSet()); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DeployAppSubProcessEndListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DeployAppSubProcessEndListener.java index a5d2259a1f..a8e0234ab8 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DeployAppSubProcessEndListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DeployAppSubProcessEndListener.java @@ -7,6 +7,7 @@ import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -27,9 +28,15 @@ public class DeployAppSubProcessEndListener extends AbstractProcessExecutionList protected DeployAppSubProcessEndListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { - super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, - flowableFacade, configuration); + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { + super(progressMessageService, + stepLoggerFactory, + processLoggerProvider, + processLoggerPersister, + historicOperationEventService, + flowableFacade, + configuration, + operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DetermineServiceCreateUpdateActionsListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DetermineServiceCreateUpdateActionsListener.java index a00ecaa40f..dbab6adde6 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DetermineServiceCreateUpdateActionsListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DetermineServiceCreateUpdateActionsListener.java @@ -4,10 +4,10 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudServiceInstanceExtended; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -29,14 +29,16 @@ protected DetermineServiceCreateUpdateActionsListener(ProgressMessageService pro StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, - FlowableFacade flowableFacade, ApplicationConfiguration configuration) { + FlowableFacade flowableFacade, ApplicationConfiguration configuration, + OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); } public static String buildExportedVariableName(String serviceName) { diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DoNotDeleteServicesListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DoNotDeleteServicesListener.java index a2445ef612..3db1bab558 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DoNotDeleteServicesListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/DoNotDeleteServicesListener.java @@ -1,9 +1,9 @@ package org.cloudfoundry.multiapps.controller.process.listeners; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -20,14 +20,15 @@ public class DoNotDeleteServicesListener extends AbstractProcessExecutionListene protected DoNotDeleteServicesListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListener.java index 4a00b41734..4f6c4d35c1 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListener.java @@ -2,11 +2,11 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.api.model.Operation; import org.cloudfoundry.multiapps.controller.api.model.ProcessType; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -35,14 +35,16 @@ public EndProcessListener(ProgressMessageService progressMessageService, StepLog ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, ApplicationConfiguration configuration, OperationInFinalStateHandler eventHandler, - DynatracePublisher dynatracePublisher, ProcessTypeParser processTypeParser) { + DynatracePublisher dynatracePublisher, ProcessTypeParser processTypeParser, + OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); this.eventHandler = eventHandler; this.dynatracePublisher = dynatracePublisher; this.processTypeParser = processTypeParser; @@ -61,7 +63,8 @@ private void publishDynatraceEvent(DelegateExecution execution, ProcessType proc .processId(VariableHandling.get(execution, Variables.CORRELATION_ID)) .mtaId(VariableHandling.get(execution, Variables.MTA_ID)) - .createdBy(VariableHandling.get(execution, Variables.MTA_ARCHIVE_CREATED_BY)) + .createdBy(VariableHandling.get(execution, + Variables.MTA_ARCHIVE_CREATED_BY)) .spaceId(VariableHandling.get(execution, Variables.SPACE_GUID)) .eventType(DynatraceProcessEvent.EventType.FINISHED) .processType(processType) diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessStatisticsListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessStatisticsListener.java index 17a7fc22b9..cc82d243b7 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessStatisticsListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessStatisticsListener.java @@ -2,9 +2,9 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -21,14 +21,15 @@ public class EndProcessStatisticsListener extends AbstractProcessExecutionListen protected EndProcessStatisticsListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListener.java index 061bf009c4..aad0a8237f 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListener.java @@ -2,11 +2,11 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.api.model.ImmutableOperation; import org.cloudfoundry.multiapps.controller.api.model.Operation; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.OperationService; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; @@ -29,14 +29,16 @@ public class EnterTestingPhaseListener extends AbstractProcessExecutionListener protected EnterTestingPhaseListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration, OperationService operationService) { + ApplicationConfiguration configuration, OperationService operationService, + OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); this.operationService = operationService; } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListener.java new file mode 100644 index 0000000000..11e132f325 --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListener.java @@ -0,0 +1,60 @@ +package org.cloudfoundry.multiapps.controller.process.listeners; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; +import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; +import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; +import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; +import org.cloudfoundry.multiapps.controller.process.flowable.FlowableFacade; +import org.cloudfoundry.multiapps.controller.process.util.StepLogger; +import org.cloudfoundry.multiapps.controller.process.variables.Serializer; +import org.cloudfoundry.multiapps.controller.process.variables.Variable; +import org.cloudfoundry.multiapps.controller.process.variables.VariableHandling; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.flowable.engine.delegate.DelegateExecution; + +@Named("exportCloudLoggingConfigurationListener") +public class ExportCloudLoggingConfigurationListener extends AbstractProcessExecutionListener { + + @Inject + protected ExportCloudLoggingConfigurationListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, + ProcessLoggerProvider processLoggerProvider, + ProcessLoggerPersister processLoggerPersister, + HistoricOperationEventService historicOperationEventService, + FlowableFacade flowableFacade, ApplicationConfiguration configuration, + OperationLogsExporter operationLogsExporter) { + super(progressMessageService, + stepLoggerFactory, + processLoggerProvider, + processLoggerPersister, + historicOperationEventService, + flowableFacade, + configuration, + operationLogsExporter); + } + + @Override + protected void notifyInternal(DelegateExecution execution) throws Exception { + LoggingConfiguration loggingConfiguration = VariableHandling.get(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION); + if (loggingConfiguration == null) { + return; + } + Variable loggingConfigurationVariable = Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION; + Serializer loggingConfigurationSerializer = loggingConfigurationVariable.getSerializer(); + String parentProcessInstanceId = VariableHandling.get(execution, Variables.PARENT_PROCESS_INSTANCE_ID); + + if (parentProcessInstanceId != null && !parentProcessInstanceId.isEmpty()) { + setVariableInParentProcessXSA(execution, loggingConfigurationVariable.getName(), + loggingConfigurationSerializer.serialize(loggingConfiguration)); + } else if (hasSuperExecution(execution)) { + setVariableInParentProcess(execution, loggingConfigurationVariable.getName(), + loggingConfigurationSerializer.serialize(loggingConfiguration)); + } else { + VariableHandling.set(execution, loggingConfigurationVariable, loggingConfiguration); + } + } +} diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/HooksEndProcessListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/HooksEndProcessListener.java index 6ec5b3f721..6bc5f3fda0 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/HooksEndProcessListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/HooksEndProcessListener.java @@ -1,7 +1,9 @@ package org.cloudfoundry.multiapps.controller.process.listeners; +import jakarta.inject.Named; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -10,22 +12,21 @@ import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.flowable.engine.delegate.DelegateExecution; -import jakarta.inject.Named; - @Named("hooksEndProcessListener") public class HooksEndProcessListener extends AbstractProcessExecutionListener { protected HooksEndProcessListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/LeaveTestingPhaseListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/LeaveTestingPhaseListener.java index 7b808bef0e..357572c064 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/LeaveTestingPhaseListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/LeaveTestingPhaseListener.java @@ -2,9 +2,9 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -22,14 +22,15 @@ public class LeaveTestingPhaseListener extends AbstractProcessExecutionListener protected LeaveTestingPhaseListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration) { + ApplicationConfiguration configuration, OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListener.java index 6b0fdaa5ab..00c65d082a 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListener.java @@ -2,11 +2,11 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.controller.api.model.ProcessType; import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -29,14 +29,16 @@ public class ManageAppServiceBindingEndListener extends AbstractProcessExecution protected ManageAppServiceBindingEndListener(ProgressMessageService progressMessageService, StepLogger.Factory stepLoggerFactory, ProcessLoggerProvider processLoggerProvider, ProcessLoggerPersister processLoggerPersister, HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, - ApplicationConfiguration configuration, ProcessTypeParser processTypeParser) { + ApplicationConfiguration configuration, ProcessTypeParser processTypeParser, + OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); this.processTypeParser = processTypeParser; } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListener.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListener.java index fbca97c731..362c7cfb41 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListener.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListener.java @@ -9,7 +9,6 @@ import jakarta.inject.Inject; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.common.util.JsonUtil; import org.cloudfoundry.multiapps.controller.api.model.ImmutableOperation; @@ -24,6 +23,7 @@ import org.cloudfoundry.multiapps.controller.persistence.services.FileService; import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.OperationService; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; @@ -63,14 +63,16 @@ public StartProcessListener(ProgressMessageService progressMessageService, StepL HistoricOperationEventService historicOperationEventService, FlowableFacade flowableFacade, ApplicationConfiguration configuration, ProcessTypeParser processTypeParser, OperationService operationService, ProcessTypeToOperationMetadataMapper operationMetadataMapper, - DynatracePublisher dynatracePublisher, FileService fileService) { + DynatracePublisher dynatracePublisher, FileService fileService, + OperationLogsExporter operationLogsExporter) { super(progressMessageService, stepLoggerFactory, processLoggerProvider, processLoggerPersister, historicOperationEventService, flowableFacade, - configuration); + configuration, + operationLogsExporter); this.processTypeParser = processTypeParser; this.operationService = operationService; this.operationMetadataMapper = operationMetadataMapper; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CollectCloudLoggingServiceParametersStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CollectCloudLoggingServiceParametersStep.java new file mode 100644 index 0000000000..f0746e120f --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/CollectCloudLoggingServiceParametersStep.java @@ -0,0 +1,188 @@ +package org.cloudfoundry.multiapps.controller.process.steps; + +import java.util.List; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.api.model.ProcessType; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; +import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; +import org.cloudfoundry.multiapps.controller.core.cf.v2.ResourceType; +import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.OperationLogEntry; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; +import org.cloudfoundry.multiapps.controller.process.util.ExternalLoggingServiceConfigurationsCalculator; +import org.cloudfoundry.multiapps.controller.process.util.ProcessTypeParser; +import org.cloudfoundry.multiapps.controller.process.variables.VariableHandling; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Resource; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Scope; + +import static org.apache.commons.lang3.StringUtils.EMPTY; + +@Named("collectCloudLoggingServiceParametersStep") +@Scope(BeanDefinition.SCOPE_PROTOTYPE) +public class CollectCloudLoggingServiceParametersStep extends SyncFlowableStep { + + @Inject + private TokenService tokenService; + + @Inject + private CloudControllerClientFactory clientFactory; + + @Inject + private OperationLogsExporter operationLogsExporter; + + @Inject + private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + + @Inject + private ProcessTypeParser processTypeParser; + + @Inject + private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; + + @Override + protected StepPhase executeStep(ProcessContext context) throws Exception { + LoggingConfiguration loggingConfiguration = getLoggingConfiguration(context); + if (loggingConfiguration == null) { + return StepPhase.DONE; + } + List operationLogEntries = operationLogsExporter.getUnsendProcessLogs(loggingConfiguration); + + for (OperationLogEntry operationLogEntry : operationLogEntries) { + operationLogsExporter.sendLogsToCloudLoggingService(loggingConfiguration, operationLogEntry); + } + context.setVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + return StepPhase.DONE; + } + + private LoggingConfiguration getLoggingConfiguration(ProcessContext context) { + ProcessType processType = processTypeParser.getProcessType(context.getExecution()); + LoggingConfiguration loggingConfiguration = getExistingLoggingConfiguration(context); + + if (processType.equals(ProcessType.UNDEPLOY)) { + return processUndeployLoggingConfiguration(context, loggingConfiguration); + } else { + return processDeployLoggingConfiguration(context, loggingConfiguration); + } + } + + private LoggingConfiguration processUndeployLoggingConfiguration(ProcessContext context, + LoggingConfiguration existingLoggingConfiguration) { + if (existingLoggingConfiguration == null) { + return null; + } + return setExternalLoggingServiceConfigurationIfRequired(context, existingLoggingConfiguration); + } + + private LoggingConfiguration processDeployLoggingConfiguration(ProcessContext context, + LoggingConfiguration existingLoggingConfiguration) { + DeploymentDescriptor deploymentDescriptor = context.getVariable(Variables.DEPLOYMENT_DESCRIPTOR); + if (!isCloudLoggingEnabled(deploymentDescriptor)) { + if (existingLoggingConfiguration != null) { + cloudLoggingServiceConfigurationAuditLog.logDeleteLoggingConfiguration(context.getVariable(Variables.USER), + context.getVariable(Variables.SPACE_GUID), + existingLoggingConfiguration); + cloudLoggingServiceConfigurationService.deleteCloudLoggingServiceConfiguration(existingLoggingConfiguration.getId()); + } + return null; + } + + existingLoggingConfiguration = setExternalLoggingServiceConfigurationIfRequired(context, deploymentDescriptor); + if (existingLoggingConfiguration == null) { + return null; + } + storeOrUpdateLoggingConfiguration(context, existingLoggingConfiguration, getExistingLoggingConfiguration(context)); + return existingLoggingConfiguration; + } + + private void storeOrUpdateLoggingConfiguration(ProcessContext context, LoggingConfiguration loggingConfiguration, + LoggingConfiguration existingLoggingConfiguration) { + if (existingLoggingConfiguration == null) { + cloudLoggingServiceConfigurationAuditLog.logCreateLoggingConfiguration(context.getVariable(Variables.USER), + context.getVariable(Variables.SPACE_GUID), + loggingConfiguration); + cloudLoggingServiceConfigurationService.storeCloudLoggingServiceConfiguration(loggingConfiguration); + } else { + cloudLoggingServiceConfigurationAuditLog.logUpdateLoggingConfiguration(context.getVariable(Variables.USER), + context.getVariable(Variables.SPACE_GUID), + loggingConfiguration); + cloudLoggingServiceConfigurationService.updateCloudLoggingServiceConfiguration(loggingConfiguration); + } + } + + private LoggingConfiguration getExistingLoggingConfiguration(ProcessContext context) { + + LoggingConfiguration loggingConfiguration = cloudLoggingServiceConfigurationService.getCloudLoggingServiceConfiguration( + context.getVariable(Variables.SPACE_NAME), context.getVariable(Variables.MTA_ID), context.getVariable(Variables.MTA_NAMESPACE)); + + if (loggingConfiguration != null) { + cloudLoggingServiceConfigurationAuditLog.logGetLoggingConfiguration(context.getVariable(Variables.USER), + context.getVariable(Variables.SPACE_GUID), + loggingConfiguration); + } + return loggingConfiguration; + } + + @Override + protected String getStepErrorMessage(ProcessContext context) { + return "Well, failed! Deal with it!"; + } + + protected boolean isRootProcess(DelegateExecution execution) { + String correlationId = VariableHandling.get(execution, Variables.CORRELATION_ID); + String processInstanceId = execution.getProcessInstanceId(); + return processInstanceId.equals(correlationId); + } + + protected LoggingConfiguration setExternalLoggingServiceConfigurationIfRequired(ProcessContext context, + DeploymentDescriptor deploymentDescriptor) { + ExternalLoggingServiceConfigurationsCalculator calculator = new ExternalLoggingServiceConfigurationsCalculator(clientFactory, + context, + tokenService); + Resource resource = getLoggingServiceResource(deploymentDescriptor.getResources()); + return calculator.exportOperationLogsToExternalSystem(resource); + } + + protected LoggingConfiguration setExternalLoggingServiceConfigurationIfRequired(ProcessContext context, + LoggingConfiguration loggingConfiguration) { + ExternalLoggingServiceConfigurationsCalculator calculator = new ExternalLoggingServiceConfigurationsCalculator(clientFactory, + context, + tokenService); + return calculator.exportOperationLogsToExternalSystem(loggingConfiguration, context); + } + + private boolean isCloudLoggingEnabled(DeploymentDescriptor deploymentDescriptor) { + if (deploymentDescriptor.getResources() + .isEmpty()) { + return false; + } + + return deploymentDescriptor.getResources() + .stream() + .anyMatch(CollectCloudLoggingServiceParametersStep::isCloudLoggingServiceResource); + } + + private Resource getLoggingServiceResource(List resources) { + return resources.stream() + .filter(CollectCloudLoggingServiceParametersStep::isCloudLoggingServiceResource) + .findFirst() + .get(); + } + + private static boolean isCloudLoggingServiceResource(Resource resource) { + String resourceType = resource.getType() + .replace("org.cloudfoundry.", EMPTY); + ResourceType resourceType1 = ResourceType.get(resourceType); + if (resourceType1 == null) { + return false; + } + return ResourceType.CLOUD_LOGGING_SERVICE.equals(resourceType1); + } +} diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java index 57f2e88526..fcc6f8d2a3 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ExecuteTaskStep.java @@ -12,6 +12,7 @@ import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; import org.cloudfoundry.multiapps.controller.process.variables.Variables; @@ -28,6 +29,8 @@ public class ExecuteTaskStep extends TimeoutAsyncFlowableStep { private CloudControllerClientFactory clientFactory; @Inject private TokenService tokenService; + @Inject + private OperationLogsExporter operationLogsExporter; @Override protected StepPhase executeAsyncStep(ProcessContext context) { @@ -51,7 +54,7 @@ protected String getStepErrorMessage(ProcessContext context) { @Override protected List getAsyncStepExecutions(ProcessContext context) { - return List.of(new PollExecuteTaskStatusExecution(clientFactory, tokenService)); + return List.of(new PollExecuteTaskStatusExecution(clientFactory, tokenService, operationLogsExporter)); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/IncrementalAppInstancesUpdateStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/IncrementalAppInstancesUpdateStep.java index 7f46f4e753..8685147a06 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/IncrementalAppInstancesUpdateStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/IncrementalAppInstancesUpdateStep.java @@ -18,6 +18,7 @@ import org.cloudfoundry.multiapps.controller.core.model.DeployedMtaApplication; import org.cloudfoundry.multiapps.controller.core.model.ImmutableIncrementalAppInstanceUpdateConfiguration; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; import org.cloudfoundry.multiapps.controller.process.variables.Variables; @@ -38,11 +39,14 @@ public class IncrementalAppInstancesUpdateStep extends TimeoutAsyncFlowableStep private final CloudControllerClientFactory clientFactory; private final TokenService tokenService; + private final OperationLogsExporter operationLogsExportero; @Inject - public IncrementalAppInstancesUpdateStep(CloudControllerClientFactory clientFactory, TokenService tokenService) { + public IncrementalAppInstancesUpdateStep(CloudControllerClientFactory clientFactory, TokenService tokenService, + OperationLogsExporter operationLogsExportero) { this.clientFactory = clientFactory; this.tokenService = tokenService; + this.operationLogsExportero = operationLogsExportero; } @Override @@ -174,8 +178,8 @@ private void setExecutionIndexToTriggerNewApplicationRollingUpdate(ProcessContex protected List getAsyncStepExecutions(ProcessContext context) { // The sequence of executions is crucial, as the incremental blue-green deployment alternates between them during the polling // process - return List.of(new PollStartLiveAppExecution(clientFactory, tokenService), - new PollStartAppExecutionWithRollbackExecution(clientFactory, tokenService), + return List.of(new PollStartLiveAppExecution(clientFactory, tokenService, operationLogsExportero), + new PollStartAppExecutionWithRollbackExecution(clientFactory, tokenService, operationLogsExportero), new PollIncrementalAppInstanceUpdateExecution()); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteAppStatusExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteAppStatusExecution.java index 3ad349d46e..33c8641556 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteAppStatusExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteAppStatusExecution.java @@ -17,6 +17,7 @@ import org.cloudfoundry.multiapps.controller.core.helpers.ApplicationAttributes; import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.variables.Variables; @@ -58,13 +59,16 @@ String getMessage() { private final CloudControllerClientFactory clientFactory; private final TokenService tokenService; + private final OperationLogsExporter operationLogsExporter; private static final String DEFAULT_SUCCESS_MARKER = "STDOUT:SUCCESS"; private static final String DEFAULT_FAILURE_MARKER = "STDERR:FAILURE"; - public PollExecuteAppStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService) { + public PollExecuteAppStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService, + OperationLogsExporter operationLogsExporter) { this.clientFactory = clientFactory; this.tokenService = tokenService; + this.operationLogsExporter = operationLogsExporter; } @Override @@ -90,7 +94,7 @@ public AsyncExecutionState execute(ProcessContext context) { AppExecutionDetailedStatus status = getAppExecutionStatus(context, appAttributes, recentLogs); ProcessLoggerProvider processLoggerProvider = context.getStepLogger() .getProcessLoggerProvider(); - StepsUtil.saveAppLogs(context, logCacheClient, appGuid, app.getName(), LOGGER, processLoggerProvider); + StepsUtil.saveAppLogs(context, logCacheClient, appGuid, app.getName(), LOGGER, processLoggerProvider, operationLogsExporter); return checkAppExecutionStatus(context, app, appAttributes, status); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteTaskStatusExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteTaskStatusExecution.java index cc214da344..62f82ee27d 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteTaskStatusExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollExecuteTaskStatusExecution.java @@ -9,6 +9,7 @@ import org.cloudfoundry.multiapps.controller.client.lib.domain.CloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.process.Constants; import org.cloudfoundry.multiapps.controller.process.Messages; @@ -25,10 +26,13 @@ public class PollExecuteTaskStatusExecution implements AsyncExecution { private final CloudControllerClientFactory clientFactory; private final TokenService tokenService; + private final OperationLogsExporter operationLogsExporter; - public PollExecuteTaskStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService) { + public PollExecuteTaskStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService, + OperationLogsExporter operationLogsExporter) { this.clientFactory = clientFactory; this.tokenService = tokenService; + this.operationLogsExporter = operationLogsExporter; } @Override @@ -50,7 +54,7 @@ public AsyncExecutionState execute(ProcessContext context) { var logCacheClient = clientFactory.createLogCacheClient(tokenService.getToken(userGuid), correlationId); UUID appGuid = client.getApplicationGuid(app.getName()); - StepsUtil.saveAppLogs(context, logCacheClient, appGuid, app.getName(), LOGGER, processLoggerProvider); + StepsUtil.saveAppLogs(context, logCacheClient, appGuid, app.getName(), LOGGER, processLoggerProvider, operationLogsExporter); if (currentState == CloudTask.State.SUCCEEDED) { return AsyncExecutionState.FINISHED; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecution.java index e057ed7b89..c550bc8601 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecution.java @@ -8,6 +8,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.PackageState; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.ApplicationStager; @@ -24,12 +25,14 @@ public class PollStageAppStatusExecution implements AsyncExecution { private final ApplicationStager applicationStager; private final CloudControllerClientFactory clientFactory; private final TokenService tokenService; + private final OperationLogsExporter operationLogsExporter; public PollStageAppStatusExecution(ApplicationStager applicationStager, CloudControllerClientFactory clientFactory, - TokenService tokenService) { + TokenService tokenService, OperationLogsExporter operationLogsExporter) { this.applicationStager = applicationStager; this.clientFactory = clientFactory; this.tokenService = tokenService; + this.operationLogsExporter = operationLogsExporter; } @Override @@ -49,7 +52,8 @@ public AsyncExecutionState execute(ProcessContext context) { var logCacheClient = clientFactory.createLogCacheClient(tokenService.getToken(userGuid), correlationId); UUID appGuid = client.getApplicationGuid(application.getName()); - StepsUtil.saveAppLogs(context, logCacheClient, appGuid, application.getName(), LOGGER, processLoggerProvider); + StepsUtil.saveAppLogs(context, logCacheClient, appGuid, application.getName(), LOGGER, processLoggerProvider, + operationLogsExporter); if (state.getState() != PackageState.STAGED) { return checkStagingState(context.getStepLogger(), application.getName(), state); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecution.java index a1fbc35243..69410eeadd 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecution.java @@ -6,6 +6,7 @@ import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.model.IncrementalAppInstanceUpdateConfiguration; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.variables.Variables; @@ -13,8 +14,9 @@ public class PollStartAppExecutionWithRollbackExecution extends PollStartAppStatusExecution { - public PollStartAppExecutionWithRollbackExecution(CloudControllerClientFactory clientFactory, TokenService tokenService) { - super(clientFactory, tokenService); + public PollStartAppExecutionWithRollbackExecution(CloudControllerClientFactory clientFactory, TokenService tokenService, + OperationLogsExporter operationLogsExporter) { + super(clientFactory, tokenService, operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecution.java index a0012d229e..b6ed2cc870 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecution.java @@ -12,6 +12,7 @@ import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; import org.cloudfoundry.multiapps.controller.core.util.UriUtil; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.ReadinessHealthCheckUtil; @@ -26,10 +27,13 @@ public class PollStartAppStatusExecution implements AsyncExecution { private static final Logger LOGGER = LoggerFactory.getLogger(PollStartAppStatusExecution.class); private final CloudControllerClientFactory clientFactory; private final TokenService tokenService; + private final OperationLogsExporter operationLogsExporter; - public PollStartAppStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService) { + public PollStartAppStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService, + OperationLogsExporter operationLogsExporter) { this.clientFactory = clientFactory; this.tokenService = tokenService; + this.operationLogsExporter = operationLogsExporter; } @Override @@ -51,7 +55,7 @@ public AsyncExecutionState execute(ProcessContext context) { var correlationId = context.getVariable(Variables.CORRELATION_ID); var logCacheClient = clientFactory.createLogCacheClient(tokenService.getToken(userGuid), correlationId); - StepsUtil.saveAppLogs(context, logCacheClient, app.getGuid(), app.getName(), LOGGER, processLoggerProvider); + StepsUtil.saveAppLogs(context, logCacheClient, app.getGuid(), app.getName(), LOGGER, processLoggerProvider, operationLogsExporter); return checkStartupStatus(context, app, status); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecution.java index 169ee05bc8..327e7d36c6 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecution.java @@ -7,13 +7,15 @@ import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.model.DeployedMtaApplication; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.variables.Variables; public class PollStartLiveAppExecution extends PollStartAppStatusExecution { - public PollStartLiveAppExecution(CloudControllerClientFactory clientFactory, TokenService tokenService) { - super(clientFactory, tokenService); + public PollStartLiveAppExecution(CloudControllerClientFactory clientFactory, TokenService tokenService, + OperationLogsExporter operationLogsExporter) { + super(clientFactory, tokenService, operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartServiceBrokerSubscriberStatusExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartServiceBrokerSubscriberStatusExecution.java index 4840c8b664..9435942c92 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartServiceBrokerSubscriberStatusExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartServiceBrokerSubscriberStatusExecution.java @@ -3,11 +3,13 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; public class PollStartServiceBrokerSubscriberStatusExecution extends PollStartAppStatusExecution { - public PollStartServiceBrokerSubscriberStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService) { - super(clientFactory, tokenService); + public PollStartServiceBrokerSubscriberStatusExecution(CloudControllerClientFactory clientFactory, TokenService tokenService, + OperationLogsExporter operationLogsExporter) { + super(clientFactory, tokenService, operationLogsExporter); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelper.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelper.java index 78666e3308..6ba8977412 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelper.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelper.java @@ -10,6 +10,7 @@ import org.cloudfoundry.multiapps.controller.persistence.model.HistoricOperationEvent; import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableProgressMessage; import org.cloudfoundry.multiapps.controller.persistence.model.ProgressMessage.ProgressMessageType; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLogger; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -36,9 +37,9 @@ public static boolean isProcessAborted(List historicOper } protected void postExecuteStep(ProcessContext context, StepPhase state) { - logDebug(MessageFormat.format(Messages.STEP_FINISHED, context.getExecution() - .getCurrentFlowElement() - .getName())); + logDebug(context, MessageFormat.format(Messages.STEP_FINISHED, context.getExecution() + .getCurrentFlowElement() + .getName())); getProcessLoggerPersister().persistLogs(context.getVariable(Variables.CORRELATION_ID), context.getVariable(Variables.TASK_ID)); context.setVariable(Variables.STEP_EXECUTION, state.toString()); @@ -72,8 +73,13 @@ protected void logExceptionAndStoreProgressMessage(ProcessContext context, Throw private void logException(ProcessContext context, Throwable t) { LOGGER.error(Messages.EXCEPTION_CAUGHT, t); - getProcessLogger().error(Messages.EXCEPTION_CAUGHT, t); + ProcessLogger a = getProcessLogger(); + a.error(Messages.EXCEPTION_CAUGHT, t); + if (context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION) != null) { + getOperationLogsExporter().sendLogsToCloudLoggingService(context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION), + a.getLogMessage()); + } if (t instanceof ContentException) { context.setVariable(Variables.ERROR_TYPE, ErrorType.CONTENT_ERROR); } else { @@ -90,7 +96,14 @@ private void storeExceptionInProgressMessageService(ProcessContext context, Thro .text(throwable.getMessage()) .build()); } catch (SLException e) { - getProcessLogger().error(Messages.SAVING_ERROR_MESSAGE_FAILED, e); + ProcessLogger a = getProcessLogger(); + a.error(Messages.SAVING_ERROR_MESSAGE_FAILED, e); + + if (context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION) != null) { + getOperationLogsExporter().sendLogsToCloudLoggingService( + context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION), + a.getLogMessage()); + } } } @@ -112,8 +125,14 @@ private String getCurrentActivityId(DelegateExecution execution) { .getActivityId(); } - private void logDebug(String message) { - getProcessLogger().debug(message); + private void logDebug(ProcessContext context, String message) { + ProcessLogger a = getProcessLogger(); + a.debug(message); + + if (context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION) != null) { + getOperationLogsExporter().sendLogsToCloudLoggingService(context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION), + a.getLogMessage()); + } } private ProcessLogger getProcessLogger() { @@ -134,6 +153,8 @@ public void failStepIfProcessIsAborted(ProcessContext context) { public abstract ProcessLoggerPersister getProcessLoggerPersister(); + public abstract OperationLogsExporter getOperationLogsExporter(); + public abstract ProcessEngineConfiguration getProcessEngineConfiguration(); public abstract ProcessHelper getProcessHelper(); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartAppStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartAppStep.java index 8a897002a6..08b1149af7 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartAppStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartAppStep.java @@ -12,6 +12,7 @@ import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.model.HookPhase; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.ReadinessHealthCheckUtil; import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; @@ -28,6 +29,8 @@ public class RestartAppStep extends TimeoutAsyncFlowableStepWithHooks implements protected CloudControllerClientFactory clientFactory; @Inject protected TokenService tokenService; + @Inject + protected OperationLogsExporter operationLogsExporter; @Override public StepPhase executePollingStep(ProcessContext context) { @@ -92,8 +95,8 @@ public List getHookPhasesBeforeStep(ProcessContext context) { @Override protected List getAsyncStepExecutions(ProcessContext context) { - return List.of(new PollStartAppStatusExecution(clientFactory, tokenService), - new PollExecuteAppStatusExecution(clientFactory, tokenService)); + return List.of(new PollStartAppStatusExecution(clientFactory, tokenService, operationLogsExporter), + new PollExecuteAppStatusExecution(clientFactory, tokenService, operationLogsExporter)); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartServiceBrokerSubscriberStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartServiceBrokerSubscriberStep.java index aaaa25f930..fec9181b20 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartServiceBrokerSubscriberStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/RestartServiceBrokerSubscriberStep.java @@ -25,7 +25,7 @@ protected CloudApplication getAppToRestart(ProcessContext context) { @Override protected List getAsyncStepExecutions(ProcessContext context) { - return List.of(new PollStartServiceBrokerSubscriberStatusExecution(clientFactory, tokenService)); + return List.of(new PollStartServiceBrokerSubscriberStatusExecution(clientFactory, tokenService, operationLogsExporter)); } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StageAppStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StageAppStep.java index 8e11f5cafa..6a52302727 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StageAppStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StageAppStep.java @@ -9,6 +9,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.util.ApplicationStager; import org.cloudfoundry.multiapps.controller.process.util.TimeoutType; @@ -24,6 +25,8 @@ public class StageAppStep extends TimeoutAsyncFlowableStep { protected CloudControllerClientFactory clientFactory; @Inject protected TokenService tokenService; + @Inject + protected OperationLogsExporter operationLogsExporter; @Override protected StepPhase executeAsyncStep(ProcessContext context) { @@ -40,7 +43,7 @@ protected String getStepErrorMessage(ProcessContext context) { @Override protected List getAsyncStepExecutions(ProcessContext context) { - return List.of(new PollStageAppStatusExecution(new ApplicationStager(context), clientFactory, tokenService)); + return List.of(new PollStageAppStatusExecution(new ApplicationStager(context), clientFactory, tokenService, operationLogsExporter)); } @Override diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StepsUtil.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StepsUtil.java index 3dba3e9306..dffe6be5af 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StepsUtil.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/StepsUtil.java @@ -32,6 +32,8 @@ import org.cloudfoundry.multiapps.controller.core.model.DeployedMta; import org.cloudfoundry.multiapps.controller.core.model.Phase; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; +import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLogger; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.process.Constants; import org.cloudfoundry.multiapps.controller.process.Messages; @@ -162,7 +164,7 @@ static CloudTask getTask(ProcessContext context) { } static void saveAppLogs(ProcessContext context, LogCacheClient client, UUID appGuid, String appName, Logger logger, - ProcessLoggerProvider processLoggerProvider) { + ProcessLoggerProvider processLoggerProvider, OperationLogsExporter operationLogsExporter) { LocalDateTime offset = context.getVariable(Variables.LOGS_OFFSET); var recentLogs = getRecentLogsSafely(client, appGuid, offset, logger); if (recentLogs.isEmpty()) { @@ -173,8 +175,12 @@ static void saveAppLogs(ProcessContext context, LogCacheClient client, UUID appG } var loggerPrefix = getLoggerPrefix(logger); for (ApplicationLog log : recentLogs) { - processLoggerProvider.getLogger(context.getExecution(), appName) - .debug(loggerPrefix + log.toString()); + ProcessLogger processLogger = processLoggerProvider.getLogger(context.getExecution(), appName); + processLogger.debug(loggerPrefix + log.toString()); + if (context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION) != null) { + operationLogsExporter.sendLogsToCloudLoggingService(context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION), + processLogger.getLogMessage()); + } } var lastLog = recentLogs.get(recentLogs.size() - 1); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStep.java index 7241c4805c..fcfeafee1d 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStep.java @@ -1,5 +1,8 @@ package org.cloudfoundry.multiapps.controller.process.steps; +import java.util.function.BiFunction; +import java.util.function.Consumer; + import io.netty.handler.timeout.TimeoutException; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -14,6 +17,7 @@ import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.core.util.LoggingUtil; import org.cloudfoundry.multiapps.controller.persistence.services.FileService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -33,9 +37,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.function.BiFunction; -import java.util.function.Consumer; - public abstract class SyncFlowableStep implements JavaDelegate { protected final Logger logger = LoggerFactory.getLogger(getClass()); @@ -66,6 +67,8 @@ public abstract class SyncFlowableStep implements JavaDelegate { private StepLogger stepLogger; @Inject private ProcessHelper processHelper; + @Inject + private OperationLogsExporter operationLogsExporter; @Override public void execute(DelegateExecution execution) { @@ -209,7 +212,7 @@ protected StepLogger getStepLogger() { } protected void initializeStepLogger(DelegateExecution execution) { - stepLogger = stepLoggerFactory.create(execution, progressMessageService, processLoggerProvider, logger); + stepLogger = stepLoggerFactory.create(execution, progressMessageService, processLoggerProvider, logger, operationLogsExporter); } protected Exception getWithProperMessage(Exception e) { @@ -225,6 +228,7 @@ protected ProcessStepHelper getStepHelper() { .progressMessageService(getProgressMessageService()) .stepLogger(getStepLogger()) .processLoggerPersister(processLoggerPersister) + .operationLogsExporter(operationLogsExporter) .processEngineConfiguration(processEngineConfiguration) .processHelper(processHelper) .build(); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecution.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecution.java index f68e1bd55f..4da2cc59c2 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecution.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecution.java @@ -4,6 +4,7 @@ import java.text.MessageFormat; import java.time.Duration; import java.time.LocalDateTime; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -23,6 +24,8 @@ import org.cloudfoundry.multiapps.controller.core.helpers.MtaArchiveElements; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.core.util.FileUtils; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.context.ApplicationToUploadContext; @@ -44,13 +47,16 @@ public class UploadAppAsyncExecution implements AsyncExecution { private final ProcessLoggerPersister processLoggerPersister; private final ApplicationConfiguration applicationConfiguration; private final ExecutorService appUploaderThreadPool; + private final OperationLogsExporter operationLogsExporter; public UploadAppAsyncExecution(ApplicationZipBuilder applicationZipBuilder, ProcessLoggerPersister processLoggerPersister, - ApplicationConfiguration applicationConfiguration, ExecutorService appUploaderThreadPool) { + ApplicationConfiguration applicationConfiguration, ExecutorService appUploaderThreadPool, + OperationLogsExporter operationLogsExporter) { this.applicationZipBuilder = applicationZipBuilder; this.processLoggerPersister = processLoggerPersister; this.applicationConfiguration = applicationConfiguration; this.appUploaderThreadPool = appUploaderThreadPool; + this.operationLogsExporter = operationLogsExporter; } @Override @@ -111,15 +117,16 @@ private CloudPackage doUpload(ProcessContext context, CloudApplicationExtended a context.getStepLogger() .debug(Messages.UPLOAD_OF_APPLICATION_0_STARTED_ON_INSTANCE_1, applicationToProcess.getName(), applicationConfiguration.getApplicationInstanceIndex()); - return proceedWithUpload(context.getControllerClient(), applicationToUploadContext); + return proceedWithUpload(context.getControllerClient(), applicationToUploadContext, context); } - private CloudPackage proceedWithUpload(CloudControllerClient client, ApplicationToUploadContext applicationToUploadContext) { + private CloudPackage proceedWithUpload(CloudControllerClient client, ApplicationToUploadContext applicationToUploadContext, + ProcessContext context) { applicationToUploadContext.getStepLogger() .debug(Messages.UPLOADING_FILE_0_FOR_APP_1, applicationToUploadContext.getModuleFileName(), applicationToUploadContext.getApplication() .getName()); - CloudPackage cloudPackage = asyncUploadFiles(client, applicationToUploadContext); + CloudPackage cloudPackage = asyncUploadFiles(client, applicationToUploadContext, context); applicationToUploadContext.getStepLogger() .info(Messages.STARTED_ASYNC_UPLOAD_OF_APP_0, applicationToUploadContext.getApplication() .getName()); @@ -127,7 +134,8 @@ private CloudPackage proceedWithUpload(CloudControllerClient client, Application return cloudPackage; } - private CloudPackage asyncUploadFiles(CloudControllerClient client, ApplicationToUploadContext applicationToUploadContext) { + private CloudPackage asyncUploadFiles(CloudControllerClient client, ApplicationToUploadContext applicationToUploadContext, + ProcessContext context) { Path extractedAppPath = extractApplicationFromArchive(applicationToUploadContext); LOGGER.debug(MessageFormat.format(Messages.APPLICATION_WITH_NAME_0_SAVED_TO_1, applicationToUploadContext.getApplication() .getName(), @@ -137,7 +145,7 @@ private CloudPackage asyncUploadFiles(CloudControllerClient client, ApplicationT .getName(), extractedAppPath.toFile() .length()); - return upload(client, applicationToUploadContext, extractedAppPath); + return upload(client, applicationToUploadContext, extractedAppPath, context); } private Path extractApplicationFromArchive(ApplicationToUploadContext applicationToUploadContext) { @@ -164,15 +172,16 @@ protected Path extractFromMtar(ApplicationArchiveContext applicationArchiveConte } private CloudPackage upload(CloudControllerClient client, ApplicationToUploadContext applicationToUploadContext, - Path extractedModulePath) { + Path extractedModulePath, ProcessContext context) { try { return client.asyncUploadApplicationWithExponentialBackoff(applicationToUploadContext.getApplication() .getName(), extractedModulePath, getMonitorUploadStatusCallback( - applicationToUploadContext.getApplication(), extractedModulePath, + applicationToUploadContext.getApplication(), + extractedModulePath, applicationToUploadContext.getStepLogger(), applicationToUploadContext.getCorrelationId(), - applicationToUploadContext.getTaskId()), null); + applicationToUploadContext.getTaskId(), context), null); } catch (Exception e) { FileUtils.cleanUp(extractedModulePath, LOGGER); throw new SLException(e, Messages.ERROR_WHILE_STARTING_ASYNC_UPLOAD_OF_APP_WITH_NAME_0, @@ -208,8 +217,8 @@ public String getPollingErrorMessage(ProcessContext context) { } MonitorUploadStatusCallback getMonitorUploadStatusCallback(CloudApplication app, Path file, StepLogger stepLogger, String correlationId, - String taskId) { - return new MonitorUploadStatusCallback(app, file, stepLogger, correlationId, taskId); + String taskId, ProcessContext context) { + return new MonitorUploadStatusCallback(app, file, stepLogger, correlationId, taskId, context); } class MonitorUploadStatusCallback implements UploadStatusCallbackExtended { @@ -219,13 +228,16 @@ class MonitorUploadStatusCallback implements UploadStatusCallbackExtended { private final StepLogger stepLogger; private final String correlationId; private final String taskId; + private final ProcessContext context; - public MonitorUploadStatusCallback(CloudApplication app, Path file, StepLogger stepLogger, String correlationId, String taskId) { + public MonitorUploadStatusCallback(CloudApplication app, Path file, StepLogger stepLogger, String correlationId, String taskId, + ProcessContext context) { this.app = app; this.file = file; this.stepLogger = stepLogger; this.correlationId = correlationId; this.taskId = taskId; + this.context = context; } @Override @@ -248,6 +260,7 @@ public boolean onProgress(String status) { stepLogger.debug(Messages.UPLOAD_STATUS_0, status); if (status.equals(Status.READY.toString())) { FileUtils.cleanUp(file, LOGGER); + sendApplicationLogsToCloudLoggingService(correlationId, taskId); processLoggerPersister.persistLogs(correlationId, taskId); } return false; @@ -264,6 +277,14 @@ public void onError(String description) { stepLogger.error(Messages.ERROR_UPLOADING_APP_0_STATUS_1_DESCRIPTION_2, app.getName(), Status.FAILED, description); FileUtils.cleanUp(file, LOGGER); } + + private void sendApplicationLogsToCloudLoggingService(String correlationId, String taskId) { + LoggingConfiguration loggingConfiguration = context.getVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION); + List processLogs = processLoggerPersister.getApplicationProcessLogsMessages(correlationId, taskId); + for (String processLog : processLogs) { + operationLogsExporter.sendLogsToCloudLoggingService(loggingConfiguration, processLog); + } + } } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppStep.java index 0f19e1fa0e..07cca4c632 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppStep.java @@ -24,6 +24,7 @@ import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.security.util.SecureLoggingUtil; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.util.ApplicationArchiveContext; import org.cloudfoundry.multiapps.controller.process.util.ApplicationDigestCalculator; import org.cloudfoundry.multiapps.controller.process.util.ApplicationStager; @@ -53,6 +54,8 @@ public class UploadAppStep extends TimeoutAsyncFlowableStep { protected CloudPackagesGetter cloudPackagesGetter; @Inject private ExecutorService appUploaderThreadPool; + @Inject + protected OperationLogsExporter operationLogsExporter; @Override public StepPhase executeAsyncStep(ProcessContext context) throws FileStorageException { @@ -185,7 +188,8 @@ private void removeApplicationDigestIfSet(ProcessContext context, Map getAsyncStepExecutions(ProcessContext context) { - return List.of(new UploadAppAsyncExecution(applicationZipBuilder, getProcessLogsPersister(), configuration, appUploaderThreadPool), + return List.of(new UploadAppAsyncExecution(applicationZipBuilder, getProcessLogsPersister(), configuration, appUploaderThreadPool, + operationLogsExporter), new PollUploadAppStatusExecution()); } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/ExternalLoggingServiceConfigurationsCalculator.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/ExternalLoggingServiceConfigurationsCalculator.java new file mode 100644 index 0000000000..07f69c9104 --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/ExternalLoggingServiceConfigurationsCalculator.java @@ -0,0 +1,251 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.text.MessageFormat; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.common.util.MiscUtil; +import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; +import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; +import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.process.steps.ProcessContext; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.Resource; + +public class ExternalLoggingServiceConfigurationsCalculator { + + private final CloudControllerClientFactory clientFactory; + private final ProcessContext context; + private final TokenService tokenService; + + public ExternalLoggingServiceConfigurationsCalculator(CloudControllerClientFactory clientFactory, ProcessContext context, + TokenService tokenService) { + this.clientFactory = clientFactory; + this.context = context; + this.tokenService = tokenService; + } + + public LoggingConfiguration exportOperationLogsToExternalSystem(Resource resource) { + + LoggingConfiguration loggingConfiguration = getCredentialsFromServiceKey(resource); + if (loggingConfiguration == null) { + return null; + } + + String correlationId = context.getVariable(Variables.CORRELATION_ID); + String spaceId = getTargetSpace(resource, context.getVariable(Variables.SPACE_NAME)); + String orgId = getTargetOrg(resource, context.getVariable(Variables.ORGANIZATION_NAME)); + LogLevel logLevel = getLogLevelsFromConfiguration(resource); + + return ImmutableLoggingConfiguration.copyOf(loggingConfiguration) + .withId(UUID.randomUUID() + .toString()) + .withOperationId(correlationId) + .withTargetSpace(spaceId) + .withTargetOrg(orgId) + .withMtaId(context.getVariable(Variables.MTA_ID)) + .withMtaSpaceId(context.getVariable(Variables.SPACE_GUID)) + .withMtaSpace(context.getVariable(Variables.SPACE_NAME)) + .withMtaOrg(context.getVariable(Variables.ORGANIZATION_NAME)) + .withNamespace(context.getVariable(Variables.MTA_NAMESPACE)) + .withLogLevel(logLevel) + .withIsFailSafe(resource.isOptional()); + } + + public LoggingConfiguration exportOperationLogsToExternalSystem(LoggingConfiguration incommingLoggingConfiguration, + ProcessContext context) { + return getCredentialsFromServiceKey(incommingLoggingConfiguration, context); + } + + private LogLevel getLogLevelsFromConfiguration(Resource resource) { + LogLevel logLevel = LogLevel.INFO; + if (resource.getParameters() + .containsKey(SupportedParameters.LOG_LEVEL)) { + String logLevelFromDescriptor = MiscUtil.cast(resource.getParameters() + .get(SupportedParameters.LOG_LEVEL)); + logLevel = LogLevel.get(logLevelFromDescriptor); + } + return logLevel; + } + + private CloudServiceKey getCloudLoggingServiceKey(String serviceInstanceName, String serviceKeyName, String destinationOrg, + String destinationSpace, boolean isFailSafe) { + String correlationId = context.getVariable(Variables.CORRELATION_ID); + if (areCloudLoggingParametersValid(serviceInstanceName, serviceKeyName)) { + if (isFailSafe) { + return null; + } else { + throw new SLException( + MessageFormat.format("No logging service key found for operation {0}, skipping log export", correlationId)); + } + } + CloudControllerClient client1 = calculateExternalLoggingServiceConfiguration(destinationOrg, destinationSpace); + try { + CloudServiceKey loggingServiceKey = client1.getServiceKey(serviceInstanceName, serviceKeyName); + if (loggingServiceKey == null) { + if (isFailSafe) { + return null; + } else { + throw new SLException( + MessageFormat.format("No logging service key found for operation {0}, skipping log export", correlationId)); + } + } + return loggingServiceKey; + } catch (CloudOperationException e) { + if (isFailSafe) { + return null; + } else { + throw new SLException(e); + } + } + } + + private boolean areCloudLoggingParametersValid(String serviceInstanceName, String serviceKeyName) { + return serviceInstanceName == null || serviceInstanceName.isBlank() || serviceKeyName == null || serviceKeyName.isBlank(); + } + + private String getServiceKeyName(Resource resource) { + List> serviceKeys = MiscUtil.cast(resource.getParameters() + .get(SupportedParameters.SERVICE_KEYS)); + if (serviceKeys == null || serviceKeys.isEmpty()) { + return null; + } + return MiscUtil.cast(serviceKeys.get(0) + .get(SupportedParameters.NAME)); + } + + private String getServiceInstanceName(Resource resource) { + if (resource.getParameters() + .containsKey(SupportedParameters.SERVICE_NAME)) { + return MiscUtil.cast(resource.getParameters() + .get(SupportedParameters.SERVICE_NAME)); + } else { + return resource.getName(); + } + } + + private LoggingConfiguration getCredentialsFromServiceKey(LoggingConfiguration loggingConfiguration, ProcessContext context) { + CloudServiceKey loggingServiceKey = getServiceKeyWithLoggingConfiguration(loggingConfiguration); + if (loggingServiceKey == null) { + return null; + } + Map credentials = loggingServiceKey.getCredentials(); + + String endpoint = getCredentialFromServiceKey("ingest-mtls-endpoint", credentials); + String serverCa = getCredentialFromServiceKey("server-ca", credentials); + String ingestMtlsCert = getCredentialFromServiceKey("ingest-mtls-cert", credentials); + String ingestMtlsKey = getCredentialFromServiceKey("ingest-mtls-key", credentials); + + return ImmutableLoggingConfiguration.copyOf(loggingConfiguration) + .withOperationId(context.getVariable(Variables.CORRELATION_ID)) + .withMtaSpaceId(context.getVariable(Variables.SPACE_GUID)) + .withServerCa(serverCa) + .withEndpointUrl(endpoint) + .withClientCert(ingestMtlsCert) + .withClientKey(ingestMtlsKey); + } + + private CloudServiceKey getServiceKeyWithResource(Resource resource) { + return getCloudLoggingServiceKey(getServiceInstanceName(resource), getServiceKeyName(resource), + getTargetOrg(resource, context.getVariable(Variables.ORGANIZATION_NAME)), + getTargetSpace(resource, context.getVariable(Variables.SPACE_NAME)), + resource.isOptional()); + } + + private CloudServiceKey getServiceKeyWithLoggingConfiguration(LoggingConfiguration loggingConfiguration) { + return getCloudLoggingServiceKey(loggingConfiguration.getServiceInstanceName(), loggingConfiguration.getServiceKeyName(), + loggingConfiguration.getTargetOrg(), loggingConfiguration.getTargetSpace(), + loggingConfiguration.isFailSafe()); + } + + private LoggingConfiguration getCredentialsFromServiceKey(Resource resource) { + CloudServiceKey loggingServiceKey = getServiceKeyWithResource(resource); + if (loggingServiceKey == null) { + return null; + } + Map credentials = loggingServiceKey.getCredentials(); + + String endpoint = getCredentialFromServiceKey("ingest-mtls-endpoint", credentials); + String serverCa = getCredentialFromServiceKey("server-ca", credentials); + String ingestMtlsCert = getCredentialFromServiceKey("ingest-mtls-cert", credentials); + String ingestMtlsKey = getCredentialFromServiceKey("ingest-mtls-key", credentials); + + return ImmutableLoggingConfiguration.builder() + .serverCa(serverCa) + .endpointUrl(endpoint) + .clientKey(ingestMtlsKey) + .clientCert(ingestMtlsCert) + .serviceInstanceName(getServiceInstanceName(resource)) + .serviceKeyName(loggingServiceKey.getName()) + .build(); + } + + private String getCredentialFromServiceKey(String credentialsName, Map credentials) { + String credential = (String) credentials.get(credentialsName); + + if (credential == null) { + throw new IllegalArgumentException("Missing required " + credentialsName + " credential for SAP Cloud Logging export"); + } + + return credential; + } + + private CloudControllerClient calculateExternalLoggingServiceConfiguration(String destinationOrg, String destinationSpace) { + String currentTargetOrg = context.getVariable(Variables.ORGANIZATION_NAME); + String currentTargetSpace = context.getVariable(Variables.SPACE_NAME); + CloudControllerClient client = context.getControllerClient(); + + String targetOrg = getTargetOrg(destinationOrg, currentTargetOrg); + String targetSpace = getTargetSpace(destinationSpace, currentTargetSpace); + + if (!targetOrg.equals(currentTargetOrg) || !targetSpace.equals(currentTargetSpace)) { + client = clientFactory.createClient(tokenService.getToken(context.getVariable(Variables.USER_GUID)), targetOrg, + targetSpace, context.getVariable(Variables.CORRELATION_ID)); + } + + return client; + } + + private String getTargetOrg(String existingLoggingConfigurationOrg, String org) { + return existingLoggingConfigurationOrg == null ? org : existingLoggingConfigurationOrg; + } + + private String getTargetSpace(String existingLoggingConfigurationSpace, String space) { + return existingLoggingConfigurationSpace == null ? space : existingLoggingConfigurationSpace; + } + + private String getTargetOrg(Resource resource, String org) { + Map destination = getDestination(resource); + if (destination == null) { + return org; + } + return getDestination(resource).get("org-name") == null + ? org + : getDestination(resource).get("org-name") + .toString(); + } + + private String getTargetSpace(Resource resource, String space) { + Map destination = getDestination(resource); + if (destination == null) { + return space; + } + return destination.get("space-name") == null + ? space + : destination.get("space-name") + .toString(); + } + + private Map getDestination(Resource resource) { + return MiscUtil.cast(resource.getParameters() + .get(SupportedParameters.DESTINATION)); + } +} diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandler.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandler.java index 87f8f900bd..b9c43150e1 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandler.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandler.java @@ -1,5 +1,12 @@ package org.cloudfoundry.multiapps.controller.process.util; +import java.text.MessageFormat; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + import jakarta.inject.Inject; import jakarta.inject.Named; import org.cloudfoundry.client.v3.Metadata; @@ -8,6 +15,7 @@ import org.cloudfoundry.multiapps.controller.api.model.Operation.State; import org.cloudfoundry.multiapps.controller.api.model.ProcessType; import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataAnnotations; import org.cloudfoundry.multiapps.controller.core.model.DeployedMta; @@ -16,10 +24,13 @@ import org.cloudfoundry.multiapps.controller.core.util.SafeExecutor; import org.cloudfoundry.multiapps.controller.persistence.model.HistoricOperationEvent; import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableHistoricOperationEvent; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; import org.cloudfoundry.multiapps.controller.persistence.services.DescriptorBackupService; import org.cloudfoundry.multiapps.controller.persistence.services.FileService; import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.OperationService; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.dynatrace.DynatraceProcessDuration; @@ -34,13 +45,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.text.MessageFormat; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; - import static java.text.MessageFormat.format; @Named @@ -64,6 +68,14 @@ public class OperationInFinalStateHandler { private DynatracePublisher dynatracePublisher; @Inject private SecretTokenStoreFactory secretTokenStoreFactory; + @Inject + private OperationLogsExporter operationLogsExporter; + @Inject + private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + @Inject + private ProcessTypeParser processTypeParser; + @Inject + private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; private final SafeExecutor safeExecutor = new SafeExecutor(); @@ -81,6 +93,8 @@ private void handleInternal(DelegateExecution execution, ProcessType processType safeExecutor.execute(() -> deletePreviousBackupDescriptors(execution, processType, state)); safeExecutor.execute(() -> deleteSecretTokensForProcess(correlationId)); safeExecutor.execute(() -> trackOperationDuration(correlationId, execution, processType, state)); + safeExecutor.execute(() -> deleteCloudLoggingServiceConfiguration(execution)); + operationLogsExporter.removeClientFromCache(correlationId); } protected void deleteDeploymentFiles(String correlationId, DelegateExecution execution) throws FileStorageException { @@ -191,6 +205,21 @@ private void deleteSecretTokensForProcess(String correlationId) { secretTokenStore.deleteByProcessInstanceId(correlationId); } + private void deleteCloudLoggingServiceConfiguration(DelegateExecution execution) { + LoggingConfiguration loggingConfiguration = VariableHandling.get(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION); + if (loggingConfiguration == null || loggingConfiguration.getId() == null) { + return; + } + ProcessType processType = processTypeParser.getProcessType(execution); + if (processType.equals(ProcessType.UNDEPLOY)) { + + cloudLoggingServiceConfigurationService.deleteCloudLoggingServiceConfiguration(loggingConfiguration.getId()); + cloudLoggingServiceConfigurationAuditLog.logDeleteLoggingConfiguration(VariableHandling.get(execution, Variables.USER), + VariableHandling.get(execution, Variables.SPACE_GUID), + loggingConfiguration); + } + } + private void deleteDisposableUserProvidedServiceForProcess(DelegateExecution execution, String correlationId) { boolean isDisposableUserProvidedServiceEnabled = VariableHandling.get(execution, Variables.IS_DISPOSABLE_USER_PROVIDED_SERVICE_ENABLED); diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/StepLogger.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/StepLogger.java index b28b7c83ed..7123276f78 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/StepLogger.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/StepLogger.java @@ -3,11 +3,12 @@ import java.text.MessageFormat; import jakarta.inject.Named; - import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.core.util.UserMessageLogger; import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableProgressMessage; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; import org.cloudfoundry.multiapps.controller.persistence.model.ProgressMessage.ProgressMessageType; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLogger; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -19,7 +20,6 @@ /** * The purpose of this class is to group logging of progress messages and process logs in a single place. - * */ public class StepLogger implements UserMessageLogger { @@ -27,13 +27,15 @@ public class StepLogger implements UserMessageLogger { protected final ProgressMessageService progressMessageService; protected final ProcessLoggerProvider processLoggerProvider; protected final Logger simpleStepLogger; + protected final OperationLogsExporter operationLogsExporter; public StepLogger(DelegateExecution execution, ProgressMessageService progressMessageService, - ProcessLoggerProvider processLoggerProvider, Logger simpleStepLogger) { + ProcessLoggerProvider processLoggerProvider, Logger simpleStepLogger, OperationLogsExporter operationLogsExporter) { this.execution = execution; this.progressMessageService = progressMessageService; this.processLoggerProvider = processLoggerProvider; this.simpleStepLogger = simpleStepLogger; + this.operationLogsExporter = operationLogsExporter; } public void logFlowableTask() { @@ -46,7 +48,10 @@ public void infoWithoutProgressMessage(String pattern, Object... arguments) { public void infoWithoutProgressMessage(String message) { simpleStepLogger.info(message); - getProcessLogger().info(getPrefix(simpleStepLogger) + message); + ProcessLogger processLogger = getProcessLogger(); + processLogger.info(getPrefix(simpleStepLogger) + message); + String formattedMessage = processLogger.getLogMessage(); + sendLogsToCLoudLoggingService(formattedMessage); } public void info(String pattern, Object... arguments) { @@ -72,7 +77,12 @@ public void errorWithoutProgressMessage(String pattern, Object... arguments) { public void errorWithoutProgressMessage(String message) { simpleStepLogger.error(message); - getProcessLogger().error(getPrefix(simpleStepLogger) + message); + ProcessLogger processLogger = getProcessLogger(); + processLogger.error(getPrefix(simpleStepLogger) + message); + String formattedMessage = processLogger.getLogMessage(); + + sendLogsToCLoudLoggingService(formattedMessage); + } public void error(Exception e, String pattern, Object... arguments) { @@ -98,16 +108,27 @@ public void warnWithoutProgressMessage(Exception e, String pattern, Object... ar public void warnWithoutProgressMessage(Exception e, String message) { simpleStepLogger.warn(message, e); - getProcessLogger().warn(getPrefix(simpleStepLogger) + message, e); + ProcessLogger processLogger = getProcessLogger(); + processLogger.warn(getPrefix(simpleStepLogger) + message, e); + String formattedMessage = processLogger.getLogMessage(); + + sendLogsToCLoudLoggingService(formattedMessage); + } public void warnWithoutProgressMessage(String pattern, Object... arguments) { warnWithoutProgressMessage(MessageFormat.format(pattern, arguments)); } + @Override public void warnWithoutProgressMessage(String message) { simpleStepLogger.warn(message); - getProcessLogger().warn(getPrefix(simpleStepLogger) + message); + ProcessLogger processLogger = getProcessLogger(); + processLogger.warn(getPrefix(simpleStepLogger) + message); + String formattedMessage = processLogger.getLogMessage(); + + sendLogsToCLoudLoggingService(formattedMessage); + } public void warn(Exception e, String pattern, Object... arguments) { @@ -139,7 +160,11 @@ public void debug(String pattern, Object... arguments) { public void debug(String message) { simpleStepLogger.debug(message); - getProcessLogger().debug(getPrefix(simpleStepLogger) + message); + + ProcessLogger processLogger = getProcessLogger(); + processLogger.debug(getPrefix(simpleStepLogger) + message); + String formattedMessage = processLogger.getLogMessage(); + sendLogsToCLoudLoggingService(formattedMessage); } public void trace(String pattern, Object... arguments) { @@ -148,7 +173,17 @@ public void trace(String pattern, Object... arguments) { public void trace(String message) { simpleStepLogger.trace(message); - getProcessLogger().trace(getPrefix(simpleStepLogger) + message); + ProcessLogger processLogger = getProcessLogger(); + processLogger.trace(getPrefix(simpleStepLogger) + message); + String formattedMessage = processLogger.getLogMessage(); + sendLogsToCLoudLoggingService(formattedMessage); + } + + private void sendLogsToCLoudLoggingService(String message) { + LoggingConfiguration loggingConfiguration = VariableHandling.get(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION); + if (loggingConfiguration != null) { + operationLogsExporter.sendLogsToCloudLoggingService(loggingConfiguration, message); + } } private static String getExtendedMessage(String message, Exception e) { @@ -191,8 +226,8 @@ private static String getPrefix(Logger logger) { public static class Factory { public StepLogger create(DelegateExecution execution, ProgressMessageService progressMessageService, - ProcessLoggerProvider processLoggerProvider, Logger logger) { - return new StepLogger(execution, progressMessageService, processLoggerProvider, logger); + ProcessLoggerProvider processLoggerProvider, Logger logger, OperationLogsExporter operationLogsExporter) { + return new StepLogger(execution, progressMessageService, processLoggerProvider, logger, operationLogsExporter); } } diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java index b041ef2a74..2852856de5 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/variables/Variables.java @@ -35,6 +35,7 @@ import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationSubscription; import org.cloudfoundry.multiapps.controller.persistence.model.FileEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; import org.cloudfoundry.multiapps.controller.process.DeployStrategy; import org.cloudfoundry.multiapps.controller.process.steps.StepPhase; import org.cloudfoundry.multiapps.controller.process.util.ArchiveEntryWithStreamPositions; @@ -969,4 +970,27 @@ public Serializer> getSerializer() { .defaultValue(false) .build(); + Variable EXTERNAL_LOGGING_SERVICE_CONFIGURATION = ImmutableJsonStringVariable. builder() + .name( + "externalLoggingServiceConfigurations") + .type( + Variable.typeReference( + LoggingConfiguration.class)) + .defaultValue(null) + .build(); + + Variable IS_EXTERNAL_LOGGING_SERVICE_ENABLED = ImmutableSimpleVariable. builder() + .name("isExternalLoggingServiceEnabled") + .defaultValue(false) + .build(); + + Variable IS_LOG_CACHE_CLEARED = ImmutableSimpleVariable. builder() + .name("isLogCacheCleared") + .defaultValue(false) + .build(); + + Variable PARENT_PROCESS_INSTANCE_ID = ImmutableSimpleVariable. builder() + .name("parentProcessInstanceId") + .defaultValue("") + .build(); } diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn index 350f22b579..90cbd7d391 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/backup-existing-app.bpmn @@ -30,6 +30,8 @@ + + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/delete-services.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/delete-services.bpmn index 30f3e19405..afd51de023 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/delete-services.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/delete-services.bpmn @@ -37,6 +37,8 @@ + + @@ -69,6 +71,8 @@ + + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn index db163d450d..2ba37bec14 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/deploy-app.bpmn @@ -93,6 +93,8 @@ + + @@ -121,6 +123,7 @@ + @@ -155,6 +158,7 @@ + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/process-batches-sequentially.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/process-batches-sequentially.bpmn index 88c5f94466..fcf00a8cb9 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/process-batches-sequentially.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/process-batches-sequentially.bpmn @@ -40,6 +40,8 @@ + + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/recreate-service-keys.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/recreate-service-keys.bpmn index 1fc0ad3a6c..9f4248d08d 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/recreate-service-keys.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/recreate-service-keys.bpmn @@ -21,6 +21,8 @@ + + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/stop-dependent-modules.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/stop-dependent-modules.bpmn index 46a2558e93..b5f40f9387 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/stop-dependent-modules.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/stop-dependent-modules.bpmn @@ -62,6 +62,8 @@ + + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn index 9b393908d4..13f505be6b 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/undeploy-app.bpmn @@ -31,6 +31,8 @@ + + @@ -67,6 +69,7 @@ + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-bg-deploy.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-bg-deploy.bpmn index 984c919265..a274e269ec 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-bg-deploy.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-bg-deploy.bpmn @@ -1,5 +1,5 @@ - + @@ -24,7 +24,7 @@ - + @@ -80,10 +80,14 @@ - + + + - + + + @@ -93,13 +97,19 @@ - + + + - + + + - + + + @@ -107,14 +117,18 @@ - + + + - + + + @@ -123,7 +137,9 @@ - + + + @@ -172,20 +188,22 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + @@ -225,7 +243,9 @@ - + + + @@ -243,11 +263,17 @@ + + + + + + - + @@ -259,10 +285,10 @@ - + - + @@ -271,13 +297,13 @@ - + - + - + @@ -391,7 +417,7 @@ - + @@ -432,342 +458,349 @@ - + + + + - + - + - + - + - + - + - + - + - + - - - + + + - + - - - + + + - + - + - - - + + + - + - + - + - + - - - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + + + - + - + - + - + - + - + - + - + - + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + + + - + - + - - - + + + - + - + - + - + - + - + - + - + - + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-deploy.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-deploy.bpmn index 643263993b..aa3ddbd87b 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-deploy.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-deploy.bpmn @@ -1,5 +1,5 @@ - + @@ -30,7 +30,6 @@ - @@ -75,10 +74,14 @@ - + + + - + + + @@ -95,13 +98,17 @@ - + + + - + + + @@ -113,10 +120,11 @@ - + + + - @@ -154,20 +162,24 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + @@ -176,11 +188,19 @@ + + + + + + + + - + @@ -195,10 +215,10 @@ - + - + @@ -207,13 +227,13 @@ - + - + - + @@ -228,7 +248,7 @@ - + @@ -291,7 +311,7 @@ - + @@ -314,11 +334,14 @@ - + + + + - + @@ -326,233 +349,237 @@ - + - + - + - - - + + + - - - + + + - - - + + + - + - + - - - - - - - - - + + + - - - + + + - + - + - + - - - + + + - + - - + + - + - - - + + + - + - + - + - + - - - - + + + + - + + + + + + + - + - + - - - + + + - + - + - + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + + + - - - + + + - + - + - + - + - + - + - + - - - + + + diff --git a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-undeploy.bpmn b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-undeploy.bpmn index 691e6e4f6c..01d49b80c4 100644 --- a/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-undeploy.bpmn +++ b/multiapps-controller-process/src/main/resources/org/cloudfoundry/multiapps/controller/process/xs2-undeploy.bpmn @@ -1,5 +1,5 @@ - + @@ -7,7 +7,7 @@ - + @@ -24,14 +24,10 @@ - + - - - - @@ -62,11 +58,15 @@ - + + + - + + + @@ -110,24 +110,10 @@ - - - - - - - - - - - - - - - - - - + + + + @@ -155,300 +141,320 @@ + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListenerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListenerTest.java index c9c770e640..aa8d138d7f 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListenerTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EndProcessListenerTest.java @@ -1,11 +1,10 @@ package org.cloudfoundry.multiapps.controller.process.listeners; -import static org.junit.jupiter.api.Assertions.assertEquals; - import org.cloudfoundry.multiapps.controller.api.model.Operation; import org.cloudfoundry.multiapps.controller.api.model.ProcessType; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -24,6 +23,8 @@ import org.mockito.Mock; import org.mockito.Mockito; +import static org.junit.jupiter.api.Assertions.assertEquals; + class EndProcessListenerTest { private final static String SPACE_ID = "9ba1dfc7-9c2c-40d5-8bf9-fd04fa7a1722"; @@ -50,6 +51,8 @@ class EndProcessListenerTest { private StepLogger stepLogger; @Mock private ProcessLoggerPersister processLoggerPersister; + @Mock + private OperationLogsExporter operationLogsExporter; @Test void testNotifyInternal() { @@ -62,7 +65,8 @@ void testNotifyInternal() { configuration, eventHandler, dynatracePublisher, - processTypeParser); + processTypeParser, + operationLogsExporter); // set the process as root process VariableHandling.set(execution, Variables.CORRELATION_ID, execution.getProcessInstanceId()); VariableHandling.set(execution, Variables.SPACE_GUID, SPACE_ID); diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListenerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListenerTest.java index 5274db4857..2e195a4c5f 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListenerTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/EnterTestingPhaseListenerTest.java @@ -93,7 +93,7 @@ private void prepareOperationService() { } private void prepareStepLogger() { - Mockito.when(stepLoggerFactory.create(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + Mockito.when(stepLoggerFactory.create(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(stepLogger); } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListenerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListenerTest.java new file mode 100644 index 0000000000..47de656c19 --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ExportCloudLoggingConfigurationListenerTest.java @@ -0,0 +1,166 @@ +package org.cloudfoundry.multiapps.controller.process.listeners; + +import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; +import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; +import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; +import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; +import org.cloudfoundry.multiapps.controller.process.flowable.FlowableFacade; +import org.cloudfoundry.multiapps.controller.process.util.MockDelegateExecution; +import org.cloudfoundry.multiapps.controller.process.util.StepLogger; +import org.cloudfoundry.multiapps.controller.process.variables.VariableHandling; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.Execution; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ExportCloudLoggingConfigurationListenerTest { + + private static final LoggingConfiguration LOGGING_CONFIGURATION = ImmutableLoggingConfiguration.builder() + .operationId("op-1") + .logLevel(LogLevel.INFO) + .isFailSafe(true) + .build(); + + @Mock + private ProgressMessageService progressMessageService; + @Mock + private ProcessLoggerProvider processLoggerProvider; + @Mock + private HistoricOperationEventService historicOperationEventService; + @Mock + private FlowableFacade flowableFacade; + @Mock + private ApplicationConfiguration configuration; + @Mock + private StepLogger.Factory stepLoggerFactory; + @Mock + private StepLogger stepLogger; + @Mock + private ProcessLoggerPersister processLoggerPersister; + @Mock + private OperationLogsExporter operationLogsExporter; + + @InjectMocks + private ExportCloudLoggingConfigurationListener listener; + + private final DelegateExecution execution = MockDelegateExecution.createSpyInstance(); + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + when(stepLoggerFactory.create(any(), any(), any(), any(), any())).thenReturn(stepLogger); + } + + @Test + void testNotifyInternal_withNoLoggingConfiguration_doesNothing() { + listener.notify(execution); + + verify(flowableFacade, never()).setVariableInParentProcess(any(), anyString(), any()); + verify(flowableFacade, never()).setVariableInParentProcessXSA(any(), anyString(), any()); + } + + @Test + void testNotifyInternal_withParentProcessInstanceId_setsVariableInParentProcessXSA() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + VariableHandling.set(execution, Variables.PARENT_PROCESS_INSTANCE_ID, "parent-process-1"); + + listener.notify(execution); + + verify(flowableFacade).setVariableInParentProcessXSA(eq(execution), + eq(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION.getName()), any()); + } + + @Test + void testNotifyInternal_withParentProcessInstanceId_setsSerializedValue() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + VariableHandling.set(execution, Variables.PARENT_PROCESS_INSTANCE_ID, "parent-process-1"); + + listener.notify(execution); + + verify(flowableFacade).setVariableInParentProcessXSA(eq(execution), + eq(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION.getName()), + anyString()); + } + + @Test + void testNotifyInternal_withSuperExecution_setsVariableInParentProcess() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + prepareSuperExecution(); + + listener.notify(execution); + + verify(flowableFacade).setVariableInParentProcess(eq(execution), + eq(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION.getName()), any()); + } + + @Test + void testNotifyInternal_withSuperExecution_setsSerializedValue() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + prepareSuperExecution(); + + listener.notify(execution); + + verify(flowableFacade).setVariableInParentProcess(eq(execution), + eq(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION.getName()), + anyString()); + } + + @Test + void testNotifyInternal_withNoParentAndNoSuperExecution_setsVariableInCurrentExecution() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + + listener.notify(execution); + + LoggingConfiguration result = VariableHandling.get(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION); + assertEquals(LOGGING_CONFIGURATION, result); + } + + @Test + void testNotifyInternal_withNoParentAndNoSuperExecution_doesNotPropagateToParent() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + + listener.notify(execution); + + verify(flowableFacade, never()).setVariableInParentProcess(any(), anyString(), any()); + verify(flowableFacade, never()).setVariableInParentProcessXSA(any(), anyString(), any()); + } + + @Test + void testNotifyInternal_parentProcessInstanceIdTakesPrecedenceOverSuperExecution() { + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, LOGGING_CONFIGURATION); + VariableHandling.set(execution, Variables.PARENT_PROCESS_INSTANCE_ID, "parent-process-1"); + prepareSuperExecution(); + + listener.notify(execution); + + verify(flowableFacade).setVariableInParentProcessXSA(any(), anyString(), any()); + verify(flowableFacade, never()).setVariableInParentProcess(any(), anyString(), any()); + } + + private void prepareSuperExecution() { + String parentId = "parent-execution-id"; + Mockito.doReturn(parentId).when(execution).getParentId(); + Execution parentExecution = Mockito.mock(Execution.class); + when(parentExecution.getSuperExecutionId()).thenReturn("super-execution-id"); + when(flowableFacade.getParentExecution(parentId)).thenReturn(parentExecution); + } +} diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListenerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListenerTest.java index 285f3c6ee6..db64a474ab 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListenerTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/ManageAppServiceBindingEndListenerTest.java @@ -8,6 +8,7 @@ import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudApplication; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -53,6 +54,9 @@ class ManageAppServiceBindingEndListenerTest { private ProcessLoggerPersister processLoggerPersister; @Mock private ApplicationConfiguration configuration; + @Mock + private OperationLogsExporter operationLogsExporter; + private ManageAppServiceBindingEndListener manageAppServiceBindingEndListener; @BeforeEach @@ -66,7 +70,8 @@ void setUp() throws Exception { historicOperationEventService, flowableFacade, configuration, - processTypeParser); + processTypeParser, + operationLogsExporter); } // @formatter:off diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListenerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListenerTest.java index bd4c656ab0..8b86ebbdf6 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListenerTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/listeners/StartProcessListenerTest.java @@ -1,8 +1,5 @@ package org.cloudfoundry.multiapps.controller.process.listeners; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; - import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -21,6 +18,7 @@ import org.cloudfoundry.multiapps.controller.persistence.services.FileService; import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.OperationService; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; @@ -47,6 +45,9 @@ import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; + class StartProcessListenerTest { private static final String SPACE_ID = "9ba1dfc7-9c2c-40d5-8bf9-fd04fa7a1722"; @@ -88,15 +89,17 @@ class StartProcessListenerTest { private FlowableFacade flowableFacade; @Mock private ProcessLoggerPersister processLoggerPersister; + @Mock + private OperationLogsExporter operationLogsExporter; private StartProcessListener listener; static Stream testVerify() { return Stream.of( - // (0) Create Operation for process undeploy - Arguments.of("process-instance-id", ProcessType.UNDEPLOY), - // (1) Create Operation for process deploy - Arguments.of("process-instance-id", ProcessType.DEPLOY)); + // (0) Create Operation for process undeploy + Arguments.of("process-instance-id", ProcessType.UNDEPLOY), + // (1) Create Operation for process deploy + Arguments.of("process-instance-id", ProcessType.DEPLOY)); } @BeforeEach @@ -114,7 +117,8 @@ void setUp() throws Exception { operationService, operationMetadataMapper, dynatracePublisher, - fileService); + fileService, + operationLogsExporter); } @ParameterizedTest @@ -132,7 +136,7 @@ void testVerify(String processInstanceId, ProcessType processType) throws Except private void prepare() { prepareContext(); - Mockito.when(stepLoggerFactory.create(any(), any(), any(), any())) + Mockito.when(stepLoggerFactory.create(any(), any(), any(), any(), any())) .thenReturn(stepLogger); Mockito.when(operationService.createQuery()) .thenReturn(operationQuery); diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/IncrementalAppInstanceUpdateStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/IncrementalAppInstanceUpdateStepTest.java index 18e55a7f7a..b37880647b 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/IncrementalAppInstanceUpdateStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/IncrementalAppInstanceUpdateStepTest.java @@ -22,6 +22,7 @@ import org.cloudfoundry.multiapps.controller.core.model.IncrementalAppInstanceUpdateConfiguration; import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.Messages; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.junit.jupiter.api.BeforeEach; @@ -48,6 +49,7 @@ class IncrementalAppInstanceUpdateStepTest extends SyncFlowableStepTest testStep() { diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollIncrementalAppInstanceUpdateExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollIncrementalAppInstanceUpdateExecutionTest.java index eea3294143..0e87f0e986 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollIncrementalAppInstanceUpdateExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollIncrementalAppInstanceUpdateExecutionTest.java @@ -12,6 +12,7 @@ import org.cloudfoundry.multiapps.controller.core.model.ImmutableDeployedMtaApplication; import org.cloudfoundry.multiapps.controller.core.model.ImmutableIncrementalAppInstanceUpdateConfiguration; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -31,6 +32,7 @@ class PollIncrementalAppInstanceUpdateExecutionTest extends AsyncStepOperationTe private CloudControllerClientFactory clientFactory; private TokenService tokenService; + private OperationLogsExporter operationLogsExporter; private AsyncExecutionState expectedAsyncExecutionState; @@ -128,6 +130,6 @@ protected void validateOperationExecutionResult(AsyncExecutionState result) { protected IncrementalAppInstancesUpdateStep createStep() { clientFactory = Mockito.mock(CloudControllerClientFactory.class); tokenService = Mockito.mock(TokenService.class); - return new IncrementalAppInstancesUpdateStep(clientFactory, tokenService); + return new IncrementalAppInstancesUpdateStep(clientFactory, tokenService, operationLogsExporter); } } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecutionTest.java index d580c993a7..77077ba6e1 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStageAppStatusExecutionTest.java @@ -12,6 +12,7 @@ import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.util.ApplicationStager; import org.cloudfoundry.multiapps.controller.process.util.ImmutableStagingState; import org.cloudfoundry.multiapps.controller.process.util.MockDelegateExecution; @@ -51,6 +52,8 @@ class PollStageAppStatusExecutionTest { private CloudControllerClientFactory clientFactory; @Mock private TokenService tokenService; + @Mock + private OperationLogsExporter operationLogsExporter; private ProcessContext context; private DelegateExecution execution; @@ -62,7 +65,7 @@ void setUp() throws Exception { .close(); execution = MockDelegateExecution.createSpyInstance(); context = new ProcessContext(execution, stepLogger, clientProvider); - step = new PollStageAppStatusExecution(applicationStager, clientFactory, tokenService); + step = new PollStageAppStatusExecution(applicationStager, clientFactory, tokenService, operationLogsExporter); } static Stream testStep() { diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecutionTest.java index a7bcb41110..dc6c47d182 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppExecutionWithRollbackExecutionTest.java @@ -18,6 +18,7 @@ import org.cloudfoundry.multiapps.controller.core.model.ImmutableIncrementalAppInstanceUpdateConfiguration; import org.cloudfoundry.multiapps.controller.core.model.IncrementalAppInstanceUpdateConfiguration; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -39,6 +40,7 @@ class PollStartAppExecutionWithRollbackExecutionTest extends AsyncStepOperationT private CloudControllerClientFactory clientFactory; private TokenService tokenService; + private OperationLogsExporter operationLogsExporter; private AsyncExecutionState expectedAsyncExecutionState; @@ -151,7 +153,7 @@ void testOnSuccessWhenNewAppStillIsStarted() { @Override protected List getAsyncOperations(ProcessContext wrapper) { - return List.of(new PollStartAppExecutionWithRollbackExecution(clientFactory, tokenService)); + return List.of(new PollStartAppExecutionWithRollbackExecution(clientFactory, tokenService, operationLogsExporter)); } @Override @@ -163,7 +165,7 @@ protected void validateOperationExecutionResult(AsyncExecutionState result) { protected IncrementalAppInstancesUpdateStep createStep() { clientFactory = Mockito.mock(CloudControllerClientFactory.class); tokenService = Mockito.mock(TokenService.class); - return new IncrementalAppInstancesUpdateStep(clientFactory, tokenService); + return new IncrementalAppInstancesUpdateStep(clientFactory, tokenService, operationLogsExporter); } } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecutionTest.java index 0a5d2cebb3..c593c7bfdd 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartAppStatusExecutionTest.java @@ -21,6 +21,7 @@ import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.util.MockDelegateExecution; import org.cloudfoundry.multiapps.controller.process.util.StepLogger; import org.cloudfoundry.multiapps.controller.process.variables.Variables; @@ -60,6 +61,8 @@ class PollStartAppStatusExecutionTest { private CloudControllerClientFactory clientFactory; @Mock private TokenService tokenService; + @Mock + private OperationLogsExporter operationLogsExporter; private ProcessContext context; private PollStartAppStatusExecution step; @@ -70,7 +73,7 @@ void setUp() throws Exception { .close(); DelegateExecution execution = MockDelegateExecution.createSpyInstance(); context = new ProcessContext(execution, stepLogger, clientProvider); - step = new PollStartAppStatusExecution(clientFactory, tokenService); + step = new PollStartAppStatusExecution(clientFactory, tokenService, operationLogsExporter); } static Stream testStep() { diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecutionTest.java index 9e321293e1..039199c6df 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/PollStartLiveAppExecutionTest.java @@ -14,6 +14,7 @@ import org.cloudfoundry.multiapps.controller.core.model.ImmutableDeployedMta; import org.cloudfoundry.multiapps.controller.core.model.ImmutableDeployedMtaApplication; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.process.variables.Variables; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,6 +36,8 @@ class PollStartLiveAppExecutionTest { private TokenService tokenService; @Mock private ProcessContext context; + @Mock + private OperationLogsExporter operationLogsExporter; private PollStartLiveAppExecution pollStartLiveAppExecution; @@ -42,7 +45,7 @@ class PollStartLiveAppExecutionTest { void setUp() throws Exception { MockitoAnnotations.openMocks(this) .close(); - pollStartLiveAppExecution = new PollStartLiveAppExecution(clientFactory, tokenService); + pollStartLiveAppExecution = new PollStartLiveAppExecution(clientFactory, tokenService, operationLogsExporter); } @Test diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelperTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelperTest.java index 7db241c7e4..4b4b117a27 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelperTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ProcessStepHelperTest.java @@ -1,7 +1,5 @@ package org.cloudfoundry.multiapps.controller.process.steps; -import static org.mockito.ArgumentMatchers.any; - import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -12,6 +10,7 @@ import org.cloudfoundry.multiapps.controller.core.model.ErrorType; import org.cloudfoundry.multiapps.controller.persistence.model.HistoricOperationEvent; import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableHistoricOperationEvent; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLogger; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProgressMessageService; @@ -33,6 +32,8 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import static org.mockito.ArgumentMatchers.any; + class ProcessStepHelperTest { private static final String CORRELATION_GUID = UUID.randomUUID() @@ -54,6 +55,9 @@ class ProcessStepHelperTest { private ProcessContext context; @Mock private DelegateExecution execution; + @Mock + private OperationLogsExporter operationLogsExporter; + private ProcessStepHelper processStepHelper; @BeforeEach @@ -68,6 +72,7 @@ void setUp() throws Exception { .processEngineConfiguration(processEngineConfiguration) .processHelper(processHelper) .processLoggerPersister(processLoggerPersister) + .operationLogsExporter(operationLogsExporter) .build(); } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStepTest.java index a083c69ff0..984e032fde 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/SyncFlowableStepTest.java @@ -21,6 +21,7 @@ import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.FileService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerPersister; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLoggerProvider; import org.cloudfoundry.multiapps.controller.persistence.services.ProcessLogsPersistenceService; @@ -109,6 +110,8 @@ public abstract class SyncFlowableStepTest { protected final ProcessLoggerProvider processLoggerProvider = Mockito.spy(ProcessLoggerProvider.class); @Mock protected ProcessHelper processHelper; + @Mock + protected OperationLogsExporter operationLogsExporter; protected ProcessContext context; @InjectMocks @@ -120,9 +123,10 @@ public abstract class SyncFlowableStepTest { public void initMocks() throws Exception { MockitoAnnotations.openMocks(this) .close(); - this.stepLogger = Mockito.spy(new StepLogger(execution, progressMessageService, processLoggerProvider, LOGGER)); + this.stepLogger = Mockito.spy( + new StepLogger(execution, progressMessageService, processLoggerProvider, LOGGER, operationLogsExporter)); + when(stepLoggerFactory.create(any(), any(), any(), any(), any())).thenReturn(stepLogger); this.context = step.createProcessContext(execution); - when(stepLoggerFactory.create(any(), any(), any(), any())).thenReturn(stepLogger); context.setVariable(Variables.SPACE_NAME, SPACE_NAME); context.setVariable(Variables.SPACE_GUID, SPACE_GUID); context.setVariable(Variables.USER, USER_NAME); diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecutionTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecutionTest.java index 452fb59c15..4e832ac010 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecutionTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/UploadAppAsyncExecutionTest.java @@ -26,6 +26,9 @@ import org.cloudfoundry.multiapps.controller.client.lib.domain.ImmutableCloudApplicationExtended; import org.cloudfoundry.multiapps.controller.core.helpers.MtaArchiveElements; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; import org.cloudfoundry.multiapps.controller.persistence.services.FileContentConsumer; import org.cloudfoundry.multiapps.controller.persistence.services.FileService; import org.cloudfoundry.multiapps.controller.process.util.ApplicationArchiveIterator; @@ -37,6 +40,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; import org.springframework.http.HttpStatus; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -49,7 +53,9 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class UploadAppAsyncExecutionTest extends AsyncStepOperationTest { @@ -198,6 +204,93 @@ void testSkippingUpload() { testExecuteOperations(); } + @Test + void testSendApplicationLogsToCloudLoggingService_sendsLogsForEachProcessLog() { + prepareExecutorService(); + context.setVariable(Variables.ARCHIVE_ENTRIES_POSITIONS, List.of(ARCHIVE_ENTRY_WITH_STREAM_POSITIONS)); + LoggingConfiguration loggingConfiguration = buildLoggingConfiguration(); + context.setVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + when(client.asyncUploadApplicationWithExponentialBackoff(eq(APP_NAME), eq(appFile), any(UploadStatusCallback.class), + any())).thenReturn(CLOUD_PACKAGE); + when(step.getProcessLogsPersister().getApplicationProcessLogsMessages(TEST_CORRELATION_ID, + TEST_TASK_ID)).thenReturn(List.of("log-1", "log-2")); + expectedStatus = AsyncExecutionState.FINISHED; + + testExecuteOperations(); + triggerOnProgress(Status.READY.toString()); + + verify(operationLogsExporter).sendLogsToCloudLoggingService(loggingConfiguration, "log-1"); + verify(operationLogsExporter).sendLogsToCloudLoggingService(loggingConfiguration, "log-2"); + } + + @Test + void testSendApplicationLogsToCloudLoggingService_withNullLoggingConfiguration_stillSendsLogs() { + prepareExecutorService(); + context.setVariable(Variables.ARCHIVE_ENTRIES_POSITIONS, List.of(ARCHIVE_ENTRY_WITH_STREAM_POSITIONS)); + context.setVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, null); + when(client.asyncUploadApplicationWithExponentialBackoff(eq(APP_NAME), eq(appFile), any(UploadStatusCallback.class), + any())).thenReturn(CLOUD_PACKAGE); + when(step.getProcessLogsPersister().getApplicationProcessLogsMessages(TEST_CORRELATION_ID, + TEST_TASK_ID)).thenReturn(List.of("log-1")); + expectedStatus = AsyncExecutionState.FINISHED; + + testExecuteOperations(); + triggerOnProgress(Status.READY.toString()); + + verify(operationLogsExporter).sendLogsToCloudLoggingService(null, "log-1"); + } + + @Test + void testSendApplicationLogsToCloudLoggingService_withNoLogs_doesNotSendProcessLogs() { + prepareExecutorService(); + context.setVariable(Variables.ARCHIVE_ENTRIES_POSITIONS, List.of(ARCHIVE_ENTRY_WITH_STREAM_POSITIONS)); + LoggingConfiguration loggingConfiguration = buildLoggingConfiguration(); + context.setVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + when(client.asyncUploadApplicationWithExponentialBackoff(eq(APP_NAME), eq(appFile), any(UploadStatusCallback.class), + any())).thenReturn(CLOUD_PACKAGE); + when(step.getProcessLogsPersister().getApplicationProcessLogsMessages(TEST_CORRELATION_ID, + TEST_TASK_ID)).thenReturn(Collections.emptyList()); + expectedStatus = AsyncExecutionState.FINISHED; + + testExecuteOperations(); + triggerOnProgress(Status.READY.toString()); + + verify(operationLogsExporter, never()).sendLogsToCloudLoggingService(eq(loggingConfiguration), eq("process-log-content")); + } + + @Test + void testSendApplicationLogsToCloudLoggingService_notCalledWhenStatusIsNotReady() { + prepareExecutorService(); + context.setVariable(Variables.ARCHIVE_ENTRIES_POSITIONS, List.of(ARCHIVE_ENTRY_WITH_STREAM_POSITIONS)); + LoggingConfiguration loggingConfiguration = buildLoggingConfiguration(); + context.setVariable(Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + when(client.asyncUploadApplicationWithExponentialBackoff(eq(APP_NAME), eq(appFile), any(UploadStatusCallback.class), + any())).thenReturn(CLOUD_PACKAGE); + when(step.getProcessLogsPersister().getApplicationProcessLogsMessages(TEST_CORRELATION_ID, + TEST_TASK_ID)).thenReturn(List.of("process-log-content")); + expectedStatus = AsyncExecutionState.FINISHED; + + testExecuteOperations(); + triggerOnProgress(Status.AWAITING_UPLOAD.toString()); + + verify(operationLogsExporter, never()).sendLogsToCloudLoggingService(eq(loggingConfiguration), eq("process-log-content")); + } + + private void triggerOnProgress(String status) { + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(UploadStatusCallback.class); + verify(client).asyncUploadApplicationWithExponentialBackoff(eq(APP_NAME), eq(appFile), callbackCaptor.capture(), any()); + callbackCaptor.getValue() + .onProgress(status); + } + + private LoggingConfiguration buildLoggingConfiguration() { + return ImmutableLoggingConfiguration.builder() + .operationId(TEST_CORRELATION_ID) + .logLevel(LogLevel.INFO) + .isFailSafe(true) + .build(); + } + @Override protected List getAsyncOperations(ProcessContext wrapper) { return step.getAsyncStepExecutions(wrapper); @@ -220,7 +313,8 @@ protected List getAsyncStepExecutions(ProcessContext context) { return List.of(new UploadAppAsyncExecution(applicationZipBuilder, getProcessLogsPersister(), configuration, - appUploaderThreadPool) { + appUploaderThreadPool, + operationLogsExporter) { }); } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ExternalLoggingServiceConfigurationsCalculatorTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ExternalLoggingServiceConfigurationsCalculatorTest.java new file mode 100644 index 0000000000..915614b013 --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ExternalLoggingServiceConfigurationsCalculatorTest.java @@ -0,0 +1,354 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudMetadata; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudServiceKey; +import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; +import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; +import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LogLevel; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.process.steps.ProcessContext; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.Resource; +import org.flowable.engine.delegate.DelegateExecution; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ExternalLoggingServiceConfigurationsCalculatorTest { + + private static final String CORRELATION_ID = "op-1"; + private static final String SPACE_NAME = "my-space"; + private static final String SPACE_GUID = "space-guid-1"; + private static final String ORG_NAME = "my-org"; + private static final String USER_GUID = "user-guid-1"; + private static final String MTA_ID = "my-mta"; + private static final String NAMESPACE = "dev"; + private static final String SERVICE_INSTANCE = "my-cls-instance"; + private static final String SERVICE_KEY_NAME = "my-cls-key"; + + private static final Map SERVICE_KEY_CREDENTIALS = Map.of( + "ingest-mtls-endpoint", "https://cls.example.com", + "server-ca", "server-ca-cert", + "ingest-mtls-cert", "client-cert", + "ingest-mtls-key", "client-key" + ); + + @Mock + private CloudControllerClientFactory clientFactory; + @Mock + private CloudControllerClient client; + @Mock + private CloudControllerClientProvider clientProvider; + @Mock + private TokenService tokenService; + @Mock + private StepLogger stepLogger; + + private ProcessContext context; + private ExternalLoggingServiceConfigurationsCalculator calculator; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + DelegateExecution execution = MockDelegateExecution.createSpyInstance(); + when(clientProvider.getControllerClient(anyString(), anyString(), anyString())).thenReturn(client); + context = new ProcessContext(execution, stepLogger, clientProvider); + context.setVariable(Variables.CORRELATION_ID, CORRELATION_ID); + context.setVariable(Variables.SPACE_NAME, SPACE_NAME); + context.setVariable(Variables.SPACE_GUID, SPACE_GUID); + context.setVariable(Variables.ORGANIZATION_NAME, ORG_NAME); + context.setVariable(Variables.USER_GUID, USER_GUID); + context.setVariable(Variables.MTA_ID, MTA_ID); + context.setVariable(Variables.MTA_NAMESPACE, NAMESPACE); + calculator = new ExternalLoggingServiceConfigurationsCalculator(clientFactory, context, tokenService); + } + + // --- exportOperationLogsToExternalSystem(Resource) --- + + @Test + void testExportWithResource_returnsNullWhenServiceKeyIsNull() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(null); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNull(result); + } + + @Test + void testExportWithResource_throwsWhenServiceKeyIsNullAndNotFailSafe() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(null); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, false); + + assertThrows(SLException.class, () -> calculator.exportOperationLogsToExternalSystem(resource)); + } + + @Test + void testExportWithResource_returnsNullWhenServiceInstanceNameIsBlank() { + Resource resource = buildResource("", SERVICE_KEY_NAME, true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNull(result); + verify(client, never()).getServiceKey(anyString(), anyString()); + } + + @Test + void testExportWithResource_returnsNullWhenServiceKeyNameIsBlank() { + Resource resource = buildResource(SERVICE_INSTANCE, "", true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNull(result); + verify(client, never()).getServiceKey(anyString(), anyString()); + } + + @Test + void testExportWithResource_throwsWhenMissingParametersAndNotFailSafe() { + Resource resource = buildResource(null, SERVICE_KEY_NAME, false); + + assertThrows(SLException.class, () -> calculator.exportOperationLogsToExternalSystem(resource)); + } + + @Test + void testExportWithResource_returnsNullWhenCloudOperationExceptionAndFailSafe() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNull(result); + } + + @Test + void testExportWithResource_throwsWhenCloudOperationExceptionAndNotFailSafe() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, false); + + assertThrows(SLException.class, () -> calculator.exportOperationLogsToExternalSystem(resource)); + } + + @Test + void testExportWithResource_populatesCredentialsFromServiceKey() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals("https://cls.example.com", result.getEndpointUrl()); + assertEquals("server-ca-cert", result.getServerCa()); + assertEquals("client-cert", result.getClientCert()); + assertEquals("client-key", result.getClientKey()); + } + + @Test + void testExportWithResource_populatesContextFields() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals(CORRELATION_ID, result.getOperationId()); + assertEquals(MTA_ID, result.getMtaId()); + assertEquals(SPACE_GUID, result.getMtaSpaceId()); + assertEquals(SPACE_NAME, result.getMtaSpace()); + assertEquals(ORG_NAME, result.getMtaOrg()); + assertEquals(NAMESPACE, result.getNamespace()); + } + + @Test + void testExportWithResource_setsFailSafeFromResourceOptional() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals(true, result.isFailSafe()); + } + + @Test + void testExportWithResource_defaultLogLevelIsInfo() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals(LogLevel.INFO, result.getLogLevel()); + } + + static Stream testExportWithResource_logLevelFromDescriptor() { + return Stream.of(Arguments.of("INFO", LogLevel.INFO), Arguments.of("WARN", LogLevel.WARN), Arguments.of("DEBUG", LogLevel.DEBUG), + Arguments.of("ERROR", LogLevel.ERROR), Arguments.of("TRACE", LogLevel.TRACE)); + } + + @ParameterizedTest + @MethodSource + void testExportWithResource_logLevelFromDescriptor(String descriptorLevel, LogLevel expectedLevel) { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true, Map.of(SupportedParameters.LOG_LEVEL, descriptorLevel)); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals(expectedLevel, result.getLogLevel()); + } + + @Test + void testExportWithResource_usesResourceNameAsServiceInstanceWhenNoServiceNameParameter() { + when(client.getServiceKey("resource-name", SERVICE_KEY_NAME)).thenReturn(buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = Resource.createV3() + .setName("resource-name") + .setOptional(true) + .setParameters(Map.of(SupportedParameters.SERVICE_KEYS, + List.of(Map.of(SupportedParameters.NAME, SERVICE_KEY_NAME)))); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals("resource-name", result.getServiceInstanceName()); + } + + @Test + void testExportWithResource_usesDestinationOrgAndSpace() { + when(clientFactory.createClient(any(), anyString(), anyString(), anyString())).thenReturn(client); + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + Resource resource = buildResource(SERVICE_INSTANCE, SERVICE_KEY_NAME, true, + Map.of(SupportedParameters.DESTINATION, + Map.of("org-name", "other-org", "space-name", "other-space"))); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(resource); + + assertNotNull(result); + assertEquals("other-org", result.getTargetOrg()); + assertEquals("other-space", result.getTargetSpace()); + } + + // --- exportOperationLogsToExternalSystem(LoggingConfiguration, ProcessContext) --- + + @Test + void testExportWithLoggingConfiguration_returnsNullWhenServiceKeyIsNull() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(null); + LoggingConfiguration incomingConfig = buildIncomingConfig(true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(incomingConfig, context); + + assertNull(result); + } + + @Test + void testExportWithLoggingConfiguration_throwsWhenServiceKeyIsNullAndNotFailSafe() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(null); + LoggingConfiguration incomingConfig = buildIncomingConfig(false); + + assertThrows(SLException.class, () -> calculator.exportOperationLogsToExternalSystem(incomingConfig, context)); + } + + @Test + void testExportWithLoggingConfiguration_populatesCredentialsFromServiceKey() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + LoggingConfiguration incomingConfig = buildIncomingConfig(true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(incomingConfig, context); + + assertNotNull(result); + assertEquals("https://cls.example.com", result.getEndpointUrl()); + assertEquals("server-ca-cert", result.getServerCa()); + assertEquals("client-cert", result.getClientCert()); + assertEquals("client-key", result.getClientKey()); + } + + @Test + void testExportWithLoggingConfiguration_setsOperationIdAndSpaceIdFromContext() { + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(buildServiceKey(SERVICE_KEY_NAME, SERVICE_KEY_CREDENTIALS)); + LoggingConfiguration incomingConfig = buildIncomingConfig(true); + + LoggingConfiguration result = calculator.exportOperationLogsToExternalSystem(incomingConfig, context); + + assertNotNull(result); + assertEquals(CORRELATION_ID, result.getOperationId()); + assertEquals(SPACE_GUID, result.getMtaSpaceId()); + } + + @Test + void testExportWithLoggingConfiguration_throwsWhenMissingCredentialInServiceKey() { + Map incompleteCredentials = Map.of("ingest-mtls-endpoint", "https://cls.example.com"); + when(client.getServiceKey(SERVICE_INSTANCE, SERVICE_KEY_NAME)).thenReturn(buildServiceKey(SERVICE_KEY_NAME, incompleteCredentials)); + LoggingConfiguration incomingConfig = buildIncomingConfig(true); + + assertThrows(IllegalArgumentException.class, () -> calculator.exportOperationLogsToExternalSystem(incomingConfig, context)); + } + + // --- Helpers --- + + private static Resource buildResource(String serviceInstanceName, String serviceKeyName, boolean optional) { + return buildResource(serviceInstanceName, serviceKeyName, optional, Map.of()); + } + + private static Resource buildResource(String serviceInstanceName, String serviceKeyName, boolean optional, + Map extraParameters) { + java.util.Map params = new java.util.HashMap<>(extraParameters); + if (serviceInstanceName != null) { + params.put(SupportedParameters.SERVICE_NAME, serviceInstanceName); + } + if (serviceKeyName != null) { + params.put(SupportedParameters.SERVICE_KEYS, List.of(Map.of(SupportedParameters.NAME, serviceKeyName))); + } + return Resource.createV3() + .setName("cls-resource") + .setOptional(optional) + .setParameters(params); + } + + private static LoggingConfiguration buildIncomingConfig(boolean failSafe) { + return ImmutableLoggingConfiguration.builder() + .serviceInstanceName(SERVICE_INSTANCE) + .serviceKeyName(SERVICE_KEY_NAME) + .targetOrg(ORG_NAME) + .targetSpace(SPACE_NAME) + .logLevel(LogLevel.INFO) + .isFailSafe(failSafe) + .build(); + } + + private static ImmutableCloudServiceKey buildServiceKey(String name, Map credentials) { + return ImmutableCloudServiceKey.builder() + .name(name) + .metadata(ImmutableCloudMetadata.builder() + .build()) + .credentials(credentials) + .build(); + } + + private static T any() { + return org.mockito.ArgumentMatchers.any(); + } +} diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandlerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandlerTest.java index 15ffab11e9..7639b48a83 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandlerTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/OperationInFinalStateHandlerTest.java @@ -1,19 +1,37 @@ package org.cloudfoundry.multiapps.controller.process.util; import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.stream.Stream; +import org.cloudfoundry.client.v3.Metadata; import org.cloudfoundry.multiapps.controller.api.model.ImmutableOperation; import org.cloudfoundry.multiapps.controller.api.model.Operation; import org.cloudfoundry.multiapps.controller.api.model.Operation.State; import org.cloudfoundry.multiapps.controller.api.model.ProcessType; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; +import org.cloudfoundry.multiapps.controller.core.cf.metadata.MtaMetadataAnnotations; +import org.cloudfoundry.multiapps.controller.core.model.DeployedMta; +import org.cloudfoundry.multiapps.controller.core.model.DeployedMtaApplication; +import org.cloudfoundry.multiapps.controller.core.model.ImmutableDeployedMta; +import org.cloudfoundry.multiapps.controller.core.model.ImmutableDeployedMtaApplication; import org.cloudfoundry.multiapps.controller.persistence.model.FileEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.HistoricOperationEvent; import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableFileEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableLoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.query.DescriptorBackupQuery; import org.cloudfoundry.multiapps.controller.persistence.query.impl.OperationQueryImpl; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; +import org.cloudfoundry.multiapps.controller.persistence.services.DescriptorBackupService; import org.cloudfoundry.multiapps.controller.persistence.services.FileService; import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; +import org.cloudfoundry.multiapps.controller.persistence.services.HistoricOperationEventService; +import org.cloudfoundry.multiapps.controller.persistence.services.OperationLogsExporter; import org.cloudfoundry.multiapps.controller.persistence.services.OperationService; import org.cloudfoundry.multiapps.controller.process.dynatrace.DynatraceProcessDuration; import org.cloudfoundry.multiapps.controller.process.dynatrace.DynatracePublisher; @@ -21,6 +39,7 @@ import org.cloudfoundry.multiapps.controller.process.security.store.SecretTokenStoreFactory; import org.cloudfoundry.multiapps.controller.process.variables.VariableHandling; import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; import org.flowable.engine.delegate.DelegateExecution; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,9 +54,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class OperationInFinalStateHandlerTest { @@ -50,6 +73,10 @@ class OperationInFinalStateHandlerTest { private static final String USER_GUID = "test-user"; private static final long PROCESS_DURATION = 1000; private static final String DISPOSABLE_USER_PROVIDED_SERVICE_NAME = "__mta-secure-my-mta-fake343"; + private static final String LOGGING_CONFIG_ID = "logging-config-1"; + private static final String USER_NAME = "test-user-name"; + private static final String NAMESPACE = "dev"; + private static final String MTA_VERSION_1 = "1.0.0"; private static final Operation OPERATION = createOperation("1", ProcessType.DEPLOY, "spaceId", "mtaId", "user", true, ZonedDateTime.parse("2010-10-08T10:00:00.000Z[UTC]"), @@ -79,6 +106,20 @@ class OperationInFinalStateHandlerTest { private CloudControllerClientProvider cloudControllerClientProvider; @Mock private CloudControllerClient cloudControllerClient; + @Mock + private OperationLogsExporter operationLogsExporter; + @Mock + private HistoricOperationEventService historicOperationEventService; + @Mock + private DescriptorBackupService descriptorBackupService; + @Mock + private DescriptorBackupQuery descriptorBackupQuery; + @Mock + private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + @Mock + private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; + @Mock + private ProcessTypeParser processTypeParser; @InjectMocks private final OperationInFinalStateHandler eventHandler = new OperationInFinalStateHandler(); @@ -105,7 +146,7 @@ public static Stream testHandle() { public void setUp() throws Exception { MockitoAnnotations.openMocks(this) .close(); - Mockito.when(stepLoggerFactory.create(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + Mockito.when(stepLoggerFactory.create(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(stepLogger); Mockito.when(secretTokenStoreFactory.createSecretTokenStoreDeletionRelated()) .thenReturn(secretTokenStoreDeletion); @@ -232,6 +273,232 @@ private void verifyDynatracePublisher() { assertEquals(PROCESS_DURATION, actualDynatraceEvent.getProcessDuration()); } + @Test + void testRemovesClientFromCacheAfterHandle() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + + eventHandler.handle(execution, PROCESS_TYPE, OPERATION_STATE); + + verify(operationLogsExporter).removeClientFromCache(PROCESS_ID); + } + + @Test + void testHistoricOperationEventEmittedWithFinishedEventTypeWhenStateIsFinished() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + + eventHandler.handle(execution, PROCESS_TYPE, State.FINISHED); + + ArgumentCaptor captor = ArgumentCaptor.forClass(HistoricOperationEvent.class); + verify(historicOperationEventService).add(captor.capture()); + assertEquals(HistoricOperationEvent.EventType.FINISHED, captor.getValue() + .getType()); + } + + @Test + void testHistoricOperationEventEmittedWithAbortedEventTypeWhenStateIsAborted() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + + eventHandler.handle(execution, PROCESS_TYPE, State.ABORTED); + + ArgumentCaptor captor = ArgumentCaptor.forClass(HistoricOperationEvent.class); + verify(historicOperationEventService).add(captor.capture()); + assertEquals(HistoricOperationEvent.EventType.ABORTED, captor.getValue() + .getType()); + } + + @Test + void testSetOperationStateSkippedWhenOperationAlreadyFinal() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + OperationQueryImpl operationQuery = Mockito.mock(OperationQueryImpl.class); + when(operationService.createQuery()).thenReturn(operationQuery); + when(operationQuery.processId(any())).thenReturn(operationQuery); + Operation alreadyFinalOperation = ImmutableOperation.builder() + .from(OPERATION) + .state(State.FINISHED) + .hasAcquiredLock(false) + .endedAt(ZonedDateTime.parse("2010-10-15T10:00:00.000Z[UTC]")) + .build(); + when(operationQuery.singleResult()).thenReturn(alreadyFinalOperation); + + eventHandler.handle(execution, PROCESS_TYPE, State.FINISHED); + + verify(operationService, never()).update(any(), any()); + verify(historicOperationEventService, never()).add(any()); + } + + @Test + void testDeleteCloudLoggingServiceConfiguration_skippedWhenLoggingConfigurationIsNull() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + + eventHandler.handle(execution, PROCESS_TYPE, OPERATION_STATE); + + verify(cloudLoggingServiceConfigurationService, never()).deleteCloudLoggingServiceConfiguration(anyString()); + verify(cloudLoggingServiceConfigurationAuditLog, never()).logDeleteLoggingConfiguration(anyString(), anyString(), any()); + } + + @Test + void testDeleteCloudLoggingServiceConfiguration_skippedWhenLoggingConfigurationIdIsNull() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + LoggingConfiguration loggingConfiguration = ImmutableLoggingConfiguration.builder() + .build(); + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + + eventHandler.handle(execution, PROCESS_TYPE, OPERATION_STATE); + + verify(cloudLoggingServiceConfigurationService, never()).deleteCloudLoggingServiceConfiguration(anyString()); + verify(cloudLoggingServiceConfigurationAuditLog, never()).logDeleteLoggingConfiguration(anyString(), anyString(), any()); + } + + @Test + void testDeleteCloudLoggingServiceConfiguration_skippedWhenProcessTypeIsNotUndeploy() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + LoggingConfiguration loggingConfiguration = ImmutableLoggingConfiguration.builder() + .id(LOGGING_CONFIG_ID) + .build(); + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + when(processTypeParser.getProcessType(execution)).thenReturn(ProcessType.DEPLOY); + + eventHandler.handle(execution, PROCESS_TYPE, OPERATION_STATE); + + verify(cloudLoggingServiceConfigurationService, never()).deleteCloudLoggingServiceConfiguration(anyString()); + verify(cloudLoggingServiceConfigurationAuditLog, never()).logDeleteLoggingConfiguration(anyString(), anyString(), any()); + } + + @Test + void testDeleteCloudLoggingServiceConfiguration_deletesAndAuditsWhenProcessTypeIsUndeploy() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + LoggingConfiguration loggingConfiguration = ImmutableLoggingConfiguration.builder() + .id(LOGGING_CONFIG_ID) + .build(); + VariableHandling.set(execution, Variables.EXTERNAL_LOGGING_SERVICE_CONFIGURATION, loggingConfiguration); + VariableHandling.set(execution, Variables.USER, USER_NAME); + when(processTypeParser.getProcessType(execution)).thenReturn(ProcessType.UNDEPLOY); + + eventHandler.handle(execution, PROCESS_TYPE, OPERATION_STATE); + + verify(cloudLoggingServiceConfigurationService).deleteCloudLoggingServiceConfiguration(LOGGING_CONFIG_ID); + verify(cloudLoggingServiceConfigurationAuditLog).logDeleteLoggingConfiguration(eq(USER_NAME), eq(SPACE_ID), + eq(loggingConfiguration)); + } + + @Test + void testDeletePreviousBackupDescriptors_skippedWhenStateIsNotFinished() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + + eventHandler.handle(execution, PROCESS_TYPE, State.ABORTED); + + verify(descriptorBackupService, never()).createQuery(); + } + + @Test + void testDeletePreviousBackupDescriptors_undeployWithDeployedMtaVersion_deletesByVersion() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + prepareDescriptorBackupQuery(); + VariableHandling.set(execution, Variables.MTA_NAMESPACE, NAMESPACE); + DeployedMta deployedMta = buildDeployedMta(MTA_VERSION_1); + VariableHandling.set(execution, Variables.DEPLOYED_MTA, deployedMta); + + eventHandler.handle(execution, ProcessType.UNDEPLOY, State.FINISHED); + + verify(descriptorBackupQuery).mtaId(MTA_ID); + verify(descriptorBackupQuery).spaceId(SPACE_ID); + verify(descriptorBackupQuery).namespace(NAMESPACE); + verify(descriptorBackupQuery).mtaVersion(MTA_VERSION_1); + verify(descriptorBackupQuery).delete(); + } + + @Test + void testDeletePreviousBackupDescriptors_undeployWithoutDeployedMta_doesNotDelete() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + VariableHandling.set(execution, Variables.MTA_NAMESPACE, NAMESPACE); + + eventHandler.handle(execution, ProcessType.UNDEPLOY, State.FINISHED); + + verify(descriptorBackupService, never()).createQuery(); + } + + @Test + void testDeletePreviousBackupDescriptors_deployWithCurrentDescriptorVersion_skipsCurrentVersion() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + prepareDescriptorBackupQuery(); + VariableHandling.set(execution, Variables.MTA_NAMESPACE, NAMESPACE); + DeploymentDescriptor descriptor = DeploymentDescriptor.createV3() + .setVersion(MTA_VERSION_1); + VariableHandling.set(execution, Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR, descriptor); + VariableHandling.set(execution, Variables.APPS_TO_UNDEPLOY, Collections.emptyList()); + + eventHandler.handle(execution, ProcessType.DEPLOY, State.FINISHED); + + ArgumentCaptor> versionsCaptor = ArgumentCaptor.forClass(List.class); + verify(descriptorBackupQuery).mtaVersionsNotMatch(versionsCaptor.capture()); + assertEquals(List.of(MTA_VERSION_1), versionsCaptor.getValue()); + verify(descriptorBackupQuery).delete(); + } + + @Test + void testDeletePreviousBackupDescriptors_deployWithNoVersionsToSkip_doesNotDelete() { + prepareContext(null, null, true); + prepareOperationTimeAggregator(); + prepareOperationService(); + VariableHandling.set(execution, Variables.MTA_NAMESPACE, NAMESPACE); + DeploymentDescriptor descriptor = DeploymentDescriptor.createV3(); + VariableHandling.set(execution, Variables.COMPLETE_DEPLOYMENT_DESCRIPTOR, descriptor); + VariableHandling.set(execution, Variables.APPS_TO_UNDEPLOY, Collections.emptyList()); + + eventHandler.handle(execution, ProcessType.DEPLOY, State.FINISHED); + + verify(descriptorBackupService, never()).createQuery(); + } + + private void prepareDescriptorBackupQuery() { + when(descriptorBackupService.createQuery()).thenReturn(descriptorBackupQuery); + when(descriptorBackupQuery.mtaId(anyString())).thenReturn(descriptorBackupQuery); + when(descriptorBackupQuery.spaceId(anyString())).thenReturn(descriptorBackupQuery); + when(descriptorBackupQuery.namespace(any())).thenReturn(descriptorBackupQuery); + when(descriptorBackupQuery.mtaVersion(anyString())).thenReturn(descriptorBackupQuery); + when(descriptorBackupQuery.mtaVersionsNotMatch(any())).thenReturn(descriptorBackupQuery); + } + + private DeployedMta buildDeployedMta(String mtaVersion) { + DeployedMtaApplication application = ImmutableDeployedMtaApplication.builder() + .name("app-1") + .moduleName("module-1") + .v3Metadata(Metadata.builder() + .annotation(MtaMetadataAnnotations.MTA_VERSION, + mtaVersion) + .build()) + .build(); + return ImmutableDeployedMta.builder() + .metadata(org.cloudfoundry.multiapps.controller.core.cf.metadata.ImmutableMtaMetadata.builder() + .id(MTA_ID) + .build()) + .applications(List.of(application)) + .build(); + } + private static Operation createOperation(String processId, ProcessType type, String spaceId, String mtaId, String user, boolean acquiredLock, ZonedDateTime startedAt, ZonedDateTime endedAt, Operation.State state) { return ImmutableOperation.builder() diff --git a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/configuration/AsyncProcessLoggerConfiguration.java b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/configuration/AsyncProcessLoggerConfiguration.java index ea8963e9a9..b4be88b299 100644 --- a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/configuration/AsyncProcessLoggerConfiguration.java +++ b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/configuration/AsyncProcessLoggerConfiguration.java @@ -1,5 +1,7 @@ package org.cloudfoundry.multiapps.controller.web.configuration; +import java.util.concurrent.Executor; + import jakarta.inject.Inject; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.springframework.context.annotation.Bean; @@ -7,12 +9,10 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import java.util.concurrent.Executor; - @Configuration @EnableAsync public class AsyncProcessLoggerConfiguration { - + @Inject private ApplicationConfiguration configuration; @@ -26,4 +26,13 @@ public Executor getAsyncExecutor() { return executor; } + @Bean("cloudLoggingServiceAsyncExecutor") + public Executor getCloudLoggingServiceAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(configuration.getFlowableJobExecutorCoreThreads()); + executor.setMaxPoolSize(configuration.getFlowableJobExecutorMaxThreads()); + executor.setQueueCapacity(configuration.getFlowableJobExecutorQueueCapacity()); + executor.initialize(); + return executor; + } } diff --git a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/resources/ConfigurationEntriesResource.java b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/resources/ConfigurationEntriesResource.java index 7a2a36dddd..09f8974cba 100644 --- a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/resources/ConfigurationEntriesResource.java +++ b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/resources/ConfigurationEntriesResource.java @@ -3,6 +3,7 @@ import jakarta.inject.Inject; import jakarta.inject.Named; import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; +import org.cloudfoundry.multiapps.controller.core.auditlogging.CloudLoggingServiceConfigurationAuditLog; import org.cloudfoundry.multiapps.controller.core.auditlogging.MtaConfigurationPurgerAuditLog; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientFactory; import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; @@ -10,6 +11,7 @@ import org.cloudfoundry.multiapps.controller.core.helpers.MtaConfigurationPurger; import org.cloudfoundry.multiapps.controller.core.security.token.TokenService; import org.cloudfoundry.multiapps.controller.core.util.UserInfo; +import org.cloudfoundry.multiapps.controller.persistence.services.CloudLoggingServiceConfigurationService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationEntryService; import org.cloudfoundry.multiapps.controller.persistence.services.ConfigurationSubscriptionService; import org.cloudfoundry.multiapps.controller.web.Constants; @@ -44,6 +46,10 @@ public class ConfigurationEntriesResource { private TokenService tokenService; @Inject private MtaConfigurationPurgerAuditLog mtaConfigurationPurgerAuditLog; + @Inject + private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; + @Inject + private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; @PostMapping(value = Constants.Endpoints.PURGE) public ResponseEntity purgeConfigurationRegistry(@RequestParam(REQUEST_PARAM_ORGANIZATION) String organization, @@ -58,7 +64,9 @@ public ResponseEntity purgeConfigurationRegistry(@RequestParam(REQUEST_PAR .toString()); MtaConfigurationPurger configurationPurger = new MtaConfigurationPurger(client, spaceClient, configurationEntryService, configurationSubscriptionService, mtaMetadataParser, - mtaConfigurationPurgerAuditLog); + mtaConfigurationPurgerAuditLog, + cloudLoggingServiceConfigurationService, + cloudLoggingServiceConfigurationAuditLog); configurationPurger.purge(organization, space); return ResponseEntity.status(HttpStatus.NO_CONTENT) .build(); From bb7fc04783a7b03361212b2abc9139d1c25671f4 Mon Sep 17 00:00:00 2001 From: Yavor16 Date: Thu, 28 May 2026 11:00:31 +0300 Subject: [PATCH 2/2] fix rebase problems --- ...ggingServiceConfigurationAuditLogTest.java | 6 ++-- multiapps-controller-persistence/pom.xml | 4 +++ .../src/main/java/module-info.java | 1 + ...gingServiceConfigurationQueryProvider.java | 32 ++++++++++++++----- ...oudLoggingServiceConfigurationService.java | 12 ++++--- .../services/OperationLogsExporter.java | 4 +-- multiapps-controller-process/pom.xml | 5 +++ pom.xml | 10 ++++++ 8 files changed, 56 insertions(+), 18 deletions(-) diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLogTest.java index a407ac08bd..bf7a38b9a8 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLogTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CloudLoggingServiceConfigurationAuditLogTest.java @@ -178,7 +178,7 @@ void testLogDeleteLoggingConfiguration_omitsNullValuesFromConfigurationIdentifie assertEquals(LOGGING_CONFIG_ID, identifiers.get("id")); // null fields should not be exposed in getConfigurationIdentifiers for (ConfigurationIdentifier identifier : captured.getConfigurationIdentifiers()) { - assertNotNull(identifier.getValue(), "Configuration identifier value should not be null"); + assertNotNull(identifier.getIdentifierValue(), "Configuration identifier value should not be null"); } } @@ -295,7 +295,7 @@ private Map identifiersFromConfig(AuditLogConfiguration config) Map result = new HashMap<>(); List configurationIdentifiers = config.getConfigurationIdentifiers(); for (ConfigurationIdentifier identifier : configurationIdentifiers) { - result.put(identifier.getName(), identifier.getValue()); + result.put(identifier.getIdentifierName(), identifier.getIdentifierValue()); } return result; } @@ -303,7 +303,7 @@ private Map identifiersFromConfig(AuditLogConfiguration config) private int countNonReservedIdentifiers(AuditLogConfiguration config) { int count = 0; for (ConfigurationIdentifier identifier : config.getConfigurationIdentifiers()) { - String name = identifier.getName(); + String name = identifier.getIdentifierName(); if (!"performed_action".equals(name) && !"time".equals(name) && !"spaceId".equals(name)) { count++; } diff --git a/multiapps-controller-persistence/pom.xml b/multiapps-controller-persistence/pom.xml index 70875d56f8..080f5a6181 100644 --- a/multiapps-controller-persistence/pom.xml +++ b/multiapps-controller-persistence/pom.xml @@ -235,5 +235,9 @@ org.springframework spring-webflux + + io.projectreactor.netty + reactor-netty-http + diff --git a/multiapps-controller-persistence/src/main/java/module-info.java b/multiapps-controller-persistence/src/main/java/module-info.java index 40ade3562f..a0094c66ee 100644 --- a/multiapps-controller-persistence/src/main/java/module-info.java +++ b/multiapps-controller-persistence/src/main/java/module-info.java @@ -67,4 +67,5 @@ requires io.netty.handler; requires reactor.netty.http; requires reactor.netty.core; + requires reactor.core; } diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/CloudLoggingServiceConfigurationQueryProvider.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/CloudLoggingServiceConfigurationQueryProvider.java index fed2429419..0b3fafcb0f 100644 --- a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/CloudLoggingServiceConfigurationQueryProvider.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/providers/CloudLoggingServiceConfigurationQueryProvider.java @@ -73,10 +73,8 @@ public SqlQuery getGetLoggingConfigurationQuery(String mta PreparedStatement statement = null; ResultSet resultSet = null; try { - if (namespace == null) { - statement = connection.prepareStatement(String.format(GET_CLOUD_LOGGING_CONFIGURATION_NULL_NAMESPACE, tableName)); - } else { - statement = connection.prepareStatement(String.format(GET_CLOUD_LOGGING_CONFIGURATION, tableName)); + statement = connection.prepareStatement(getGetLoggingConfigurationQueryString(namespace)); + if (namespace != null) { statement.setString(3, namespace); } statement.setString(1, mtaSpace); @@ -111,9 +109,7 @@ public SqlQuery getUpdateLoggingConfigurationQuery(LoggingConfiguration return (Connection connection) -> { PreparedStatement statement = null; try { - String queryTemplate = loggingConfiguration.getNamespace() == null - ? UPDATE_CLOUD_LOGGING_CONFIGURATION_NULL_NAMESPACE - : UPDATE_CLOUD_LOGGING_CONFIGURATION; + String queryTemplate = getUpdateLoggingConfigurationQueryString(loggingConfiguration.getNamespace()); statement = connection.prepareStatement(String.format(queryTemplate, tableName)); statement.setString(1, loggingConfiguration.getTargetSpace()); statement.setString(2, loggingConfiguration.getTargetOrg()); @@ -140,7 +136,7 @@ public SqlQuery> getAllCloudLoggingServiceConfigurati PreparedStatement statement = null; ResultSet resultSet = null; try { - statement = connection.prepareStatement(String.format(GET_ALL_CLOUD_LOGGING_CONFIGURATIONS, tableName)); + statement = connection.prepareStatement(getAllLoggingConfigurationQueryString()); statement.setString(1, spaceId); resultSet = statement.executeQuery(); List result = new ArrayList<>(); @@ -178,4 +174,24 @@ private String getStoreLoggingConfigurationQueryString() { private String getDeleteLoggingConfigurationQueryString() { return String.format(DELETE_CLOUD_LOGGING_CONFIGURATION, tableName); } + + private String getAllLoggingConfigurationQueryString() { + return String.format(GET_ALL_CLOUD_LOGGING_CONFIGURATIONS, tableName); + } + + private String getUpdateLoggingConfigurationQueryString(String namespace) { + if (namespace == null) { + return String.format(UPDATE_CLOUD_LOGGING_CONFIGURATION_NULL_NAMESPACE, tableName); + } else { + return String.format(UPDATE_CLOUD_LOGGING_CONFIGURATION, tableName); + } + } + + private String getGetLoggingConfigurationQueryString(String namespace) { + if (namespace == null) { + return String.format(GET_CLOUD_LOGGING_CONFIGURATION_NULL_NAMESPACE, tableName); + } else { + return String.format(GET_CLOUD_LOGGING_CONFIGURATION, tableName); + } + } } diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationService.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationService.java index aa839cd58f..75848ed327 100644 --- a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationService.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/CloudLoggingServiceConfigurationService.java @@ -4,6 +4,7 @@ import java.util.List; import jakarta.inject.Named; +import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.persistence.DataSourceWithDialect; import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; import org.cloudfoundry.multiapps.controller.persistence.query.providers.CloudLoggingServiceConfigurationQueryProvider; @@ -24,10 +25,11 @@ public CloudLoggingServiceConfigurationService(DataSourceWithDialect dataSourceW public void storeCloudLoggingServiceConfiguration(LoggingConfiguration loggingConfiguration) { try { + getSqlQueryExecutor().execute( cloudLoggingServiceConfigurationQueryProvider.getStoreLoggingConfigurationQuery(loggingConfiguration)); } catch (SQLException e) { - throw new RuntimeException(e); + throw new SLException(e); } } @@ -36,7 +38,7 @@ public LoggingConfiguration getCloudLoggingServiceConfiguration(String mtaSpace, return getSqlQueryExecutor().execute( cloudLoggingServiceConfigurationQueryProvider.getGetLoggingConfigurationQuery(mtaSpace, mtaId, namespace)); } catch (SQLException e) { - throw new RuntimeException(e); + throw new SLException(e); } } @@ -44,7 +46,7 @@ public void deleteCloudLoggingServiceConfiguration(String id) { try { getSqlQueryExecutor().execute(cloudLoggingServiceConfigurationQueryProvider.getDeleteLoggingConfigurationQuery(id)); } catch (SQLException e) { - throw new RuntimeException(e); + throw new SLException(e); } } @@ -53,7 +55,7 @@ public void updateCloudLoggingServiceConfiguration(LoggingConfiguration loggingC getSqlQueryExecutor().execute( cloudLoggingServiceConfigurationQueryProvider.getUpdateLoggingConfigurationQuery(loggingConfiguration)); } catch (SQLException e) { - throw new RuntimeException(e); + throw new SLException(e); } } @@ -62,7 +64,7 @@ public List getAllCloudLoggingServiceConfigurationsFromSpa return getSqlQueryExecutor().execute( cloudLoggingServiceConfigurationQueryProvider.getAllCloudLoggingServiceConfigurationsFromSpace(spaceId)); } catch (SQLException e) { - throw new RuntimeException(e); + throw new SLException(e); } } diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporter.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporter.java index 6674b0f4f9..a420a20b1d 100644 --- a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporter.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogsExporter.java @@ -36,8 +36,8 @@ import org.springframework.web.reactive.function.client.WebClient; import reactor.netty.http.client.HttpClient; -import static com.azure.core.http.ContentType.APPLICATION_JSON; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @Named("operationLogsExporter") public class OperationLogsExporter { @@ -182,7 +182,7 @@ private boolean hasRequestFailed(ResponseEntity response) { private ResponseEntity executeSendLongHttpRequest(WebClient webClient, List logEntryBatch) { return webClient.post() - .header(CONTENT_TYPE, APPLICATION_JSON) + .header(CONTENT_TYPE, APPLICATION_JSON_VALUE) .bodyValue(JsonUtil.toJson(logEntryBatch)) .retrieve() .toBodilessEntity() diff --git a/multiapps-controller-process/pom.xml b/multiapps-controller-process/pom.xml index f9b7f05211..96a21125bd 100644 --- a/multiapps-controller-process/pom.xml +++ b/multiapps-controller-process/pom.xml @@ -110,5 +110,10 @@ org.cloudfoundry.multiapps multiapps-controller-shutdown-client + + org.jetbrains + annotations + 13.0 + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 7376507892..b197596f08 100644 --- a/pom.xml +++ b/pom.xml @@ -863,6 +863,16 @@ reactor-netty ${reactor-netty.version} + + io.projectreactor.netty + reactor-netty-http + ${reactor-netty.version} + + + io.projectreactor.netty + reactor-netty-core + ${reactor-netty.version} + org.cloudfoundry.multiapps