-
Notifications
You must be signed in to change notification settings - Fork 76
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Format timestamp as ISO-8601 string - first three properties are `@timestamp`, `log.level` and `message` - Add padding for `log.level`, so that `message`s align
- Loading branch information
1 parent
717720c
commit c6068fb
Showing
8 changed files
with
208 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
113 changes: 113 additions & 0 deletions
113
ecs-logging-core/src/main/java/co/elastic/logging/TimestampSerializer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package co.elastic.logging; | ||
|
||
import java.text.SimpleDateFormat; | ||
import java.util.Date; | ||
import java.util.TimeZone; | ||
|
||
/** | ||
* This class serializes an epoch timestamp in milliseconds to a ISO 8601 date time sting, | ||
* for example {@code 1970-01-01T00:00:00.000Z} | ||
* <p> | ||
* The main advantage of this class is that is able to serialize the timestamp in a garbage free way, | ||
* i.e. without object allocations and that it is faster than {@link java.text.DateFormat#format(Date)}. | ||
* </p> | ||
* <p> | ||
* The most complex part when formatting a ISO date is to determine the actual year, | ||
* month and date as you have to account for leap years. | ||
* Leveraging the fact that for a whole day this stays the same | ||
* and that logging only requires to serialize the current timestamp and not arbitrary ones, | ||
* we offload this task to {@link java.text.DateFormat#format(Date)} and cache the result. | ||
* So we only have to serialize the time part of the ISO timestamp which is easy | ||
* as a day has exactly {@code 1000 * 60 * 60 * 24} milliseconds. | ||
* Also, we don't have to worry about leap seconds when dealing with the epoch timestamp. | ||
* </p> | ||
* <p> | ||
* This class is thread safe. | ||
* </p> | ||
*/ | ||
class TimestampSerializer { | ||
|
||
private static final long MILLIS_PER_SECOND = 1000; | ||
private static final long MILLIS_PER_MINUTE = MILLIS_PER_SECOND * 60; | ||
private static final long MILLIS_PER_HOUR = MILLIS_PER_MINUTE * 60; | ||
private static final long MILLIS_PER_DAY = MILLIS_PER_HOUR * 24; | ||
private static final char TIME_SEPARATOR = 'T'; | ||
private static final char TIME_ZONE_SEPARATOR = 'Z'; | ||
private static final char COLON = ':'; | ||
private static final char DOT = '.'; | ||
private static final char ZERO = '0'; | ||
|
||
private volatile CachedDate cachedDate = new CachedDate(System.currentTimeMillis()); | ||
|
||
void serializeEpochTimestampAsIsoDateTime(StringBuilder builder, long epochTimestamp) { | ||
CachedDate cachedDateLocal = cachedDate; | ||
if (cachedDateLocal == null || !cachedDateLocal.isDateCached(epochTimestamp)) { | ||
cachedDate = cachedDateLocal = new CachedDate(epochTimestamp); | ||
} | ||
builder.append(cachedDateLocal.getCachedDateIso()); | ||
|
||
builder.append(TIME_SEPARATOR); | ||
|
||
// hours | ||
long remainder = epochTimestamp % MILLIS_PER_DAY; | ||
serializeWithLeadingZero(builder, remainder / MILLIS_PER_HOUR, 2); | ||
builder.append(COLON); | ||
|
||
// minutes | ||
remainder %= MILLIS_PER_HOUR; | ||
serializeWithLeadingZero(builder, remainder / MILLIS_PER_MINUTE, 2); | ||
builder.append(COLON); | ||
|
||
// seconds | ||
remainder %= MILLIS_PER_MINUTE; | ||
serializeWithLeadingZero(builder, remainder / MILLIS_PER_SECOND, 2); | ||
builder.append(DOT); | ||
|
||
// milliseconds | ||
remainder %= MILLIS_PER_SECOND; | ||
serializeWithLeadingZero(builder, remainder, 3); | ||
|
||
builder.append(TIME_ZONE_SEPARATOR); | ||
} | ||
|
||
private void serializeWithLeadingZero(StringBuilder builder, long value, int minLength) { | ||
for (int i = minLength - 1; i > 0; i--) { | ||
if (value < Math.pow(10, i)) { | ||
builder.append(ZERO); | ||
} | ||
} | ||
builder.append(value); | ||
} | ||
|
||
private static class CachedDate { | ||
private final String cachedDateIso; | ||
private final long startOfCachedDate; | ||
private final long endOfCachedDate; | ||
|
||
private CachedDate(long epochTimestamp) { | ||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); | ||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); | ||
cachedDateIso = dateFormat.format(new Date(epochTimestamp)); | ||
startOfCachedDate = atStartOfDay(epochTimestamp); | ||
endOfCachedDate = atEndOfDay(epochTimestamp); | ||
} | ||
|
||
private static long atStartOfDay(long epochTimestamp) { | ||
return epochTimestamp - epochTimestamp % MILLIS_PER_DAY; | ||
} | ||
|
||
private static long atEndOfDay(long epochTimestamp) { | ||
return atStartOfDay(epochTimestamp) + MILLIS_PER_DAY - 1; | ||
} | ||
|
||
private boolean isDateCached(long epochTimestamp) { | ||
return epochTimestamp >= startOfCachedDate && epochTimestamp <= endOfCachedDate; | ||
} | ||
|
||
public String getCachedDateIso() { | ||
return cachedDateIso; | ||
} | ||
} | ||
|
||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
59 changes: 59 additions & 0 deletions
59
ecs-logging-core/src/test/java/co/elastic/logging/TimestampSerializerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package co.elastic.logging; | ||
|
||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import java.time.Instant; | ||
import java.time.LocalDateTime; | ||
import java.time.ZoneId; | ||
import java.time.ZoneOffset; | ||
import java.time.format.DateTimeFormatter; | ||
import java.time.temporal.ChronoUnit; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
class TimestampSerializerTest { | ||
|
||
private TimestampSerializer dateSerializer; | ||
private DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneId.of("UTC")); | ||
|
||
|
||
@BeforeEach | ||
void setUp() { | ||
dateSerializer = new TimestampSerializer(); | ||
} | ||
|
||
@Test | ||
void testSerializeEpochTimestampAsIsoDateTime() { | ||
long timestamp = 0; | ||
long lastTimestampToCheck = LocalDateTime.now() | ||
.plus(1, ChronoUnit.YEARS) | ||
.toInstant(ZoneOffset.UTC) | ||
.toEpochMilli(); | ||
// interval is approximately a hour but not exactly | ||
// to get different values for the minutes, seconds and milliseconds | ||
long interval = 997 * 61 * 61; | ||
for (; timestamp <= lastTimestampToCheck; timestamp += interval) { | ||
assertDateFormattingIsCorrect(Instant.ofEpochMilli(timestamp)); | ||
} | ||
StringBuilder builder = new StringBuilder(); | ||
dateSerializer.serializeEpochTimestampAsIsoDateTime(builder, 1565093352375L); | ||
System.out.println(builder); | ||
builder.setLength(0); | ||
dateSerializer.serializeEpochTimestampAsIsoDateTime(builder, 1565093352379L); | ||
System.out.println(builder); | ||
builder.setLength(0); | ||
dateSerializer.serializeEpochTimestampAsIsoDateTime(builder, 1565100520199L); | ||
System.out.println(builder); | ||
} | ||
|
||
|
||
|
||
private void assertDateFormattingIsCorrect(Instant instant) { | ||
StringBuilder builder = new StringBuilder(); | ||
dateSerializer.serializeEpochTimestampAsIsoDateTime(builder, instant.toEpochMilli()); | ||
assertThat(builder.toString()).isEqualTo(dateTimeFormatter.format(instant)); | ||
} | ||
|
||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters