Skip to content

Commit a22dd17

Browse files
authored
fix: start log appender to ensure propagation to websockets (#71)
1 parent c045d63 commit a22dd17

File tree

5 files changed

+121
-5
lines changed

5 files changed

+121
-5
lines changed

cloudnet-rest-module/src/main/java/eu/cloudnetservice/ext/modules/rest/v3/V3HttpHandlerNode.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package eu.cloudnetservice.ext.modules.rest.v3;
1818

19+
import ch.qos.logback.classic.Level;
1920
import ch.qos.logback.classic.spi.ILoggingEvent;
2021
import ch.qos.logback.core.ConsoleAppender;
2122
import ch.qos.logback.core.OutputStreamAppender;
@@ -28,6 +29,7 @@
2829
import eu.cloudnetservice.driver.provider.GroupConfigurationProvider;
2930
import eu.cloudnetservice.driver.provider.ServiceTaskProvider;
3031
import eu.cloudnetservice.ext.modules.rest.dto.JsonConfigurationDto;
32+
import eu.cloudnetservice.ext.modules.rest.validation.LogLevel;
3133
import eu.cloudnetservice.ext.rest.api.HttpContext;
3234
import eu.cloudnetservice.ext.rest.api.HttpMethod;
3335
import eu.cloudnetservice.ext.rest.api.HttpResponseCode;
@@ -63,6 +65,7 @@
6365
import org.slf4j.Logger;
6466

6567
@Singleton
68+
@EnableValidation
6669
public final class V3HttpHandlerNode {
6770

6871
private final Logger logger;
@@ -175,16 +178,23 @@ public V3HttpHandlerNode(
175178
@RequestHandler(path = "/api/v3/node/liveConsole")
176179
public @NonNull IntoResponse<?> handleLiveConsoleRequest(
177180
@NonNull HttpContext context,
181+
@FirstRequestQueryParam("threshold") @Optional @Valid @LogLevel String threshold,
178182
@Authentication(
179183
providers = {"ticket", "jwt"},
180184
scopes = {"cloudnet_rest:node_read", "cloudnet_rest:node_live_console"}) @NonNull RestUser restUser
181185
) {
182186
if (this.logger instanceof ch.qos.logback.classic.Logger logbackLogger) {
183187
context.upgrade().thenAccept(channel -> {
184-
var webSocketAppender = new WebSocketLogAppender(logbackLogger, restUser, channel);
188+
var webSocketAppender = new WebSocketLogAppender(
189+
logbackLogger,
190+
Level.toLevel(threshold, null),
191+
restUser,
192+
channel);
185193
var appender = logbackLogger.getAppender("Rolling");
186-
if (appender instanceof OutputStreamAppender<ILoggingEvent> consoleAppender) {
187-
webSocketAppender.setEncoder(consoleAppender.getEncoder());
194+
if (appender instanceof OutputStreamAppender<ILoggingEvent> fileAppender) {
195+
webSocketAppender.setContext(fileAppender.getContext());
196+
webSocketAppender.setEncoder(fileAppender.getEncoder());
197+
webSocketAppender.start();
188198
}
189199

190200
logbackLogger.addAppender(webSocketAppender);
@@ -210,15 +220,18 @@ private void reloadConfig() {
210220
protected class WebSocketLogAppender extends ConsoleAppender<ILoggingEvent> implements WebSocketListener {
211221

212222
protected final ch.qos.logback.classic.Logger logger;
223+
protected final Level thresholdLevel;
213224
protected final RestUser user;
214225
protected final WebSocketChannel channel;
215226

216227
public WebSocketLogAppender(
217228
@NonNull ch.qos.logback.classic.Logger logger,
229+
@Nullable Level thresholdLevel,
218230
@NonNull RestUser user,
219231
@NonNull WebSocketChannel channel
220232
) {
221233
this.logger = logger;
234+
this.thresholdLevel = thresholdLevel;
222235
this.user = user;
223236
this.channel = channel;
224237
}
@@ -249,7 +262,9 @@ public void handleClose(
249262

250263
@Override
251264
protected void append(@NonNull ILoggingEvent event) {
252-
this.channel.sendWebSocketFrame(WebSocketFrameType.TEXT, this.encoder.encode(event));
265+
if (this.thresholdLevel == null || event.getLevel().isGreaterOrEqual(this.thresholdLevel)) {
266+
this.channel.sendWebSocketFrame(WebSocketFrameType.TEXT, this.encoder.encode(event));
267+
}
253268
}
254269
}
255270
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2019-2024 CloudNetService team & contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package eu.cloudnetservice.ext.modules.rest.validation;
18+
19+
import eu.cloudnetservice.ext.modules.rest.validation.validator.LogLevelValidator;
20+
import jakarta.validation.Constraint;
21+
import jakarta.validation.Payload;
22+
import java.lang.annotation.Documented;
23+
import java.lang.annotation.ElementType;
24+
import java.lang.annotation.Retention;
25+
import java.lang.annotation.RetentionPolicy;
26+
import java.lang.annotation.Target;
27+
28+
/**
29+
* This validator annotation ensures that any provided string is a valid logback log level.
30+
*/
31+
@Documented
32+
@Retention(RetentionPolicy.RUNTIME)
33+
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.CONSTRUCTOR})
34+
@Constraint(validatedBy = LogLevelValidator.class)
35+
public @interface LogLevel {
36+
37+
String message() default "Log level must be a valid logback log level";
38+
39+
Class<?>[] groups() default {};
40+
41+
Class<? extends Payload>[] payload() default {};
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2019-2024 CloudNetService team & contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package eu.cloudnetservice.ext.modules.rest.validation.validator;
18+
19+
import ch.qos.logback.classic.Level;
20+
import eu.cloudnetservice.ext.modules.rest.validation.LogLevel;
21+
import jakarta.validation.ConstraintValidator;
22+
import jakarta.validation.ConstraintValidatorContext;
23+
import lombok.NonNull;
24+
import org.jetbrains.annotations.Nullable;
25+
26+
public final class LogLevelValidator implements ConstraintValidator<LogLevel, String> {
27+
28+
@Override
29+
public boolean isValid(@Nullable String value, @NonNull ConstraintValidatorContext context) {
30+
return value == null || Level.toLevel(value, null) != null;
31+
}
32+
}

cloudnet-rest-module/src/main/resources/documentation/swagger.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,15 @@ paths:
681681
The ticket secret used for authentication purposes. Can be omitted if jwt authentication is used.
682682
schema:
683683
type: string
684+
- name: threshold
685+
in: query
686+
required: false
687+
description: |
688+
The log level threshold. Only log entries with the same or higher log level will be sent.
689+
The lowest level that is actually sent is the configured log level of the node itself,
690+
requesting lower levels is possible but there is no effect.
691+
schema:
692+
$ref: '#/components/schemas/LogLevel'
684693
summary: Live console
685694
description: |
686695
Upgrades the connection to a web socket connection and sends all new
@@ -689,9 +698,14 @@ paths:
689698
One of the following scopes is needed to execute the request:
690699
- `cloudnet_rest:node_read`
691700
- `cloudnet_rest:node_live_console`
701+
702+
Sending commands to the console using the websocket is only allowed if the following scope is set:
703+
- `cloudnet_rest:node_send_commands`
692704
responses:
693705
'101':
694706
description: Switching the protocol to a websocket
707+
'400':
708+
$ref: '#/components/responses/Problem'
695709
'401':
696710
$ref: '#/components/responses/Problem'
697711
'403':
@@ -4662,6 +4676,16 @@ components:
46624676
examples:
46634677
- CloudNet-Bridge
46644678
- CloudNet-Signs
4679+
LogLevel:
4680+
type: string
4681+
enum:
4682+
- ALL
4683+
- TRACE
4684+
- DEBUG
4685+
- INFO
4686+
- WARN
4687+
- ERROR
4688+
- OFF
46654689
CloudNetVersion:
46664690
type: object
46674691
properties:

web-api/src/main/java/eu/cloudnetservice/ext/rest/api/auth/ScopedRestUserDelegate.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,10 @@ public record ScopedRestUserDelegate(@NonNull RestUser delegate, @NonNull Set<St
102102
*/
103103
@Override
104104
public boolean hasScope(@NonNull String scope) {
105-
return (this.scopes.isEmpty() || this.scopes.contains(scope)) && this.delegate.hasScope(scope);
105+
var hasScopedAccess = this.scopes.isEmpty()
106+
|| this.scopes.contains(scope)
107+
|| this.scopes.contains(RestUser.GLOBAL_ADMIN_SCOPE);
108+
return hasScopedAccess && this.delegate.hasScope(scope);
106109
}
107110

108111
/**

0 commit comments

Comments
 (0)