Skip to content

Add cloud logging service feature#1847

Open
Yavor16 wants to merge 6 commits into
masterfrom
export-logs-to-logging-service-squash
Open

Add cloud logging service feature#1847
Yavor16 wants to merge 6 commits into
masterfrom
export-logs-to-logging-service-squash

Conversation

@Yavor16

@Yavor16 Yavor16 commented May 28, 2026

Copy link
Copy Markdown
Contributor

No description provided.

# 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
@Yavor16 Yavor16 force-pushed the export-logs-to-logging-service-squash branch from 20788df to 7220532 Compare May 28, 2026 06:57
@Yavor16 Yavor16 force-pushed the export-logs-to-logging-service-squash branch from c1abde6 to bb7fc04 Compare June 1, 2026 10:51
import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions;
import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration;

public class CloudLoggingServiceConfigurationAuditLog {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least audit logging part maybe can be moved in internal project? SAP Cloud Logging is internal service but API endpoints are public, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any changes here.

Comment on lines +294 to +296
InputStream serverCaStream = getCredentialInputStream(loggingConfiguration.getServerCa());
InputStream clientCertStream = getCredentialInputStream(loggingConfiguration.getClientCert());
InputStream clientKeyStream = getCredentialInputStream(loggingConfiguration.getClientKey());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these streams closed?

@IvanBorislavovDimitrov IvanBorislavovDimitrov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional comments on OperationLogsExporter not yet covered in the earlier review.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
77.4% Coverage on New Code (required ≥ 80%)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions;
import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration;

public class CloudLoggingServiceConfigurationAuditLog {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least audit logging part maybe can be moved in internal project? SAP Cloud Logging is internal service but API endpoints are public, right?

Comment thread multiapps-controller-process/pom.xml Outdated
Comment thread pom.xml
Comment on lines +866 to +875
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these dependencies are required? WebClient provided by spring webflux isn't it enough?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use HttpClient in CloudLoggingServiceHttpClient which required these dependencies

@Yavor16 Yavor16 force-pushed the export-logs-to-logging-service-squash branch from 70685f6 to 9558829 Compare June 26, 2026 05:36
return executor;
}

@Bean("cloudLoggingServiceAsyncExecutor")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this cloudLoggingServiceAsyncExecutor used?

auditLog = new CloudLoggingServiceConfigurationAuditLog(auditLoggingFacade);
}

// --- logCreateLoggingConfiguration ---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this?

assertEquals(NAMESPACE, identifiers.get("namespace"));
}

// --- logDeleteLoggingConfiguration ---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove these kinds of comments

Comment on lines +17 to +19
@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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this length enforcements? Same for others

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use our impl of CachedMap?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its in the core package so it is not accessible


INFO, WARN, DEBUG, ERROR, TRACE;

private static final Map<LogLevel, List<LogLevel>> logLevelLoggingType = setupLogLevels();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case

sendLogsToCloudLoggingService(externalOperationLogEntryBatches, cloudLoggingServiceWebClient, loggingConfiguration);
}

public List<OperationLogEntry> getUnsendProcessLogs(LoggingConfiguration loggingConfiguration) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this class does too many things and should be split

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still applicable?

Comment on lines +72 to +74
// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this?

Comment on lines +140 to +148
if (areCloudLoggingParametersInvalid(serviceInstanceName, serviceKeyName)) {
return handleMissingServiceKey(isFailSafe, null);
}
CloudControllerClient client = calculateExternalLoggingServiceConfiguration(destinationOrg, destinationSpace);
try {
CloudServiceKey loggingServiceKey = client.getServiceKey(serviceInstanceName, serviceKeyName);
if (loggingServiceKey != null) {
return loggingServiceKey;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These methods return null or throw exception

super(entityManagerFactory);
}

public void storeCloudLoggingServiceConfiguration(LoggingConfiguration loggingConfiguration) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this method needed, as it just invokes add(loggingConfiguration)?

Comment on lines +39 to +58
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();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still applicable?

}
}

public void removeClientFromCache(String operationId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens with processes that are not in final state? like ERROR?

private final CloudLoggingServiceWebClientCache webClientCache;

public CloudLoggingServiceHttpClient() {
this(new CloudLoggingServiceWebClientFactory(), new CloudLoggingServiceWebClientCache());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these classes are beans and still created with new?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for the unit tests

CloudLoggingServiceUtil.logErrorOrThrowExceptionBasedOnFailSafe(loggingConfiguration, LOGGER,
Messages.FAILED_TO_SEND_LOG_MESSAGE_TO_CLS);
}
} catch (RuntimeException e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still applicable?

import org.slf4j.LoggerFactory;

@Named("operationLogsExporter")
public class OperationLogsExporter {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be better if we move this class in the internal project (to be extended in the internal project)

import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions;
import org.cloudfoundry.multiapps.controller.persistence.model.LoggingConfiguration;

public class CloudLoggingServiceConfigurationAuditLog {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any changes here.

private CloudLoggingServiceConfigurationService cloudLoggingServiceConfigurationService;
@Mock
private CloudLoggingServiceConfigurationAuditLog cloudLoggingServiceConfigurationAuditLog;
@InjectMocks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This annotation can be removed now when mocks are injected through the constructor.

super(entityManagerFactory);
}

public void storeCloudLoggingServiceConfiguration(LoggingConfiguration loggingConfiguration) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can skip this method and just invoke directly add method.

}
}

public void updateCloudLoggingServiceConfiguration(LoggingConfiguration loggingConfiguration) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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<>());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still missing assertion

converter = new CloudLoggingServiceMessageConverter();
}

// --- extractLogName ---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove odd comments

flowableFacade.setVariableInParentProcessXSA(execution, variableName, value);
}

protected boolean hasSuperExecution(DelegateExecution execution) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check method isRootProcess in the same class. I think it's doing the same thing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants