Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8079 display log files #8148

Merged
merged 29 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0f94f03
replace second parameter of ratingCalculation() metod from type 'EcoN…
Warded120 Jan 28, 2025
ef631db
created endpoints to: get logs list; view log file content; download …
Warded120 Feb 3, 2025
60afa3c
Merge remote-tracking branch 'origin/dev' into dev
Warded120 Feb 3, 2025
6ed806d
Merge branch 'dev' into 8079-display-log-files
Warded120 Feb 3, 2025
438dbc0
created DotenvService; added endpoint to delete .env file; configured…
Warded120 Feb 3, 2025
1b610a1
replaced String literals to constants
Warded120 Feb 4, 2025
7094e61
added appender to create log files in logback.xml
Warded120 Feb 4, 2025
ced96ac
refactored responses statuses
Warded120 Feb 5, 2025
dfe4077
added tests
Warded120 Feb 13, 2025
da4ff37
added more tests; refactored code
Warded120 Feb 13, 2025
fcbeee7
Merge branch 'dev' into 8079-display-log-files
Warded120 Feb 13, 2025
dc9d634
formatted
Warded120 Feb 13, 2025
4525e9c
added tests for coverage
Warded120 Feb 13, 2025
0922e02
added tests for coverage
Warded120 Feb 13, 2025
10fffd7
Merge remote-tracking branch 'origin/8079-display-log-files' into 807…
Warded120 Feb 13, 2025
36349c1
resolved issues
Warded120 Feb 13, 2025
5356034
resolved issues from comments
Warded120 Feb 18, 2025
7fbfb5c
fixed LogFileRequestDto to be required
Warded120 Feb 20, 2025
615b5e9
removed wildcard imports
Warded120 Feb 20, 2025
68ff066
moved filename constant to AppConstant class
Warded120 Feb 20, 2025
d0fc63c
removed 'get' words from method names related to POST
Warded120 Feb 21, 2025
ab260b1
removed duplicate PasswordEncoder bean to use the existing one in the…
Warded120 Feb 21, 2025
6c37e94
updated file filtering to reduce memory usage; added new small features
Warded120 Feb 21, 2025
a5f8aba
Merge remote-tracking branch 'origin/8079-display-log-files' into 807…
Warded120 Feb 21, 2025
cceb682
resolved issue with unhandled exception at project startup when .env …
Warded120 Feb 24, 2025
92d6b64
resolved issue with unhandled exception at project startup when .env …
Warded120 Feb 24, 2025
43ebaf3
formatted
Warded120 Feb 24, 2025
9731918
Merge remote-tracking branch 'origin/8079-display-log-files' into 807…
Warded120 Feb 24, 2025
f5e59c4
solved issue with exception when paginating log files
Warded120 Feb 24, 2025
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
4 changes: 3 additions & 1 deletion core/src/main/java/greencity/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public class SecurityConfig {
private static final String HABIT_INVITE = "/habit/invite";
private static final String INVITATION_ID = "/{invitationId}";
private static final String COMMIT_INFO = "/commit-info";
public static final String LOGS = "/logs/**";
private final JwtTool jwtTool;
private final UserService userService;
private final AuthenticationConfiguration authenticationConfiguration;
Expand Down Expand Up @@ -345,7 +346,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
FRIENDS + "/{friendId}",
ECO_NEWS + "/{ecoNewsId}/favorites",
"/habit/assign/{habitId}/invite",
"place/v2/save")
"place/v2/save",
LOGS)
.hasAnyRole(USER, ADMIN, MODERATOR, UBS_EMPLOYEE)
.requestMatchers(HttpMethod.PUT,
"/habit/statistic/{id}",
Expand Down
1 change: 1 addition & 0 deletions core/src/main/java/greencity/constant/HttpStatuses.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public class HttpStatuses {
public static final String NOT_FOUND = "Not Found";
public static final String SEE_OTHER = "See Other";
public static final String FOUND = "Found";
public static final String SERVICE_UNAVAILABLE = "Service Unavailable";
}
134 changes: 134 additions & 0 deletions core/src/main/java/greencity/controller/LogFileController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package greencity.controller;

import greencity.annotations.ApiPageable;
import greencity.constant.HttpStatuses;
import greencity.dto.PageableDto;
import greencity.dto.logs.LogFileRequestDto;
import greencity.dto.logs.LogFileMetadataDto;
import greencity.service.DotenvService;
import greencity.service.LogFileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.Resource;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Lazy
@RequiredArgsConstructor
@RequestMapping("/logs")
@Profile({"dev", "test"})
public class LogFileController {
private final LogFileService logFileService;
private final DotenvService dotenvService;

@Operation(summary = "Returns a list of log files metadata from project directory")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = HttpStatuses.OK,
content = @Content(schema = @Schema(example = LogFileMetadataDto.defaultJson))),
@ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED,
content = @Content(examples = @ExampleObject(HttpStatuses.UNAUTHORIZED))),
@ApiResponse(responseCode = "403", description = HttpStatuses.FORBIDDEN,
content = @Content(examples = @ExampleObject(HttpStatuses.FORBIDDEN))),
@ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND,
content = @Content(examples = @ExampleObject(HttpStatuses.NOT_FOUND)))
})
@ApiPageable
@PostMapping
public ResponseEntity<PageableDto<LogFileMetadataDto>> listLogFiles(
@Schema(
description = "Filters for logs",
name = "LogFileFilterDto",
type = "object",
example = LogFileRequestDto.defaultJson) @RequestBody @Valid LogFileRequestDto requestDto,
@Parameter(hidden = true) Pageable page) {
return ResponseEntity.status(HttpStatus.OK)
.body(logFileService.listLogFiles(page, requestDto.filterDto(), requestDto.secretKey()));
}

@Operation(summary = "Returns content of a file with given filename",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
content = @Content(mediaType = "text/plain",
schema = @Schema(type = "string"))))
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = HttpStatuses.OK,
content = @Content(schema = @Schema(example = "string"))),
@ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED,
content = @Content(examples = @ExampleObject(HttpStatuses.UNAUTHORIZED))),
@ApiResponse(responseCode = "403", description = HttpStatuses.FORBIDDEN,
content = @Content(examples = @ExampleObject(HttpStatuses.FORBIDDEN))),
@ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND,
content = @Content(examples = @ExampleObject(HttpStatuses.NOT_FOUND))),
@ApiResponse(responseCode = "503", description = HttpStatuses.SERVICE_UNAVAILABLE,
content = @Content(examples = @ExampleObject(HttpStatuses.SERVICE_UNAVAILABLE)))
})
@PostMapping("/view/{filename}")
public ResponseEntity<String> viewLogFileContent(
@RequestBody String secretKey,
@PathVariable String filename) {
return ResponseEntity.status(HttpStatus.OK)
.body(logFileService.viewLogFileContent(logFileService.sanitizeFilename(filename), secretKey));
}

@Operation(summary = "Returns a url that triggers file download in a browser",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
content = @Content(mediaType = "text/plain",
schema = @Schema(type = "string"))))
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = HttpStatuses.OK,
content = @Content(schema = @Schema(example = HttpStatuses.OK))),
@ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED,
content = @Content(examples = @ExampleObject(HttpStatuses.UNAUTHORIZED))),
@ApiResponse(responseCode = "403", description = HttpStatuses.FORBIDDEN,
content = @Content(examples = @ExampleObject(HttpStatuses.FORBIDDEN))),
@ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND,
content = @Content(examples = @ExampleObject(HttpStatuses.NOT_FOUND)))
})
@PostMapping("/download/{filename}")
public ResponseEntity<Resource> downloadLogFile(
@RequestBody String secretKey,
@PathVariable String filename) {
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(
HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + logFileService.sanitizeFilename(filename) + "\"")
.body(logFileService.generateDownloadLogFileUrl(logFileService.sanitizeFilename(filename), secretKey));
}

@Operation(summary = "deletes '.env' file to make functionality that is dependent on it unavailable",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
content = @Content(mediaType = "text/plain",
schema = @Schema(type = "string"))))
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = HttpStatuses.OK,
content = @Content(schema = @Schema(example = HttpStatuses.OK))),
@ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED,
content = @Content(examples = @ExampleObject(HttpStatuses.UNAUTHORIZED))),
@ApiResponse(responseCode = "403", description = HttpStatuses.FORBIDDEN,
content = @Content(examples = @ExampleObject(HttpStatuses.FORBIDDEN)))
})
@PostMapping("/delete-dotenv")
public ResponseEntity<Object> deleteDotenvFile(
@RequestBody String secretKey) {
dotenvService.deleteDotenvFile(secretKey);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
import greencity.constant.ValidationConstants;
import greencity.exception.exceptions.BadCategoryRequestException;
import greencity.exception.exceptions.BadRequestException;
import greencity.exception.exceptions.BadSecretKeyException;
import greencity.exception.exceptions.BadSocialNetworkLinksException;
import greencity.exception.exceptions.EventDtoValidationException;
import greencity.exception.exceptions.FileReadException;
import greencity.exception.exceptions.FunctionalityNotAvailableException;
import greencity.exception.exceptions.InvalidStatusException;
import greencity.exception.exceptions.InvalidURLException;
import greencity.exception.exceptions.LowRoleLevelException;
Expand Down Expand Up @@ -553,4 +556,53 @@ public final ResponseEntity<Object> handleResourceNotFoundException(ResourceNotF

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exceptionResponse);
}

/**
* Method intercepts exception {@link FileReadException}.
*
* @param ex Exception that should be intercepted.
* @param request Contains details about the occurred exception.
* @return {@code ResponseEntity} which contains the HTTP status and body with
* the exception message.
*/
@ExceptionHandler(FileReadException.class)
public final ResponseEntity<Object> handleFileReadException(FileReadException ex,
WebRequest request) {
log.error(ex.getMessage(), ex);

ExceptionResponse exceptionResponse = new ExceptionResponse(getErrorAttributes(request));
exceptionResponse.setMessage(ex.getMessage());

return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(exceptionResponse);
}

/**
* Method intercepts exception {@link BadSecretKeyException}.
*
* @param ex Exception that should be intercepted.
* @param request Contains details about the occurred exception.
* @return {@code ResponseEntity} which contains the HTTP status and body with
* the exception message.
*/
@ExceptionHandler(BadSecretKeyException.class)
public final ResponseEntity<Object> handleBadSecretKeyException(BadSecretKeyException ex,
WebRequest request) {
log.error(ex.getMessage(), ex);

ExceptionResponse exceptionResponse = new ExceptionResponse(getErrorAttributes(request));
exceptionResponse.setMessage(ex.getMessage());

return ResponseEntity.status(HttpStatus.FORBIDDEN).body(exceptionResponse);
}

@ExceptionHandler(FunctionalityNotAvailableException.class)
public final ResponseEntity<Object> handleFunctionalityNotAvailableException(FunctionalityNotAvailableException ex,
WebRequest request) {
log.error(ex.getMessage(), ex);

ExceptionResponse exceptionResponse = new ExceptionResponse(getErrorAttributes(request));
exceptionResponse.setMessage(ex.getMessage());

return ResponseEntity.status(HttpStatus.FORBIDDEN).body(exceptionResponse);
}
}
29 changes: 23 additions & 6 deletions core/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
<configuration>

<!-- Define the custom date format -->
<property name="DATE_FORMAT" value="yyyy-MM-dd HH:mm:ss" />

<!-- Define the log pattern with the custom date format -->
<property name="LOG_PATH" value="./logs" />
<property name="LOG_MAX_SIZE" value="5MB" />
<property name="LOG_PATTERN" value="[%d{${DATE_FORMAT}}] %highlight(%level) %cyan(%-40.40logger{40}) %magenta(%-5.5(:%line)) %msg%n" />

<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Use the log pattern with the custom date format -->
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>

<!-- Define loggers and root level -->
<appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/application.log</file>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- Daily log file rotation -->
<fileNamePattern>${LOG_PATH}/application-%d{yyyy-MM-dd}.%i.log</fileNamePattern>

<!-- Max file size before rolling -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>${LOG_MAX_SIZE}</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>

<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="console" />
<appender-ref ref="console"/>
<appender-ref ref="rollingFile"/>
</root>

</configuration>
14 changes: 13 additions & 1 deletion core/src/test/java/greencity/ModelUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import greencity.dto.language.LanguageTranslationDTO;
import greencity.dto.location.LocationDto;
import greencity.dto.location.MapBoundsDto;
import greencity.dto.logs.filter.LogFileFilterDto;
import greencity.dto.place.PlaceByBoundsDto;
import greencity.dto.todolistitem.CustomToDoListItemResponseDto;
import greencity.dto.todolistitem.ToDoListItemPostDto;
Expand Down Expand Up @@ -83,6 +84,8 @@
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import org.springframework.boot.logging.LogLevel;
import org.springframework.data.domain.Pageable;

import static greencity.TestConst.ROLE_ADMIN;
Expand Down Expand Up @@ -595,6 +598,15 @@ public static List<PlaceByBoundsDto> getPlaceByBoundsDto() {
.build());
}

public static LogFileFilterDto getLogFileFilterDto() {
return new LogFileFilterDto(
"filename",
"fileContent",
null,
null,
LogLevel.INFO);
}

public static EventResponseDto getEventResponseDto() {
return new EventResponseDto(
1L,
Expand Down Expand Up @@ -648,4 +660,4 @@ public static AddressDto getAddressDtoCorrect() {
.countryEn("Country")
.build();
}
}
}
Loading
Loading