Skip to content

Commit

Permalink
fix: start log appender to ensure propagation to websockets (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
0utplay authored Dec 22, 2024
1 parent c045d63 commit a22dd17
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -63,6 +65,7 @@
import org.slf4j.Logger;

@Singleton
@EnableValidation
public final class V3HttpHandlerNode {

private final Logger logger;
Expand Down Expand Up @@ -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<ILoggingEvent> consoleAppender) {
webSocketAppender.setEncoder(consoleAppender.getEncoder());
if (appender instanceof OutputStreamAppender<ILoggingEvent> fileAppender) {
webSocketAppender.setContext(fileAppender.getContext());
webSocketAppender.setEncoder(fileAppender.getEncoder());
webSocketAppender.start();
}

logbackLogger.addAppender(webSocketAppender);
Expand All @@ -210,15 +220,18 @@ private void reloadConfig() {
protected class WebSocketLogAppender extends ConsoleAppender<ILoggingEvent> 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;
}
Expand Down Expand Up @@ -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));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<? extends Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -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<LogLevel, String> {

@Override
public boolean isValid(@Nullable String value, @NonNull ConstraintValidatorContext context) {
return value == null || Level.toLevel(value, null) != null;
}
}
24 changes: 24 additions & 0 deletions cloudnet-rest-module/src/main/resources/documentation/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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':
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ public record ScopedRestUserDelegate(@NonNull RestUser delegate, @NonNull Set<St
*/
@Override
public boolean hasScope(@NonNull String scope) {
return (this.scopes.isEmpty() || this.scopes.contains(scope)) && this.delegate.hasScope(scope);
var hasScopedAccess = this.scopes.isEmpty()
|| this.scopes.contains(scope)
|| this.scopes.contains(RestUser.GLOBAL_ADMIN_SCOPE);
return hasScopedAccess && this.delegate.hasScope(scope);
}

/**
Expand Down

0 comments on commit a22dd17

Please sign in to comment.