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

Migrate PutLogLevel to MigratingEndpointAdapter #5798

Merged
merged 5 commits into from
Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Next Next commit
Migrate PutLogLevel to MigratingEndpointAdapter
  • Loading branch information
courtneyeh committed Jun 15, 2022
commit 4c2c295bd5a3fdaa1d0e6e40330623eb55621bef
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"put" : {
"tags" : [ "Teku" ],
"operationId" : "putLogLevel",
"summary" : "Changes the log level without restarting.",
"description" : "Changes the log level without restarting. You can change the log level for all logs, or the log level for specific packages or classes.",
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/PutLogLevelRequest"
}
}
}
},
"responses" : {
"204" : {
"description" : "The LogLevel was accepted and applied",
"content" : { }
},
"400" : {
"description" : "The request could not be processed, check the response for more information.",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/HttpErrorResponse"
}
}
}
},
"500" : {
"description" : "Internal server error",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/HttpErrorResponse"
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type" : "string",
"title" : "Level",
"description" : "Level string",
"example" : "ERROR",
"format" : "string"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"title" : "PutLogLevelRequest",
"type" : "object",
"required" : [ "level" ],
"properties" : {
"level" : {
"$ref" : "#/components/schemas/Level"
},
"log_filter" : {
"type" : "array",
"items" : {
"type" : "string"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ private static Info createApplicationInfo() {

private void addTekuSpecificHandlers(
final DataProvider provider, final Eth1DataProvider eth1DataProvider) {
app.put(PutLogLevel.ROUTE, new PutLogLevel(jsonProvider));
addMigratedEndpoint(new PutLogLevel());
app.get(GetStateByBlockRoot.ROUTE, new GetStateByBlockRoot(provider, jsonProvider));
addMigratedEndpoint(new Liveness(provider));
addMigratedEndpoint(new Readiness(provider));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,46 @@

package tech.pegasys.teku.beaconrestapi.handlers.tekuv1.admin;

import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.INVALID_BODY_SUPPLIED;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.RES_BAD_REQUEST;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.RES_INTERNAL_ERROR;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.RES_NO_CONTENT;
import static tech.pegasys.teku.infrastructure.http.RestApiConstants.TAG_TEKU;
import static tech.pegasys.teku.infrastructure.json.types.CoreTypes.STRING_TYPE;

import com.fasterxml.jackson.core.JsonProcessingException;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import io.javalin.plugin.openapi.annotations.HttpMethod;
import io.javalin.plugin.openapi.annotations.OpenApi;
import io.javalin.plugin.openapi.annotations.OpenApiContent;
import io.javalin.plugin.openapi.annotations.OpenApiRequestBody;
import io.javalin.plugin.openapi.annotations.OpenApiResponse;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.apache.logging.log4j.Level;
import tech.pegasys.teku.api.schema.LogLevel;
import tech.pegasys.teku.beaconrestapi.handlers.AbstractHandler;
import tech.pegasys.teku.beaconrestapi.schema.BadRequest;
import tech.pegasys.teku.beaconrestapi.MigratingEndpointAdapter;
import tech.pegasys.teku.infrastructure.json.types.DeserializableTypeDefinition;
import tech.pegasys.teku.infrastructure.logging.LoggingConfigurator;
import tech.pegasys.teku.provider.JsonProvider;

public class PutLogLevel extends AbstractHandler implements Handler {
import tech.pegasys.teku.infrastructure.restapi.endpoints.EndpointMetadata;
import tech.pegasys.teku.infrastructure.restapi.endpoints.RestApiRequest;

public class PutLogLevel extends MigratingEndpointAdapter {
public static final String ROUTE = "/teku/v1/admin/log_level";

public PutLogLevel(final JsonProvider jsonProvider) {
super(jsonProvider);
public PutLogLevel() {
super(
EndpointMetadata.put(ROUTE)
.operationId("putLogLevel")
.summary("Changes the log level without restarting.")
.description(
"Changes the log level without restarting. You can change the log level for all logs, or the log level for specific packages or classes.")
.tags(TAG_TEKU)
.requestBodyType(LogLevel.getJsonTypeDefinition())
.response(SC_NO_CONTENT, "The LogLevel was accepted and applied")
.build());
}

@OpenApi(
Expand All @@ -49,7 +62,7 @@ public PutLogLevel(final JsonProvider jsonProvider) {
tags = {TAG_TEKU},
requestBody =
@OpenApiRequestBody(
content = {@OpenApiContent(from = LogLevel.class)},
content = {@OpenApiContent(from = tech.pegasys.teku.api.schema.LogLevel.class)},
description =
"```\n{\n \"level\": (String; acceptable values: ALL, TRACE, DEBUG, INFO, ERROR, FATAL, OFF ),\n"
+ " \"log_filter\": [(String; Optional)]\n}\n```"),
Expand All @@ -64,19 +77,94 @@ public PutLogLevel(final JsonProvider jsonProvider) {
})
@Override
public void handle(final Context ctx) throws Exception {
try {
final LogLevel params = parseRequestBody(ctx.body(), LogLevel.class);
adapt(ctx);
}

@Override
public void handleRequest(RestApiRequest request) throws JsonProcessingException {
final LogLevel requestBody = request.getRequestBody();
final List<String> logFilters = requestBody.getLogFilter().orElse(List.of(""));

for (final String logFilter : logFilters) {
LoggingConfigurator.setAllLevels(logFilter, requestBody.getLevel());
}
request.respondWithCode(SC_NO_CONTENT);
}

final String[] logFilters = params.getLogFilter().orElseGet(() -> new String[] {""});
static class LogLevel {
private Level level;
private Optional<List<String>> logFilter = Optional.empty();

for (final String logFilter : logFilters) {
LoggingConfigurator.setAllLevels(logFilter, params.getLevel());
private static final DeserializableTypeDefinition<Level> LEVEL_TYPE =
DeserializableTypeDefinition.string(Level.class)
.name("Level")
.formatter(Level::toString)
.parser(Level::toLevel)
.example("ERROR")
.description("Level string")
.format("string")
.build();

private LogLevel() {}

public LogLevel(final String level) {
this.level = Level.valueOf(level);
}

public LogLevel(final String level, final List<String> logFilter) {
this.level = Level.valueOf(level);
this.logFilter = Optional.of(logFilter);
}

public Level getLevel() {
return level;
}

public void setLevel(Level level) {
this.level = level;
}

public Optional<List<String>> getLogFilter() {
return logFilter;
}

public void setLogFilter(Optional<List<String>> logFilter) {
this.logFilter = logFilter;
}

static DeserializableTypeDefinition<LogLevel> getJsonTypeDefinition() {
return DeserializableTypeDefinition.object(LogLevel.class)
.name("PutLogLevelRequest")
.initializer(LogLevel::new)
.withField("level", LEVEL_TYPE, LogLevel::getLevel, LogLevel::setLevel)
.withOptionalField(
"log_filter",
DeserializableTypeDefinition.listOf(STRING_TYPE),
LogLevel::getLogFilter,
LogLevel::setLogFilter)
.build();
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
LogLevel logLevel = (LogLevel) o;
return Objects.equals(level, logLevel.level) && Objects.equals(logFilter, logLevel.logFilter);
}

@Override
public int hashCode() {
return Objects.hash(level, logFilter);
}

ctx.status(SC_NO_CONTENT);
} catch (final IllegalArgumentException e) {
ctx.json(jsonProvider.objectToJSON(new BadRequest(e.getMessage())));
ctx.status(SC_BAD_REQUEST);
@Override
public String toString() {
return "LogLevel{" + "level=" + level + ", logFilter=" + logFilter + '}';
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ void getRouteExists(final String route, final Class<Handler> type) {

@Test
void shouldHavePutLogLevel() {
verify(app).put(eq(PutLogLevel.ROUTE), any(PutLogLevel.class));
verify(app).addHandler(eq(HandlerType.PUT), eq(PutLogLevel.ROUTE), any(PutLogLevel.class));
}
courtneyeh marked this conversation as resolved.
Show resolved Hide resolved
courtneyeh marked this conversation as resolved.
Show resolved Hide resolved

static Stream<Arguments> getParameters() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,68 +13,59 @@

package tech.pegasys.teku.beaconrestapi.handlers.tekuv1.admin;

import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static tech.pegasys.teku.infrastructure.restapi.MetadataTestUtil.getRequestBodyFromMetadata;

import io.javalin.http.Context;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import java.io.IOException;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import tech.pegasys.teku.provider.JsonProvider;
import tech.pegasys.teku.beaconrestapi.AbstractMigratedBeaconHandlerTest;

public class PutLogLevelTest {

private final JsonProvider jsonProvider = new JsonProvider();

private Context context = mock(Context.class);
private PutLogLevel handler;
public class PutLogLevelTest extends AbstractMigratedBeaconHandlerTest {

@BeforeEach
public void setup() {
handler = new PutLogLevel(jsonProvider);
setHandler(new PutLogLevel());
}

@Test
public void shouldReturnBadRequestWhenLevelIsMissing() throws Exception {
when(context.body()).thenReturn("{\"a\": \"field\"}");
handler.handle(context);

verify(context).status(SC_BAD_REQUEST);
public void shouldReturnBadRequestWhenLevelIsInvalid() {
assertThatThrownBy(() -> request.setRequestBody(new PutLogLevel.LogLevel("I do not exist")))
.hasMessageContaining("Unknown level constant [I DO NOT EXIST].")
.isInstanceOf(IllegalArgumentException.class);
}

@Test
public void shouldReturnBadRequestWhenLevelIsInvalid() throws Exception {
when(context.body()).thenReturn("{\"level\": \"I do not exist\"}");
handler.handle(context);
public void shouldReturnNoContentWhenLevelIsValid() throws Exception {
request.setRequestBody(new PutLogLevel.LogLevel("inFO"));
handler.handleRequest(request);

verify(context).status(SC_BAD_REQUEST);
assertThat(request.getResponseCode()).isEqualTo(SC_NO_CONTENT);
}

@Test
public void shouldReturnNoContentWhenLevelIsValid() throws Exception {
when(context.body()).thenReturn("{\"level\": \"inFO\"}");
handler.handle(context);
public void shouldReturnNoContentWhenLevelAndFilterAreValid() throws Exception {
request.setRequestBody(new PutLogLevel.LogLevel("InfO", List.of("a.class.somewhere")));
handler.handleRequest(request);

verify(context).status(SC_NO_CONTENT);
assertThat(request.getResponseCode()).isEqualTo(SC_NO_CONTENT);
}

@Test
public void shouldReturnBadRequestWhenLFilterIsInvalidJson() throws Exception {
when(context.body())
.thenReturn("{\"level\": \"I do not exist\", \"log_filter\": \"a.class.somewhere\"}");
handler.handle(context);

verify(context).status(SC_BAD_REQUEST);
void shouldNotReadInvalidRequestBody() {
final String data = "{\"level\": \"I do not exist\", \"log_filter\": \"a.class.somewhere\"}";
assertThatThrownBy(() -> getRequestBodyFromMetadata(handler, data))
.isInstanceOf(MismatchedInputException.class);
}

@Test
public void shouldReturnNoContentWhenLevelAndFilterAreValid() throws Exception {
when(context.body())
.thenReturn("{\"level\": \"InfO\", \"log_filter\": [\"a.class.somewhere\"]}");
handler.handle(context);

verify(context).status(SC_NO_CONTENT);
void shouldReadRequestBody() throws IOException {
final String data = "{\"level\": \"InfO\", \"log_filter\": [\"a.class.somewhere\"]}";
assertThat(getRequestBodyFromMetadata(handler, data))
.isEqualTo(new PutLogLevel.LogLevel("INFO", List.of("a.class.somewhere")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ public static EndpointMetaDataBuilder post(final String path) {
return new EndpointMetaDataBuilder().method(HandlerType.POST).path(path);
}

public static EndpointMetaDataBuilder put(final String path) {
return new EndpointMetaDataBuilder().method(HandlerType.PUT).path(path);
}

public static EndpointMetaDataBuilder delete(final String path) {
return new EndpointMetaDataBuilder().method(HandlerType.DELETE).path(path);
}
Expand Down