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
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.microsoft.applicationinsights.alerting.config;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;

/**
* Configuration for the file-based manual profile trigger.
*
* <p>When enabled, the alerting subsystem periodically checks for the existence of a trigger file.
* If the file exists and was recently modified (within {@link #MANUAL_TRIGGER_FILE_MAX_AGE_MS}), it
* is deleted and a manual profile recording is initiated.
*
* <p>This provides an operator-friendly mechanism for triggering on-demand profiles without
* requiring JMX access – simply {@code touch} the trigger file from a shell or orchestration tool.
*/
public class AlertingProfileFileTriggerConfiguration {

/**
* Maximum age (in milliseconds) of the trigger file for it to be considered valid. Files older
* than this threshold are ignored to prevent stale trigger files from initiating unexpected
* recordings (e.g., after a restart).
*/
public static final long MANUAL_TRIGGER_FILE_MAX_AGE_MS = 60_000; // 1 minute

// Whether the file-based manual trigger is enabled.
private final boolean enabled;

// Path to the file that triggers a manual profile when created/touched.
// If relative, it is resolved against the agent's temp directory.
private final File filePath;

/** The default duration (in seconds) used for profiles triggered via this file mechanism. */
private final int defaultProfileDurationSeconds;

private AlertingProfileFileTriggerConfiguration(
boolean enabled, File filePath, int defaultProfileDurationSeconds) {
this.enabled = enabled;
this.filePath = filePath;
this.defaultProfileDurationSeconds = defaultProfileDurationSeconds;
}

/**
* Creates a file trigger configuration, resolving relative paths against the provided temp
* directory.
*
* @param enabled whether file-based triggering is active
* @param filePath path to the trigger file (absolute or relative to {@code tempDir})
* @param defaultProfileDurationSeconds duration in seconds for the profile if no override is
* configured in the collection plan
* @param tempDir base directory used to resolve relative file paths
* @return a fully resolved configuration instance
*/
@SuppressFBWarnings(
value = "SECPTI",
justification = "File path is set by trusted user configuration (applicationinsights.json)")
public static AlertingProfileFileTriggerConfiguration create(
boolean enabled, String filePath, int defaultProfileDurationSeconds, File tempDir) {

File manualTriggerFile = new File(filePath);
if (!manualTriggerFile.isAbsolute()) {
manualTriggerFile = new File(tempDir, filePath);
}

return new AlertingProfileFileTriggerConfiguration(
enabled, manualTriggerFile, defaultProfileDurationSeconds);
}

/** Creates a disabled configuration suitable for tests. */
public static AlertingProfileFileTriggerConfiguration createDefault() {
return new AlertingProfileFileTriggerConfiguration(
false, new File("applicationinsights-agent-profile-trigger"), 120);
}

/** Returns the default profile duration in seconds for file-triggered recordings. */
public int getDefaultProfileDurationSeconds() {
return defaultProfileDurationSeconds;
}

/** Returns the resolved path of the trigger file. */
public File getFilePath() {
return filePath;
}

/** Returns whether the file-based manual trigger is enabled. */
public boolean isEnabled() {
return enabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
import com.microsoft.applicationinsights.alerting.config.AlertConfiguration;
import com.microsoft.applicationinsights.alerting.config.AlertMetricType;
import com.microsoft.applicationinsights.alerting.config.AlertingConfiguration;
import com.microsoft.applicationinsights.alerting.config.AlertingProfileFileTriggerConfiguration;
import com.microsoft.applicationinsights.alerting.config.CollectionPlanConfiguration;
import com.microsoft.applicationinsights.alerting.config.CollectionPlanConfiguration.EngineMode;
import com.microsoft.applicationinsights.alerting.config.DefaultConfiguration;
import java.io.File;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
Expand All @@ -33,7 +35,6 @@
public class AlertingSubsystem {

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

// Downstream observer of alerts produced by the alerting system
private final Consumer<AlertBreach> alertHandler;

Expand All @@ -46,25 +47,40 @@ public class AlertingSubsystem {
// Current configuration of the alerting subsystem
private AlertingConfiguration alertConfig;

private boolean enableRequestTriggerUpdates;
/** Configuration controlling the file-based manual profile trigger. */
private final AlertingProfileFileTriggerConfiguration alertingProfileFileTriggerConfiguration;

protected AlertingSubsystem(Consumer<AlertBreach> alertHandler) {
this(alertHandler, TimeSource.DEFAULT, false);
}
private boolean enableRequestTriggerUpdates;

protected AlertingSubsystem(
Consumer<AlertBreach> alertHandler,
TimeSource timeSource,
boolean enableRequestTriggerUpdates) {
boolean enableRequestTriggerUpdates,
AlertingProfileFileTriggerConfiguration alertingProfileFileTriggerConfiguration) {
this.alertHandler = alertHandler;
this.alertPipelines = new AlertPipelines(alertHandler);
this.timeSource = timeSource;
this.enableRequestTriggerUpdates = enableRequestTriggerUpdates;
this.alertingProfileFileTriggerConfiguration = alertingProfileFileTriggerConfiguration;
}

/**
* Creates and initializes an {@link AlertingSubsystem} with an initially-disabled configuration.
*
* @param alertHandler downstream consumer that handles generated alert breaches
* @param timeSource time source used for alert evaluation windows
* @param alertingProfileFileTriggerConfiguration configuration for the file-based manual trigger
* @return a fully initialized alerting subsystem ready to receive configuration updates
*/
public static AlertingSubsystem create(
Consumer<AlertBreach> alertHandler, TimeSource timeSource) {
AlertingSubsystem alertingSubsystem = new AlertingSubsystem(alertHandler, timeSource, true);
Consumer<AlertBreach> alertHandler,
TimeSource timeSource,
AlertingProfileFileTriggerConfiguration alertingProfileFileTriggerConfiguration) {

AlertingSubsystem alertingSubsystem =
new AlertingSubsystem(
alertHandler, timeSource, true, alertingProfileFileTriggerConfiguration);

// init with disabled config
alertingSubsystem.initialize(
AlertingConfiguration.create(
Expand Down Expand Up @@ -145,8 +161,17 @@ private void updateRequestPipelineConfig(
}
}

/** Determine if a manual alert has been requested. */
/**
* Determine if a manual alert has been requested via any supported mechanism. Currently evaluates
* both the server-side collection plan and the local file-based trigger.
*/
private void evaluateManualTrigger(AlertingConfiguration alertConfig) {
evaluateCollectionPlanTrigger(alertConfig);
evaluateFileTrigger();
}

/** Check if the collection plan configuration requests a manual profile. */
private void evaluateCollectionPlanTrigger(AlertingConfiguration alertConfig) {
CollectionPlanConfiguration config = alertConfig.getCollectionPlanConfiguration();

boolean shouldTrigger =
Expand Down Expand Up @@ -176,6 +201,61 @@ private void evaluateManualTrigger(AlertingConfiguration alertConfig) {
}
}

/**
* Check if a trigger file is present on the local file system and was recently modified. If so,
* delete the file and trigger a manual profile. The global cooldown in Profiler prevents
* overlapping profiles.
*/
public void evaluateFileTrigger() {
if (!alertingProfileFileTriggerConfiguration.isEnabled()) {
return;
}

File manualTriggerFile = alertingProfileFileTriggerConfiguration.getFilePath();
if (manualTriggerFile == null || !manualTriggerFile.exists()) {
return;
}

long lastModified = manualTriggerFile.lastModified();
long age = timeSource.getNow().toEpochMilli() - lastModified;

if (age > AlertingProfileFileTriggerConfiguration.MANUAL_TRIGGER_FILE_MAX_AGE_MS) {
return;
}

// Delete the trigger file to prevent re-triggering
if (!manualTriggerFile.delete()) {
logger.warn(
"Failed to delete manual profile trigger file: {}", manualTriggerFile.getAbsolutePath());
}

logger.info("Manual profile trigger file detected, initiating profile recording");

// Use the collection plan's duration if configured, otherwise fall back to the
// file trigger's default duration setting.
CollectionPlanConfiguration collectionPlan = alertConfig.getCollectionPlanConfiguration();
int durationSeconds = collectionPlan.getImmediateProfilingDurationSeconds();
if (durationSeconds <= 0) {
durationSeconds = alertingProfileFileTriggerConfiguration.getDefaultProfileDurationSeconds();
Comment on lines +236 to +239
}

AlertBreach alertBreach =
AlertBreach.builder()
.setType(AlertMetricType.MANUAL)
.setAlertValue(0.0)
.setAlertConfiguration(
AlertConfiguration.builder()
.setType(AlertMetricType.MANUAL)
.setEnabled(true)
.setProfileDurationSeconds(durationSeconds)
.build())
.setProfileId(UUID.randomUUID().toString())
.setCpuMetric(0)
.setMemoryUsage(0)
.build();
alertHandler.accept(alertBreach);
}

public void setPipeline(AlertMetricType type, AlertPipeline alertPipeline) {
alertPipelines.setAlertPipeline(type, alertPipeline);
}
Expand Down
Loading
Loading