Skip to content

Commit 605dc1e

Browse files
committed
Lambda-style chaining
1 parent e9a7218 commit 605dc1e

File tree

18 files changed

+203
-81
lines changed

18 files changed

+203
-81
lines changed

README.md

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ ZeroLog (abbreviated to Zlg) is a logging façade with two fundamental design ob
1212
1. **Ultra-low overhead for suppressed logging.** In other words, the cost of calling a log method when logging for that level has been disabled is negligible.
1313
2. **Uncompromised code coverage.** Suppression of logging should not impact statement and branch coverage metrics. A log entry is a statement like any other.
1414

15-
Collectively, these objectives make Zlg suitable for use in ultra-high performance, low-latency applications and in high-assurance environments.
15+
Collectively, these goals make Zlg suitable for use in ultra-high performance, low-latency applications and in high-assurance environments.
1616

1717
# How fast is it?
1818
A JMH benchmark conducted on an [i7-4770 Haswell](https://ark.intel.com/products/75122/Intel-Core-i7-4770-Processor-8M-Cache-up-to-3_90-GHz) CPU with logging suppressed compares the per-invocation penalties for Zlg with some of the major loggers. Four primitives are passed to each logger for formatting, which is a fair representation of a typical log entry.
@@ -55,11 +55,13 @@ public final class SysOutLoggingSample {
5555
private static final Zlg zlg = Zlg.forClass(SysOutLoggingSample.class).get();
5656

5757
public static void open(String address, int port, double timeoutSeconds) {
58-
zlg.i("Connecting to %s:%d [timeout: %.1f sec]").arg(address).arg(port).arg(timeoutSeconds).log();
58+
zlg.i("Pi is %.2f", z -> z.arg(Math.PI));
59+
zlg.i("Connecting to %s:%d [timeout: %.1f sec]", z -> z.arg(address).arg(port).arg(timeoutSeconds));
60+
5961
try {
6062
openSocket(address, port, timeoutSeconds);
6163
} catch (IOException e) {
62-
zlg.w("Error connecting to %s:%d").arg(address).arg(port).tag("I/O").threw(e).log();
64+
zlg.w("Error connecting to %s:%d", z -> z.arg(address).arg(port).tag("I/O").threw(e));
6365
}
6466
}
6567
}
@@ -69,9 +71,61 @@ Some important things to note:
6971

7072
* A logger is a `Zlg` instance, created for a specific class (`forClass()`) or an arbitrary name (`forName()`). By convention, we name the field `zlg`.
7173
* Logging is invoked via a fluent chain, starting with the log level (abbreviated to the first letter) specifying a mandatory format string, followed by any optional arguments (primitives or object types), an optional tag, and an optional exception.
72-
* Each chain _must_ end with a `log()` for the log entry to be printed.
7374
* The format string is printf-style, unlike most other loggers that use the `{}` (stash) notation.
7475

76+
# Lazy args
77+
The chained args pattern works well when the values are _already_ available and can be fed to the logger as-is. If further work is needed to formulate the arguments, then log suppression will not prevent those expressions from being evaluated. For example, the following call will invoke the `size()` method on a `List` irrespective of whether logging is enabled or suppressed.
78+
79+
```java
80+
final List<Integer> numbers = Arrays.asList(5, 6, 7, 8);
81+
zlg.i("The list %s has %d elements", z -> z.arg(numbers).arg(numbers.size()).tag("list"));
82+
```
83+
84+
## Suppliers
85+
To avoid unnecessary argument evaluation, Zlg supports FP-style suppliers and transforms. The above example can be re-written, using a method reference to supply the primitive value via a getter method reference.
86+
87+
```java
88+
zlg.i("The list %s has %d elements", z -> z.arg(numbers).arg(numbers::size).tag("list"));
89+
```
90+
91+
By simply changing `list.size()` to `list::size` we avoid a potentially superfluous method call. Our recommendation is to always favour method references over lambda-style closures. This way _no new code is produced_ and there is no impact on code coverage.
92+
93+
## Transforms
94+
Often we won't have the luxury of invoking a single no-arg method on an object to obtain a nice, log-friendly representation. Zlg provides a convenient way of extracting a lazily-evaluated transform into a separate static method, taking a single argument — the object to transform.
95+
96+
In the next example, we are searching for a person's name from a list of people. If the name isn't found, we'd like to log the list's contents, but not reveal people's surnames. The transform in question is a static `tokeniseSurnames()` function, taking a collection of `Name` objects. To append the transform, we call the overloaded `arg(T value, Function<? super T, ?> transform)` method in the log chain, providing both the raw (untransformed) value and the transform method reference. The rest is Zlg's problem.
97+
98+
```java
99+
private static final Zlg zlg = Zlg.forClass(MethodHandles.lookup().lookupClass()).get();
100+
101+
public static final class Name {
102+
final String forename;
103+
final String surname;
104+
105+
Name(String forename, String surname) {
106+
this.forename = forename;
107+
this.surname = surname;
108+
}
109+
}
110+
111+
public static void logWithDescriber() {
112+
final List<Name> hackers = Arrays.asList(new Name("Kevin", "Flynn"),
113+
new Name("Thomas", "Anderson"),
114+
new Name("Angela", "Bennett"));
115+
final String surnameToFind = "Smith";
116+
117+
if (! hackers.stream().anyMatch(n -> n.surname.contains(surnameToFind))) {
118+
zlg.i("%s not found among %s", z -> z.arg(surnameToFind).arg(hackers, LazyLogSample::tokeniseSurnames));
119+
}
120+
}
121+
122+
public static List<String> tokeniseSurnames(Collection<Name> names) {
123+
return names.stream().map(n -> n.forename + " " + n.surname.replaceAll(".", "X")).collect(toList());
124+
}
125+
```
126+
127+
One thing to note about transforms and suppliers: they are code like any other and should be unit tested accordingly. You might have a buggy transform and, due to its lazy evaluation, fail to pick up on it when testing code that contains the log instruction (if logging was suppressed). Because transforms and suppliers are simple, single-responsibility 'pure' functions, unit testing them should be straightforward.
128+
75129
# Tags
76130
Zlg adds the concept of a _tag_ — an optional string value that can be used to decorate a log entry. A tag is equivalent to a marker in SLF4J, adding another dimension for slicing and dicing your log output.
77131

@@ -229,7 +283,7 @@ logger.debug(String.format("Connecting to %s:%d [timeout: %,.1f sec]", address,
229283

230284
There are two subtle problems with this approach. Firstly, `String.format()` will be unconditionally evaluated, irrespective of whether logging is enabled. This can be rather costly. Secondly, the penalty for getting the format specifiers wrong is severe — `format()` will throw an `IllegalFormatException`. The last thing you need when logging an error or a warning is to have the log call bail on you.
231285

232-
Zlg uses a safe form of `String.format()` (called `SafeFormat.format()`) which is tolerant to format errors, printing the description of the error along with the original format string and arguments. It looks like this:
286+
Zlg uses a safe form of `String.format()` (called `SafeFormat.format()`) which is tolerant of format errors, printing the description of the error along with the original format string and arguments. It looks like this:
233287

234288
```
235289
11:29:43.425 INF [main]: WARNING - could not format 'Pi is %d' with args [3.14]:
@@ -250,7 +304,9 @@ private static final Zlg zlg = Zlg.forClass(MethodHandles.lookup().lookupClass()
250304
private static final boolean TRACE_ENABLED = false;
251305

252306
public static void withStaticConstant(String address, int port, double timeoutSeconds) {
253-
if (TRACE_ENABLED) zlg.t("Connecting to %s:%d [timeout: %.1f sec]").arg(address).arg(port).arg(timeoutSeconds).log();
307+
if (TRACE_ENABLED) {
308+
zlg.t("Connecting to %s:%d [timeout: %.1f sec]", z -> z.arg(address).arg(port).arg(timeoutSeconds));
309+
}
254310
}
255311
```
256312

@@ -260,14 +316,16 @@ public static void withStaticConstant(String address, int port, double timeoutSe
260316
private static final Zlg zlg = Zlg.forClass(MethodHandles.lookup().lookupClass()).get();
261317

262318
public static void withAssert(String address, int port, double timeoutSeconds) {
263-
assert zlg.t("Connecting to %s:%d [timeout: %.1f sec]").arg(address).arg(port).arg(timeoutSeconds).logb();
319+
assert zlg.t("Connecting to %s:%d [timeout: %.1f sec]").arg(address).arg(port).arg(timeoutSeconds).log();
264320
}
265321
```
266322

267-
**Note:** Rather than using `log()`, the assertion example uses `logb()`, which works identically to `log()` but returns a constant `true`. If assertions are enabled with the `-ea` JVM argument, the log instruction will be evaluated and will never fail the assertion. Otherwise, the entire fluent chain will be dropped by DCE.
323+
**Note:** Rather than chaining arguments within a lambda, the assertion example uses a continuous chaining style, culminating with a call to `log()`, which returns a constant `true`. If assertions are enabled with the `-ea` JVM argument, the log instruction will be evaluated and will never fail the assertion. Otherwise, the entire fluent chain will be dropped by DCE.
268324

269325
The choice of using option one or two depends on whether you are targeting zero overhead for both production and testing scenarios or only for production. In case of the latter, the `-ea` flag naturally solves the problem, without forcing you to change your class before building. In either case, you will sacrifice code coverage, as both techniques introduce a parasitic branching instruction behind the scenes; only one path is traversed during the test.
270326

327+
**Note:** Outside of the `assert` example, using of the continuous chaining style is strongly discouraged. You run the risk of forgetting to append the final `log()` at the end of the chain, which will have the effect of 'swallowing' the log without forwarding the log event to the underlying logger.
328+
271329
## Can Zlg be mocked?
272330
Zlg's design is heavily interface-driven, to simplify mocking and testing, which in itself allows us to maintain Zlg with 100% instruction and branch coverage. Even with interfaces, using mocking frameworks (like Mockito) didn't feel like a natural fit for the fluent-style chaining — there are too many methods to mock and verification needs to be depth-aware. (That's probably the only practical drawback of fluent chaining.)
273331

@@ -278,19 +336,22 @@ final MockLogTarget target = new MockLogTarget();
278336
final Zlg zlg = target.logger();
279337

280338
// do some logging...
281-
zlg.t("Pi is %.2f").arg(Math.PI).tag("math").log();
282-
zlg.d("Euler's number is %.2f").arg(Math.E).tag("math").log();
283-
zlg.c("Avogadro constant is %.3e").arg(6.02214086e23).tag("chemistry").log();
284-
zlg.w("An I/O error has occurred").threw(new FileNotFoundException()).log();
339+
zlg.t("Pi is %.2f", z -> z.arg(Math.PI).tag("math"));
340+
zlg.d("Euler's number is %.2f", z -> z.arg(Math.E).tag("math"));
341+
zlg.c("Avogadro constant is %.3e", z -> z.arg(6.02214086e23).tag("chemistry"));
342+
zlg.w("An I/O error has occurred", z -> z.threw(new FileNotFoundException()));
285343

286344
// find entries tagged with 'math'
287345
final List<Entry> math = target.entries().tagged("math").list();
346+
System.out.println(math);
288347

289348
// find entries at or above debug
290349
final List<Entry> debugAndAbove = target.entries().forLevelAndAbove(LogLevel.DEBUG).list();
350+
System.out.println(debugAndAbove);
291351

292352
// find entries containing an IOException (or subclass thereof)
293353
final List<Entry> withException = target.entries().withException(IOException.class).list();
354+
System.out.println(withException);
294355
```
295356

296357
**Note:** If all you need is a no-op logger to quiesce any potential output, and don't care about mocking the fluent call chain, you can instantiate the logger with `LogLevel.OFF`:
@@ -309,9 +370,9 @@ when(logChain.format(any())).thenReturn(logChain);
309370
when(logChain.arg(anyDouble())).thenReturn(logChain);
310371
when(zlg.level(anyInt())).thenReturn(logChain);
311372

312-
zlg.t("the value of Pi is %.2f").arg(Math.PI).log();
373+
zlg.t("the value of Pi is %.2f", z -> z.arg(Math.PI));
313374

314375
verify(logChain).format(contains("the value of Pi"));
315376
verify(logChain).arg(eq(Math.PI));
316-
verify(logChain).log();
377+
verify(logChain).done();
317378
```

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ apply plugin: "maven-publish"
55
apply plugin: "com.jfrog.bintray"
66

77
group = "com.obsidiandynamics.zerolog"
8-
version = "0.6.0-SNAPSHOT"
8+
version = "0.6.0"
99

1010
def envUser = "BINTRAY_USER"
1111
def envKey = "BINTRAY_KEY"

slf4j17/src/test/java/com/obsidiandynamics/zerolog/sample/Slf4jLoggingSample.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public final class Slf4jLoggingSample {
99
private static final Zlg zlg = Zlg.forClass(MethodHandles.lookup().lookupClass()).get();
1010

1111
public static void main(String[] args) {
12-
zlg.i("Starting with %d args: %s").arg(args.length).arg(Arrays.asList(args)).log();
13-
zlg.w("An error occurred at %s").arg(new Date()).threw(new RuntimeException()).tag("I/O").log();
12+
zlg.i("Starting with %d args: %s").arg(args.length).arg(Arrays.asList(args)).done();
13+
zlg.w("An error occurred at %s").arg(new Date()).threw(new RuntimeException()).tag("I/O").done();
1414
}
1515
}

src/main/java/com/obsidiandynamics/zerolog/NopLogChain.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,5 +102,5 @@ public LogChain threw(Throwable throwable) {
102102
}
103103

104104
@Override
105-
public void log() {}
105+
public void done() {}
106106
}

src/main/java/com/obsidiandynamics/zerolog/Zlg.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ interface LogChain {
4444

4545
default void with(Consumer<LogChain> logChainConsumer) {
4646
logChainConsumer.accept(this);
47-
log();
47+
done();
4848
}
4949

50-
void log();
50+
void done();
5151

52-
default boolean logb() {
53-
log();
52+
default boolean log() {
53+
done();
5454
return true;
5555
}
5656
}
@@ -61,16 +61,40 @@ default boolean logb() {
6161

6262
default LogChain e(String format) { return level(LogLevel.ERROR).format(format); }
6363

64+
default void e(String format, Consumer<LogChain> logChainConsumer) {
65+
level(LogLevel.ERROR).format(format).with(logChainConsumer);
66+
}
67+
6468
default LogChain w(String format) { return level(LogLevel.WARN).format(format); }
6569

70+
default void w(String format, Consumer<LogChain> logChainConsumer) {
71+
level(LogLevel.WARN).format(format).with(logChainConsumer);
72+
}
73+
6674
default LogChain i(String format) { return level(LogLevel.INFO).format(format); }
6775

76+
default void i(String format, Consumer<LogChain> logChainConsumer) {
77+
level(LogLevel.INFO).format(format).with(logChainConsumer);
78+
}
79+
6880
default LogChain c(String format) { return level(LogLevel.CONF).format(format); }
6981

82+
default void c(String format, Consumer<LogChain> logChainConsumer) {
83+
level(LogLevel.CONF).format(format).with(logChainConsumer);
84+
}
85+
7086
default LogChain d(String format) { return level(LogLevel.DEBUG).format(format); }
7187

88+
default void d(String format, Consumer<LogChain> logChainConsumer) {
89+
level(LogLevel.DEBUG).format(format).with(logChainConsumer);
90+
}
91+
7292
default LogChain t(String format) { return level(LogLevel.TRACE).format(format); }
7393

94+
default void t(String format, Consumer<LogChain> logChainConsumer) {
95+
level(LogLevel.TRACE).format(format).with(logChainConsumer);
96+
}
97+
7498
static ZlgBuilder forName(String name) {
7599
return new ZlgBuilder(name);
76100
}

src/main/java/com/obsidiandynamics/zerolog/ZlgImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public LogChain threw(Throwable throwable) {
139139
}
140140

141141
@Override
142-
public void log() {
142+
public void done() {
143143
if (format == null) throw new MissingValueException("Missing call to format()");
144144
target.log(level, tag, format, argc, argv, throwable);
145145
reset();

src/test/java/com/obsidiandynamics/zerolog/MockLogTargetTest.java

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public void testEntryToString() {
1616
final MockLogTarget target = new MockLogTarget(LogLevel.TRACE);
1717
final Zlg zlg = target.logger();
1818

19-
zlg.t("message").log();
19+
zlg.t("message").done();
2020
final List<Entry> entries = target.entries().list();
2121
assertNotNull(entries);
2222
assertEquals(1, entries.size());
@@ -45,7 +45,7 @@ public void testEntryRetention() {
4545
final String format = "entry #%d";
4646
final Exception cause = new Exception("simulated");
4747
for (int i = 0; i < numEntries; i++) {
48-
zlg.level(logLevel).format(format).arg(i).tag(String.valueOf(i)).threw(cause).log();
48+
zlg.level(logLevel).format(format).arg(i).tag(String.valueOf(i)).threw(cause).done();
4949
}
5050

5151
final List<Entry> entries = target.entries().list();
@@ -67,7 +67,7 @@ public void testEntryRetention() {
6767
public void testEntriesIterator() {
6868
final MockLogTarget target = new MockLogTarget();
6969
final Zlg zlg = target.logger();
70-
zlg.t("message").log();
70+
zlg.t("message").done();
7171

7272
final Iterator<Entry> it = target.entries().iterator();
7373
assertTrue(it.hasNext());
@@ -79,9 +79,9 @@ public void testEntriesIterator() {
7979
public void testForLevel() {
8080
final MockLogTarget target = new MockLogTarget();
8181
final Zlg zlg = target.logger();
82-
zlg.t("trace").log();
83-
zlg.d("debug").log();
84-
zlg.c("conf").log();
82+
zlg.t("trace").done();
83+
zlg.d("debug").done();
84+
zlg.c("conf").done();
8585

8686
final List<Entry> entries = target.entries().forLevel(LogLevel.DEBUG).list();
8787
assertEquals(1, entries.size());
@@ -92,9 +92,9 @@ public void testForLevel() {
9292
public void testForLevelAndAbove() {
9393
final MockLogTarget target = new MockLogTarget();
9494
final Zlg zlg = target.logger();
95-
zlg.t("trace").log();
96-
zlg.d("debug").log();
97-
zlg.c("conf").log();
95+
zlg.t("trace").done();
96+
zlg.d("debug").done();
97+
zlg.c("conf").done();
9898

9999
final List<Entry> entries = target.entries().forLevelAndAbove(LogLevel.DEBUG).list();
100100
assertEquals(2, entries.size());
@@ -107,9 +107,9 @@ public void testBeforeAfter() {
107107
final long startTime = System.currentTimeMillis();
108108
final MockLogTarget target = new MockLogTarget();
109109
final Zlg zlg = target.logger();
110-
zlg.t("trace").log();
111-
zlg.d("debug").log();
112-
zlg.c("conf").log();
110+
zlg.t("trace").done();
111+
zlg.d("debug").done();
112+
zlg.c("conf").done();
113113
final long endTime = System.currentTimeMillis();
114114

115115
assertEquals(3, target.entries().after(startTime - 1).list().size());
@@ -123,9 +123,9 @@ public void testBeforeAfter() {
123123
public void testTag() {
124124
final MockLogTarget target = new MockLogTarget();
125125
final Zlg zlg = target.logger();
126-
zlg.t("trace").log();
127-
zlg.d("debug").tag("tag").log();
128-
zlg.c("conf").log();
126+
zlg.t("trace").done();
127+
zlg.d("debug").tag("tag").done();
128+
zlg.c("conf").done();
129129

130130
assertEquals(1, target.entries().tagged("tag").list().size());
131131
}
@@ -134,9 +134,9 @@ public void testTag() {
134134
public void testWithException() {
135135
final MockLogTarget target = new MockLogTarget();
136136
final Zlg zlg = target.logger();
137-
zlg.t("trace").log();
138-
zlg.d("debug").threw(new IOException("simulated")).log();
139-
zlg.c("conf").log();
137+
zlg.t("trace").done();
138+
zlg.d("debug").threw(new IOException("simulated")).done();
139+
zlg.c("conf").done();
140140

141141
assertEquals(0, target.entries().withException(RuntimeException.class).list().size());
142142
assertEquals(1, target.entries().withException(IOException.class).list().size());
@@ -146,9 +146,9 @@ public void testWithException() {
146146
public void testContaining() {
147147
final MockLogTarget target = new MockLogTarget();
148148
final Zlg zlg = target.logger();
149-
zlg.t("trace").log();
150-
zlg.d("debug").log();
151-
zlg.c("conf").log();
149+
zlg.t("trace").done();
150+
zlg.d("debug").done();
151+
zlg.c("conf").done();
152152

153153
assertEquals(1, target.entries().containing("bug").list().size());
154154
}
@@ -157,9 +157,9 @@ public void testContaining() {
157157
public void testReset() {
158158
final MockLogTarget target = new MockLogTarget();
159159
final Zlg zlg = target.logger();
160-
zlg.t("trace").log();
161-
zlg.d("debug").log();
162-
zlg.c("conf").log();
160+
zlg.t("trace").done();
161+
zlg.d("debug").done();
162+
zlg.c("conf").done();
163163

164164
assertEquals(3, target.entries().list().size());
165165

src/test/java/com/obsidiandynamics/zerolog/NopLogChainTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public void testFluentChain() {
2929
.threw(null);
3030
assertSame(chain, end);
3131

32-
end.log(); // does nothing
32+
end.done(); // does nothing
3333
}
3434

3535
@Test
@@ -52,7 +52,7 @@ public void testLazy() {
5252
.arg("test", transform);
5353
assertSame(chain, end);
5454

55-
end.log(); // does nothing
55+
end.done(); // does nothing
5656

5757
verifyNoMoreInteractions(booleanSupplier);
5858
verifyNoMoreInteractions(doubleSupplier);

0 commit comments

Comments
 (0)