diff --git a/config/logback-console.xml b/config/logback-console.xml index eb6c4b68..17ec16c3 100644 --- a/config/logback-console.xml +++ b/config/logback-console.xml @@ -22,6 +22,7 @@ + true diff --git a/config/logback.xml b/config/logback.xml index 796820c2..60fd6288 100644 --- a/config/logback.xml +++ b/config/logback.xml @@ -32,6 +32,7 @@ + true diff --git a/pom.xml b/pom.xml index f58421ab..ac2f70e1 100644 --- a/pom.xml +++ b/pom.xml @@ -528,6 +528,7 @@ io.netty:netty-common:jar:* + ch.qos.logback:logback-core:jar:* @@ -546,6 +547,11 @@ logback-classic ${logback.version} + + ch.qos.logback + logback-core + ${logback.version} + com.arpnetworking.logback logback-steno diff --git a/src/main/java/com/arpnetworking/configuration/jackson/module/pekko/ActorContextSerializer.java b/src/main/java/com/arpnetworking/configuration/jackson/module/pekko/ActorContextSerializer.java new file mode 100644 index 00000000..8bd52a61 --- /dev/null +++ b/src/main/java/com/arpnetworking/configuration/jackson/module/pekko/ActorContextSerializer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Brandon Arp + * + * 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 com.arpnetworking.configuration.jackson.module.pekko; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.apache.pekko.actor.ActorContext; + +import java.io.IOException; + +/** + * Serializer for a Pekko timer message. + * + * @author Brandon Arp (brandon dot arp at inscopemetrics dot com) + */ +public final class ActorContextSerializer extends JsonSerializer { + @Override + public void serialize( + final ActorContext context, + final JsonGenerator jsonGenerator, + final SerializerProvider serializerProvider) + throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeObjectField("type", context.getClass().getTypeName()); + jsonGenerator.writeObjectField("self", context.self()); + jsonGenerator.writeObjectField("sender", context.sender()); + jsonGenerator.writeEndObject(); + } +} diff --git a/src/main/java/com/arpnetworking/configuration/jackson/module/pekko/PekkoLoggingModule.java b/src/main/java/com/arpnetworking/configuration/jackson/module/pekko/PekkoLoggingModule.java index 2f1ee545..135771f0 100644 --- a/src/main/java/com/arpnetworking/configuration/jackson/module/pekko/PekkoLoggingModule.java +++ b/src/main/java/com/arpnetworking/configuration/jackson/module/pekko/PekkoLoggingModule.java @@ -16,6 +16,7 @@ package com.arpnetworking.configuration.jackson.module.pekko; import com.fasterxml.jackson.databind.module.SimpleModule; +import org.apache.pekko.actor.ActorContext; import org.apache.pekko.actor.ActorRef; import org.apache.pekko.actor.LocalActorRef; import org.apache.pekko.actor.TimerSchedulerImpl; @@ -37,12 +38,14 @@ public PekkoLoggingModule() { } @Override public void setupModule(final SetupContext context) { - addSerializer(ActorRef.class, new ActorRefLoggingSerializer()); - addSerializer(LocalActorRef.class, new ActorRefLoggingSerializer()); - addSerializer(TimerSchedulerImpl.TimerMsg.class, new TimerMessageSerializer()); - addSerializer(TimerSchedulerImpl.InfluenceReceiveTimeoutTimerMsg.class, new TimerMessageSerializer()); - addSerializer(TimerSchedulerImpl.NotInfluenceReceiveTimeoutTimerMsg.class, new TimerMessageSerializer()); - super.setupModule(context); + addSerializer(ActorRef.class, new ActorRefLoggingSerializer()); + addSerializer(LocalActorRef.class, new ActorRefLoggingSerializer()); + addSerializer(TimerSchedulerImpl.TimerMsg.class, new TimerMessageSerializer()); + addSerializer(TimerSchedulerImpl.InfluenceReceiveTimeoutTimerMsg.class, new TimerMessageSerializer()); + addSerializer(TimerSchedulerImpl.NotInfluenceReceiveTimeoutTimerMsg.class, new TimerMessageSerializer()); + addSerializer(TimerSchedulerImpl.class, new TimerSchedulerSerializer()); + addSerializer(ActorContext.class, new ActorContextSerializer()); + super.setupModule(context); } @Serial diff --git a/src/main/java/com/arpnetworking/configuration/jackson/module/pekko/TimerSchedulerSerializer.java b/src/main/java/com/arpnetworking/configuration/jackson/module/pekko/TimerSchedulerSerializer.java new file mode 100644 index 00000000..41c3a923 --- /dev/null +++ b/src/main/java/com/arpnetworking/configuration/jackson/module/pekko/TimerSchedulerSerializer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2025 Brandon Arp + * + * 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 com.arpnetworking.configuration.jackson.module.pekko; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.apache.pekko.actor.TimerSchedulerImpl; + +import java.io.IOException; +import java.io.InvalidObjectException; +import java.lang.reflect.Field; + +/** + * Serializer for a Pekko timer message. + * + * @author Brandon Arp (brandon dot arp at inscopemetrics dot com) + */ +public final class TimerSchedulerSerializer extends JsonSerializer { + @Override + public void serialize( + final TimerSchedulerImpl timerScheduler, + final JsonGenerator jsonGenerator, + final SerializerProvider serializerProvider) + throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeObjectField("type", timerScheduler.getClass().getTypeName()); + try { + jsonGenerator.writeObjectField("key", CTX.get(timerScheduler)); + } catch (final IllegalAccessException e) { + throw new InvalidObjectException("Unable to access context field"); + } + jsonGenerator.writeEndObject(); + } + private static final Field CTX; + static { + try { + CTX = TimerSchedulerImpl.class.getDeclaredField("ctx"); + CTX.setAccessible(true); + } catch (final NoSuchFieldException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/arpnetworking/metrics/mad/PeriodWorker.java b/src/main/java/com/arpnetworking/metrics/mad/PeriodWorker.java index 9acd6b13..52956294 100644 --- a/src/main/java/com/arpnetworking/metrics/mad/PeriodWorker.java +++ b/src/main/java/com/arpnetworking/metrics/mad/PeriodWorker.java @@ -167,7 +167,7 @@ private void checkForIdle() { private void shutdown(final Aggregator.PeriodWorkerShutdown shutdown) { timers().cancelAll(); - self().tell(PoisonPill.getInstance(), self()); + timers().startSingleTimer("SELF_SHUTDOWN", PoisonPill.getInstance(), Duration.ofSeconds(5)); } private void scheduleRotation(final ZonedDateTime now) { diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 6bfe06c6..8c5f6e8c 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -30,6 +30,7 @@ %date %t [%level] %logger : %message %ex%n + diff --git a/src/test/java/com/arpnetworking/metrics/mad/PekkoLoggingModuleTest.java b/src/test/java/com/arpnetworking/metrics/mad/PekkoLoggingModuleTest.java new file mode 100644 index 00000000..1c41d7f6 --- /dev/null +++ b/src/test/java/com/arpnetworking/metrics/mad/PekkoLoggingModuleTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2025 Brandon Arp + * + * 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 com.arpnetworking.metrics.mad; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.classic.util.LogbackMDCAdapter; +import ch.qos.logback.core.joran.spi.JoranException; +import com.arpnetworking.steno.Logger; +import com.arpnetworking.steno.TestLoggerFactory; +import org.apache.pekko.actor.AbstractActor; +import org.apache.pekko.actor.ActorRef; +import org.apache.pekko.actor.ActorSystem; +import org.apache.pekko.actor.Props; +import org.apache.pekko.actor.Terminated; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; + +/** + * Unit test class for verifying the functionality and integration of the AkkaLoggingModule. + * This class tests various aspects of logging and actor-based communication within an Akka-based + * system using Logback as the logging backend. + * + * @author Brandon Arp (brandon dot arp at inscopemetrics dot io) + */ +public class PekkoLoggingModuleTest { + /** + * Sets up the test environment by initializing the required components. + * This method is executed before each test, ensuring a fresh setup. + * + * The setup process includes the following: + * - Creation of an Akka {@link ActorSystem} instance. + * - Loading the specific XML configuration file corresponding to the test class. + * - Initializing and configuring a Logback {@link LoggerContext}. + * - Setting up the {@link JoranConfigurator} to parse the configuration file. + * - Resetting and associating a {@link LogbackMDCAdapter} instance with the LoggerContext. + * + * If an error occurs during the configuration process using the JoranConfigurator, + * a {@link RuntimeException} is thrown, encapsulating the underlying {@link JoranException}. + * + * The initialized {@link ActorSystem} and {@link LoggerContext} serve as the foundation for + * executing tests that involve logging and actor-based messaging. + */ + @Before + public void setUp() { + _actorSystem = ActorSystem.create(); + final URL configuration = getClass().getResource( + this.getClass().getSimpleName() + ".xml"); + _loggerContext = new LoggerContext(); + final JoranConfigurator configurator = new JoranConfigurator(); + configurator.setContext(_loggerContext); + _loggerContext.reset(); + final LogbackMDCAdapter mdcAdapter = new LogbackMDCAdapter(); + _loggerContext.setMDCAdapter(mdcAdapter); + try { + configurator.doConfigure(configuration); + } catch (final JoranException e) { + throw new RuntimeException(e); + } + } + + @After + public void tearDown() throws Exception { + final CompletionStage terminated = _actorSystem.getWhenTerminated(); + _actorSystem.terminate(); + terminated.toCompletableFuture().get(10, TimeUnit.SECONDS); + } + + @Test + public void testLoggingActorRefs() { + final ActorRef actorRef = _actorSystem.actorOf(Props.create(SimpleActor.class, SimpleActor::new), "simple-actor"); + getLogger().info() + .setMessage("LoggingActorRefs") + .addData("actorRef", actorRef) + .log(); + assertOutput(); + } + + protected void assertOutput() { + final URL expectedResource = getClass().getResource( + getClass().getSimpleName() + ".expected"); + final File actualFile = new File("target/test-logs/" + this.getClass().getSimpleName() + ".log"); + final String actualOutput; + try { + actualOutput = Files.readString(actualFile.toPath()); + } catch (final IOException e) { + throw new RuntimeException(e); + } + try { + assertOutput(Files.readString(Paths.get(expectedResource.toURI())), actualOutput); + } catch (final IOException | URISyntaxException e) { + Assert.fail("Failed with exception: " + e); + } + } + + protected void assertOutput(final String expected, final String actual) { + Assert.assertEquals(expected.trim(), sanitizeOutput(actual.trim())); + } + + protected String sanitizeOutput(final String output) { + return output.replaceAll("\"time\":\"[^\"]+\"", "\"time\":\"