Skip to content

Commit 5869395

Browse files
committed
Add UUID 'EVENT' option based on LogEvent hash
1 parent dc37687 commit 5869395

File tree

2 files changed

+76
-11
lines changed

2 files changed

+76
-11
lines changed

log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/UuidPatternConverter.java

+31-11
Original file line numberDiff line numberDiff line change
@@ -29,40 +29,60 @@
2929
@Plugin("UuidPatternConverter")
3030
@ConverterKeys({"u", "uuid"})
3131
public final class UuidPatternConverter extends LogEventPatternConverter {
32+
private enum UuidType {
33+
TIME,
34+
RANDOM,
35+
HASH
36+
}
3237

33-
private final boolean isRandom;
38+
private final UuidType uuidType;
3439

3540
/**
3641
* Private constructor.
3742
*/
38-
private UuidPatternConverter(final boolean isRandom) {
43+
private UuidPatternConverter(final UuidType uuidType) {
3944
super("u", "uuid");
40-
this.isRandom = isRandom;
45+
this.uuidType = uuidType;
4146
}
4247

4348
/**
44-
* Obtains an instance of SequencePatternConverter.
49+
* Obtains an instance of UuidPatternConverter.
4550
*
46-
* @param options options, currently ignored, may be null.
47-
* @return instance of SequencePatternConverter.
51+
* @param options options
52+
* @return instance of UuidPatternConverter.
4853
*/
4954
public static UuidPatternConverter newInstance(final String[] options) {
5055
if (options.length == 0) {
51-
return new UuidPatternConverter(false);
56+
return new UuidPatternConverter(UuidType.TIME);
5257
}
5358

54-
if (options.length > 1 || (!options[0].equalsIgnoreCase("RANDOM") && !options[0].equalsIgnoreCase("Time"))) {
55-
LOGGER.error("UUID Pattern Converter only accepts a single option with the value \"RANDOM\" or \"TIME\"");
59+
if (options.length == 1) {
60+
switch (options[0].toUpperCase()) {
61+
case "TIME":
62+
return new UuidPatternConverter(UuidType.TIME);
63+
case "RANDOM":
64+
return new UuidPatternConverter(UuidType.RANDOM);
65+
case "HASH":
66+
return new UuidPatternConverter(UuidType.HASH);
67+
}
5668
}
57-
return new UuidPatternConverter(options[0].equalsIgnoreCase("RANDOM"));
69+
70+
LOGGER.error(
71+
"UUID Pattern Converter only accepts a single option with the value \"TIME\" or \"RANDOM\" or \"HASH\"");
72+
return new UuidPatternConverter(UuidType.TIME);
5873
}
5974

6075
/**
6176
* {@inheritDoc}
6277
*/
6378
@Override
6479
public void format(final LogEvent event, final StringBuilder toAppendTo) {
65-
final UUID uuid = isRandom ? UUID.randomUUID() : UuidUtil.getTimeBasedUuid();
80+
final UUID uuid =
81+
switch (uuidType) {
82+
case TIME -> UuidUtil.getTimeBasedUuid();
83+
case RANDOM -> UUID.randomUUID();
84+
case HASH -> UuidUtil.getLogEventBasedUuid(event);
85+
};
6686
toAppendTo.append(uuid.toString());
6787
}
6888
}

log4j-core/src/main/java/org/apache/logging/log4j/core/util/UuidUtil.java

+45
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Random;
2222
import java.util.UUID;
2323
import java.util.concurrent.atomic.AtomicInteger;
24+
import org.apache.logging.log4j.core.LogEvent;
2425
import org.apache.logging.log4j.core.impl.CoreProperties.UuidProperties;
2526
import org.apache.logging.log4j.kit.env.PropertyEnvironment;
2627

@@ -50,6 +51,7 @@ public final class UuidUtil {
5051
private static final int HUNDRED_NANOS_PER_MILLI = 10000;
5152

5253
private static final long LEAST = initialize(NetUtils.getMacAddress());
54+
private static final long SALT = new SecureRandom().nextLong();
5355

5456
/* This class cannot be instantiated */
5557
private UuidUtil() {}
@@ -140,4 +142,47 @@ public static UUID getTimeBasedUuid() {
140142
final long most = timeLow | timeMid | TYPE1 | timeHi;
141143
return new UUID(most, LEAST);
142144
}
145+
146+
/**
147+
* Generates a Type 4 UUID based on the deterministic LogEvent hash.
148+
* Meant for generating consistent, correlatable UUID values across multiple Appenders for the same LogEvent.
149+
*
150+
* @param logEvent
151+
* @return universally unique identifiers (UUID)
152+
*/
153+
public static UUID getLogEventBasedUuid(LogEvent logEvent) {
154+
// TODO: better hashing algorithm - include other LogEvent fields?
155+
long epochSecond = logEvent.getInstant().getEpochSecond();
156+
// Enable 'log4j.configuration.usePreciseClock' system property otherwise will be truncated to millis
157+
long nanoOfSecond = logEvent.getInstant().getNanoOfSecond();
158+
// Thread IDs typically increment from 0 producing a narrow range
159+
long threadId = logEvent.getThreadId();
160+
161+
// Increase entropy
162+
long most = mix(epochSecond, nanoOfSecond, threadId);
163+
long least = mix(threadId, ~nanoOfSecond, epochSecond);
164+
// Set UUID v4 bits
165+
most &= 0xFFFFFFFFFFFF0FFFL;
166+
most |= 0x0000000000004000L;
167+
least &= 0x3FFFFFFFFFFFFFFFL;
168+
least |= 0x8000000000000000L;
169+
170+
return new UUID(most, least);
171+
}
172+
173+
private static long mix(long v1, long v2, long v3) {
174+
// XOR with large primes
175+
long hash = v1 * 0x9E3779B97F4A7C15L;
176+
hash ^= (v2 * 0xC6BC279692B5C323L);
177+
hash ^= (v3 * 0x3243F6A8885A308DL);
178+
// Scramble
179+
hash ^= (hash >>> 33);
180+
hash *= 0xff51afd7ed558ccdL;
181+
hash ^= (hash >>> 33);
182+
hash *= 0xc4ceb9fe1a85ec53L;
183+
hash ^= (hash >>> 33);
184+
// Add salt
185+
hash ^= SALT;
186+
return hash;
187+
}
143188
}

0 commit comments

Comments
 (0)