From a22dd177e1117083a2502381599e767d00ad0473 Mon Sep 17 00:00:00 2001 From: Aldin Date: Sun, 22 Dec 2024 22:17:37 +0100 Subject: [PATCH] fix: start log appender to ensure propagation to websockets (#71) --- .../modules/rest/v3/V3HttpHandlerNode.java | 23 ++++++++-- .../ext/modules/rest/validation/LogLevel.java | 42 +++++++++++++++++++ .../validator/LogLevelValidator.java | 32 ++++++++++++++ .../main/resources/documentation/swagger.yaml | 24 +++++++++++ .../rest/api/auth/ScopedRestUserDelegate.java | 5 ++- 5 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/validation/LogLevel.java create mode 100644 cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/validation/validator/LogLevelValidator.java diff --git a/cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/v3/V3HttpHandlerNode.java b/cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/v3/V3HttpHandlerNode.java index 371b486e..8db669c0 100644 --- a/cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/v3/V3HttpHandlerNode.java +++ b/cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/v3/V3HttpHandlerNode.java @@ -16,6 +16,7 @@ package eu.cloudnetservice.ext.modules.rest.v3; +import ch.qos.logback.classic.Level; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.ConsoleAppender; import ch.qos.logback.core.OutputStreamAppender; @@ -28,6 +29,7 @@ import eu.cloudnetservice.driver.provider.GroupConfigurationProvider; import eu.cloudnetservice.driver.provider.ServiceTaskProvider; import eu.cloudnetservice.ext.modules.rest.dto.JsonConfigurationDto; +import eu.cloudnetservice.ext.modules.rest.validation.LogLevel; import eu.cloudnetservice.ext.rest.api.HttpContext; import eu.cloudnetservice.ext.rest.api.HttpMethod; import eu.cloudnetservice.ext.rest.api.HttpResponseCode; @@ -63,6 +65,7 @@ import org.slf4j.Logger; @Singleton +@EnableValidation public final class V3HttpHandlerNode { private final Logger logger; @@ -175,16 +178,23 @@ public V3HttpHandlerNode( @RequestHandler(path = "/api/v3/node/liveConsole") public @NonNull IntoResponse handleLiveConsoleRequest( @NonNull HttpContext context, + @FirstRequestQueryParam("threshold") @Optional @Valid @LogLevel String threshold, @Authentication( providers = {"ticket", "jwt"}, scopes = {"cloudnet_rest:node_read", "cloudnet_rest:node_live_console"}) @NonNull RestUser restUser ) { if (this.logger instanceof ch.qos.logback.classic.Logger logbackLogger) { context.upgrade().thenAccept(channel -> { - var webSocketAppender = new WebSocketLogAppender(logbackLogger, restUser, channel); + var webSocketAppender = new WebSocketLogAppender( + logbackLogger, + Level.toLevel(threshold, null), + restUser, + channel); var appender = logbackLogger.getAppender("Rolling"); - if (appender instanceof OutputStreamAppender consoleAppender) { - webSocketAppender.setEncoder(consoleAppender.getEncoder()); + if (appender instanceof OutputStreamAppender fileAppender) { + webSocketAppender.setContext(fileAppender.getContext()); + webSocketAppender.setEncoder(fileAppender.getEncoder()); + webSocketAppender.start(); } logbackLogger.addAppender(webSocketAppender); @@ -210,15 +220,18 @@ private void reloadConfig() { protected class WebSocketLogAppender extends ConsoleAppender implements WebSocketListener { protected final ch.qos.logback.classic.Logger logger; + protected final Level thresholdLevel; protected final RestUser user; protected final WebSocketChannel channel; public WebSocketLogAppender( @NonNull ch.qos.logback.classic.Logger logger, + @Nullable Level thresholdLevel, @NonNull RestUser user, @NonNull WebSocketChannel channel ) { this.logger = logger; + this.thresholdLevel = thresholdLevel; this.user = user; this.channel = channel; } @@ -249,7 +262,9 @@ public void handleClose( @Override protected void append(@NonNull ILoggingEvent event) { - this.channel.sendWebSocketFrame(WebSocketFrameType.TEXT, this.encoder.encode(event)); + if (this.thresholdLevel == null || event.getLevel().isGreaterOrEqual(this.thresholdLevel)) { + this.channel.sendWebSocketFrame(WebSocketFrameType.TEXT, this.encoder.encode(event)); + } } } } diff --git a/cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/validation/LogLevel.java b/cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/validation/LogLevel.java new file mode 100644 index 00000000..92f52903 --- /dev/null +++ b/cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/validation/LogLevel.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019-2024 CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.ext.modules.rest.validation; + +import eu.cloudnetservice.ext.modules.rest.validation.validator.LogLevelValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This validator annotation ensures that any provided string is a valid logback log level. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.CONSTRUCTOR}) +@Constraint(validatedBy = LogLevelValidator.class) +public @interface LogLevel { + + String message() default "Log level must be a valid logback log level"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/validation/validator/LogLevelValidator.java b/cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/validation/validator/LogLevelValidator.java new file mode 100644 index 00000000..78d23b9c --- /dev/null +++ b/cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/validation/validator/LogLevelValidator.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019-2024 CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.ext.modules.rest.validation.validator; + +import ch.qos.logback.classic.Level; +import eu.cloudnetservice.ext.modules.rest.validation.LogLevel; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.NonNull; +import org.jetbrains.annotations.Nullable; + +public final class LogLevelValidator implements ConstraintValidator { + + @Override + public boolean isValid(@Nullable String value, @NonNull ConstraintValidatorContext context) { + return value == null || Level.toLevel(value, null) != null; + } +} diff --git a/cloudnet-rest-module/src/main/resources/documentation/swagger.yaml b/cloudnet-rest-module/src/main/resources/documentation/swagger.yaml index 1ce55a78..5989d40f 100644 --- a/cloudnet-rest-module/src/main/resources/documentation/swagger.yaml +++ b/cloudnet-rest-module/src/main/resources/documentation/swagger.yaml @@ -681,6 +681,15 @@ paths: The ticket secret used for authentication purposes. Can be omitted if jwt authentication is used. schema: type: string + - name: threshold + in: query + required: false + description: | + The log level threshold. Only log entries with the same or higher log level will be sent. + The lowest level that is actually sent is the configured log level of the node itself, + requesting lower levels is possible but there is no effect. + schema: + $ref: '#/components/schemas/LogLevel' summary: Live console description: | Upgrades the connection to a web socket connection and sends all new @@ -689,9 +698,14 @@ paths: One of the following scopes is needed to execute the request: - `cloudnet_rest:node_read` - `cloudnet_rest:node_live_console` + + Sending commands to the console using the websocket is only allowed if the following scope is set: + - `cloudnet_rest:node_send_commands` responses: '101': description: Switching the protocol to a websocket + '400': + $ref: '#/components/responses/Problem' '401': $ref: '#/components/responses/Problem' '403': @@ -4662,6 +4676,16 @@ components: examples: - CloudNet-Bridge - CloudNet-Signs + LogLevel: + type: string + enum: + - ALL + - TRACE + - DEBUG + - INFO + - WARN + - ERROR + - OFF CloudNetVersion: type: object properties: diff --git a/web-api/src/main/java/eu/cloudnetservice/ext/rest/api/auth/ScopedRestUserDelegate.java b/web-api/src/main/java/eu/cloudnetservice/ext/rest/api/auth/ScopedRestUserDelegate.java index 03a81989..69374730 100644 --- a/web-api/src/main/java/eu/cloudnetservice/ext/rest/api/auth/ScopedRestUserDelegate.java +++ b/web-api/src/main/java/eu/cloudnetservice/ext/rest/api/auth/ScopedRestUserDelegate.java @@ -102,7 +102,10 @@ public record ScopedRestUserDelegate(@NonNull RestUser delegate, @NonNull Set