Skip to content

Commit 886546f

Browse files
joaomatosdevJoão Araújo
andauthored
Add handler for new /duration endpoint to update logger level tempora… (#383)
* Add handler for new /duration endpoint to update logger level temporarily and update OpenAPI spec * refactor: change the new endpoint name * Refactor POST /logging-manager to support temporary log level via query param with duration in body; updated OpenAPI specs and README --------- Co-authored-by: João Araújo <joao.p.araujo@feedzai.com>
1 parent cdf4d20 commit 886546f

File tree

4 files changed

+125
-15
lines changed

4 files changed

+125
-15
lines changed

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
The **Quarkus Logging Manager** Extension provides you endpoints to visualize and manage the
1818
log level of your loggers.
1919

20-
| Endpoint | Http Method | Description |
21-
|----------------------------------------------|:-----------:|:----------------------------------------------------------------------------------------------------:|
22-
| `/q/logging-manager` | `GET` | Returns the list of all loggers, with information about the configured and effective level |
23-
| `/q/logging-manager?loggerName={loggerName}` | `GET` | Returns the logger specified by this name, with information about the configured and effective level |
24-
| `/q/logging-manager` | `POST` | Changes the log level of the specified logger |
25-
| `/q/logging-manager/levels` | `GET` | Get all the available level |
20+
| Endpoint | Http Method | Description |
21+
|----------------------------------------------|:-----------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------:|
22+
| `/q/logging-manager` | `GET` | Returns the list of all loggers, with information about the configured and effective level |
23+
| `/q/logging-manager?loggerName={loggerName}` | `GET` | Returns the logger specified by this name, with information about the configured and effective level |
24+
| `/q/logging-manager` | `POST` | Changes the log level of the specified logger |
25+
| `/q/logging-manager?temporary=true` | `POST` | Temporarily changes the log level of the specified logger for a given duration, then restores it to the original level |
26+
| `/q/logging-manager/levels` | `GET` | Get all the available level |
2627

2728
## Security
2829
Security of endpoints is important, and we do not want to allow unknown people to know (or worse, change!) the log levels of

deployment/src/main/java/io/quarkiverse/loggingmanager/deployment/LoggingManagerOpenAPIFilter.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,13 @@ private Operation createLoggerPostOperation() {
106106
Map<String, Schema> properties = new LinkedHashMap<>();
107107
properties.put("loggerName", OASFactory.createSchema().type(List.of(Schema.SchemaType.STRING)));
108108
properties.put("loggerLevel", OASFactory.createSchema().ref(REF_LOGGER_LEVEL));
109+
properties.put("duration", OASFactory.createSchema()
110+
.type(List.of(Schema.SchemaType.INTEGER))
111+
.description("Duration in seconds, used only if temporary=true"));
109112
return OASFactory.createOperation()
110113
.operationId("logging_manager_update")
111114
.summary("Update log level")
112-
.description("Update a log level for a certain logger")
115+
.description("Update a log level for a certain logger. Use query param `temporary=true` for temporary level.")
113116
.tags(Collections.singletonList(tag))
114117
.requestBody(OASFactory.createRequestBody()
115118
.content(OASFactory.createContent().addMediaType(
@@ -118,7 +121,10 @@ private Operation createLoggerPostOperation() {
118121
.type(List.of(Schema.SchemaType.OBJECT))
119122
.properties(properties)))))
120123
.responses(OASFactory.createAPIResponses()
121-
.addAPIResponse("201", OASFactory.createAPIResponse().description("Created")));
124+
.addAPIResponse("201", OASFactory.createAPIResponse().description("Created"))
125+
.addAPIResponse("200", OASFactory.createAPIResponse().description("Log level already set"))
126+
.addAPIResponse("400", OASFactory.createAPIResponse().description("Invalid request"))
127+
.addAPIResponse("404", OASFactory.createAPIResponse().description("Logger not found")));
122128
}
123129

124130
private PathItem createLevelsPathItem() {

runtime/src/main/java/io/quarkiverse/loggingmanager/LogController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,20 @@ public static String getEffectiveLogLevel(Logger logger) {
107107
return getEffectiveLogLevel(logger.getParent());
108108
}
109109

110+
public static boolean doesLoggerExist(String loggerName) {
111+
LogContext logContext = LogContext.getLogContext();
112+
Enumeration<String> loggerNames = logContext.getLoggerNames();
113+
114+
while (loggerNames.hasMoreElements()) {
115+
String existingLoggerName = loggerNames.nextElement();
116+
if (existingLoggerName.equals(loggerName)) {
117+
return true;
118+
}
119+
}
120+
121+
return false;
122+
}
123+
110124
public static final List<String> LEVELS = List.of(
111125
OFF.getName(),
112126
SEVERE.getName(),

runtime/src/main/java/io/quarkiverse/loggingmanager/LoggerHandler.java

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import static io.vertx.core.http.HttpMethod.POST;
55

66
import java.util.Objects;
7+
import java.util.concurrent.ConcurrentHashMap;
8+
9+
import org.jboss.logmanager.LogContext;
710

811
import io.vertx.core.Handler;
912
import io.vertx.core.http.HttpMethod;
@@ -18,6 +21,20 @@
1821
public class LoggerHandler implements Handler<RoutingContext> {
1922
private static final String LOGGER_NAME_PARAM = "loggerName";
2023
private static final String LOGGER_LEVEL_PARAM = "loggerLevel";
24+
private static final String LOGGER_DURATION = "duration";
25+
private static final String TEMPORARY_ENABLED = "temporary";
26+
27+
private final ConcurrentHashMap<String, TemporaryLogState> temporaryStates = new ConcurrentHashMap<>();
28+
29+
private static class TemporaryLogState {
30+
final String originalLevel;
31+
long timerId;
32+
33+
TemporaryLogState(String originalLevel, long timerId) {
34+
this.originalLevel = originalLevel;
35+
this.timerId = timerId;
36+
}
37+
}
2138

2239
/**
2340
* Handles incoming HTTP requests by delegating to appropriate method handlers.
@@ -33,15 +50,15 @@ public void handle(RoutingContext routingContext) {
3350
if (GET == method) {
3451
handleGet(request, response);
3552
} else if (POST == method) {
36-
handlePost(request, response);
53+
handlePost(request, response, routingContext);
3754
} else {
3855
response.setStatusCode(405).end(); // Method not allowed
3956
}
4057
}
4158

4259
/**
43-
* Handles GET requests to retrieve logger information.
44-
* Returns all loggers if no logger name is specified, or details for a specific logger.
60+
* Handles GET requests to retrieve logger information. Returns all loggers if no logger name is specified, or
61+
* details for a specific logger.
4562
*
4663
* @param request The HTTP request
4764
* @param response The HTTP response
@@ -58,13 +75,13 @@ private void handleGet(HttpServerRequest request, HttpServerResponse response) {
5875
}
5976

6077
/**
61-
* Handles POST requests to update logger levels.
62-
* Updates the specified logger with the provided level or removes the level if none specified.
78+
* Handles POST requests to update logger levels. Updates the specified logger with the provided level or removes
79+
* the level if none specified.
6380
*
6481
* @param request The HTTP request
6582
* @param response The HTTP response
6683
*/
67-
private void handlePost(HttpServerRequest request, HttpServerResponse response) {
84+
private void handlePost(HttpServerRequest request, HttpServerResponse response, RoutingContext routingContext) {
6885
String contentType = request.getHeader("Content-Type");
6986
if (!"application/x-www-form-urlencoded".equals(contentType)) {
7087
response.setStatusCode(415).end();
@@ -74,6 +91,14 @@ private void handlePost(HttpServerRequest request, HttpServerResponse response)
7491
String loggerName = request.getFormAttribute(LOGGER_NAME_PARAM);
7592
String loggerLevel = request.getFormAttribute(LOGGER_LEVEL_PARAM);
7693

94+
String temporaryEnabled = request.getParam(TEMPORARY_ENABLED);
95+
96+
if ("true".equalsIgnoreCase(temporaryEnabled)) {
97+
String loggerDuration = request.getFormAttribute(LOGGER_DURATION);
98+
handleTemporaryPost(loggerName, loggerLevel, loggerDuration, routingContext, response);
99+
return;
100+
}
101+
77102
if (loggerLevel == null || loggerLevel.isEmpty()) {
78103
LogController.updateLogLevel(loggerName, null);
79104
} else {
@@ -82,4 +107,68 @@ private void handlePost(HttpServerRequest request, HttpServerResponse response)
82107

83108
response.setStatusCode(201).end();
84109
}
85-
}
110+
111+
/**
112+
* Handles POST requests to update logger levels temporarily. Updates the specified logger to the provided level
113+
* for a given duration, then restores the original level automatically.
114+
*
115+
* @param loggerName The name of the logger to update
116+
* @param loggerLevel The new temporary log level
117+
* @param loggerDuration Duration in seconds for which the temporary level should apply
118+
* @param routingContext The Vert.x routing context
119+
* @param response The HTTP response
120+
*/
121+
private void handleTemporaryPost(
122+
String loggerName,
123+
String loggerLevel,
124+
String loggerDuration,
125+
RoutingContext routingContext,
126+
HttpServerResponse response) {
127+
128+
if (loggerLevel == null || !LogController.LEVELS.contains(loggerLevel.toUpperCase())) {
129+
response.setStatusCode(400).end("{\"error\":\"Invalid logger level\"}");
130+
return;
131+
}
132+
133+
if (!LogController.doesLoggerExist(loggerName)) {
134+
response.setStatusCode(404).end("{\"error\":\"Logger '" + loggerName + "' not found\"}");
135+
return;
136+
}
137+
138+
int durationSeconds;
139+
try {
140+
durationSeconds = Integer.parseInt(loggerDuration);
141+
if (durationSeconds <= 0)
142+
throw new NumberFormatException();
143+
} catch (NumberFormatException e) {
144+
response.setStatusCode(400).end("{\"error\":\"Duration must be a positive integer\"}");
145+
return;
146+
}
147+
148+
String currentLevel = LogController.getConfiguredLogLevel(LogContext.getLogContext().getLogger(loggerName));
149+
TemporaryLogState previousState = temporaryStates.get(loggerName);
150+
151+
if (loggerLevel.equalsIgnoreCase(currentLevel) && previousState == null) {
152+
response.setStatusCode(200).end("{\"message\":\"Log level already set to " + loggerLevel + "\"}");
153+
return;
154+
}
155+
156+
if (previousState != null) {
157+
routingContext.vertx().cancelTimer(previousState.timerId);
158+
}
159+
160+
String originalLevel = previousState != null ? previousState.originalLevel : currentLevel;
161+
162+
LogController.updateLogLevel(loggerName, loggerLevel);
163+
164+
long timerId = routingContext.vertx().setTimer(durationSeconds * 1000L, id -> {
165+
LogController.updateLogLevel(loggerName, originalLevel);
166+
temporaryStates.remove(loggerName);
167+
});
168+
169+
temporaryStates.put(loggerName, new TemporaryLogState(originalLevel, timerId));
170+
171+
response.setStatusCode(201)
172+
.end("{\"message\":\"Temporary log level '" + loggerLevel + "' set for " + durationSeconds + " seconds\"}");
173+
}
174+
}

0 commit comments

Comments
 (0)