Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions config/logback-console.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<jacksonModule class="com.fasterxml.jackson.datatype.guava.GuavaModule" />
<jacksonModule class="com.fasterxml.jackson.datatype.jdk8.Jdk8Module" />
<jacksonModule class="com.fasterxml.jackson.datatype.jsr310.JavaTimeModule" />
<jacksonModule class="com.arpnetworking.configuration.jackson.module.pekko.PekkoLoggingModule" />
<injectBeanIdentifier>true</injectBeanIdentifier>
</encoder>
</appender>
Expand Down
1 change: 1 addition & 0 deletions config/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<jacksonModule class="com.fasterxml.jackson.datatype.guava.GuavaModule" />
<jacksonModule class="com.fasterxml.jackson.datatype.jdk8.Jdk8Module" />
<jacksonModule class="com.fasterxml.jackson.datatype.jsr310.JavaTimeModule" />
<jacksonModule class="com.arpnetworking.configuration.jackson.module.pekko.PekkoLoggingModule" />
<injectBeanIdentifier>true</injectBeanIdentifier>
</encoder>
</appender>
Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@
<configuration>
<ignoredDependencies combine.children="append">
<ignoredDependency>io.netty:netty-common:jar:*</ignoredDependency>
<ignoredDependency>ch.qos.logback:logback-core:jar:*</ignoredDependency>
</ignoredDependencies>
</configuration>
</plugin>
Expand All @@ -546,6 +547,11 @@
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>com.arpnetworking.logback</groupId>
<artifactId>logback-steno</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ActorContext> {
@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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TimerSchedulerImpl> {
@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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%date %t [%level] %logger : %message %ex%n</pattern>
</layout>
<jacksonModule class="com.arpnetworking.configuration.jackson.module.pekko.PekkoLoggingModule" />
</encoder>
</appender>

Expand Down
Original file line number Diff line number Diff line change
@@ -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> 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\":\"<TIME>\"")
.replaceAll("\"id\":\"[^\"]+\"", "\"id\":\"<ID>\"")
.replaceAll("Actor\\[pekko://default/user/simple-actor[^\"]+]\"", "Actor[pekko://default/user/simple-actor]\"")
.replaceAll("\"host\":\"[^\"]+\"", "\"host\":\"<HOST>\"")
.replaceAll("\"processId\":\"[^\"]+\"", "\"processId\":\"<PROCESS_ID>\"")
.replaceAll("\"threadId\":\"[^\"]+\"", "\"threadId\":\"<THREAD_ID>\"")
.replaceAll("\"backtrace\":\\[[^\\]]+\\]", "\"backtrace\":[]")
.replaceAll("\"_id\":\"[^\"]+\"", "\"_id\":\"<ID>\"");
}

protected Logger getLogger() {
return TestLoggerFactory.getLogger(_loggerContext.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME));
}

private ActorSystem _actorSystem;
private LoggerContext _loggerContext;

static class SimpleActor extends AbstractActor {

@Override
public Receive createReceive() {
return receiveBuilder().build();
}
}
}
36 changes: 36 additions & 0 deletions src/test/java/com/arpnetworking/steno/TestLoggerFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2015 Groupon.com
*
* 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.steno;

/**
* Wraps a {@link org.slf4j.Logger} instance in a {@link com.arpnetwoprking.steno.Logger} instance.
*
* @author Ville Koskela (ville dot koskela at inscopemetrics dot io)
*/
public final class TestLoggerFactory {

/**
* Return a Steno {@link Logger} around a {@link org.slf4j.Logger} instance.
*
* @param logger The {@link org.slf4j.Logger} instance.
* @return Steno {@link Logger} instance.
*/
public static Logger getLogger(final org.slf4j.Logger logger) {
return new Logger(logger);
}

private TestLoggerFactory() {}
}
23 changes: 23 additions & 0 deletions src/test/java/com/arpnetworking/steno/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.
*/

@ParametersAreNonnullByDefault
@ReturnValuesAreNonnullByDefault
package com.arpnetworking.steno;

import com.arpnetworking.commons.javax.annotation.ReturnValuesAreNonnullByDefault;

import javax.annotation.ParametersAreNonnullByDefault;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"time":"<TIME>","name":"log","level":"info","data":{"message":"LoggingActorRefs","actorRef":"Actor[pekko://default/user/simple-actor]"},"context":{"host":"<HOST>","processId":"<PROCESS_ID>","threadId":"<THREAD_ID>","logger":"ROOT","line":"99","file":"PekkoLoggingModuleTest.java","class":"com.arpnetworking.metrics.mad.PekkoLoggingModuleTest"},"id":"<ID>","version":"0"}
Loading