Skip to content

Commit bf5cdc0

Browse files
authored
Pekko logging (#543)
* feat: Add Pekko ActorContext serialization to structured logs
1 parent 4d7d4bd commit bf5cdc0

File tree

15 files changed

+363
-7
lines changed

15 files changed

+363
-7
lines changed

config/logback-console.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<jacksonModule class="com.fasterxml.jackson.datatype.guava.GuavaModule" />
2323
<jacksonModule class="com.fasterxml.jackson.datatype.jdk8.Jdk8Module" />
2424
<jacksonModule class="com.fasterxml.jackson.datatype.jsr310.JavaTimeModule" />
25+
<jacksonModule class="com.arpnetworking.configuration.jackson.module.pekko.PekkoLoggingModule" />
2526
<injectBeanIdentifier>true</injectBeanIdentifier>
2627
</encoder>
2728
</appender>

config/logback.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<jacksonModule class="com.fasterxml.jackson.datatype.guava.GuavaModule" />
3333
<jacksonModule class="com.fasterxml.jackson.datatype.jdk8.Jdk8Module" />
3434
<jacksonModule class="com.fasterxml.jackson.datatype.jsr310.JavaTimeModule" />
35+
<jacksonModule class="com.arpnetworking.configuration.jackson.module.pekko.PekkoLoggingModule" />
3536
<injectBeanIdentifier>true</injectBeanIdentifier>
3637
</encoder>
3738
</appender>

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@
528528
<configuration>
529529
<ignoredDependencies combine.children="append">
530530
<ignoredDependency>io.netty:netty-common:jar:*</ignoredDependency>
531+
<ignoredDependency>ch.qos.logback:logback-core:jar:*</ignoredDependency>
531532
</ignoredDependencies>
532533
</configuration>
533534
</plugin>
@@ -546,6 +547,11 @@
546547
<artifactId>logback-classic</artifactId>
547548
<version>${logback.version}</version>
548549
</dependency>
550+
<dependency>
551+
<groupId>ch.qos.logback</groupId>
552+
<artifactId>logback-core</artifactId>
553+
<version>${logback.version}</version>
554+
</dependency>
549555
<dependency>
550556
<groupId>com.arpnetworking.logback</groupId>
551557
<artifactId>logback-steno</artifactId>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2025 Brandon Arp
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+
package com.arpnetworking.configuration.jackson.module.pekko;
17+
18+
import com.fasterxml.jackson.core.JsonGenerator;
19+
import com.fasterxml.jackson.databind.JsonSerializer;
20+
import com.fasterxml.jackson.databind.SerializerProvider;
21+
import org.apache.pekko.actor.ActorContext;
22+
23+
import java.io.IOException;
24+
25+
/**
26+
* Serializer for a Pekko timer message.
27+
*
28+
* @author Brandon Arp (brandon dot arp at inscopemetrics dot com)
29+
*/
30+
public final class ActorContextSerializer extends JsonSerializer<ActorContext> {
31+
@Override
32+
public void serialize(
33+
final ActorContext context,
34+
final JsonGenerator jsonGenerator,
35+
final SerializerProvider serializerProvider)
36+
throws IOException {
37+
jsonGenerator.writeStartObject();
38+
jsonGenerator.writeObjectField("type", context.getClass().getTypeName());
39+
jsonGenerator.writeObjectField("self", context.self());
40+
jsonGenerator.writeObjectField("sender", context.sender());
41+
jsonGenerator.writeEndObject();
42+
}
43+
}

src/main/java/com/arpnetworking/configuration/jackson/module/pekko/PekkoLoggingModule.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.arpnetworking.configuration.jackson.module.pekko;
1717

1818
import com.fasterxml.jackson.databind.module.SimpleModule;
19+
import org.apache.pekko.actor.ActorContext;
1920
import org.apache.pekko.actor.ActorRef;
2021
import org.apache.pekko.actor.LocalActorRef;
2122
import org.apache.pekko.actor.TimerSchedulerImpl;
@@ -37,12 +38,14 @@ public PekkoLoggingModule() { }
3738

3839
@Override
3940
public void setupModule(final SetupContext context) {
40-
addSerializer(ActorRef.class, new ActorRefLoggingSerializer());
41-
addSerializer(LocalActorRef.class, new ActorRefLoggingSerializer());
42-
addSerializer(TimerSchedulerImpl.TimerMsg.class, new TimerMessageSerializer());
43-
addSerializer(TimerSchedulerImpl.InfluenceReceiveTimeoutTimerMsg.class, new TimerMessageSerializer());
44-
addSerializer(TimerSchedulerImpl.NotInfluenceReceiveTimeoutTimerMsg.class, new TimerMessageSerializer());
45-
super.setupModule(context);
41+
addSerializer(ActorRef.class, new ActorRefLoggingSerializer());
42+
addSerializer(LocalActorRef.class, new ActorRefLoggingSerializer());
43+
addSerializer(TimerSchedulerImpl.TimerMsg.class, new TimerMessageSerializer());
44+
addSerializer(TimerSchedulerImpl.InfluenceReceiveTimeoutTimerMsg.class, new TimerMessageSerializer());
45+
addSerializer(TimerSchedulerImpl.NotInfluenceReceiveTimeoutTimerMsg.class, new TimerMessageSerializer());
46+
addSerializer(TimerSchedulerImpl.class, new TimerSchedulerSerializer());
47+
addSerializer(ActorContext.class, new ActorContextSerializer());
48+
super.setupModule(context);
4649
}
4750

4851
@Serial
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2025 Brandon Arp
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+
package com.arpnetworking.configuration.jackson.module.pekko;
17+
18+
import com.fasterxml.jackson.core.JsonGenerator;
19+
import com.fasterxml.jackson.databind.JsonSerializer;
20+
import com.fasterxml.jackson.databind.SerializerProvider;
21+
import org.apache.pekko.actor.TimerSchedulerImpl;
22+
23+
import java.io.IOException;
24+
import java.io.InvalidObjectException;
25+
import java.lang.reflect.Field;
26+
27+
/**
28+
* Serializer for a Pekko timer message.
29+
*
30+
* @author Brandon Arp (brandon dot arp at inscopemetrics dot com)
31+
*/
32+
public final class TimerSchedulerSerializer extends JsonSerializer<TimerSchedulerImpl> {
33+
@Override
34+
public void serialize(
35+
final TimerSchedulerImpl timerScheduler,
36+
final JsonGenerator jsonGenerator,
37+
final SerializerProvider serializerProvider)
38+
throws IOException {
39+
jsonGenerator.writeStartObject();
40+
jsonGenerator.writeObjectField("type", timerScheduler.getClass().getTypeName());
41+
try {
42+
jsonGenerator.writeObjectField("key", CTX.get(timerScheduler));
43+
} catch (final IllegalAccessException e) {
44+
throw new InvalidObjectException("Unable to access context field");
45+
}
46+
jsonGenerator.writeEndObject();
47+
}
48+
private static final Field CTX;
49+
static {
50+
try {
51+
CTX = TimerSchedulerImpl.class.getDeclaredField("ctx");
52+
CTX.setAccessible(true);
53+
} catch (final NoSuchFieldException e) {
54+
throw new RuntimeException(e);
55+
}
56+
}
57+
}

src/main/java/com/arpnetworking/metrics/mad/PeriodWorker.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ private void checkForIdle() {
167167

168168
private void shutdown(final Aggregator.PeriodWorkerShutdown shutdown) {
169169
timers().cancelAll();
170-
self().tell(PoisonPill.getInstance(), self());
170+
timers().startSingleTimer("SELF_SHUTDOWN", PoisonPill.getInstance(), Duration.ofSeconds(5));
171171
}
172172

173173
private void scheduleRotation(final ZonedDateTime now) {

src/main/resources/logback.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<layout class="ch.qos.logback.classic.PatternLayout">
3131
<pattern>%date %t [%level] %logger : %message %ex%n</pattern>
3232
</layout>
33+
<jacksonModule class="com.arpnetworking.configuration.jackson.module.pekko.PekkoLoggingModule" />
3334
</encoder>
3435
</appender>
3536

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright 2025 Brandon Arp
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+
package com.arpnetworking.metrics.mad;
17+
18+
import ch.qos.logback.classic.LoggerContext;
19+
import ch.qos.logback.classic.joran.JoranConfigurator;
20+
import ch.qos.logback.classic.util.LogbackMDCAdapter;
21+
import ch.qos.logback.core.joran.spi.JoranException;
22+
import com.arpnetworking.steno.Logger;
23+
import com.arpnetworking.steno.TestLoggerFactory;
24+
import org.apache.pekko.actor.AbstractActor;
25+
import org.apache.pekko.actor.ActorRef;
26+
import org.apache.pekko.actor.ActorSystem;
27+
import org.apache.pekko.actor.Props;
28+
import org.apache.pekko.actor.Terminated;
29+
import org.junit.After;
30+
import org.junit.Assert;
31+
import org.junit.Before;
32+
import org.junit.Test;
33+
34+
import java.io.File;
35+
import java.io.IOException;
36+
import java.net.URISyntaxException;
37+
import java.net.URL;
38+
import java.nio.file.Files;
39+
import java.nio.file.Paths;
40+
import java.util.concurrent.CompletionStage;
41+
import java.util.concurrent.TimeUnit;
42+
43+
/**
44+
* Unit test class for verifying the functionality and integration of the AkkaLoggingModule.
45+
* This class tests various aspects of logging and actor-based communication within an Akka-based
46+
* system using Logback as the logging backend.
47+
*
48+
* @author Brandon Arp (brandon dot arp at inscopemetrics dot io)
49+
*/
50+
public class PekkoLoggingModuleTest {
51+
/**
52+
* Sets up the test environment by initializing the required components.
53+
* This method is executed before each test, ensuring a fresh setup.
54+
*
55+
* The setup process includes the following:
56+
* - Creation of an Akka {@link ActorSystem} instance.
57+
* - Loading the specific XML configuration file corresponding to the test class.
58+
* - Initializing and configuring a Logback {@link LoggerContext}.
59+
* - Setting up the {@link JoranConfigurator} to parse the configuration file.
60+
* - Resetting and associating a {@link LogbackMDCAdapter} instance with the LoggerContext.
61+
*
62+
* If an error occurs during the configuration process using the JoranConfigurator,
63+
* a {@link RuntimeException} is thrown, encapsulating the underlying {@link JoranException}.
64+
*
65+
* The initialized {@link ActorSystem} and {@link LoggerContext} serve as the foundation for
66+
* executing tests that involve logging and actor-based messaging.
67+
*/
68+
@Before
69+
public void setUp() {
70+
_actorSystem = ActorSystem.create();
71+
final URL configuration = getClass().getResource(
72+
this.getClass().getSimpleName() + ".xml");
73+
_loggerContext = new LoggerContext();
74+
final JoranConfigurator configurator = new JoranConfigurator();
75+
configurator.setContext(_loggerContext);
76+
_loggerContext.reset();
77+
final LogbackMDCAdapter mdcAdapter = new LogbackMDCAdapter();
78+
_loggerContext.setMDCAdapter(mdcAdapter);
79+
try {
80+
configurator.doConfigure(configuration);
81+
} catch (final JoranException e) {
82+
throw new RuntimeException(e);
83+
}
84+
}
85+
86+
@After
87+
public void tearDown() throws Exception {
88+
final CompletionStage<Terminated> terminated = _actorSystem.getWhenTerminated();
89+
_actorSystem.terminate();
90+
terminated.toCompletableFuture().get(10, TimeUnit.SECONDS);
91+
}
92+
93+
@Test
94+
public void testLoggingActorRefs() {
95+
final ActorRef actorRef = _actorSystem.actorOf(Props.create(SimpleActor.class, SimpleActor::new), "simple-actor");
96+
getLogger().info()
97+
.setMessage("LoggingActorRefs")
98+
.addData("actorRef", actorRef)
99+
.log();
100+
assertOutput();
101+
}
102+
103+
protected void assertOutput() {
104+
final URL expectedResource = getClass().getResource(
105+
getClass().getSimpleName() + ".expected");
106+
final File actualFile = new File("target/test-logs/" + this.getClass().getSimpleName() + ".log");
107+
final String actualOutput;
108+
try {
109+
actualOutput = Files.readString(actualFile.toPath());
110+
} catch (final IOException e) {
111+
throw new RuntimeException(e);
112+
}
113+
try {
114+
assertOutput(Files.readString(Paths.get(expectedResource.toURI())), actualOutput);
115+
} catch (final IOException | URISyntaxException e) {
116+
Assert.fail("Failed with exception: " + e);
117+
}
118+
}
119+
120+
protected void assertOutput(final String expected, final String actual) {
121+
Assert.assertEquals(expected.trim(), sanitizeOutput(actual.trim()));
122+
}
123+
124+
protected String sanitizeOutput(final String output) {
125+
return output.replaceAll("\"time\":\"[^\"]+\"", "\"time\":\"<TIME>\"")
126+
.replaceAll("\"id\":\"[^\"]+\"", "\"id\":\"<ID>\"")
127+
.replaceAll("Actor\\[pekko://default/user/simple-actor[^\"]+]\"", "Actor[pekko://default/user/simple-actor]\"")
128+
.replaceAll("\"host\":\"[^\"]+\"", "\"host\":\"<HOST>\"")
129+
.replaceAll("\"processId\":\"[^\"]+\"", "\"processId\":\"<PROCESS_ID>\"")
130+
.replaceAll("\"threadId\":\"[^\"]+\"", "\"threadId\":\"<THREAD_ID>\"")
131+
.replaceAll("\"backtrace\":\\[[^\\]]+\\]", "\"backtrace\":[]")
132+
.replaceAll("\"_id\":\"[^\"]+\"", "\"_id\":\"<ID>\"");
133+
}
134+
135+
protected Logger getLogger() {
136+
return TestLoggerFactory.getLogger(_loggerContext.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME));
137+
}
138+
139+
private ActorSystem _actorSystem;
140+
private LoggerContext _loggerContext;
141+
142+
static class SimpleActor extends AbstractActor {
143+
144+
@Override
145+
public Receive createReceive() {
146+
return receiveBuilder().build();
147+
}
148+
}
149+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2015 Groupon.com
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+
package com.arpnetworking.steno;
17+
18+
/**
19+
* Wraps a {@link org.slf4j.Logger} instance in a {@link com.arpnetwoprking.steno.Logger} instance.
20+
*
21+
* @author Ville Koskela (ville dot koskela at inscopemetrics dot io)
22+
*/
23+
public final class TestLoggerFactory {
24+
25+
/**
26+
* Return a Steno {@link Logger} around a {@link org.slf4j.Logger} instance.
27+
*
28+
* @param logger The {@link org.slf4j.Logger} instance.
29+
* @return Steno {@link Logger} instance.
30+
*/
31+
public static Logger getLogger(final org.slf4j.Logger logger) {
32+
return new Logger(logger);
33+
}
34+
35+
private TestLoggerFactory() {}
36+
}

0 commit comments

Comments
 (0)