Skip to content
Draft
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
1 change: 1 addition & 0 deletions agent/agent-tooling/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies {
annotationProcessor("com.google.auto.value:auto-value")

implementation("io.opentelemetry.contrib:opentelemetry-jfr-connection")
compileOnly("org.gradle.jfr.polyfill:jfr-polyfill:1.0.2")
implementation("com.azure:azure-storage-blob")

implementation(project(":agent:agent-profiler:agent-alerting-api"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1531,6 +1531,7 @@ public static class ProfilerConfiguration {
public boolean enableRequestTriggering = false;
public List<RequestTrigger> requestTriggerEndpoints = new ArrayList<>();
@Nullable public String cgroupPath = null;
@Nullable public String localProfilerConfigDir = null;
}

public static class GcEventConfiguration {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import javax.management.MBeanServerConnection;
import jdk.jfr.FlightRecorder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -65,6 +68,13 @@ public class Profiler {
private final RecordingConfiguration spanRecordingConfiguration;
private final RecordingConfiguration manualRecordingConfiguration;

// Events to force-disable regardless of .jfc configuration
private static final List<String> DISABLED_EVENTS = Arrays.asList(
"jdk.InitialSystemProperty",
"jdk.InitialEnvironmentVariable",
"jdk.InitialSecurityProperty",
"jdk.OldObjectSample");

private final File temporaryDirectory;

public Profiler(Configuration.ProfilerConfiguration config, File tempDir) {
Expand Down Expand Up @@ -194,6 +204,7 @@ private void executeProfile(

try {
newRecording.start();
disableEvents(newRecording.getId());

// schedule closing the recording
scheduledExecutorService.schedule(
Expand All @@ -210,6 +221,24 @@ private void executeProfile(
}
}

/**
* Disable configured events on the recording using the direct JFR API. This acts as a safety net
* to override .jfc settings that may have been misconfigured.
*/
@SuppressWarnings("Java8ApiChecker")
private static void disableEvents(long recordingId) {
FlightRecorder.getFlightRecorder().getRecordings().stream()
.filter(r -> r.getId() == recordingId)
.findFirst()
.ifPresent(
recording -> {
for (String event : DISABLED_EVENTS) {
recording.disable(event);
logger.debug("Disabled JFR event: {}", event);
}
});
}

/** When a profile has been created, upload it to service profiler. */
@SuppressWarnings(
"CatchingUnchecked") // catching unchecked exception is necessary for proper error handling
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.microsoft.applicationinsights.agent.internal.diagnostics.SdkVersionFinder;
import com.microsoft.applicationinsights.agent.internal.httpclient.LazyHttpClient;
import com.microsoft.applicationinsights.agent.internal.profiler.config.ConfigService;
import com.microsoft.applicationinsights.agent.internal.profiler.config.LocalProfilerConfigService;
import com.microsoft.applicationinsights.agent.internal.profiler.config.ProfilerConfiguration;
import com.microsoft.applicationinsights.agent.internal.profiler.service.ServiceProfilerClient;
import com.microsoft.applicationinsights.agent.internal.profiler.triggers.AlertConfigParser;
Expand All @@ -29,6 +30,7 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -59,6 +61,7 @@ public class ProfilingInitializer {
private HttpPipeline httpPipeline;
private ScheduledExecutorService serviceProfilerExecutorService;
private ServiceProfilerClient serviceProfilerClient;
@Nullable private LocalProfilerConfigService localConfigService;
//////////////////////////////////////////////////////////

private PerformanceMonitoringService performanceMonitoringService;
Expand Down Expand Up @@ -142,6 +145,14 @@ private synchronized void performInit() {
httpPipeline,
userAgent);

if (configuration.localProfilerConfigDir != null) {
File localConfigDir = new File(configuration.localProfilerConfigDir);
localConfigService = new LocalProfilerConfigService(localConfigDir);
logger.info(
"Local profiler configuration directory configured: {}",
localConfigDir.getAbsolutePath());
}

// Monitor service remains alive permanently due to scheduling an periodic config pull
startPollingForConfigUpdates();
}
Expand All @@ -157,12 +168,27 @@ private void startPollingForConfigUpdates() {

private void pullProfilerSettings(ConfigService configService) {
try {
// Local config takes precedence over remote when the local file is present
if (localConfigService != null && localConfigService.isLocalConfigPresent()) {
localConfigService
.pullSettings()
.subscribe(this::applyConfiguration, ProfilingInitializer::logLocalConfigError);
return;
}

configService.pullSettings().subscribe(this::applyConfiguration, this::logProfilerPullError);
} catch (Throwable t) {
logProfilerPullError(t);
}
}

private static void logLocalConfigError(Throwable e) {
logger.error(
"Error reading local profiler configuration. "
+ "Fix or remove the file to restore normal operation.",
e);
}

private void logProfilerPullError(Throwable e) {
if (currentlyEnabled.get()) {
logger.error("Error pulling service profiler settings", e);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.microsoft.applicationinsights.agent.internal.profiler.config;

import com.azure.json.JsonProviders;
import com.azure.json.JsonReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;

/**
* Checks a local directory for a profiler configuration file. If a valid file exists, it takes
* precedence over the remote Azure service profiler configuration.
*
* <p>The configuration file must be named {@code profiler-config.json} and use the same JSON format
* as {@link ProfilerConfiguration}.
*/
public class LocalProfilerConfigService {

private static final Logger logger = LoggerFactory.getLogger(LocalProfilerConfigService.class);

static final String CONFIG_FILE_NAME = "profiler-config.json";

private final File configFile;
private volatile long lastModifiedTime;

public LocalProfilerConfigService(File configDir) {
this.configFile = new File(configDir, CONFIG_FILE_NAME);
this.lastModifiedTime = 0;
}

/**
* Checks for a local profiler configuration file. Returns a configuration if the file exists and
* has been modified since the last check. Returns empty if no file exists or if it has not
* changed.
*
* @return Mono containing the configuration if changed, or Mono.error if the file is malformed
*/
public Mono<ProfilerConfiguration> pullSettings() {
if (!configFile.exists()) {
return Mono.empty();
}

long fileLastModified = configFile.lastModified();
if (fileLastModified == lastModifiedTime) {
// File has not changed since last successful read
return Mono.empty();
}

try {
ProfilerConfiguration config = readConfigFile();
logger.info(
"Successfully read local profiler configuration from: {}", configFile.getAbsolutePath());

// Delete the file after successful read - this is a one-shot configuration mechanism
if (!configFile.delete()) {
logger.warn(
"Failed to delete local profiler configuration file after reading: {}",
configFile.getAbsolutePath());
} else {
logger.info(
"Deleted local profiler configuration file after successful read: {}",
configFile.getAbsolutePath());
}

lastModifiedTime = fileLastModified;
return Mono.just(config);
} catch (Exception e) {
logger.error(
"Failed to parse local profiler configuration file: {}. "
+ "Fix or remove the file to restore normal operation.",
configFile.getAbsolutePath(),
e);
return Mono.error(e);
}
}

/**
* Returns true if a local config file is present (regardless of whether it has changed), meaning
* local override mode is active.
*/
public boolean isLocalConfigPresent() {
return configFile.exists();
}

private ProfilerConfiguration readConfigFile() throws IOException {
try (Reader reader =
new InputStreamReader(new FileInputStream(configFile), StandardCharsets.UTF_8);
JsonReader jsonReader = JsonProviders.createReader(reader)) {

ProfilerConfiguration config = ProfilerConfiguration.fromJson(jsonReader);

// Default lastModified from file timestamp if not provided
if (config.getLastModified() == null
|| config.getLastModified().compareTo(ProfilerConfiguration.DEFAULT_DATE) == 0) {
config.setLastModified(new Date(configFile.lastModified()));
}

// Default enabledLastModified if not set
if (config.getEnabledLastModified() == null) {
config.setEnabledLastModified(config.getLastModified());
}

// Default requestTriggerConfiguration to empty list if null
if (config.getRequestTriggerConfiguration() == null) {
config.setRequestTriggerConfiguration(new ArrayList<>());
}

return config;
}
}

// visible for testing
@Nullable
File getConfigFile() {
return configFile;
}
}
Loading