Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d531a4e
add debug mode - only used for modification application
klesaulnier Feb 12, 2026
c424ca0
improve naming and comments
klesaulnier Feb 12, 2026
d52722d
fix checkstyle
klesaulnier Feb 12, 2026
4ccd728
fix tests + copyright/author
klesaulnier Feb 12, 2026
76f4b47
pr remarks
klesaulnier Feb 12, 2026
1656f57
fix checkstyle
klesaulnier Feb 12, 2026
ffd04df
fix test
klesaulnier Feb 12, 2026
be0f7d6
fix tests
klesaulnier Feb 12, 2026
37eaf0c
fix sonar issues regarding temp files
klesaulnier Feb 12, 2026
17f1733
export file compression to specific service
klesaulnier Feb 12, 2026
a0d8419
increase test coverage
klesaulnier Feb 12, 2026
ed55f03
test coverage
klesaulnier Feb 13, 2026
daf367c
add test covrage
klesaulnier Feb 16, 2026
ad12cc6
can now fetch debug files + tests
klesaulnier Feb 16, 2026
4bc4a1f
fix checkstyle
klesaulnier Feb 16, 2026
6c46d1a
fix test
klesaulnier Feb 16, 2026
c63fb10
fix checkstyle
klesaulnier Feb 16, 2026
ddba4dd
fix tests
klesaulnier Feb 16, 2026
41184a8
fix tests
klesaulnier Feb 16, 2026
cb303c3
fix: tests
klesaulnier Feb 16, 2026
ec1dcb4
fix sonar issues
klesaulnier Feb 17, 2026
fe3b18d
improve error handling
klesaulnier Feb 17, 2026
38d4044
PR remark
klesaulnier Feb 20, 2026
3f78c8e
PR remarks - mostly removing isDebug to use debugLocation only instead
klesaulnier Feb 23, 2026
97d7039
remove unused method
klesaulnier Feb 23, 2026
c9ecb54
copyright and author
klesaulnier Feb 23, 2026
45b4e32
fix tests
klesaulnier Feb 23, 2026
2414039
add comment
klesaulnier Feb 23, 2026
c4535da
fix test
klesaulnier Feb 23, 2026
c4521be
fix tests by adding test container for minio
klesaulnier Feb 23, 2026
5e368df
remove test container
klesaulnier Feb 23, 2026
f0988a4
add test coverage
klesaulnier Feb 23, 2026
1277850
improve slightly exportCompressedToS3
klesaulnier Feb 23, 2026
ad5cff6
fix checkstyle and changelog
klesaulnier Feb 23, 2026
ee824f9
fix: add test to cover potential empty network debug file
klesaulnier Feb 23, 2026
f1bcfe4
fix debug file location
klesaulnier Feb 23, 2026
9e89241
PR remarks
klesaulnier Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public record ProcessRunMessage<T extends ProcessConfig>(
@JsonSubTypes({
@JsonSubTypes.Type(value = SecurityAnalysisConfig.class, name = "SECURITY_ANALYSIS")
})
T config
T config,
String debugFileLocation
) {
public ProcessType processType() {
return config.processType();
Expand Down
6 changes: 6 additions & 0 deletions monitor-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

<properties>
<liquibase-hibernate-package>org.gridsuite.monitor.server</liquibase-hibernate-package>
<spring-cloud-aws-starter-s3>3.3.1</spring-cloud-aws-starter-s3>
Comment thread
antoinebhs marked this conversation as resolved.
</properties>

<build>
Expand Down Expand Up @@ -122,6 +123,11 @@
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter-s3</artifactId>
<version>${spring-cloud-aws-starter-s3}</version>
</dependency>

<!-- Test dependencies -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.gridsuite.monitor.server.config;

import org.gridsuite.monitor.server.services.S3RestService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.services.s3.S3Client;

/**
* @author Kevin Le Saulnier <kevin.le-saulnier at rte-france.com>
*/
@Configuration
public class S3Configuration {
private static final Logger LOGGER = LoggerFactory.getLogger(S3Configuration.class);
private final String bucketName;

public S3Configuration(@Value("${spring.cloud.aws.bucket:ws-bucket}") String bucketName) {
this.bucketName = bucketName;
}

@SuppressWarnings("checkstyle:MethodName")
@Bean
public S3RestService s3RestService(S3Client s3Client) {
LOGGER.info("Configuring S3Service with bucket: {}", bucketName);
return new S3RestService(s3Client, bucketName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import org.gridsuite.monitor.server.dto.ProcessExecution;
import org.gridsuite.monitor.server.dto.ReportPage;
import org.gridsuite.monitor.server.services.MonitorService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

Expand Down Expand Up @@ -44,9 +46,10 @@ public MonitorController(MonitorService monitorService) {
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The security analysis execution has been started")})
public ResponseEntity<UUID> executeSecurityAnalysis(
@RequestParam UUID caseUuid,
@RequestParam(required = false, defaultValue = "false") boolean isDebug,
@RequestBody SecurityAnalysisConfig securityAnalysisConfig,
@RequestHeader(HEADER_USER_ID) String userId) {
UUID executionId = monitorService.executeProcess(caseUuid, userId, securityAnalysisConfig);
UUID executionId = monitorService.executeProcess(caseUuid, userId, securityAnalysisConfig, isDebug);
return ResponseEntity.ok(executionId);
}

Expand Down Expand Up @@ -83,6 +86,22 @@ public ResponseEntity<List<ProcessExecutionStep>> getStepsInfos(@Parameter(descr
.orElseGet(() -> ResponseEntity.notFound().build());
}

@GetMapping("/executions/{executionId}/debug-infos")
@Operation(summary = "Get execution debug file")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Debug file downloaded"),
@ApiResponse(responseCode = "404", description = "execution id was not found")})
public ResponseEntity<byte[]> getDebugInfos(@Parameter(description = "Execution UUID") @PathVariable UUID executionId) {
return monitorService.getDebugInfos(executionId)
.map(bytes -> ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"archive.zip\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(bytes.length)
.body(bytes))
.orElseGet(() -> ResponseEntity.notFound().build());
}

@DeleteMapping("/executions/{executionId}")
@Operation(summary = "Delete an execution")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Execution was deleted"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
public class ProcessExecutionEntity {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;

@Column
Expand Down Expand Up @@ -55,6 +54,9 @@ public class ProcessExecutionEntity {
@Column
private String userId;

@Column
private String debugFileLocation;

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
@JoinColumn(name = "execution_id", foreignKey = @ForeignKey(name = "processExecutionStep_processExecution_fk"))
@OrderBy("stepOrder ASC")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
*/
package org.gridsuite.monitor.server.services;

import com.powsybl.commons.PowsyblException;
import org.gridsuite.monitor.commons.ProcessConfig;
import org.gridsuite.monitor.commons.ProcessExecutionStep;
import org.gridsuite.monitor.commons.ProcessStatus;
import org.gridsuite.monitor.commons.ProcessType;
import org.gridsuite.monitor.commons.ResultInfos;
import org.gridsuite.monitor.server.utils.S3PathResolver;
import org.gridsuite.monitor.server.dto.ProcessExecution;
import org.gridsuite.monitor.server.dto.ReportPage;
import org.gridsuite.monitor.server.entities.ProcessExecutionEntity;
Expand All @@ -21,11 +23,9 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.*;

/**
* @author Antoine Bouhours <antoine.bouhours at rte-france.com>
Expand All @@ -37,29 +37,40 @@ public class MonitorService {
private final NotificationService notificationService;
private final ReportService reportService;
private final ResultService resultService;
private final S3RestService s3RestService;
private final S3PathResolver s3PathResolver;

public MonitorService(ProcessExecutionRepository executionRepository,
NotificationService notificationService,
ReportService reportService,
ResultService resultService) {
ResultService resultService,
S3RestService s3RestService,
S3PathResolver s3PathResolver) {
this.executionRepository = executionRepository;
this.notificationService = notificationService;
this.reportService = reportService;
this.resultService = resultService;
this.s3RestService = s3RestService;
this.s3PathResolver = s3PathResolver;
}

@Transactional
public UUID executeProcess(UUID caseUuid, String userId, ProcessConfig processConfig) {
public UUID executeProcess(UUID caseUuid, String userId, ProcessConfig processConfig, boolean isDebug) {
UUID executionId = UUID.randomUUID();
ProcessExecutionEntity execution = ProcessExecutionEntity.builder()
.id(executionId)
.type(processConfig.processType().name())
.caseUuid(caseUuid)
.status(ProcessStatus.SCHEDULED)
.scheduledAt(Instant.now())
.userId(userId)
.build();
if (isDebug) {
execution.setDebugFileLocation(s3PathResolver.toDebugLocation(processConfig.processType().name(), executionId));
}
executionRepository.save(execution);

notificationService.sendProcessRunMessage(caseUuid, processConfig, execution.getId());
notificationService.sendProcessRunMessage(caseUuid, processConfig, execution.getId(), execution.getDebugFileLocation());

return execution.getId();
}
Expand Down Expand Up @@ -163,6 +174,20 @@ public List<String> getResults(UUID executionId) {
.toList();
}

@Transactional(readOnly = true)
public Optional<byte[]> getDebugInfos(UUID executionId) {
return executionRepository.findById(executionId)
.map(ProcessExecutionEntity::getDebugFileLocation)
.filter(Objects::nonNull)
.map(debugFileLocation -> {
try {
return s3RestService.downloadDirectoryAsZip(debugFileLocation);
} catch (IOException e) {
throw new PowsyblException("An error occurred while downloading debug files", e);
}
});
}

private List<ResultInfos> getResultInfos(UUID executionId) {
return executionRepository.findById(executionId)
.map(execution -> Optional.ofNullable(execution.getSteps()).orElse(List.of()).stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ public class NotificationService {

private final StreamBridge publisher;

public void sendProcessRunMessage(UUID caseUuid, ProcessConfig processConfig, UUID executionId) {
public void sendProcessRunMessage(UUID caseUuid, ProcessConfig processConfig, UUID executionId, String debugFileLocation) {
String bindingName = switch (processConfig.processType()) {
case SECURITY_ANALYSIS -> "publishRunSecurityAnalysis-out-0";
};
ProcessRunMessage<?> message = new ProcessRunMessage<>(executionId, caseUuid, processConfig);
ProcessRunMessage<?> message = new ProcessRunMessage<>(executionId, caseUuid, processConfig, debugFileLocation);
publisher.send(bindingName, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.gridsuite.monitor.server.services;

import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
* @author Kevin Le Saulnier <kevin.le-saulnier at rte-france.com>
*/
public class S3RestService {
Comment thread
antoinebhs marked this conversation as resolved.
private final S3Client s3Client;

private final String bucketName;

public S3RestService(S3Client s3Client, String bucketName) {
this.s3Client = s3Client;
this.bucketName = bucketName;
}

/**
* We did not use downloadDirectory from s3 methods here because it downloads all files on device directly instead of letting us redirect the stream into zip stream
*/
public byte[] downloadDirectoryAsZip(String directoryKey) throws IOException {
List<String> filesKeys = getFilesKeysInDirectory(directoryKey);

return buildZip(
directoryKey,
filesKeys,
key -> s3Client.getObject(GetObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build())
);
}

byte[] buildZip(String directoryKey, List<String> filesS3Keys, Function<String, InputStream> s3ObjectFetcher) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
for (String fileS3Key : filesS3Keys) {
if (fileS3Key.endsWith("/")) {
// s3 files with key endpoint with "/" are most of the time DIROBJ, empty s3 objects that simulate directory for users
// we ignore them here to prevent errors
continue;
}
String zipEntryName = fileS3Key.substring(directoryKey.length() + 1);
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.

In standard S3 like our dev platform, S3 also returns the directory itself. This might be an issue here.
Like this:

bouhoursant@gm0winl660:~$ s3cmd ls s3://bucket/dev_debug/
2026-02-24 17:38       DIROBJ  s3://bucket/dev_debug/
2026-02-19 17:26       110378  s3://bucket/dev_debug/dynamic_margin_calculation_debug_10029729549746183105.zip
2026-02-23 08:32       110362  s3://bucket/dev_debug/dynamic_margin_calculation_debug_16278494592987801260.zip

?

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.

fixed by filtering ou elements ending by "/"

zipOutputStream.putNextEntry(new ZipEntry(zipEntryName));
try (InputStream in = s3ObjectFetcher.apply(fileS3Key)) {
in.transferTo(zipOutputStream);
}
zipOutputStream.closeEntry();
}
}

return outputStream.toByteArray();
}

List<String> getFilesKeysInDirectory(String directoryKey) {
Comment thread
antoinebhs marked this conversation as resolved.
List<String> filesS3Keys = new ArrayList<>();
ListObjectsV2Request request = ListObjectsV2Request.builder()
.bucket(bucketName)
.prefix(directoryKey)
.build();
ListObjectsV2Response response;
do {
response = s3Client.listObjectsV2(request);
response.contents().forEach(obj -> filesS3Keys.add(obj.key()));

request = request.toBuilder()
.continuationToken(response.nextContinuationToken())
.build();
} while (Boolean.TRUE.equals(response.isTruncated())); // S3 pagination, this loop ends if this is the last page

return filesS3Keys;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2026, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.gridsuite.monitor.server.utils;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.UUID;

/**
* @author Kevin Le Saulnier <kevin.le-saulnier at rte-france.com>
*/
@Component
public final class S3PathResolver {
private final String s3RootPath;

public S3PathResolver(@Value("${powsybl-ws.s3.subpath.prefix:}${debug-subpath:debug}") String s3RootPath) {
this.s3RootPath = s3RootPath;
}

/**
* Builds root path used to build debug file location
* @param processType
* @param executionId
* @return {executionEnvName}_debug/process/{processType}/{executionId}
*/
public String toDebugLocation(String processType, UUID executionId) {
String s3Delimiter = "/";
return String.join(s3Delimiter, s3RootPath, "process", processType, executionId.toString());
}
}
4 changes: 4 additions & 0 deletions monitor-server/src/main/resources/application-local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ server:
spring:
rabbitmq:
addresses: localhost
cloud:
aws:
s3:
endpoint: http://localhost:19000

powsybl-ws:
database:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:pro="http://www.liquibase.org/xml/ns/pro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet author="lesaulnierkev (generated)" id="1770884531328-1">
<addColumn tableName="process_execution">
<column name="debug_file_location" type="varchar(255)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ databaseChangeLog:
- include:
file: changesets/changelog_20260130T160426Z.xml
relativeToChangelogFile: true

- include:
file: changesets/changelog_20260212T082157Z.xml
relativeToChangelogFile: true
Loading