Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch FastDateFormat to DateTimeFormatter #377

Merged
merged 7 commits into from
Jan 2, 2021
45 changes: 16 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1534,39 +1534,36 @@ The value can be written as a number (instead of a string) like this:

## Customizing Timestamp

By default, timestamps are written as string values in the format `yyyy-MM-dd'T'HH:mm:ss.SSSZZ` (e.g. `2018-04-28T22:23:59.164-07:00`), in the default TimeZone of the host Java platform.
By default, timestamps are written as string values in the format specified by
[`DateTimeFormatter.ISO_OFFSET_DATE_TIME`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html#ISO_OFFSET_DATE_TIME)
(e.g. `2019-11-03T10:15:30.123+01:00`), in the default TimeZone of the host Java platform.

You can change the timezone like this:

```xml
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<timeZone>UTC</timeZone>
</encoder>
```

The value of the `timeZone` element can be any string accepted by java's `TimeZone.getTimeZone(String id)` method.

You can change the pattern used like this:
You can change the pattern like this:

```xml
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<timestampPattern>yyyy-MM-dd'T'HH:mm:ss.SSS</timestampPattern>
</encoder>
```

Use these timestamp pattern values to output the timestamp as a unix timestamp (number of milliseconds since unix epoch).
The value of the `timestampPattern` can be any of the following:

* `[UNIX_TIMESTAMP_AS_NUMBER]` - write the timestamp value as a numeric unix timestamp
* `[UNIX_TIMESTAMP_AS_STRING]` - write the timestamp value as a string verion of the numeric unix timestamp
* `[UNIX_TIMESTAMP_AS_NUMBER]` - timestamp written as a JSON number value of the milliseconds since unix epoch
* `[UNIX_TIMESTAMP_AS_STRING]` - timestamp written as a JSON string value of the milliseconds since unix epoch
* `[` _`constant`_ `]` - (e.g. `[ISO_OFFSET_DATE_TIME]`) timestamp written using the given `DateTimeFormatter` constant
* any other value - (e.g. `yyyy-MM-dd'T'HH:mm:ss.SSS`) timestamp written using a `DateTimeFormatter` created from the given pattern

For example:

You can change the timezone like this:

```xml
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<timestampPattern>[UNIX_TIMESTAMP_AS_NUMBER]</timestampPattern>
<timeZone>UTC</timeZone>
</encoder>
```

The value of the `timeZone` element can be any string accepted by java's `TimeZone.getTimeZone(String id)` method.


## Customizing Message

Expand Down Expand Up @@ -1789,12 +1786,7 @@ For LoggingEvents, the available providers and their configuration properties (d
<td><p>Event timestamp</p>
<ul>
<li><tt>fieldName</tt> - Output field name (<tt>@timestamp</tt>)</li>
<li><tt>pattern</tt> - Output format (<tt>yyyy-MM-dd'T'HH:mm:ss.SSSZZ</tt>)
<ul>
<li>If set to <tt>[UNIX_TIMESTAMP_AS_NUMBER]</tt>, then the timestamp will be written as a numeric unix timestamp value</li>
<li>If set to <tt>[UNIX_TIMESTAMP_AS_STRING]</tt>, then the timestamp will be written as a string unix timestamp value</li>
</ul>
</li>
<li><tt>pattern</tt> - Output format (<tt>[ISO_OFFSET_DATE_TIME]</tt>) See <a href="#customizing-timestamp">above</a> for possible values.</li>
<li><tt>timeZone</tt> - Timezone (local timezone)</li>
</ul>
</td>
Expand Down Expand Up @@ -2060,12 +2052,7 @@ For AccessEvents, the available providers and their configuration properties (de
<td><p>Event timestamp</p>
<ul>
<li><tt>fieldName</tt> - Output field name (<tt>@timestamp</tt>)</li>
<li><tt>pattern</tt> - Output format (<tt>yyyy-MM-dd'T'HH:mm:ss.SSSZZ</tt>)
<ul>
<li>If set to <tt>[UNIX_TIMESTAMP_AS_NUMBER]</tt>, then the timestamp will be written as a numeric unix timestamp value</li>
<li>If set to <tt>[UNIX_TIMESTAMP_AS_STRING]</tt>, then the timestamp will be written as a string unix timestamp value</li>
</ul>
</li>
<li><tt>pattern</tt> - Output format (<tt>[ISO_OFFSET_DATE_TIME]</tt>) See <a href="#customizing-timestamp">above</a> for possible values.</li>
<li><tt>timeZone</tt> - Timezone (local timezone)</li>
</ul>
</td>
Expand Down
12 changes: 1 addition & 11 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@

<!-- shaded runtime dependencies -->
<com.lmax.disruptor.version>3.4.2</com.lmax.disruptor.version>
<commons-lang3.version>3.11</commons-lang3.version>

<!-- test dependencies -->
<junit.version>5.7.0</junit.version>
Expand Down Expand Up @@ -179,11 +178,6 @@
-->
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
Expand Down Expand Up @@ -332,10 +326,6 @@
</excludes>
</artifactSet>
<relocations>
<relocation>
<pattern>org.apache.commons.lang3</pattern>
<shadedPattern>${project.groupId}.encoder.org.apache.commons.lang3</shadedPattern>
</relocation>
<relocation>
<pattern>com.lmax.disruptor</pattern>
<shadedPattern>${project.groupId}.encoder.com.lmax.disruptor</shadedPattern>
Expand Down Expand Up @@ -364,7 +354,7 @@
<!-- attach to Logback bundle as fragment -->
<Fragment-Host>ch.qos.logback.classic</Fragment-Host>
<!-- exclude following imports as required classes are relocated by shade plugin and ignore ch.qos.logback.[core,classic] because this is a fragment and gets them from parent. -->
<Import-Package>!org.apache.commons.*,!com.lmax.disruptor.*,!ch.qos.logback.classic.*,!ch.qos.logback.core.*,!org.slf4j.*,*</Import-Package>
<Import-Package>!com.lmax.disruptor.*,!ch.qos.logback.classic.*,!ch.qos.logback.core.*,!org.slf4j.*,*</Import-Package>
</instructions>
</configuration>
</plugin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
package net.logstash.logback.appender.listener;

import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.TimeZone;

import ch.qos.logback.core.spi.DeferredProcessingAware;
import net.logstash.logback.argument.StructuredArguments;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -52,7 +54,7 @@ protected void handleFailureSummary(FailureSummary failureSummary, CallbackType
logger.warn("{} {} failures since {} for {}.",
StructuredArguments.value("failEventCount", failureSummary.getConsecutiveFailures()),
StructuredArguments.value("failType", callbackType.name().toLowerCase()),
StructuredArguments.value("failStartTime", DateFormatUtils.ISO_8601_EXTENDED_DATETIME_FORMAT.format(failureSummary.getFirstFailureTime())),
StructuredArguments.value("failStartTime", DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(TimeZone.getDefault().toZoneId()).format(Instant.ofEpochMilli(failureSummary.getFirstFailureTime()))),
StructuredArguments.value("failDuration", Duration.ofMillis(System.currentTimeMillis() - failureSummary.getFirstFailureTime()).toString()),
failureSummary.getMostRecentFailure());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,21 @@
package net.logstash.logback.composite;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;

import net.logstash.logback.fieldnames.LogstashCommonFieldNames;

import org.apache.commons.lang3.time.FastDateFormat;

import ch.qos.logback.core.spi.DeferredProcessingAware;
import net.logstash.logback.fieldnames.LogstashCommonFieldNames;

import com.fasterxml.jackson.core.JsonGenerator;

/**
* Writes the timestamp field as either:
* <ul>
* <li>A string value formatted by a {@link FastDateFormat} pattern</li>
* <li>A string value formatted by a {@link DateTimeFormatter} pattern</li>
* <li>A string value representing the number of milliseconds since unix epoch (designated by specifying the pattern value as {@value #UNIX_TIMESTAMP_AS_STRING})</li>
* <li>A number value of the milliseconds since unix epoch (designated by specifying the pattern value as {@value #UNIX_TIMESTAMP_AS_NUMBER})</li>
* </ul>
Expand All @@ -48,14 +49,20 @@ public abstract class FormattedTimestampJsonProvider<Event extends DeferredProce
*/
public static final String UNIX_TIMESTAMP_AS_STRING = "[UNIX_TIMESTAMP_AS_STRING]";

private static final String DEFAULT_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZZ";
private static final TimeZone DEFAULT_TIMEZONE = null;
private static final String DEFAULT_PATTERN = "[ISO_OFFSET_DATE_TIME]";
private static final TimeZone DEFAULT_TIMEZONE = TimeZone.getDefault();

/**
* The pattern in which to format the timestamp.
* Setting this value to {@value #UNIX_TIMESTAMP_AS_NUMBER} will cause the timestamp to be written as a number value of the milliseconds since unix epoch.
* Setting this value to {@value #UNIX_TIMESTAMP_AS_STRING} will cause the timestamp to be written as a string value representing the number value of the milliseconds since unix epoch.
* Any other value will be used as a pattern for formatting the timestamp by a {@link FastDateFormat}
*
* <p>Possible values:</p>
*
* <ul>
* <li>{@value #UNIX_TIMESTAMP_AS_NUMBER} - timestamp written as a JSON number value of the milliseconds since unix epoch</li>
* <li>{@value #UNIX_TIMESTAMP_AS_STRING} - timestamp written as a JSON string value of the milliseconds since unix epoch</li>
* <li><code>[<em>constant</em>]</code> - timestamp written using the {@link DateTimeFormatter} constant specified by <code><em>constant</em></code> (e.g. {@code [ISO_OFFSET_DATE_TIME]})</li>
* <li>any other value - timestamp written by a {@link DateTimeFormatter} created from the pattern string specified
* </ul>
*/
private String pattern = DEFAULT_PATTERN;

Expand All @@ -68,12 +75,12 @@ public abstract class FormattedTimestampJsonProvider<Event extends DeferredProce
/**
* Writes the timestamp to the JsonGenerator.
*/
private TimestampWriter timestampWriter = new PatternTimestampWriter(FastDateFormat.getInstance(pattern, timeZone));
private TimestampWriter timestampWriter;

/**
* Writes the timestamp to the JsonGenerator
*/
private static interface TimestampWriter {
private interface TimestampWriter {
void writeTo(JsonGenerator generator, String fieldName, long timestampInMillis) throws IOException;

String getTimestampAsString(long timestampInMillis);
Expand All @@ -84,9 +91,9 @@ private static interface TimestampWriter {
*/
private static class PatternTimestampWriter implements TimestampWriter {

private final FastDateFormat formatter;
public PatternTimestampWriter(FastDateFormat formatter) {
private final DateTimeFormatter formatter;

public PatternTimestampWriter(DateTimeFormatter formatter) {
this.formatter = formatter;
}

Expand All @@ -98,7 +105,7 @@ public void writeTo(JsonGenerator generator, String fieldName, long timestampInM

@Override
public String getTimestampAsString(long timestampInMillis) {
return formatter.format(timestampInMillis);
return formatter.format(Instant.ofEpochMilli(timestampInMillis));
}
}

Expand Down Expand Up @@ -137,6 +144,7 @@ public String getTimestampAsString(long timestampInMillis) {

public FormattedTimestampJsonProvider() {
setFieldName(FIELD_TIMESTAMP);
updateTimestampWriter();
}

@Override
Expand All @@ -163,8 +171,27 @@ private void updateTimestampWriter() {
timestampWriter = new NumberTimestampWriter();
} else if (UNIX_TIMESTAMP_AS_STRING.equals(pattern)) {
timestampWriter = new StringTimestampWriter();
} else if (pattern.startsWith("[") && pattern.endsWith("]")) {
String constant = pattern.substring("[".length(), pattern.length() - "]".length());
try {
Field field = DateTimeFormatter.class.getField(constant);
if (Modifier.isStatic(field.getModifiers())
&& Modifier.isFinal(field.getModifiers())
&& field.getType().equals(DateTimeFormatter.class)) {
try {
DateTimeFormatter formatter = (DateTimeFormatter) field.get(null);
timestampWriter = new PatternTimestampWriter(formatter.withZone(timeZone.toZoneId()));
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(String.format("Unable to get value of constant named %s in %s", constant, DateTimeFormatter.class), e);
}
} else {
throw new IllegalArgumentException(String.format("Field named %s in %s is not a constant %s", constant, DateTimeFormatter.class, DateTimeFormatter.class));
}
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException(String.format("No constant named %s found in %s", constant, DateTimeFormatter.class), e);
}
} else {
timestampWriter = new PatternTimestampWriter(FastDateFormat.getInstance(pattern, timeZone));
timestampWriter = new PatternTimestampWriter(DateTimeFormatter.ofPattern(pattern).withZone(timeZone.toZoneId()));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* 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 net.logstash.logback.composite.loggingevent;

import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;

import ch.qos.logback.classic.spi.ILoggingEvent;
import net.logstash.logback.composite.FormattedTimestampJsonProvider;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.fasterxml.jackson.core.JsonGenerator;

@ExtendWith(MockitoExtension.class)
public class LoggingEventFormattedTimestampJsonProviderTest {


@Mock
private JsonGenerator generator;

@Mock
private ILoggingEvent event;

@Test
public void withDefaults() throws IOException {
LoggingEventFormattedTimestampJsonProvider provider = new LoggingEventFormattedTimestampJsonProvider();
when(event.getTimeStamp()).thenReturn(0L);

provider.writeTo(generator, event);

String expectedValue = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(TimeZone.getDefault().toZoneId()).format(Instant.ofEpochMilli(0));
verify(generator).writeStringField(FormattedTimestampJsonProvider.FIELD_TIMESTAMP, expectedValue);
}

@Test
public void customTimeZone() throws IOException {
LoggingEventFormattedTimestampJsonProvider provider = new LoggingEventFormattedTimestampJsonProvider();
provider.setTimeZone("UTC");
when(event.getTimeStamp()).thenReturn(0L);

provider.writeTo(generator, event);

String expectedValue = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.of("UTC")).format(Instant.ofEpochMilli(0));
verify(generator).writeStringField(FormattedTimestampJsonProvider.FIELD_TIMESTAMP, expectedValue);
}

@Test
public void constant() throws IOException {
LoggingEventFormattedTimestampJsonProvider provider = new LoggingEventFormattedTimestampJsonProvider();
provider.setPattern("[ISO_DATE_TIME]");
when(event.getTimeStamp()).thenReturn(0L);

provider.writeTo(generator, event);

String expectedValue = DateTimeFormatter.ISO_DATE_TIME.withZone(TimeZone.getDefault().toZoneId()).format(Instant.ofEpochMilli(0));
verify(generator).writeStringField(FormattedTimestampJsonProvider.FIELD_TIMESTAMP, expectedValue);
}

@Test
public void unknownConstant() {
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {
LoggingEventFormattedTimestampJsonProvider provider = new LoggingEventFormattedTimestampJsonProvider();
provider.setPattern("[foo]");
});
}

@Test
public void customPattern() throws IOException {
LoggingEventFormattedTimestampJsonProvider provider = new LoggingEventFormattedTimestampJsonProvider();
String pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS";
provider.setPattern(pattern);
when(event.getTimeStamp()).thenReturn(0L);

provider.writeTo(generator, event);

String expectedValue = DateTimeFormatter.ofPattern(pattern).withZone(TimeZone.getDefault().toZoneId()).format(Instant.ofEpochMilli(0));
verify(generator).writeStringField(FormattedTimestampJsonProvider.FIELD_TIMESTAMP, expectedValue);
}

@Test
public void unixEpochAsNumber() throws IOException {
LoggingEventFormattedTimestampJsonProvider provider = new LoggingEventFormattedTimestampJsonProvider();
provider.setPattern(FormattedTimestampJsonProvider.UNIX_TIMESTAMP_AS_NUMBER);
when(event.getTimeStamp()).thenReturn(0L);

provider.writeTo(generator, event);

verify(generator).writeNumberField(FormattedTimestampJsonProvider.FIELD_TIMESTAMP, 0L);
}

@Test
public void unixEpochAsString() throws IOException {
LoggingEventFormattedTimestampJsonProvider provider = new LoggingEventFormattedTimestampJsonProvider();
provider.setPattern(FormattedTimestampJsonProvider.UNIX_TIMESTAMP_AS_STRING);
when(event.getTimeStamp()).thenReturn(0L);

provider.writeTo(generator, event);

verify(generator).writeStringField(FormattedTimestampJsonProvider.FIELD_TIMESTAMP, "0");
}


}
Loading