All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.logstash.logback.composite.FastISOTimestampFormatter Maven / Gradle / Ivy

/*
 * Copyright 2013-2023 the original author or authors.
 *
 * 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;

import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.zone.ZoneOffsetTransition;
import java.time.zone.ZoneRules;
import java.util.Objects;

/**
 * A fast alternative to {@link DateTimeFormatter} when formatting {@link Instant} using an ISO format.
 * 
 * 

This class is thread safe. * *

Note: This class is for internal use only and subject to backward incompatible change * at any time. * * @author brenuart */ class FastISOTimestampFormatter { /** * ThreadLocal with reusable {@link StringBuilder} instances. * Initialized with a size large enough to hold formats that do not include the Zone. * Will need to grow on first use otherwise. */ private static ThreadLocal STRING_BUILDERS = ThreadLocal.withInitial(() -> new StringBuilder(40)); /** * Nanosecond decimals constants */ private static final int[] DECIMALS = {100_000_000, 10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10 }; /** * The actual DateTimeFormatter used to format the timestamp when the cached * value cannot be used */ private final DateTimeFormatter formatter; /** * Holds the cached formatted value. Can be reused only when the timestamp to format * is in the same ZoneOffset as the cached value. * * This class is immutable and a new instance is created when needed. Two concurrent threads may create two * identical instances but only one will eventually remain. This strategy is cheaper than a lock or a volatile * field. */ private ZoneOffsetState zoneOffsetState; /** * Whether trailing zero should be trimmed from the millis/nanos part. * When {@code false}, millis/nanos are output in group of 3 digits. */ private final boolean trimMillis; /* Visible for testing */ FastISOTimestampFormatter(DateTimeFormatter formatter, boolean trimMillis) { this.formatter = Objects.requireNonNull(formatter); this.trimMillis = trimMillis; if (formatter.getZone() == null) { throw new IllegalArgumentException("formatter must be configured with a Zone override to format Instant"); } } /** * Format the {@code tstamp} timestamp. * * @param tstamp the timestamp to format * @return the formatted result */ public String format(Instant tstamp) { ZoneOffsetState current = this.zoneOffsetState; if (current == null || !current.canFormat(tstamp)) { current = new ZoneOffsetState(tstamp); this.zoneOffsetState = current; } return current.format(tstamp); } /** * Create a fast formatter using the same format as {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME}. * * @param zoneId the zone override * @return a fast formatter */ public static FastISOTimestampFormatter isoOffsetDateTime(ZoneId zoneId) { return new FastISOTimestampFormatter(DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(zoneId), true); } /** * Create a fast formatter using the same format as {@link DateTimeFormatter#ISO_ZONED_DATE_TIME}. * * @param zoneId the zone override * @return a fast formatter */ public static FastISOTimestampFormatter isoZonedDateTime(ZoneId zoneId) { return new FastISOTimestampFormatter(DateTimeFormatter.ISO_ZONED_DATE_TIME.withZone(zoneId), true); } /** * Create a fast formatter using the same format as {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME}. * * @param zoneId the zone override * @return a fast formatter */ public static FastISOTimestampFormatter isoLocalDateTime(ZoneId zoneId) { return new FastISOTimestampFormatter(DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(zoneId), true); } /** * Create a fast formatter using the same format as {@link DateTimeFormatter#ISO_DATE_TIME}. * * @param zoneId the zone override * @return a fast formatter */ public static FastISOTimestampFormatter isoDateTime(ZoneId zoneId) { return new FastISOTimestampFormatter(DateTimeFormatter.ISO_DATE_TIME.withZone(zoneId), true); } /** * Create a fast formatter using the same format as {@link DateTimeFormatter#ISO_INSTANT}. * * @param zoneId the zone override * @return a fast formatter */ public static FastISOTimestampFormatter isoInstant(ZoneId zoneId) { return new FastISOTimestampFormatter(DateTimeFormatter.ISO_INSTANT.withZone(zoneId), false); } /** * State valid during a zone transition. * Does not change frequently, typically before/after day-light transition. */ private class ZoneOffsetState { private final Instant zoneTransitionStart; private final Instant zoneTransitionStop; private final boolean cachingEnabled; private TimestampPeriod cachedTimestampPeriod; ZoneOffsetState(Instant tstamp) { // Determine how long we can cache the previous result. // ZoneOffsets are usually expressed in hour:minutes but the Java time API accepts ZoneOffset with // a resolution up to the second: // - If the minutes/seconds parts of the ZoneOffset are zero, then we can cache for as long as one hour. // - If the ZoneOffset is expressed in "minutes", then we can cache the date/hour/minute part for as long as a minute. // - If the ZoneOffset is expressed in "seconds", then we can cache only for one second. // // Also, take care of ZoneOffset transition (e.g. day light saving time). ZoneRules rules = formatter.getZone().getRules(); /* * The Zone has a fixed offset that will never change. */ if (rules.isFixedOffset()) { this.zoneTransitionStart = Instant.MIN; this.zoneTransitionStop = Instant.MAX; } /* * The Zone has multiple offsets. Find the offset for the given timestamp * and determine how long it is valid. */ else { ZoneOffsetTransition previousZoneOffsetTransition = rules.previousTransition(tstamp); if (previousZoneOffsetTransition == null) { this.zoneTransitionStart = Instant.MIN; } else { this.zoneTransitionStart = previousZoneOffsetTransition.getInstant(); } ZoneOffsetTransition zoneOffsetTransition = rules.nextTransition(tstamp); if (zoneOffsetTransition == null) { this.zoneTransitionStop = Instant.MAX; } else { this.zoneTransitionStop = zoneOffsetTransition.getInstant(); } } /* * Determine the precision of the zone offset. * * If the offset is expressed with HH:mm without seconds, then the date/time part remains constant during * one minute and is not affected by the zone offset. This means we can safely deduce the second and millis * from a long timestamp. * * The same applies for the minutes part if the offset contains hours only. In this case, the date/time part * remains constant for a complete hour increasing the time we can reuse that part. However, tests have shown * that the extra computation required to extract the minutes from the timestamp during rendering negatively * impact the overall performance. * * Caching is therefore limited to one minute which is good enough for our usage. */ int offsetSeconds = rules.getOffset(tstamp).getTotalSeconds(); this.cachingEnabled = (offsetSeconds % 60 == 0); } /** * Check whether the given timestamp is within the ZoneOffset represented by this state. * * @param tstamp the timestamp to format * @return {@code true} if the timestamp is within the ZoneOffset represented by this state */ public boolean canFormat(Instant tstamp) { return tstamp.compareTo(this.zoneTransitionStart) >= 0 && tstamp.isBefore(this.zoneTransitionStop); } /** * Format a timestamp. * Note: you must first invoke {@link #canFormat(long)} to check that the state can be used to format the timestamp. * * @param tstamp the timestamp to format * @return the formatted timestamp */ public String format(Instant tstamp) { // If caching is disabled... // if (!this.cachingEnabled) { return buildFromFormatter(tstamp); } // If tstamp is within the caching period... // TimestampPeriod currentTimestampPeriod = this.cachedTimestampPeriod; if (currentTimestampPeriod != null && currentTimestampPeriod.canFormat(tstamp)) { return buildFromCache(currentTimestampPeriod, tstamp); } // ... otherwise, use the formatter and cache the formatted value // String formatted = buildFromFormatter(tstamp); cachedTimestampPeriod = createNewCache(tstamp, formatted); return formatted; } private TimestampPeriod createNewCache(Instant tstamp, String formatted) { // Examples of the supported formats: // // ISO_OFFSET_DATE_TIME 2020-01-01T10:20:30.123+01:00 // ISO_ZONED_DATE_TIME 2020-01-01T10:20:30.123+01:00[Europe/Brussels] // 2020-01-01T10:20:30.123Z[UTC] // ISO_LOCAL_DATE_TIME 2020-01-01T10:20:30.123 // ISO_DATE_TIME 2020-01-01T10:20:30.123+01:00[Europe/Brussels] // 2020-01-01T10:20:30.123Z[UTC] // ISO_INSTANT 2020-01-01T09:20:30.123Z // +---------------+ +---------------------+ // prefix suffix // // Seconds start at position 17 and are two digits long. // Millis/Nanos are optional. // The part up to the minutes (included) String prefix = formatted.substring(0, 17); // The part of after the millis (i.e. the timezone) String suffix = findSuffix(formatted, 17); // Determine how long we can use this cache -> cache is valid only during the current minute Instant cacheStart = tstamp.truncatedTo(ChronoUnit.MINUTES); Instant cacheStop = cacheStart.plus(1, ChronoUnit.MINUTES); // Store in cache return new TimestampPeriod(cacheStart, cacheStop, prefix, suffix); } private String findSuffix(String formatted, int beginIndex) { boolean dotFound = false; int pos = beginIndex; while (pos < formatted.length()) { char c = formatted.charAt(pos); // Allow for a single dot... if (c == '.') { if (dotFound) { break; } else { dotFound = true; } } else if (!Character.isDigit(c)) { break; } pos++; } if (pos < formatted.length()) { return formatted.substring(pos); } else { return ""; } } private String buildFromCache(TimestampPeriod cache, Instant tstamp) { return cache.format(tstamp); } } /* visible for testing */ String buildFromFormatter(Instant tstamp) { return FastISOTimestampFormatter.this.formatter.format(tstamp); } private class TimestampPeriod { private final Instant periodStart; private final Instant periodStop; private final String suffix; private final String prefix; TimestampPeriod(Instant periodStart, Instant periodStop, String prefix, String suffix) { this.periodStart = periodStart; this.periodStop = periodStop; this.prefix = prefix; this.suffix = suffix; } public boolean canFormat(Instant tstamp) { return tstamp.compareTo(this.periodStart) >= 0 && tstamp.isBefore(this.periodStop); } public String format(Instant tstamp) { StringBuilder sb = STRING_BUILDERS.get(); sb.setLength(0); sb.append(prefix); int nanos = tstamp.getNano(); int seconds = (int) (tstamp.getEpochSecond() - this.periodStart.getEpochSecond()); // seconds are always TWO digits... // if (seconds < 10) { sb.append('0'); } sb.append(seconds); // millis/nanos are optional, max 9 significant digits // if (nanos > 0) { int dotPos = sb.length(); sb.append('.'); // add leading 0... for (int i = 0; i < DECIMALS.length; i++) { if (nanos < DECIMALS[i]) { sb.append('0'); } else { break; } } // add millis/nanos value... sb.append(nanos); // remove trailing 0... if (FastISOTimestampFormatter.this.trimMillis) { while (sb.length() > dotPos) { if (sb.charAt(sb.length() - 1) == '0') { sb.setLength(sb.length() - 1); } else { break; } } } // ... if not trimming, keep them by group of 3 (i.e. 3, 6 or 9 digits) else { while (sb.length() > dotPos) { if (sb.charAt(sb.length() - 1) == '0' && sb.charAt(sb.length() - 2) == '0' && sb.charAt(sb.length() - 3) == '0' ) { sb.setLength(sb.length() - 3); } else { break; } } } } // suffix... // sb.append(this.suffix); return sb.toString(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy