/*
* Copyright 2014-2017 Real Logic Ltd.
*
* 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 org.agrona.concurrent.errors;
import org.agrona.concurrent.AtomicBuffer;
import org.agrona.concurrent.EpochClock;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Objects;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.agrona.BitUtil.SIZE_OF_INT;
import static org.agrona.BitUtil.SIZE_OF_LONG;
import static org.agrona.BitUtil.align;
/**
* Distinct record of error observations. Rather than grow a record indefinitely when many errors of the same type
* are logged, this log takes the approach of only recording distinct errors of the same type type and stack trace
* and keeping a count and time of observation so that the record only grows with new distinct observations.
*
* The provided {@link AtomicBuffer} can wrap a memory-mapped file so logging can be out of process. This provides
* the benefit that if a crash or lockup occurs then the log can be read externally without loss of data.
*
* This class is threadsafe to be used from multiple logging threads.
*
* The error records are recorded to the memory mapped buffer in the following format.
*
* <pre>
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* |R| Length |
* +-+-------------------------------------------------------------+
* |R| Observation Count |
* +-+-------------------------------------------------------------+
* |R| Last Observation Timestamp |
* | |
* +-+-------------------------------------------------------------+
* |R| First Observation Timestamp |
* | |
* +---------------------------------------------------------------+
* | UTF-8 Encoded Error ...
* ... |
* +---------------------------------------------------------------+
* </pre>
*/
public class DistinctErrorLog
{
/**
* Offset within a record at which the length field begins.
*/
public static final int LENGTH_OFFSET = 0;
/**
* Offset within a record at which the observation count field begins.
*/
public static final int OBSERVATION_COUNT_OFFSET = SIZE_OF_INT;
/**
* Offset within a record at which the last observation timestamp field begins.
*/
public static final int LAST_OBSERVATION_TIMESTAMP_OFFSET = OBSERVATION_COUNT_OFFSET + SIZE_OF_INT;
/**
* Offset within a record at which the first observation timestamp field begins.
*/
public static final int FIRST_OBSERVATION_TIMESTAMP_OFFSET = LAST_OBSERVATION_TIMESTAMP_OFFSET + SIZE_OF_LONG;
/**
* Offset within a record at which the encoded exception field begins.
*/
public static final int ENCODED_ERROR_OFFSET = FIRST_OBSERVATION_TIMESTAMP_OFFSET + SIZE_OF_LONG;
/**
* Alignment to be applied for record beginning.
*/
public static final int RECORD_ALIGNMENT = SIZE_OF_LONG;
private static final DistinctObservation INSUFFICIENT_SPACE = new DistinctObservation(null, 0);
private int nextOffset = 0;
private final EpochClock clock;
private final AtomicBuffer buffer;
private volatile DistinctObservation[] distinctObservations = new DistinctObservation[0];
private final Lock newObservationLock = new ReentrantLock();
/**
* Create a new error log that will be written to a provided {@link AtomicBuffer}.
*
* @param buffer into which the observation records are recorded.
* @param clock to be used for time stamping records.
*/
public DistinctErrorLog(final AtomicBuffer buffer, final EpochClock clock)
{
buffer.verifyAlignment();
this.clock = clock;
this.buffer = buffer;
}
/**
* Record an observation of an error. If it is the first observation of this error type for a stack trace
* then a new entry will be created. For subsequent observations of the same error type and stack trace a
* counter and time of last observation will be updated.
*
* @param observation to be logged as an error observation.
* @return true if successfully logged otherwise false if insufficient space remaining in the log.
*/
public boolean record(final Throwable observation)
{
final long timestamp = clock.time();
final DistinctObservation[] existingObservations = distinctObservations;
DistinctObservation existingObservation = find(existingObservations, observation);
if (null == existingObservation)
{
newObservationLock.lock();
try
{
existingObservation = newObservation(timestamp, existingObservations, observation);
if (INSUFFICIENT_SPACE == existingObservation)
{
return false;
}
}
finally
{
newObservationLock.unlock();
}
}
final int offset = existingObservation.offset;
buffer.getAndAddInt(offset + OBSERVATION_COUNT_OFFSET, 1);
buffer.putLongOrdered(offset + LAST_OBSERVATION_TIMESTAMP_OFFSET, timestamp);
return true;
}
private static DistinctObservation find(
final DistinctObservation[] existingObservations, final Throwable observation)
{
DistinctObservation existingObservation = null;
for (final DistinctObservation o : existingObservations)
{
if (equals(o.throwable, observation))
{
existingObservation = o;
break;
}
}
return existingObservation;
}
@SuppressWarnings("FinalParameters")
private static boolean equals(Throwable lhs, Throwable rhs)
{
while (true)
{
if (lhs == rhs)
{
return true;
}
if (lhs.getClass() == rhs.getClass() && equals(lhs.getStackTrace(), rhs.getStackTrace()))
{
lhs = lhs.getCause();
rhs = rhs.getCause();
if (null == lhs && null == rhs)
{
return true;
}
else if (null != lhs && null != rhs)
{
continue;
}
}
return false;
}
}
private static boolean equals(final StackTraceElement[] lhsStackTrace, final StackTraceElement[] rhsStackTrace)
{
if (lhsStackTrace.length != rhsStackTrace.length)
{
return false;
}
for (int i = 0, length = lhsStackTrace.length; i < length; i++)
{
final StackTraceElement lhs = lhsStackTrace[i];
final StackTraceElement rhs = rhsStackTrace[i];
if (lhs.getLineNumber() != rhs.getLineNumber() ||
!lhs.getClassName().equals(rhs.getClassName()) ||
!Objects.equals(lhs.getMethodName(), rhs.getMethodName()) ||
!Objects.equals(lhs.getFileName(), rhs.getFileName()))
{
return false;
}
}
return true;
}
private DistinctObservation newObservation(
final long timestamp, final DistinctObservation[] existingObservations, final Throwable observation)
{
DistinctObservation existingObservation = null;
if (existingObservations != distinctObservations)
{
existingObservation = find(distinctObservations, observation);
}
if (null == existingObservation)
{
final StringWriter stringWriter = new StringWriter();
observation.printStackTrace(new PrintWriter(stringWriter));
final byte[] encodedError = stringWriter.toString().getBytes(UTF_8);
final int length = ENCODED_ERROR_OFFSET + encodedError.length;
final int offset = nextOffset;
if ((offset + length) > buffer.capacity())
{
return INSUFFICIENT_SPACE;
}
buffer.putBytes(offset + ENCODED_ERROR_OFFSET, encodedError);
buffer.putLong(offset + FIRST_OBSERVATION_TIMESTAMP_OFFSET, timestamp);
nextOffset = align(offset + length, RECORD_ALIGNMENT);
existingObservation = new DistinctObservation(observation, offset);
distinctObservations = prepend(distinctObservations, existingObservation);
buffer.putIntOrdered(offset + LENGTH_OFFSET, length);
}
return existingObservation;
}
private static DistinctObservation[] prepend(
final DistinctObservation[] observations, final DistinctObservation observation)
{
final int length = observations.length;
final DistinctObservation[] newObservations = new DistinctObservation[length + 1];
newObservations[0] = observation;
System.arraycopy(observations, 0, newObservations, 1, length);
return newObservations;
}
public static final class DistinctObservation
{
public final Throwable throwable;
public final int offset;
public DistinctObservation(final Throwable throwable, final int offset)
{
this.throwable = throwable;
this.offset = offset;
}
}
}