Add cloud logging service feature#1847
Conversation
# 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
20788df to
7220532
Compare
c1abde6 to
bb7fc04
Compare
| import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions; | ||
| import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; | ||
|
|
||
| public class CloudLoggingServiceConfigurationAuditLog { |
There was a problem hiding this comment.
Because SAP Cloud logging service is an internal proprietary service it should not be exposed in the open source. The whole logic related to the service must be move in the internal project.
Move every SAP related thing to the internal project and only leave the what is actually open source in this PR
There was a problem hiding this comment.
At least audit logging part maybe can be moved in internal project? SAP Cloud Logging is internal service but API endpoints are public, right?
There was a problem hiding this comment.
I don't see any changes here.
| InputStream serverCaStream = getCredentialInputStream(loggingConfiguration.getServerCa()); | ||
| InputStream clientCertStream = getCredentialInputStream(loggingConfiguration.getClientCert()); | ||
| InputStream clientKeyStream = getCredentialInputStream(loggingConfiguration.getClientKey()); |
There was a problem hiding this comment.
are these streams closed?
IvanBorislavovDimitrov
left a comment
There was a problem hiding this comment.
Additional comments on OperationLogsExporter not yet covered in the earlier review.
|
| import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions; | ||
| import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; | ||
|
|
||
| public class CloudLoggingServiceConfigurationAuditLog { |
There was a problem hiding this comment.
At least audit logging part maybe can be moved in internal project? SAP Cloud Logging is internal service but API endpoints are public, right?
| <dependency> | ||
| <groupId>io.projectreactor.netty</groupId> | ||
| <artifactId>reactor-netty-http</artifactId> | ||
| <version>${reactor-netty.version}</version> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>io.projectreactor.netty</groupId> | ||
| <artifactId>reactor-netty-core</artifactId> | ||
| <version>${reactor-netty.version}</version> | ||
| </dependency> |
There was a problem hiding this comment.
Why these dependencies are required? WebClient provided by spring webflux isn't it enough?
There was a problem hiding this comment.
I use HttpClient in CloudLoggingServiceHttpClient which required these dependencies
70685f6 to
9558829
Compare
| return executor; | ||
| } | ||
|
|
||
| @Bean("cloudLoggingServiceAsyncExecutor") |
There was a problem hiding this comment.
Where is this cloudLoggingServiceAsyncExecutor used?
| auditLog = new CloudLoggingServiceConfigurationAuditLog(auditLoggingFacade); | ||
| } | ||
|
|
||
| // --- logCreateLoggingConfiguration --- |
There was a problem hiding this comment.
remove this?
| assertEquals(NAMESPACE, identifiers.get("namespace")); | ||
| } | ||
|
|
||
| // --- logDeleteLoggingConfiguration --- |
There was a problem hiding this comment.
remove these kinds of comments
| @NamedQuery(name = LoggingConfigurationDto.NamedQueries.FIND_BY_MTA_SPACE_ID_NAMESPACE, query = "SELECT c FROM LoggingConfigurationDto c WHERE c.mtaSpace = :mtaSpace AND c.mtaId = :mtaId AND c.namespace = :namespace") | ||
| @NamedQuery(name = LoggingConfigurationDto.NamedQueries.FIND_BY_MTA_SPACE_ID_NULL_NAMESPACE, query = "SELECT c FROM LoggingConfigurationDto c WHERE c.mtaSpace = :mtaSpace AND c.mtaId = :mtaId AND c.namespace IS NULL") | ||
| @NamedQuery(name = LoggingConfigurationDto.NamedQueries.FIND_ALL_BY_MTA_SPACE_ID, query = "SELECT c FROM LoggingConfigurationDto c WHERE c.mtaSpaceId = :mtaSpaceId") |
There was a problem hiding this comment.
Can this queries be created by chaining the methods?
| @Column(name = TableColumnNames.CLOUD_LOGGING_NAMESPACE) | ||
| private String namespace; | ||
|
|
||
| @Column(name = TableColumnNames.CLOUD_LOGGING_SERVICE_INSTANCE_NAME, nullable = false, length = 50) |
There was a problem hiding this comment.
Let's remove this length enforcements? Same for others
| return Optional.empty(); | ||
| } | ||
| String raw = matcher.group(1); | ||
| return Optional.of(raw.substring(raw.indexOf(".") + 1)); |
There was a problem hiding this comment.
What is the goal of this line? Why split by . ?
There was a problem hiding this comment.
we get the name of the file for example if its OPERATION.log we get OPERATION
| private static final long MAX_LIMIT_REQUEST_SIZE_BYTES = 3 * 1024 * 1024 + 512 * 1024; | ||
|
|
||
| private static final Map<String, WebClient> clientCache = Collections.synchronizedMap( | ||
| new LinkedHashMap<>(CLIENT_CACHE_MAX_SIZE, 0.75f, true) { |
There was a problem hiding this comment.
Can we use our impl of CachedMap?
There was a problem hiding this comment.
Its in the core package so it is not accessible
|
|
||
| INFO, WARN, DEBUG, ERROR, TRACE; | ||
|
|
||
| private static final Map<LogLevel, List<LogLevel>> logLevelLoggingType = setupLogLevels(); |
| sendLogsToCloudLoggingService(externalOperationLogEntryBatches, cloudLoggingServiceWebClient, loggingConfiguration); | ||
| } | ||
|
|
||
| public List<OperationLogEntry> getUnsendProcessLogs(LoggingConfiguration loggingConfiguration) { |
There was a problem hiding this comment.
this class does too many things and should be split
There was a problem hiding this comment.
still applicable?
| // NOTE: extractMessage strips one trailing character after .trim() — likely a bug | ||
| // (the -1 was probably meant to handle a trailing newline that .trim() already removes). | ||
| // The test pins current behavior; flag for follow-up. |
There was a problem hiding this comment.
What is this?
| super(entityManagerFactory); | ||
| } | ||
|
|
||
| public void storeCloudLoggingServiceConfiguration(LoggingConfiguration loggingConfiguration) { |
There was a problem hiding this comment.
is this method needed, as it just invokes add(loggingConfiguration)?
| public LoggingConfiguration getCloudLoggingServiceConfiguration(String mtaSpace, String mtaId, String namespace) { | ||
| try (EntityManager manager = createEntityManager()) { | ||
| return findByMtaSpaceIdAndNamespace(manager, mtaSpace, mtaId, namespace).getResultStream() | ||
| .findFirst() | ||
| .map(mapper::fromDto) | ||
| .orElse(null); | ||
| } | ||
| } | ||
|
|
||
| public void deleteCloudLoggingServiceConfiguration(String id) { | ||
| try (EntityManager manager = createEntityManager()) { | ||
| manager.getTransaction() | ||
| .begin(); | ||
| LoggingConfigurationDto dto = manager.find(LoggingConfigurationDto.class, id); | ||
| if (dto != null) { | ||
| manager.remove(dto); | ||
| } | ||
| manager.getTransaction() | ||
| .commit(); | ||
| } |
There was a problem hiding this comment.
Instead of having so many methods, can we remove these and reuse the pattern from other services like:
public AccessTokenQuery createQuery() {
return new AccessTokenQueryImpl(createEntityManager(), accessTokenMapper);
}
return accessTokenService.createQuery()
.userGuid(userGuid)
.orderByExpiresAt(OrderDirection.DESCENDING)
.list();
| import org.springframework.web.reactive.function.client.WebClient; | ||
|
|
||
| @Named("cloudLoggingServiceWebClientCache") | ||
| public class CloudLoggingServiceWebClientCache { |
There was a problem hiding this comment.
Instead of implementing a new cache. Can you reuse the CachedMap from our code to store the web clients?
| LoggingConfiguration loggingConfiguration = cloudLoggingServiceConfigurationService.getCloudLoggingServiceConfiguration( | ||
| context.getVariable(Variables.SPACE_NAME), context.getVariable(Variables.MTA_ID), context.getVariable(Variables.MTA_NAMESPACE)); | ||
|
|
||
| if (loggingConfiguration != null) { |
There was a problem hiding this comment.
as we are trying to get the cloud logging configuration, I guess it is ok to log that it is being fetched. Can we remove the null check here?
| sendLogsToCloudLoggingService(externalOperationLogEntryBatches, cloudLoggingServiceWebClient, loggingConfiguration); | ||
| } | ||
|
|
||
| public List<OperationLogEntry> getUnsendProcessLogs(LoggingConfiguration loggingConfiguration) { |
There was a problem hiding this comment.
still applicable?
| } | ||
| } | ||
|
|
||
| public void removeClientFromCache(String operationId) { |
There was a problem hiding this comment.
what happens with processes that are not in final state? like ERROR?
| CloudLoggingServiceUtil.logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, LOGGER, | ||
| Messages.FAILED_TO_SEND_LOG_MESSAGE_TO_CLS); | ||
| } | ||
| } catch (RuntimeException e) { |
There was a problem hiding this comment.
still applicable?
| import org.slf4j.LoggerFactory; | ||
|
|
||
| @Named("operationLogsExporter") | ||
| public class OperationLogsExporter { |
There was a problem hiding this comment.
Split this class in multiple classes because it does too many things.
| 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; |
There was a problem hiding this comment.
I think it will be better if we move this class in the internal project (to be extended in the internal project)
| import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions; | ||
| import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration; | ||
|
|
||
| public class CloudLoggingServiceConfigurationAuditLog { |
There was a problem hiding this comment.
I don't see any changes here.
| private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService; | ||
| @Mock | ||
| private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog; | ||
| @InjectMocks |
There was a problem hiding this comment.
This annotation can be removed now when mocks are injected through the constructor.
| super(entityManagerFactory); | ||
| } | ||
|
|
||
| public void storeCloudLoggingServiceConfiguration(LoggingConfiguration loggingConfiguration) { |
There was a problem hiding this comment.
I think we can skip this method and just invoke directly add method.
| } | ||
| } | ||
|
|
||
| public void updateCloudLoggingServiceConfiguration(LoggingConfiguration loggingConfiguration) { |
There was a problem hiding this comment.
Why don't use standard update method from base class?
|
|
||
| public List<LoggingConfiguration> getAllCloudLoggingServiceConfigurationsFromSpace(String spaceId) { | ||
| try (EntityManager manager = createEntityManager()) { | ||
| return manager.createQuery("SELECT c FROM LoggingConfigurationDto c WHERE c.mtaSpaceId = :mtaSpaceId", |
There was a problem hiding this comment.
Why still is use pure SQL query when you can add criteria buiilder class to filter based on different fields. Take a look at OperationQueryImpl class how it is implemented and used.
| .trim(); | ||
| if (trimmed.isEmpty()) { | ||
| return message; | ||
| } else { |
There was a problem hiding this comment.
else part can be removed and just return the value without any else block
| public class CloudLoggingServiceWebClientCache { | ||
|
|
||
| private static final Logger LOGGER = LoggerFactory.getLogger(CloudLoggingServiceWebClientCache.class); | ||
| private final Map<String, WebClient> CLIENT_CACHE = Collections.synchronizedMap(new HashMap<>()); |
There was a problem hiding this comment.
Why don't reuse org.cloudfoundry.multiapps.controller.core.model.CachedMap . I have some memories that long time ago we have using Collections.synchronizedMap but there is big performance degradation. ConcurrentHashMap provided by Spring is another recommended map used for concurrency.
|
|
||
| @Test | ||
| void testDeleteConfiguration_nonExistentIdDoesNotThrow() { | ||
| service.deleteCloudLoggingServiceConfiguration("nonexistent-id"); |
There was a problem hiding this comment.
still missing assertion
| converter = new CloudLoggingServiceMessageConverter(); | ||
| } | ||
|
|
||
| // --- extractLogName --- |
| flowableFacade.setVariableInParentProcessXSA(execution, variableName, value); | ||
| } | ||
|
|
||
| protected boolean hasSuperExecution(DelegateExecution execution) { |
There was a problem hiding this comment.
Check method isRootProcess in the same class. I think it's doing the same thing.
|
|
||
| void logDeleteLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); | ||
|
|
||
| void logGetLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); |
|
|
||
| void logDeleteLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); | ||
|
|
||
| void logGetLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); |
|
|
||
| void logDeleteLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); | ||
|
|
||
| void logGetLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); |
|
|
||
| void logUpdateLoggingConfiguration(String username, String spaceId, LoggingConfiguration newConfiguration); | ||
|
|
||
| void logDeleteLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); |
|
|
||
| void logUpdateLoggingConfiguration(String username, String spaceId, LoggingConfiguration newConfiguration); | ||
|
|
||
| void logDeleteLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); |
|
|
||
| void logUpdateLoggingConfiguration(String username, String spaceId, LoggingConfiguration newConfiguration); | ||
|
|
||
| void logDeleteLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); |
|
|
||
| void logCreateLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); | ||
|
|
||
| void logUpdateLoggingConfiguration(String username, String spaceId, LoggingConfiguration newConfiguration); |
|
|
||
| void logCreateLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); | ||
|
|
||
| void logUpdateLoggingConfiguration(String username, String spaceId, LoggingConfiguration newConfiguration); |
|
|
||
| void logCreateLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); | ||
|
|
||
| void logUpdateLoggingConfiguration(String username, String spaceId, LoggingConfiguration newConfiguration); |
|
|
||
| public interface CloudLoggingServiceConfigurationAuditLog { | ||
|
|
||
| void logCreateLoggingConfiguration(String username, String spaceId, LoggingConfiguration loggingConfiguration); |
| public LoggingConfiguration getLoggingConfiguration(String mtaSpace, String mtaId, String namespace) { | ||
| return createQuery().mtaSpace(mtaSpace) | ||
| .mtaId(mtaId) | ||
| .namespace(namespace) | ||
| .list() | ||
| .stream() | ||
| .findFirst() | ||
| .orElse(null); | ||
| } |
There was a problem hiding this comment.
Can we just use, createQuery outside of this class?
| import static org.springframework.http.HttpHeaders.CONTENT_TYPE; | ||
| import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; | ||
|
|
||
| @Named("cloudLoggingServiceHttpClient") |
There was a problem hiding this comment.
This class is a HttpClient and some methods expect passing a webClient. Consider more encapsulation.
There was a problem hiding this comment.
Create some facade and rename it.
| return webClientFactory.createWebClientWithMtls(loggingConfiguration); | ||
| } | ||
|
|
||
| private ResponseEntity<Void> executeSendLogHttpRequest(WebClient webClient, List<ExternalOperationLogEntry> logEntryBatch) { |
There was a problem hiding this comment.
Can this webClient be part of the class itself?
| private CloudServiceKey getCloudLoggingServiceKey(String serviceInstanceName, String serviceKeyName, String destinationOrg, | ||
| String destinationSpace, boolean isFailSafe) { | ||
| if (areCloudLoggingParametersInvalid(serviceInstanceName, serviceKeyName)) { | ||
| throwExceptionIfIsNotFailSafe(isFailSafe, getCloudLoggingServiceKeyErrorMessage()); | ||
| return null; | ||
| } | ||
| CloudControllerClient client = calculateExternalLoggingServiceConfiguration(destinationOrg, destinationSpace); | ||
| try { | ||
| CloudServiceKey loggingServiceKey = client.getServiceKey(serviceInstanceName, serviceKeyName); | ||
| if (loggingServiceKey != null) { | ||
| return loggingServiceKey; | ||
| } | ||
| throwExceptionIfIsNotFailSafe(isFailSafe, getCloudLoggingServiceKeyErrorMessage()); | ||
| return null; | ||
| } catch (CloudOperationException e) { | ||
| throwExceptionIfIsNotFailSafe(isFailSafe, e.getMessage()); | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
These methods return null or throw exception
| @Named("cloudLoggingServiceMessageConverter") | ||
| public class CloudLoggingServiceMessageConverter { | ||
| private static final Logger LOGGER = LoggerFactory.getLogger(CloudLoggingServiceMessageConverter.class); | ||
| // SAP Cloud Logging ingest endpoint accepts payloads up to ~4 MB; 3.5 MB leaves headroom for JSON envelope and HTTP framing. |
There was a problem hiding this comment.
what is this comment for?
| private boolean areParallelListsConsistent(List<String> messages, List<String> logLevels, List<LocalDateTime> logDates) { | ||
| return messages.size() <= logLevels.size() && messages.size() <= logDates.size(); | ||
| } |
There was a problem hiding this comment.
when can we hit this inconsistency?
| public class CloudLoggingServiceWebClientCache { | ||
|
|
||
| private static final Logger LOGGER = LoggerFactory.getLogger(CloudLoggingServiceWebClientCache.class); | ||
| private final CachedMap<String, WebClient> CLIENT_CACHE = new CachedMap<>(Duration.ofHours(1)); |
There was a problem hiding this comment.
Most operations are shorter than 1 hour. We can reduce this to ~10 minutes
| if (sslContext == null) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Why is null being returned? If we cannot create a web client shouldn't we fail?




No description provided.