Report internal exceptions with telemetry - #4521
Conversation
7d1c198 to
acf6ffb
Compare
|
FYI, if you have a new bootstrap class which is referenced during startup and you see this kind of error from the Graal smoke tests: then there are 2 options - one is to use The other option is to add it to the list of build-time classes in https://github.com/DataDog/dd-trace-java/blob/master/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java#L64 - ideally we want to list as few classes as possible there (especially since you then need to list any classes they depend on, etc...) so this should be a last resort. Excluding the feature via In this case it's the new reference to |
3b52aa6 to
15e8ed0
Compare
24ee061 to
23d455e
Compare
23d455e to
3795fe4
Compare
|
I've only skimmed this, but it seems like a reasonable minimally invasive approach. But as pointed out in conversation, I think there is challenge with cases that use guarded logging today. I'll admit I've never been a huge fan of that idiom because it seems like we shouldn't be doing "doExpensiveThing". My far more invasive way of solving this is to introduce a helper or event object that does the expensive work.
The idea here is ti use a non-capturing lambda that is only called when needed. With the hope that calling debug all the time is okay because the passing of format, foo, and the lambda are all by reference and cheap, Although, that's just my idea for something that kind of fits into standard logging. I have a crazy notion of having some helper event class that handles the details. The end call would be something like... But I feel creating those Event objects likely requires too much ceremony. |
…Code from payload, log confirmation when Log Collection is enabled
| return debugEnabled; | ||
| } | ||
|
|
||
| public synchronized void addLogEntry( |
There was a problem hiding this comment.
Using synchronized for this method might be too drastic, have you give it a thought about using thread safe data structures like:
int overflowedEntryCount➡️AtomicInteger overflowedEntryCountLinkedHashSet<TelemetryLogEntry> logEntries➡️Set<TelemetryLogEntry> logEntries = ConcurrentHashMap.newKeySet();
| if (null == msg && null == format) { | ||
| return; | ||
| } | ||
| if (logEntries.size() >= MAX_ENTRIES) { |
There was a problem hiding this comment.
It worries me a bit that there is no priority between log entries, I'll prefer not losing ERROR ones in a very noisy scenario.
|
|
||
| private final boolean iastDeduplicationEnabled; | ||
|
|
||
| private final boolean telemetryEnabled; |
There was a problem hiding this comment.
Why the extra field if already present at InstrumenterConfig?
| import org.slf4j.helpers.MessageFormatter; | ||
|
|
||
| public class LogCollector { | ||
| public static final int MAX_ENTRIES = 10000; |
There was a problem hiding this comment.
Looks weird to me having a collector with room for 10.000 entries that will be capped by the telemetry service which only has room for 1.024 (private final BlockingQueue<TelemetryLogEntry> logEntries = new LinkedBlockingQueue<>(1024);)
| if (null != t || marker == LogCollector.SEND_TELEMETRY) { | ||
| if (level == LogLevel.WARN | ||
| || level == LogLevel.ERROR | ||
| || (level == LogLevel.DEBUG && LogCollector.get().isDebugEnabled())) { |
There was a problem hiding this comment.
What about LogLevel.INFO? in Node we are treating them as DEBUG for telemetry purposes
| StringBuilder stackTrace = new StringBuilder(); | ||
| stackTrace.append(t.getClass().getCanonicalName()); | ||
|
|
||
| String firstStackElementClassName = t.getStackTrace()[0].getClassName(); |
There was a problem hiding this comment.
blocker: it's possible that the stacktrace returned is empty so you should guard aginst this
| if (null != msg) { | ||
| log.setMessage(msg); | ||
| } else { | ||
| if (null != format) { |
There was a problem hiding this comment.
nit: yoda condition, no reason in Java to reversed the condition against null
| } | ||
|
|
||
| boolean isDataDogCode(Throwable t) { | ||
| String firstStackElementClassName = t.getStackTrace()[0].getClassName(); |
| public static class Holder { | ||
| public static final LogCollector INSTANCE = new LogCollector(); | ||
| } |
There was a problem hiding this comment.
nit: I think the Holder pattern here is useless as creating an instance of LogCollector is not expensive
| private static int overflowedEntryCount = 0; | ||
| private static boolean enabled = false; | ||
| public static boolean debugEnabled = false; | ||
| private static LinkedHashSet<TelemetryLogEntry> logEntries = new LinkedHashSet<>(); | ||
| private static String[] packageList = {"datadog.", "com.datadog.", "java.", "javax."}; |
There was a problem hiding this comment.
why all the fields are static? as all the methods are instance method and we are creating a singleton?
| return true; | ||
| } else { | ||
| if (this.hasReferences()) { | ||
| Iterator var2 = this.referenceList.iterator(); |
There was a problem hiding this comment.
use Iterator<Marker> var2 this will avoid casting below
also use the for each:
for (Marker ref : referenceList) {
if (ref.contains(other)) {
return true;
}
}
| public class TelemetryMarker implements Marker { | ||
| private static final long serialVersionUID = -2849567615646933777L; | ||
| private final String name; | ||
| private List<Marker> referenceList = new CopyOnWriteArrayList(); |
There was a problem hiding this comment.
blocker: why using a list for that instead of a HashSet?
order is important?
| private final Application application; | ||
| private final Host host; | ||
| private final String runtimeId; | ||
| private static final Config config = Config.get(); |
There was a problem hiding this comment.
sounds useless, get method just returns the singleton instance
|
|
||
| private static <T> List<T> drainOrDefault(BlockingQueue<T> srcQueue, List<T> defaultList) { | ||
| private static <T> List<T> drainOrDefault( | ||
| BlockingQueue<T> srcQueue, List<T> defaultList, Integer maxItems) { |
There was a problem hiding this comment.
can we just use the primitive int for maxItems?
| if (!this.hasReferences()) { | ||
| return this.getName(); | ||
| } else { | ||
| Iterator<Marker> it = this.iterator(); |
There was a problem hiding this comment.
why not referencing directly referenceList and using a for each?
|
Let's try to move this forward. Other than the commands that are still not resolved (and should) and the pending rebase:
|
|
Superseded by #6057 |
What Does This Do
Implements exception reporting using the Telemetry API. Implements a marker that can be used to send debug lines to the telemetry API if the marker is used as a parameter in the debug call.
Motivation
To collect reports about exceptions in client's installations. This will make support cycles easier and ensure we know about potentially silent issues in libraries.
Additional Notes
New configuration:
DD_TELEMETRY_LOG_COLLECTION_ENABLED(environment variable) ordd.instrumentation.telemetry.log-collection.enabled(system property): This boolean flag (true,false) enables log collection by telemetry for log lines with level ERROR and WARN. It is disabled by default.DD_TELEMETRY_DEBUG_ENABLED(environment variable) ordd.instrumentation.telemetry.debug(system property): This boolean flag (true,false) enables log collection for log lines with level DEBUG. It is disabled by default.Additional conditions for log lines to be sent to telemetry:
DDLogger.SEND_TELEMETRY, e.g.logger.error(DDLogger.SEND_TELEMETRY, "message sent to telemetry").Log lines are de-duplicated during the telemetry heartbeat interval (default: 1 minute).
There is a limit in the number of log lines collected during a telemetry heartbeat interval, currently set to 10000 so we stay bounded. If we hit the limit, any extra lines are dropped silently.