Skip to content

Report internal exceptions with telemetry - #4521

Closed
DDJavierSantos wants to merge 5 commits into
masterfrom
jsantos/telemetry_error_reporting
Closed

Report internal exceptions with telemetry#4521
DDJavierSantos wants to merge 5 commits into
masterfrom
jsantos/telemetry_error_reporting

Conversation

@DDJavierSantos

@DDJavierSantos DDJavierSantos commented Jan 10, 2023

Copy link
Copy Markdown
Contributor

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) or dd.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) or dd.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:

  1. The line contains an exception, e.g. logger.error("there was an exception", throwable).
  2. The line doesn't contain an exception but the Logger was called with the marker 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.

@smola smola added comp: telemetry Telemetry tag: do not merge Do not merge changes labels Jan 11, 2023
@manuel-alvarez-alvarez manuel-alvarez-alvarez added the comp: asm iast Application Security Management (IAST) label Jan 11, 2023
@DDJavierSantos DDJavierSantos changed the title Implementation of Telemetry API Exception reporting Implementation of Telemetry API Exception and log reporting Jan 11, 2023
@DDJavierSantos DDJavierSantos changed the title Implementation of Telemetry API Exception and log reporting Implementation of Telemetry API Exception and Log reporting Jan 11, 2023
@DDJavierSantos
DDJavierSantos force-pushed the jsantos/telemetry_error_reporting branch from 7d1c198 to acf6ffb Compare January 12, 2023 11:53
@smola smola changed the title Implementation of Telemetry API Exception and Log reporting Report internal exceptions with telemetry Jan 18, 2023
@smola smola removed the comp: asm iast Application Security Management (IAST) label Jan 18, 2023
@mcculls

mcculls commented Jan 18, 2023

Copy link
Copy Markdown
Contributor

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:

No instances of SomeClass are allowed in the image heap as this class should be initialized at image runtime

then there are 2 options - one is to use !Platform.isNativeImageBuilder() to guard code we never expect to run during native image. That is useful to eliminate large chunks of code which are inapplicable to native-image, but won't work if the class is still being touched from startup code (in other words it depends how the class is being used).

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 !Platform.isNativeImageBuilder() is preferred.

In this case it's the new reference to LogCollector from DDLogger via the new sendToTelemetry method that is pulling the class in - it might be enough to check !Platform.isNativeImageBuilder() early enough so the native-image builder doesn't see LogCollector.

@DDJavierSantos
DDJavierSantos force-pushed the jsantos/telemetry_error_reporting branch from 3b52aa6 to 15e8ed0 Compare January 19, 2023 11:56
Comment thread telemetry/src/main/java/datadog/telemetry/TelemetrySystem.java Outdated
Comment thread telemetry/src/main/java/datadog/telemetry/api/LogTelemetry.java Outdated
@DDJavierSantos
DDJavierSantos requested a review from smola January 19, 2023 17:05
@DDJavierSantos
DDJavierSantos marked this pull request as ready for review January 19, 2023 20:17
@DDJavierSantos
DDJavierSantos requested a review from a team as a code owner January 19, 2023 20:17
Comment thread telemetry/src/main/java/datadog/telemetry/TelemetryServiceImpl.java Outdated
Comment thread internal-api/src/main/java/datadog/trace/api/telemetry/LogCollector.java Outdated
@DDJavierSantos
DDJavierSantos requested a review from smola January 20, 2023 11:45
@DDJavierSantos
DDJavierSantos force-pushed the jsantos/telemetry_error_reporting branch from 24ee061 to 23d455e Compare January 24, 2023 11:19
@DDJavierSantos
DDJavierSantos force-pushed the jsantos/telemetry_error_reporting branch from 23d455e to 3795fe4 Compare January 25, 2023 16:55
@dougqh

dougqh commented Jan 27, 2023

Copy link
Copy Markdown
Contributor

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.
By which I mean...
if ( logger.isDebugEnabled() ) { logger.debug("myMessage {} {}", foo.getX(), foo.doExpensiveThing()); }

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.

logger.debug("myMessage {} {}", foo, (foo) -> return new Object[] { foo.getX(), foo.doExpensiveThing() });

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...
logger.on(MY_EVENT, foo)

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(

@manuel-alvarez-alvarez manuel-alvarez-alvarez Feb 1, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 overflowedEntryCount
  • LinkedHashSet<TelemetryLogEntry> logEntries ➡️ Set<TelemetryLogEntry> logEntries = ConcurrentHashMap.newKeySet();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

if (null == msg && null == format) {
return;
}
if (logEntries.size() >= MAX_ENTRIES) {

@manuel-alvarez-alvarez manuel-alvarez-alvarez Feb 1, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the extra field if already present at InstrumenterConfig?

import org.slf4j.helpers.MessageFormatter;

public class LogCollector {
public static final int MAX_ENTRIES = 10000;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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())) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: yoda condition, no reason in Java to reversed the condition against null

}

boolean isDataDogCode(Throwable t) {
String firstStackElementClassName = t.getStackTrace()[0].getClassName();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Comment on lines +22 to +24
public static class Holder {
public static final LogCollector INSTANCE = new LogCollector();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think the Holder pattern here is useless as creating an instance of LogCollector is not expensive

Comment on lines +15 to +19
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."};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we just use the primitive int for maxItems?

if (!this.hasReferences()) {
return this.getName();
} else {
Iterator<Marker> it = this.iterator();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not referencing directly referenceList and using a for each?

@smola

smola commented Mar 31, 2023

Copy link
Copy Markdown
Member

Let's try to move this forward. Other than the commands that are still not resolved (and should) and the pending rebase:

  • Drop dd.instrumentation.telemetry.debug and its associated logic. I think the subset we should start with is: either has an exception attached, or it has a SEND_TELEMETRY marker.
  • dd.instrumentation.telemetry.log-collection.enabled -> dd.telemetry.log-collection.enabled

@jandro996
jandro996 removed their request for review July 7, 2023 10:20
@smola

smola commented Oct 25, 2023

Copy link
Copy Markdown
Member

Superseded by #6057

@smola smola closed this Oct 25, 2023
@smola
smola deleted the jsantos/telemetry_error_reporting branch October 25, 2023 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: telemetry Telemetry tag: do not merge Do not merge changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants