blob: c6fdb461053030ff3ed0642d8f6ac82b1d5e7deb [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.logging.log4j.layout.template.json.resolver;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutDefaults;
import org.apache.logging.log4j.layout.template.json.util.InstantFormatter;
import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
import java.util.Locale;
import java.util.TimeZone;
/**
* Timestamp resolver.
*
* <h3>Configuration</h3>
*
* <pre>
* config = [ patternConfig | epochConfig ]
*
* patternConfig = "pattern" -> ( [ format ] , [ timeZone ] , [ locale ] )
* format = "format" -> string
* timeZone = "timeZone" -> string
* locale = "locale" -> (
* language |
* ( language , "_" , country ) |
* ( language , "_" , country , "_" , variant )
* )
*
* epochConfig = "epoch" -> ( unit , [ rounded ] )
* unit = "unit" -> (
* "nanos" |
* "millis" |
* "secs" |
* "millis.nanos" |
* "secs.nanos" |
* )
* rounded = "rounded" -> boolean
* </pre>
*
* If no configuration options are provided, <tt>pattern-config</tt> is
* employed. There {@link
* JsonTemplateLayoutDefaults#getTimestampFormatPattern()}, {@link
* JsonTemplateLayoutDefaults#getTimeZone()}, {@link
* JsonTemplateLayoutDefaults#getLocale()} are used as defaults for
* <tt>pattern</tt>, <tt>timeZone</tt>, and <tt>locale</tt>, respectively.
*
* In <tt>epoch-config</tt>, <tt>millis.nanos</tt>, <tt>secs.nanos</tt> stand
* for the fractional component in nanoseconds.
*
* <h3>Examples</h3>
*
* <table>
* <tr>
* <td>Configuration</td>
* <td>Output</td>
* </tr>
* <tr>
* <td><pre>
* {
* "$resolver": "timestamp"
* }
* </pre></td>
* <td><pre>
* 2020-02-07T13:38:47.098+02:00
* </pre></td>
* </tr>
* <tr>
* <td><pre>
* {
* "$resolver": "timestamp",
* "pattern": {
* "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
* "timeZone": "UTC",
* "locale": "en_US"
* }
* }
* </pre></td>
* <td><pre>
* 2020-02-07T13:38:47.098Z
* </pre></td>
* </tr>
* <tr>
* <td><pre>
* {
* "$resolver": "timestamp",
* "epoch": {
* "unit": "secs"
* }
* }
* </pre></td>
* <td><pre>
* 1581082727.982123456
* </pre></td>
* </tr>
* <tr>
* <td><pre>
* {
* "$resolver": "timestamp",
* "epoch": {
* "unit": "secs",
* "rounded": true
* }
* }
* </pre></td>
* <td><pre>
* 1581082727
* </pre></td>
* </tr>
* <tr>
* <td><pre>
* {
* "$resolver": "timestamp",
* "epoch": {
* "unit": "secs.nanos"
* }
* }
* </pre></td>
* <td><pre>
* 982123456
* </pre></td>
* </tr>
* <tr>
* <td><pre>
* {
* "$resolver": "timestamp",
* "epoch": {
* "unit": "millis"
* }
* }
* </pre></td>
* <td><pre>
* 1581082727982.123456
* </pre></td>
* </tr>
* <tr>
* <td><pre>
* {
* "$resolver": "timestamp",
* "epoch": {
* "unit": "millis",
* "rounded": true
* }
* }
* </pre></td>
* <td><pre>
* 1581082727982
* </pre></td>
* </tr>
* <tr>
* <td><pre>
* {
* "$resolver": "timestamp",
* "epoch": {
* "unit": "millis.nanos"
* }
* }
* </pre></td>
* <td><pre>
* 123456
* </pre></td>
* </tr>
* <tr>
* <td><pre>
* {
* "$resolver": "timestamp",
* "epoch": {
* "unit": "nanos"
* }
* }
* </pre></td>
* <td><pre>
* 1581082727982123456
* </pre></td>
* </tr>
* </table>
*/
public final class TimestampResolver implements EventResolver {
private final EventResolver internalResolver;
TimestampResolver(final TemplateResolverConfig config) {
this.internalResolver = createResolver(config);
}
private static EventResolver createResolver(
final TemplateResolverConfig config) {
final boolean patternProvided = config.exists("pattern");
final boolean epochProvided = config.exists("epoch");
if (patternProvided && epochProvided) {
throw new IllegalArgumentException(
"conflicting configuration options are provided: " + config);
}
return epochProvided
? createEpochResolver(config)
: createPatternResolver(config);
}
private static final class PatternResolverContext {
private final InstantFormatter formatter;
private final StringBuilder lastFormattedInstantBuffer = new StringBuilder();
private Instant lastFormattedInstant;
private PatternResolverContext(
final String pattern,
final TimeZone timeZone,
final Locale locale) {
this.formatter = InstantFormatter
.newBuilder()
.setPattern(pattern)
.setTimeZone(timeZone)
.setLocale(locale)
.build();
}
private static PatternResolverContext fromConfig(
final TemplateResolverConfig config) {
final String pattern = readPattern(config);
final TimeZone timeZone = readTimeZone(config);
final Locale locale = config.getLocale(new String[]{"pattern", "locale"});
return new PatternResolverContext(pattern, timeZone, locale);
}
private static String readPattern(final TemplateResolverConfig config) {
final String format = config.getString(new String[]{"pattern", "format"});
return format != null
? format
: JsonTemplateLayoutDefaults.getTimestampFormatPattern();
}
private static TimeZone readTimeZone(final TemplateResolverConfig config) {
final String timeZoneId = config.getString(new String[]{"pattern", "timeZone"});
if (timeZoneId == null) {
return JsonTemplateLayoutDefaults.getTimeZone();
}
boolean found = false;
for (final String availableTimeZone : TimeZone.getAvailableIDs()) {
if (availableTimeZone.equalsIgnoreCase(timeZoneId)) {
found = true;
break;
}
}
if (!found) {
throw new IllegalArgumentException(
"invalid timestamp time zone: " + config);
}
return TimeZone.getTimeZone(timeZoneId);
}
}
private static final class PatternResolver implements EventResolver {
private final PatternResolverContext patternResolverContext;
private PatternResolver(final PatternResolverContext patternResolverContext) {
this.patternResolverContext = patternResolverContext;
}
@Override
public synchronized void resolve(
final LogEvent logEvent,
final JsonWriter jsonWriter) {
// Format timestamp if it doesn't match the last cached one.
if (patternResolverContext.lastFormattedInstant == null ||
!patternResolverContext.formatter.isInstantMatching(
patternResolverContext.lastFormattedInstant,
logEvent.getInstant())) {
// Format the timestamp.
patternResolverContext.lastFormattedInstantBuffer.setLength(0);
patternResolverContext.lastFormattedInstant = logEvent.getInstant();
patternResolverContext.formatter.format(
patternResolverContext.lastFormattedInstant,
patternResolverContext.lastFormattedInstantBuffer);
// Write the formatted timestamp.
final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
final int startIndex = jsonWriterStringBuilder.length();
jsonWriter.writeString(patternResolverContext.lastFormattedInstantBuffer);
// Cache the written value.
patternResolverContext.lastFormattedInstantBuffer.setLength(0);
patternResolverContext.lastFormattedInstantBuffer.append(
jsonWriterStringBuilder,
startIndex,
jsonWriterStringBuilder.length());
}
// Write the cached formatted timestamp.
else {
jsonWriter.writeRawString(
patternResolverContext.lastFormattedInstantBuffer);
}
}
}
private static EventResolver createPatternResolver(
final TemplateResolverConfig config) {
final PatternResolverContext patternResolverContext =
PatternResolverContext.fromConfig(config);
return new PatternResolver(patternResolverContext);
}
private static EventResolver createEpochResolver(
final TemplateResolverConfig config) {
final String unit = config.getString(new String[]{"epoch", "unit"});
final Boolean rounded = config.getBoolean(new String[]{"epoch", "rounded"});
if ("nanos".equals(unit) && !Boolean.FALSE.equals(rounded)) {
return EPOCH_NANOS_RESOLVER;
} else if ("millis".equals(unit)) {
return !Boolean.TRUE.equals(rounded)
? EPOCH_MILLIS_RESOLVER
: EPOCH_MILLIS_ROUNDED_RESOLVER;
} else if ("millis.nanos".equals(unit) && rounded == null) {
return EPOCH_MILLIS_NANOS_RESOLVER;
} else if ("secs".equals(unit)) {
return !Boolean.TRUE.equals(rounded)
? EPOCH_SECS_RESOLVER
: EPOCH_SECS_ROUNDED_RESOLVER;
} else if ("secs.nanos".equals(unit) && rounded == null) {
return EPOCH_SECS_NANOS_RESOLVER;
}
throw new IllegalArgumentException(
"invalid epoch configuration: " + config);
}
private static final class EpochResolutionRecord {
private static final int MAX_LONG_LENGTH =
String.valueOf(Long.MAX_VALUE).length();
private Instant instant;
private final char[] resolution = new char[
/* integral: */ MAX_LONG_LENGTH +
/* dot: */ 1 +
/* fractional: */ MAX_LONG_LENGTH];
private int resolutionLength;
private EpochResolutionRecord() {}
}
private static abstract class EpochResolver implements EventResolver {
private final EpochResolutionRecord resolutionRecord =
new EpochResolutionRecord();
@Override
public synchronized void resolve(
final LogEvent logEvent,
final JsonWriter jsonWriter) {
final Instant logEventInstant = logEvent.getInstant();
if (logEventInstant.equals(resolutionRecord.instant)) {
jsonWriter.writeRawString(
resolutionRecord.resolution,
0,
resolutionRecord.resolutionLength);
} else {
resolutionRecord.instant = logEventInstant;
final StringBuilder stringBuilder = jsonWriter.getStringBuilder();
final int startIndex = stringBuilder.length();
resolve(logEventInstant, jsonWriter);
resolutionRecord.resolutionLength = stringBuilder.length() - startIndex;
stringBuilder.getChars(
startIndex,
stringBuilder.length(),
resolutionRecord.resolution,
0);
}
}
abstract void resolve(Instant logEventInstant, JsonWriter jsonWriter);
}
private static final EventResolver EPOCH_NANOS_RESOLVER =
new EpochResolver() {
@Override
void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
final long nanos = epochNanos(logEventInstant);
jsonWriter.writeNumber(nanos);
}
};
private static final EventResolver EPOCH_MILLIS_RESOLVER =
new EpochResolver() {
@Override
void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
final long nanos = epochNanos(logEventInstant);
jsonWriterStringBuilder.append(nanos);
jsonWriterStringBuilder.insert(jsonWriterStringBuilder.length() - 6, '.');
}
};
private static final EventResolver EPOCH_MILLIS_ROUNDED_RESOLVER =
new EpochResolver() {
@Override
void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
jsonWriter.writeNumber(logEventInstant.getEpochMillisecond());
}
};
private static final EventResolver EPOCH_MILLIS_NANOS_RESOLVER =
new EpochResolver() {
@Override
void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
final long nanos = epochNanos(logEventInstant);
final long fraction = nanos % 1_000_000L;
jsonWriter.writeNumber(fraction);
}
};
private static final EventResolver EPOCH_SECS_RESOLVER =
new EpochResolver() {
@Override
void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
final long nanos = epochNanos(logEventInstant);
jsonWriterStringBuilder.append(nanos);
jsonWriterStringBuilder.insert(jsonWriterStringBuilder.length() - 9, '.');
}
};
private static final EventResolver EPOCH_SECS_ROUNDED_RESOLVER =
new EpochResolver() {
@Override
void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
jsonWriter.writeNumber(logEventInstant.getEpochSecond());
}
};
private static final EventResolver EPOCH_SECS_NANOS_RESOLVER =
new EpochResolver() {
@Override
void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
jsonWriter.writeNumber(logEventInstant.getNanoOfSecond());
}
};
private static long epochNanos(final Instant instant) {
return 1_000_000_000L * instant.getEpochSecond() + instant.getNanoOfSecond();
}
static String getName() {
return "timestamp";
}
@Override
public void resolve(
final LogEvent logEvent,
final JsonWriter jsonWriter) {
internalResolver.resolve(logEvent, jsonWriter);
}
}