| /* |
| * 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.core.time.internal.format.FastDateFormat; |
| import org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutDefaults; |
| import org.apache.logging.log4j.layout.template.json.util.JsonWriter; |
| |
| import java.util.Calendar; |
| 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) |
| : createFormatResolver(config); |
| } |
| |
| /** |
| * Context for GC-free formatted timestamp resolver. |
| */ |
| private static final class FormatResolverContext { |
| |
| private final FastDateFormat timestampFormat; |
| |
| private final Calendar calendar; |
| |
| private final StringBuilder formattedTimestampBuilder; |
| |
| private FormatResolverContext( |
| final TimeZone timeZone, |
| final Locale locale, |
| final FastDateFormat timestampFormat) { |
| this.timestampFormat = timestampFormat; |
| this.formattedTimestampBuilder = new StringBuilder(); |
| this.calendar = Calendar.getInstance(timeZone, locale); |
| timestampFormat.format(calendar, formattedTimestampBuilder); |
| } |
| |
| private static FormatResolverContext fromConfig( |
| final TemplateResolverConfig config) { |
| final String format = readFormat(config); |
| final TimeZone timeZone = readTimeZone(config); |
| final Locale locale = readLocale(config); |
| final FastDateFormat fastDateFormat = |
| FastDateFormat.getInstance(format, timeZone, locale); |
| return new FormatResolverContext(timeZone, locale, fastDateFormat); |
| } |
| |
| private static String readFormat(final TemplateResolverConfig config) { |
| final String format = config.getString(new String[]{"pattern", "format"}); |
| if (format == null) { |
| return JsonTemplateLayoutDefaults.getTimestampFormatPattern(); |
| } |
| try { |
| FastDateFormat.getInstance(format); |
| } catch (final IllegalArgumentException error) { |
| throw new IllegalArgumentException( |
| "invalid timestamp format: " + config, |
| error); |
| } |
| return format; |
| } |
| |
| 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 Locale readLocale(final TemplateResolverConfig config) { |
| final String locale = config.getString(new String[]{"pattern", "locale"}); |
| if (locale == null) { |
| return JsonTemplateLayoutDefaults.getLocale(); |
| } |
| final String[] localeFields = locale.split("_", 3); |
| switch (localeFields.length) { |
| case 1: return new Locale(localeFields[0]); |
| case 2: return new Locale(localeFields[0], localeFields[1]); |
| case 3: return new Locale(localeFields[0], localeFields[1], localeFields[2]); |
| } |
| throw new IllegalArgumentException("invalid timestamp locale: " + config); |
| } |
| |
| } |
| |
| /** |
| * GC-free formatted timestamp resolver. |
| */ |
| private static final class FormatResolver implements EventResolver { |
| |
| private final FormatResolverContext formatResolverContext; |
| |
| private FormatResolver(final FormatResolverContext formatResolverContext) { |
| this.formatResolverContext = formatResolverContext; |
| } |
| |
| @Override |
| public synchronized void resolve( |
| final LogEvent logEvent, |
| final JsonWriter jsonWriter) { |
| |
| // Format timestamp if it doesn't match the last cached one. |
| final long timestampMillis = logEvent.getTimeMillis(); |
| if (formatResolverContext.calendar.getTimeInMillis() != timestampMillis) { |
| |
| // Format the timestamp. |
| formatResolverContext.formattedTimestampBuilder.setLength(0); |
| formatResolverContext.calendar.setTimeInMillis(timestampMillis); |
| formatResolverContext.timestampFormat.format( |
| formatResolverContext.calendar, |
| formatResolverContext.formattedTimestampBuilder); |
| |
| // Write the formatted timestamp. |
| final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder(); |
| final int startIndex = jsonWriterStringBuilder.length(); |
| jsonWriter.writeString(formatResolverContext.formattedTimestampBuilder); |
| |
| // Cache the written value. |
| formatResolverContext.formattedTimestampBuilder.setLength(0); |
| formatResolverContext.formattedTimestampBuilder.append( |
| jsonWriterStringBuilder, |
| startIndex, |
| jsonWriterStringBuilder.length()); |
| |
| } |
| |
| // Write the cached formatted timestamp. |
| else { |
| jsonWriter.writeRawString( |
| formatResolverContext.formattedTimestampBuilder); |
| } |
| |
| } |
| |
| } |
| |
| private static EventResolver createFormatResolver( |
| final TemplateResolverConfig config) { |
| final FormatResolverContext formatResolverContext = |
| FormatResolverContext.fromConfig(config); |
| return new FormatResolver(formatResolverContext); |
| } |
| |
| 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(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); |
| } |
| |
| } |