Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ private static void run(final StatsDClientManager statsDClientManager, final Con
.refreshBeansPeriod(refreshBeansPeriod)
.globalTags(globalTags)
.reporter(reporter)
.jmxfetchTelemetry(config.isTelemetryJmxEnabled())
.connectionFactory(new AgentConnectionFactory());

if (config.isJmxFetchMultipleRuntimeServicesEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ public final class GeneralConfig {
public static final String TELEMETRY_DEPENDENCY_RESOLUTION_QUEUE_SIZE =
"telemetry.dependency-resolution.queue.size";
public static final String TELEMETRY_DEBUG_REQUESTS_ENABLED = "telemetry.debug.requests.enabled";
public static final String TELEMETRY_JMX_ENABLED = "telemetry.jmx.enabled";
public static final String AGENTLESS_LOG_SUBMISSION_ENABLED = "agentless.log.submission.enabled";
public static final String AGENTLESS_LOG_SUBMISSION_QUEUE_SIZE =
"agentless.log.submission.queue.size";
Expand Down
8 changes: 8 additions & 0 deletions internal-api/src/main/java/datadog/trace/api/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@
import static datadog.trace.api.config.GeneralConfig.TELEMETRY_DEPENDENCY_RESOLUTION_QUEUE_SIZE;
import static datadog.trace.api.config.GeneralConfig.TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL;
import static datadog.trace.api.config.GeneralConfig.TELEMETRY_HEARTBEAT_INTERVAL;
import static datadog.trace.api.config.GeneralConfig.TELEMETRY_JMX_ENABLED;
import static datadog.trace.api.config.GeneralConfig.TELEMETRY_LOG_COLLECTION_ENABLED;
import static datadog.trace.api.config.GeneralConfig.TELEMETRY_METRICS_ENABLED;
import static datadog.trace.api.config.GeneralConfig.TELEMETRY_METRICS_INTERVAL;
Expand Down Expand Up @@ -1243,6 +1244,7 @@ public static String getHostName() {
private final boolean isTelemetryDependencyServiceEnabled;
private final boolean telemetryMetricsEnabled;
private final boolean isTelemetryLogCollectionEnabled;
private final boolean isTelemetryJmxEnabled;
private final int telemetryDependencyResolutionQueueSize;

private final boolean azureAppServices;
Expand Down Expand Up @@ -2176,6 +2178,8 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment())
&& configProvider.getBoolean(
TELEMETRY_LOG_COLLECTION_ENABLED, DEFAULT_TELEMETRY_LOG_COLLECTION_ENABLED);

isTelemetryJmxEnabled = configProvider.getBoolean(TELEMETRY_JMX_ENABLED, false);

isTelemetryDependencyServiceEnabled =
configProvider.getBoolean(
TELEMETRY_DEPENDENCY_COLLECTION_ENABLED,
Expand Down Expand Up @@ -3748,6 +3752,10 @@ public boolean isTelemetryLogCollectionEnabled() {
return isTelemetryLogCollectionEnabled;
}

public boolean isTelemetryJmxEnabled() {
return isTelemetryJmxEnabled;
}

public int getTelemetryDependencyResolutionQueueSize() {
return telemetryDependencyResolutionQueueSize;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
Expand Down Expand Up @@ -67,6 +69,19 @@ public static void addReporter(Reporter reporter) {
reporters.put(reporter.getClass(), reporter);
}

public static Collection<Reporter> getReporters() {
return Collections.unmodifiableCollection(reporters.values());
}

public static Reporter getReporter(String className) {
for (Reporter reporter : getReporters()) {
if (reporter.getClass().getName().equals(className)) {
return reporter;
}
}
return null;
}

public static void addText(ZipOutputStream zip, String section, String text) throws IOException {
zip.putNextEntry(new ZipEntry(section));
if (null != text) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,34 @@ class TracerFlareTest extends DDSpecification {
/^(java.lang.IllegalStateException: (bin|txt) \(expected\)\n){2}$/
}

def "test getReporter finds reporter by class name"() {
setup:
def reporter1 = Mock(Reporter1)
def reporter2 = Mock(Reporter2)
TracerFlare.addReporter(reporter1)
TracerFlare.addReporter(reporter2)

when:
def found1 = TracerFlare.getReporter(reporter1.getClass().getName())
def found2 = TracerFlare.getReporter(reporter2.getClass().getName())

then:
found1 == reporter1
found2 == reporter2
}

def "test getReporter returns null for non-existent reporter"() {
setup:
def reporter = Mock(Reporter1)
TracerFlare.addReporter(reporter)

when:
def found = TracerFlare.getReporter("com.example.NonExistentReporter")

then:
found == null
}

def buildAndExtractZip() {
TracerFlare.prepareForFlare()
def out = new ByteArrayOutputStream()
Expand Down
8 changes: 8 additions & 0 deletions metadata/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -3737,6 +3737,14 @@
"aliases": []
}
],
"DD_TELEMETRY_JMX_ENABLED": [
{
"version": "A",
"type": "boolean",
"default": "false",
"aliases": []
}
],
"DD_TELEMETRY_LOG_COLLECTION_ENABLED": [
{
"version": "A",
Expand Down
3 changes: 3 additions & 0 deletions utils/flare-utils/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ dependencies {
implementation(project(":utils:version-utils"))
implementation(project(":internal-api"))
implementation(libs.slf4j)

testImplementation(project(":utils:test-utils"))
testImplementation(project(":dd-trace-api"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package datadog.flare;

import datadog.trace.api.Config;
import datadog.trace.api.flare.TracerFlare;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* MBean implementation for managing and accessing tracer flare data.
*
* <p>This class provides JMX operations to list flare data sources and retrieve flare data either
* for individual sources or a complete flare archive. See {@link TracerFlareManagerMBean} for
* documentation on the exposed operations.
*/
public class TracerFlareManager implements TracerFlareManagerMBean {
private static final Logger LOGGER = LoggerFactory.getLogger(TracerFlareManager.class);

private final TracerFlareService flareService;
protected ObjectName mbeanName;

public TracerFlareManager(TracerFlareService flareService) {
this.flareService = flareService;
}

@Override
public String generateFullFlareZip() throws IOException {
TracerFlare.prepareForFlare();

long currentMillis = System.currentTimeMillis();
boolean dumpThreads = Config.get().isTriageEnabled() || LOGGER.isDebugEnabled();
byte[] zipBytes = flareService.buildFlareZip(currentMillis, currentMillis, dumpThreads);
return Base64.getEncoder().encodeToString(zipBytes);
}

@Override
public String listFlareFiles() throws IOException {
TracerFlare.prepareForFlare();

StringBuilder result = new StringBuilder();

for (Map.Entry<String, String[]> entry : TracerFlareService.BUILT_IN_SOURCES.entrySet()) {
String sourceName = entry.getKey();
String[] files = entry.getValue();

for (String filename : files) {
result.append(sourceName).append(" ").append(filename).append("\n");
}
}

for (TracerFlare.Reporter reporter : TracerFlare.getReporters()) {
try (ByteArrayOutputStream bytes = new ByteArrayOutputStream();
ZipOutputStream zip = new ZipOutputStream(bytes)) {
reporter.addReportToFlare(zip);
zip.finish();

try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes.toByteArray());
ZipInputStream zis = new ZipInputStream(bais)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
result
.append(reporter.getClass().getName())
.append(" ")
.append(entry.getName())
.append("\n");
zis.closeEntry();
}
}
} catch (IOException e) {
LOGGER.debug("Failed to inspect reporter {}", reporter.getClass().getName(), e);
}
}

return result.toString();
}

@Override
public String getFlareFile(String sourceName, String filename) throws IOException {
final byte[] zipBytes;
if (isBuiltInSource(sourceName)) {
zipBytes = flareService.getBuiltInSourceZip(sourceName);
} else {
zipBytes = getReporterFile(sourceName);
}
return extractFileFromZip(zipBytes, filename);
}

private boolean isBuiltInSource(String sourceName) {
return TracerFlareService.BUILT_IN_SOURCES.containsKey(sourceName);
}

/**
* Generates flare data for a specific reporter.
*
* <p>The reporter's data is generated as a ZIP file, and the specified filename is extracted. If
* the file is text, it is returned as plain text; if binary, it is returned base64-encoded.
*
* @param reporterClassName the fully qualified class name of the reporter
* @return the zip file containing the reporter's content
* @throws IOException if an error occurs while generating the flare
*/
private byte[] getReporterFile(String reporterClassName) throws IOException {
TracerFlare.Reporter reporter = TracerFlare.getReporter(reporterClassName);
if (reporter == null) {
throw new IOException("Error: Reporter not found: " + reporterClassName);
}

reporter.prepareForFlare();

try (ByteArrayOutputStream bytes = new ByteArrayOutputStream();
ZipOutputStream zip = new ZipOutputStream(bytes)) {
reporter.addReportToFlare(zip);
zip.finish();
return bytes.toByteArray();
}
}

/**
* Extracts a specific file from a ZIP archive.
*
* <p>Searches through the ZIP entries for the specified filename and returns its content. If the
* file name ends in ".txt", it is returned as plain text; if binary, it is returned
* base64-encoded.
*
* @param zipBytes the ZIP file bytes
* @param filename the name of the file to extract
* @return the file content (plain text or base64-encoded binary)
* @throws IOException if an error occurs while reading the ZIP
*/
private String extractFileFromZip(byte[] zipBytes, String filename) throws IOException {
try (ByteArrayInputStream bais = new ByteArrayInputStream(zipBytes);
ZipInputStream zis = new ZipInputStream(bais)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.getName().equals(filename)) {
ByteArrayOutputStream content = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = zis.read(buffer)) != -1) {
content.write(buffer, 0, bytesRead);
}
zis.closeEntry();

byte[] contentBytes = content.toByteArray();
if (entry.getName().endsWith(".txt")) {
return new String(contentBytes, StandardCharsets.UTF_8);
} else {
return Base64.getEncoder().encodeToString(contentBytes);
}
}
zis.closeEntry();
}

throw new IOException("Failed to extract file: " + filename);
}
}

void registerMBean() {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();

try {
mbeanName = new ObjectName("datadog.flare:type=TracerFlare");
mbs.registerMBean(this, mbeanName);
LOGGER.info("Registered TracerFlare MBean at {}", mbeanName);
} catch (MalformedObjectNameException
| InstanceAlreadyExistsException
| MBeanRegistrationException
| NotCompliantMBeanException e) {
LOGGER.warn("Failed to register TracerFlare MBean", e);
mbeanName = null;
}
}

void unregisterMBean() {
if (mbeanName != null) {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
try {
mbs.unregisterMBean(mbeanName);
LOGGER.debug("Unregistered TracerFlare MBean");
} catch (Exception e) {
LOGGER.warn("Failed to unregister TracerFlare MBean", e);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package datadog.flare;

import java.io.IOException;

/**
* MBean interface for managing and accessing tracer flare data.
*
* <p>This interface provides JMX operations to inspect flare data sources and generate flare data
* either for individual sources or as a complete ZIP archive. Sources include both registered
* reporters and built-in data (config, runtime, flare prelude, etc.).
*/
public interface TracerFlareManagerMBean {
/**
* Lists all available flare files from all sources.
*
* <p>Returns a newline-separated string where each line is formatted as "&lt;source&gt;
* &lt;file&gt;". This format makes it easy to pass the source and filename to {@link
* #getFlareFile(String, String)}.
*
* <p>Example output:
*
* <pre>
* config initial_config.txt
* ...
* datadog.trace.agent.core.CoreTracer tracer_health.txt
* ...
* </pre>
*
* @return newline-separated string listing all available files and their source name
* @throws IOException if an error occurs
*/
String listFlareFiles() throws IOException;

/**
* Returns a specific flare file by source name and filename.
*
* <p>If the file is text, it is returned as plain text; if binary, it is returned base64-encoded.
*
* @param sourceName the name of the source (reporter class name or built-in source name)
* @param filename the name of the file to retrieve
* @return the file content (plain text or base64-encoded binary)
* @throws IOException if an error occurs while generating or extracting the data
*/
String getFlareFile(String sourceName, String filename) throws IOException;

/**
* Generates a complete tracer flare as a ZIP file.
*
* @return base64-encoded ZIP file containing the complete flare data
* @throws IOException if an error occurs while generating the flare ZIP
*/
String generateFullFlareZip() throws IOException;
}
Loading