diff --git a/.gitignore b/.gitignore index 37a233e35c0..039537c035e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ build/ # VSCode /.vscode/ bin/ + +/docs/adr/targettedProfiling/targettedProfiling_goal.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 5325ecc93b8..14fb2ae47ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Enhancements +* Add Java agent support for ServiceProfiler targeted collection plans by cloud role or + role-qualified instance + * Add continuous profiling (`enableContinuousProfiling`, `continuousProfilingMaxAgeSeconds`) which keeps a single JFR recording running in a circular buffer so profile requests dump the most recent window of data immediately diff --git a/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/alert/AlertBreach.java b/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/alert/AlertBreach.java index f6e07b844b1..6ce27707c5d 100644 --- a/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/alert/AlertBreach.java +++ b/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/alert/AlertBreach.java @@ -10,6 +10,7 @@ import com.microsoft.applicationinsights.alerting.config.AlertMetricType; import java.io.IOException; import java.util.UUID; +import javax.annotation.Nullable; /** Represents a breach of an alert threshold. */ @AutoValue @@ -68,13 +69,19 @@ public AlertBreach setProfileId(String profileId) { return this; } + @Nullable + public abstract String getSettingsMoniker(); + + public abstract boolean isTargeted(); + public abstract Builder toBuilder(); public static AlertBreach.Builder builder() { return new AutoValue_AlertBreach.Builder() .setCpuMetric(0) .setMemoryUsage(0) - .setProfileId(UUID.randomUUID().toString()); + .setProfileId(UUID.randomUUID().toString()) + .setTargeted(false); } @Override @@ -88,6 +95,9 @@ public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { jsonWriter.writeDoubleField("cpuMetric", cpuMetric); jsonWriter.writeDoubleField("memoryUsage", memoryUsage); jsonWriter.writeStringField("profileId", profileId); + if (getSettingsMoniker() != null) { + jsonWriter.writeStringField("settingsMoniker", getSettingsMoniker()); + } jsonWriter.writeEndObject(); return jsonWriter; } @@ -113,6 +123,10 @@ public abstract static class Builder implements JsonSerializable { public abstract Builder setProfileId(String profileId); + public abstract Builder setSettingsMoniker(@Nullable String settingsMoniker); + + public abstract Builder setTargeted(boolean targeted); + public abstract AlertBreach build(); @Override diff --git a/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/config/AlertingConfiguration.java b/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/config/AlertingConfiguration.java index 490c9cf5d58..3b96e6e566b 100644 --- a/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/config/AlertingConfiguration.java +++ b/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/config/AlertingConfiguration.java @@ -6,6 +6,7 @@ import com.google.auto.value.AutoValue; import java.time.Instant; import java.util.List; +import javax.annotation.Nullable; /** Contains the overall configuration of the entire alerting subsystem. */ @AutoValue @@ -17,24 +18,46 @@ public static AlertingConfiguration create( DefaultConfiguration defaultConfiguration, CollectionPlanConfiguration collectionPlanConfiguration, List requestAlertConfiguration) { + return create( + cpuAlert, + memoryAlert, + defaultConfiguration, + collectionPlanConfiguration, + requestAlertConfiguration, + null); + } + + public static AlertingConfiguration create( + AlertConfiguration cpuAlert, + AlertConfiguration memoryAlert, + DefaultConfiguration defaultConfiguration, + CollectionPlanConfiguration collectionPlanConfiguration, + List requestAlertConfiguration, + @Nullable TargetedCollectionPlanConfiguration targetedCollectionPlanConfiguration) { return new AutoValue_AlertingConfiguration( cpuAlert, memoryAlert, defaultConfiguration, collectionPlanConfiguration, - requestAlertConfiguration); + requestAlertConfiguration, + targetedCollectionPlanConfiguration); } - public boolean hasAnEnabledTrigger() { + public boolean hasAnEnabledTrigger( + @Nullable String roleName, @Nullable String roleInstance, Instant now) { + CollectionPlanConfiguration collectionPlan = getCollectionPlanConfiguration(); boolean manualProfileEnabled = - getCollectionPlanConfiguration().isSingle() - && getCollectionPlanConfiguration().getMode() - == CollectionPlanConfiguration.EngineMode.immediate - && Instant.now().isBefore(getCollectionPlanConfiguration().getExpiration()); - - return getCpuAlert().isEnabled() || manualProfileEnabled || getMemoryAlert().isEnabled(); - // Sampling not enabled yet - // getDefaultConfiguration().getSamplingEnabled(); + collectionPlan.isSingle() + && collectionPlan.getMode() == CollectionPlanConfiguration.EngineMode.immediate + && now.isBefore(collectionPlan.getExpiration()); + + TargetedCollectionPlanConfiguration targetedPlan = getTargetedCollectionPlanConfiguration(); + boolean onDemandProfileEnabled = + targetedPlan == null + ? manualProfileEnabled + : targetedPlan.isActionable(roleName, roleInstance, now); + + return getCpuAlert().isEnabled() || onDemandProfileEnabled || getMemoryAlert().isEnabled(); } public boolean hasRequestAlertConfiguration() { @@ -55,4 +78,7 @@ public boolean hasRequestAlertConfiguration() { // Alert configuration for SPAN telemetry public abstract List getRequestAlertConfiguration(); + + @Nullable + public abstract TargetedCollectionPlanConfiguration getTargetedCollectionPlanConfiguration(); } diff --git a/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/config/AlertingSubsystemConfiguration.java b/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/config/AlertingSubsystemConfiguration.java new file mode 100644 index 00000000000..f7a81d7180f --- /dev/null +++ b/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/config/AlertingSubsystemConfiguration.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.alerting.config; + +import com.google.auto.value.AutoValue; +import javax.annotation.Nullable; + +@AutoValue +public abstract class AlertingSubsystemConfiguration { + + public static AlertingSubsystemConfiguration create( + @Nullable String roleName, + @Nullable String roleInstance, + AlertingProfileFileTriggerConfiguration profileFileTriggerConfiguration) { + return new AutoValue_AlertingSubsystemConfiguration( + roleName, roleInstance, profileFileTriggerConfiguration); + } + + @Nullable + public abstract String getRoleName(); + + @Nullable + public abstract String getRoleInstance(); + + public abstract AlertingProfileFileTriggerConfiguration getProfileFileTriggerConfiguration(); +} diff --git a/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/config/TargetedCollectionPlanConfiguration.java b/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/config/TargetedCollectionPlanConfiguration.java new file mode 100644 index 00000000000..4438cc5258d --- /dev/null +++ b/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/config/TargetedCollectionPlanConfiguration.java @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.alerting.config; + +import com.google.auto.value.AutoValue; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +@AutoValue +public abstract class TargetedCollectionPlanConfiguration { + + public static TargetedCollectionPlanConfiguration create( + @Nullable List roles, + @Nullable List instances, + int immediateProfilingDurationSeconds, + @Nullable Instant expiration, + @Nullable String settingsMoniker) { + return new AutoValue_TargetedCollectionPlanConfiguration( + immutableCopy(roles), + immutableCopy(instances), + immediateProfilingDurationSeconds, + expiration, + settingsMoniker); + } + + @Nullable + public abstract List getRoles(); + + @Nullable + public abstract List getInstances(); + + public abstract int getImmediateProfilingDurationSeconds(); + + @Nullable + public abstract Instant getExpiration(); + + @Nullable + public abstract String getSettingsMoniker(); + + public boolean isValid() { + List roles = getRoles(); + List instances = getInstances(); + if ((roles == null) == (instances == null) + || getImmediateProfilingDurationSeconds() < 1 + || getImmediateProfilingDurationSeconds() > 360 + || getExpiration() == null + || isBlank(getSettingsMoniker())) { + return false; + } + + if (roles != null) { + if (roles.isEmpty()) { + return false; + } + for (String role : roles) { + if (isBlank(role)) { + return false; + } + } + return true; + } + + if (instances.isEmpty()) { + return false; + } + for (TargetedInstanceConfiguration instance : instances) { + if (instance == null || isBlank(instance.getRole()) || isBlank(instance.getName())) { + return false; + } + } + return true; + } + + public boolean isSelected(@Nullable String roleName, @Nullable String roleInstance) { + if (!isValid() || isBlank(roleName)) { + return false; + } + + List roles = getRoles(); + if (roles != null) { + for (String role : roles) { + if (equalsNormalized(role, roleName)) { + return true; + } + } + return false; + } + + List instances = getInstances(); + if (isBlank(roleInstance) || instances == null) { + return false; + } + for (TargetedInstanceConfiguration instance : instances) { + if (instance != null + && equalsNormalized(instance.getRole(), roleName) + && equalsNormalized(instance.getName(), roleInstance)) { + return true; + } + } + return false; + } + + public boolean isActionable( + @Nullable String roleName, @Nullable String roleInstance, Instant now) { + Instant expiration = getExpiration(); + return expiration != null && now.isBefore(expiration) && isSelected(roleName, roleInstance); + } + + @Nullable + private static List immutableCopy(@Nullable List values) { + return values == null ? null : Collections.unmodifiableList(new ArrayList<>(values)); + } + + private static boolean equalsNormalized(@Nullable String left, @Nullable String right) { + return left != null && right != null && left.trim().equalsIgnoreCase(right.trim()); + } + + private static boolean isBlank(@Nullable String value) { + return value == null || value.trim().isEmpty(); + } +} diff --git a/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/config/TargetedInstanceConfiguration.java b/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/config/TargetedInstanceConfiguration.java new file mode 100644 index 00000000000..27e4de6f8c6 --- /dev/null +++ b/agent/agent-profiler/agent-alerting-api/src/main/java/com/microsoft/applicationinsights/alerting/config/TargetedInstanceConfiguration.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.alerting.config; + +import com.google.auto.value.AutoValue; +import javax.annotation.Nullable; + +@AutoValue +public abstract class TargetedInstanceConfiguration { + + public static TargetedInstanceConfiguration create(@Nullable String role, @Nullable String name) { + return new AutoValue_TargetedInstanceConfiguration(role, name); + } + + @Nullable + public abstract String getRole(); + + @Nullable + public abstract String getName(); +} diff --git a/agent/agent-profiler/agent-alerting/src/main/java/com/microsoft/applicationinsights/alerting/AlertingSubsystem.java b/agent/agent-profiler/agent-alerting/src/main/java/com/microsoft/applicationinsights/alerting/AlertingSubsystem.java index f8508defb72..35559202ecd 100644 --- a/agent/agent-profiler/agent-alerting/src/main/java/com/microsoft/applicationinsights/alerting/AlertingSubsystem.java +++ b/agent/agent-profiler/agent-alerting/src/main/java/com/microsoft/applicationinsights/alerting/AlertingSubsystem.java @@ -15,12 +15,11 @@ import com.microsoft.applicationinsights.alerting.config.CollectionPlanConfiguration; import com.microsoft.applicationinsights.alerting.config.CollectionPlanConfiguration.EngineMode; import com.microsoft.applicationinsights.alerting.config.DefaultConfiguration; +import com.microsoft.applicationinsights.alerting.config.TargetedCollectionPlanConfiguration; import java.io.File; import java.time.Instant; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.UUID; import java.util.function.Consumer; import javax.annotation.Nullable; @@ -38,11 +37,12 @@ public class AlertingSubsystem { // Downstream observer of alerts produced by the alerting system private final Consumer alertHandler; - // List of manual triggers that have already been processed - private final Set manualTriggersExecuted = new HashSet<>(); + private final ExecutedMonikerTracker executedMonikers; private final AlertPipelines alertPipelines; private final TimeSource timeSource; + @Nullable private final String roleName; + @Nullable private final String roleInstance; // Current configuration of the alerting subsystem private AlertingConfiguration alertConfig; @@ -57,11 +57,32 @@ protected AlertingSubsystem( TimeSource timeSource, boolean enableRequestTriggerUpdates, AlertingProfileFileTriggerConfiguration alertingProfileFileTriggerConfiguration) { + this( + alertHandler, + timeSource, + enableRequestTriggerUpdates, + alertingProfileFileTriggerConfiguration, + null, + null, + new ExecutedMonikerTracker()); + } + + AlertingSubsystem( + Consumer alertHandler, + TimeSource timeSource, + boolean enableRequestTriggerUpdates, + AlertingProfileFileTriggerConfiguration alertingProfileFileTriggerConfiguration, + @Nullable String roleName, + @Nullable String roleInstance, + ExecutedMonikerTracker executedMonikers) { this.alertHandler = alertHandler; this.alertPipelines = new AlertPipelines(alertHandler); this.timeSource = timeSource; this.enableRequestTriggerUpdates = enableRequestTriggerUpdates; this.alertingProfileFileTriggerConfiguration = alertingProfileFileTriggerConfiguration; + this.roleName = roleName; + this.roleInstance = roleInstance; + this.executedMonikers = executedMonikers; } /** @@ -76,10 +97,25 @@ public static AlertingSubsystem create( Consumer alertHandler, TimeSource timeSource, AlertingProfileFileTriggerConfiguration alertingProfileFileTriggerConfiguration) { + return create(alertHandler, timeSource, null, null, alertingProfileFileTriggerConfiguration); + } + + public static AlertingSubsystem create( + Consumer alertHandler, + TimeSource timeSource, + @Nullable String roleName, + @Nullable String roleInstance, + AlertingProfileFileTriggerConfiguration alertingProfileFileTriggerConfiguration) { AlertingSubsystem alertingSubsystem = new AlertingSubsystem( - alertHandler, timeSource, true, alertingProfileFileTriggerConfiguration); + alertHandler, + timeSource, + true, + alertingProfileFileTriggerConfiguration, + roleName, + roleInstance, + new ExecutedMonikerTracker()); // init with disabled config alertingSubsystem.initialize( @@ -166,7 +202,11 @@ private void updateRequestPipelineConfig( * both the server-side collection plan and the local file-based trigger. */ private void evaluateManualTrigger(AlertingConfiguration alertConfig) { - evaluateCollectionPlanTrigger(alertConfig); + if (alertConfig.getTargetedCollectionPlanConfiguration() == null) { + evaluateCollectionPlanTrigger(alertConfig); + } else { + evaluateTargetedCollectionPlanTrigger(alertConfig); + } evaluateFileTrigger(alertConfig); } @@ -178,27 +218,53 @@ private void evaluateCollectionPlanTrigger(AlertingConfiguration alertConfig) { config.isSingle() && config.getMode() == EngineMode.immediate && timeSource.getNow().isBefore(config.getExpiration()) - && !manualTriggersExecuted.contains(config.getSettingsMoniker()); + && executedMonikers.tryMarkExecuted(config.getSettingsMoniker(), timeSource.getNow()); if (shouldTrigger) { - manualTriggersExecuted.add(config.getSettingsMoniker()); - - AlertBreach alertBreach = - AlertBreach.builder() - .setType(AlertMetricType.MANUAL) - .setAlertValue(0.0) - .setAlertConfiguration( - AlertConfiguration.builder() - .setType(AlertMetricType.MANUAL) - .setEnabled(true) - .setProfileDurationSeconds(config.getImmediateProfilingDurationSeconds()) - .build()) - .setProfileId(UUID.randomUUID().toString()) - .setCpuMetric(0) - .setMemoryUsage(0) - .build(); - alertHandler.accept(alertBreach); + dispatchManualAlert( + config.getImmediateProfilingDurationSeconds(), config.getSettingsMoniker(), false); + } + } + + private void evaluateTargetedCollectionPlanTrigger(AlertingConfiguration alertConfig) { + TargetedCollectionPlanConfiguration config = + alertConfig.getTargetedCollectionPlanConfiguration(); + if (config == null) { + return; + } + if (!config.isValid()) { + logger.warn("Ignoring invalid targeted profiler collection plan"); + return; + } + if (!config.isActionable(roleName, roleInstance, timeSource.getNow())) { + return; } + + String settingsMoniker = config.getSettingsMoniker(); + if (settingsMoniker != null + && executedMonikers.tryMarkExecuted(settingsMoniker, timeSource.getNow())) { + dispatchManualAlert(config.getImmediateProfilingDurationSeconds(), settingsMoniker, true); + } + } + + private void dispatchManualAlert(int durationSeconds, String settingsMoniker, boolean targeted) { + 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) + .setSettingsMoniker(settingsMoniker) + .setTargeted(targeted) + .build(); + alertHandler.accept(alertBreach); } /** diff --git a/agent/agent-profiler/agent-alerting/src/main/java/com/microsoft/applicationinsights/alerting/ExecutedMonikerTracker.java b/agent/agent-profiler/agent-alerting/src/main/java/com/microsoft/applicationinsights/alerting/ExecutedMonikerTracker.java new file mode 100644 index 00000000000..311160a2dce --- /dev/null +++ b/agent/agent-profiler/agent-alerting/src/main/java/com/microsoft/applicationinsights/alerting/ExecutedMonikerTracker.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.alerting; + +import java.time.Duration; +import java.time.Instant; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +final class ExecutedMonikerTracker { + + static final Duration DEFAULT_RETENTION = Duration.ofMinutes(10); + static final int DEFAULT_CAPACITY = 1024; + + private final Duration retention; + private final int capacity; + private final LinkedHashMap executed = new LinkedHashMap<>(); + + ExecutedMonikerTracker() { + this(DEFAULT_RETENTION, DEFAULT_CAPACITY); + } + + ExecutedMonikerTracker(Duration retention, int capacity) { + if (retention.isNegative() || retention.isZero()) { + throw new IllegalArgumentException("retention must be positive"); + } + if (capacity < 1) { + throw new IllegalArgumentException("capacity must be positive"); + } + this.retention = retention; + this.capacity = capacity; + } + + synchronized boolean tryMarkExecuted(String moniker, Instant now) { + if (moniker == null || moniker.trim().isEmpty()) { + return false; + } + + removeExpired(now); + String normalizedMoniker = moniker.trim(); + if (executed.containsKey(normalizedMoniker)) { + return false; + } + + while (executed.size() >= capacity) { + Iterator iterator = executed.keySet().iterator(); + iterator.next(); + iterator.remove(); + } + executed.put(normalizedMoniker, now); + return true; + } + + private void removeExpired(Instant now) { + Instant cutoff = now.minus(retention); + Iterator> iterator = executed.entrySet().iterator(); + while (iterator.hasNext()) { + if (iterator.next().getValue().isBefore(cutoff)) { + iterator.remove(); + } + } + } +} diff --git a/agent/agent-profiler/agent-alerting/src/test/java/com/microsoft/applicationinsights/alerting/AlertingSubsystemTest.java b/agent/agent-profiler/agent-alerting/src/test/java/com/microsoft/applicationinsights/alerting/AlertingSubsystemTest.java index 8f53a417731..b2b0418fee1 100644 --- a/agent/agent-profiler/agent-alerting/src/test/java/com/microsoft/applicationinsights/alerting/AlertingSubsystemTest.java +++ b/agent/agent-profiler/agent-alerting/src/test/java/com/microsoft/applicationinsights/alerting/AlertingSubsystemTest.java @@ -13,9 +13,12 @@ import com.microsoft.applicationinsights.alerting.config.CollectionPlanConfiguration; import com.microsoft.applicationinsights.alerting.config.CollectionPlanConfiguration.EngineMode; import com.microsoft.applicationinsights.alerting.config.DefaultConfiguration; +import com.microsoft.applicationinsights.alerting.config.TargetedCollectionPlanConfiguration; +import com.microsoft.applicationinsights.alerting.config.TargetedInstanceConfiguration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Collections; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import org.junit.jupiter.api.Test; @@ -77,6 +80,7 @@ void alertTriggerIsCalled() { assertThat(called.get().getType()).isEqualTo(AlertMetricType.CPU); assertThat(called.get().getAlertValue()).isEqualTo(90.0); + assertThat(called.get().getSettingsMoniker()).isNull(); } @Test @@ -120,6 +124,8 @@ void manualAlertWorks() { new ArrayList<>())); assertThat(called.get().getType()).isEqualTo(AlertMetricType.MANUAL); + assertThat(called.get().getSettingsMoniker()).isEqualTo("a-settings-moniker"); + assertThat(called.get().isTargeted()).isFalse(); } @Test @@ -165,4 +171,111 @@ void manualAlertDoesNotTriggerAfterExpired() { assertThat(called.get()).isNull(); } + + @Test + void targetedAlertTriggersOnlyForMatchingIdentity() { + AtomicReference matchingBreach = new AtomicReference<>(); + TestTimeSource timeSource = new TestTimeSource(); + AlertingConfiguration config = targetedAlertingConfig(false); + + AlertingSubsystem matching = + AlertingSubsystem.create( + matchingBreach::set, + timeSource, + "frontend", + "instance-1", + AlertingProfileFileTriggerConfiguration.createDefault()); + matching.updateConfiguration(config); + + AtomicReference unmatchedBreach = new AtomicReference<>(); + AlertingSubsystem unmatched = + AlertingSubsystem.create( + unmatchedBreach::set, + timeSource, + "backend", + "instance-1", + AlertingProfileFileTriggerConfiguration.createDefault()); + unmatched.updateConfiguration(config); + + assertThat(matchingBreach.get()).isNotNull(); + assertThat(matchingBreach.get().getType()).isEqualTo(AlertMetricType.MANUAL); + assertThat(matchingBreach.get().getSettingsMoniker()).isEqualTo("Portal_test"); + assertThat(matchingBreach.get().isTargeted()).isTrue(); + assertThat(unmatchedBreach.get()).isNull(); + } + + @Test + void targetedSelectionNormalizesRoleAndInstance() { + TargetedCollectionPlanConfiguration instancePlan = + TargetedCollectionPlanConfiguration.create( + null, + Collections.singletonList( + TargetedInstanceConfiguration.create(" frontend ", " instance-1 ")), + 120, + Instant.ofEpochSecond(60), + "Portal_test"); + + assertThat(instancePlan.isSelected("FRONTEND", "INSTANCE-1")).isTrue(); + assertThat(instancePlan.isSelected("frontend", "instance-2")).isFalse(); + assertThat(instancePlan.isSelected(null, "instance-1")).isFalse(); + } + + @Test + void targetedPlanIsActionableOnlyBeforeExpiration() { + TargetedCollectionPlanConfiguration rolePlan = + TargetedCollectionPlanConfiguration.create( + Collections.singletonList("frontend"), + null, + 120, + Instant.ofEpochSecond(60), + "Portal_test"); + + assertThat(rolePlan.isActionable("frontend", "instance-1", Instant.ofEpochSecond(59))).isTrue(); + assertThat(rolePlan.isActionable("frontend", "instance-1", Instant.ofEpochSecond(60))) + .isFalse(); + } + + @Test + void targetedPlanTakesPrecedenceOverLegacyPlan() { + AtomicReference breach = new AtomicReference<>(); + TestTimeSource timeSource = new TestTimeSource(); + AlertingSubsystem subsystem = + AlertingSubsystem.create( + breach::set, + timeSource, + "frontend", + "instance-1", + AlertingProfileFileTriggerConfiguration.createDefault()); + + subsystem.updateConfiguration(targetedAlertingConfig(true)); + + assertThat(breach.get()).isNotNull(); + assertThat(breach.get().getAlertConfiguration().getProfileDurationSeconds()).isEqualTo(120); + } + + private static AlertingConfiguration targetedAlertingConfig(boolean legacyEnabled) { + CollectionPlanConfiguration legacyPlan = + CollectionPlanConfiguration.builder() + .setSingle(legacyEnabled) + .setMode(EngineMode.immediate) + .setExpiration(Instant.ofEpochSecond(60)) + .setImmediateProfilingDurationSeconds(30) + .setSettingsMoniker("legacy") + .build(); + TargetedCollectionPlanConfiguration targetedPlan = + TargetedCollectionPlanConfiguration.create( + null, + Collections.singletonList( + TargetedInstanceConfiguration.create("frontend", "instance-1")), + 120, + Instant.ofEpochSecond(60), + "Portal_test"); + return AlertingConfiguration.create( + AlertConfiguration.builder().setType(AlertMetricType.CPU).build(), + AlertConfiguration.builder().setType(AlertMetricType.MEMORY).build(), + DefaultConfiguration.builder().build(), + legacyPlan, + new ArrayList<>(), + targetedPlan); + } } diff --git a/agent/agent-profiler/agent-alerting/src/test/java/com/microsoft/applicationinsights/alerting/ExecutedMonikerTrackerTest.java b/agent/agent-profiler/agent-alerting/src/test/java/com/microsoft/applicationinsights/alerting/ExecutedMonikerTrackerTest.java new file mode 100644 index 00000000000..b0f04461196 --- /dev/null +++ b/agent/agent-profiler/agent-alerting/src/test/java/com/microsoft/applicationinsights/alerting/ExecutedMonikerTrackerTest.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.alerting; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.time.Instant; +import org.junit.jupiter.api.Test; + +class ExecutedMonikerTrackerTest { + + @Test + void rejectsDuplicateWithinRetentionWindow() { + ExecutedMonikerTracker tracker = new ExecutedMonikerTracker(Duration.ofMinutes(10), 10); + Instant now = Instant.parse("2026-07-24T13:58:12Z"); + + assertThat(tracker.tryMarkExecuted("Portal_test", now)).isTrue(); + assertThat(tracker.tryMarkExecuted("Portal_test", now.plusSeconds(60))).isFalse(); + assertThat(tracker.tryMarkExecuted("Portal_test", now.plusSeconds(601))).isTrue(); + } + + @Test + void evictsOldestEntryAtCapacity() { + ExecutedMonikerTracker tracker = new ExecutedMonikerTracker(Duration.ofMinutes(10), 2); + Instant now = Instant.parse("2026-07-24T13:58:12Z"); + + assertThat(tracker.tryMarkExecuted("one", now)).isTrue(); + assertThat(tracker.tryMarkExecuted("two", now)).isTrue(); + assertThat(tracker.tryMarkExecuted("three", now)).isTrue(); + assertThat(tracker.tryMarkExecuted("one", now.plusSeconds(1))).isTrue(); + } + + @Test + void rejectsBlankMoniker() { + ExecutedMonikerTracker tracker = new ExecutedMonikerTracker(Duration.ofMinutes(10), 10); + Instant now = Instant.parse("2026-07-24T13:58:12Z"); + + assertThat(tracker.tryMarkExecuted(null, now)).isFalse(); + assertThat(tracker.tryMarkExecuted(" ", now)).isFalse(); + } +} diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/RuntimeConfigurator.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/RuntimeConfigurator.java index 2109d0cb9cb..b8201fa8de3 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/RuntimeConfigurator.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/RuntimeConfigurator.java @@ -152,6 +152,7 @@ public void apply(RuntimeConfiguration runtimeConfig) { runtimeConfig.role.instance, telemetryClient); } catch (RuntimeException e) { + profilerStarted.set(false); logger.warn("Failed to initialize profiler", e); } } else { @@ -167,7 +168,9 @@ public void apply(RuntimeConfiguration runtimeConfig) { long intervalSeconds = Math.min(runtimeConfig.heartbeatIntervalSeconds, MINUTES.toSeconds(15)); HeartbeatExporter.start( - intervalSeconds, telemetryClient::populateDefaults, heartbeatTelemetryItemsConsumer); + intervalSeconds, + telemetryClient::populateDefaultsForHeartbeat, + heartbeatTelemetryItemsConsumer); } else { logger.debug("Heartbeat has already started."); } diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/SecondEntryPoint.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/SecondEntryPoint.java index ee8323465be..58ac2a0ab5f 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/SecondEntryPoint.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/init/SecondEntryPoint.java @@ -146,14 +146,7 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { .build(); Consumer> heartbeatTelemetryItemConsumer = - telemetryItems -> { - for (TelemetryItem telemetryItem : telemetryItems) { - TelemetryObservers.INSTANCE - .getObservers() - .forEach(consumer -> consumer.accept(telemetryItem)); - telemetryClient.getMetricsBatchItemProcessor().trackAsync(telemetryItem); - } - }; + createHeartbeatTelemetryItemConsumer(telemetryClient); if (telemetryClient.getConnectionString() != null) { startupLogger.verbose("connection string is not null, start HeartbeatExporter"); @@ -161,7 +154,9 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { long intervalSeconds = Math.min(configuration.heartbeat.intervalSeconds, MINUTES.toSeconds(15)); HeartbeatExporter.start( - intervalSeconds, telemetryClient::populateDefaults, heartbeatTelemetryItemConsumer); + intervalSeconds, + telemetryClient::populateDefaultsForHeartbeat, + heartbeatTelemetryItemConsumer); } TelemetryClient.setActive(telemetryClient); @@ -321,6 +316,18 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { }); } + private static Consumer> createHeartbeatTelemetryItemConsumer( + TelemetryClient telemetryClient) { + return telemetryItems -> { + for (TelemetryItem telemetryItem : telemetryItems) { + TelemetryObservers.INSTANCE + .getObservers() + .forEach(consumer -> consumer.accept(telemetryItem)); + telemetryClient.getMetricsBatchItemProcessor().trackAsync(telemetryItem); + } + }; + } + private static LogRecordProcessor wrapBatchLogRecordProcessor( LogRecordProcessor logRecordProcessor, Configuration configuration) { List logRecordProcessors = getLogRecordProcessors(configuration); diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/PerformanceMonitoringService.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/PerformanceMonitoringService.java index bf8e12fc240..ea2b7912123 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/PerformanceMonitoringService.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/PerformanceMonitoringService.java @@ -16,6 +16,7 @@ import com.microsoft.applicationinsights.alerting.AlertingSubsystem; import com.microsoft.applicationinsights.alerting.config.AlertingConfiguration; import com.microsoft.applicationinsights.alerting.config.AlertingProfileFileTriggerConfiguration; +import com.microsoft.applicationinsights.alerting.config.AlertingSubsystemConfiguration; import com.microsoft.applicationinsights.diagnostics.DiagnosticEngine; import com.microsoft.applicationinsights.diagnostics.DiagnosticEngineFactory; import com.microsoft.applicationinsights.diagnostics.appinsights.CodeOptimizerApplicationInsightFactoryJfr; @@ -120,7 +121,8 @@ synchronized void enableProfiler( telemetryClient, diagnosticEngine, alertServiceExecutorService, - alertingProfileFileTriggerConfiguration); + AlertingSubsystemConfiguration.create( + roleName, machineName, alertingProfileFileTriggerConfiguration)); uploadService = new UploadService( diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/Profiler.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/Profiler.java index d6e2bab02b8..5ad882bdc2e 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/Profiler.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/Profiler.java @@ -25,7 +25,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.UUID; -import java.util.concurrent.CompletableFuture; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -206,15 +206,29 @@ public void updateConfiguration(ProfilerConfiguration newConfig) { // visible for tests void profileAndUpload(AlertBreach alertBreach, Duration duration, UploadListener uploadListener) { + profileAndUpload(alertBreach, duration, uploadListener, () -> {}); + } + + private void profileAndUpload( + AlertBreach alertBreach, + Duration duration, + UploadListener uploadListener, + Runnable diagnosticAction) { Instant recordingStart = timeSource.getNow(); - if (continuousProfilingEnabled) { - captureContinuousRecording(alertBreach, recordingStart, uploadListener); + if (usesContinuousRecordingSnapshot(alertBreach)) { + captureContinuousRecording( + alertBreach, recordingStart, duration, uploadListener, diagnosticAction); return; } executeProfile( alertBreach.getType(), duration, - uploadNewRecording(alertBreach, recordingStart, uploadListener)); + uploadNewRecording(alertBreach, recordingStart, uploadListener), + diagnosticAction); + } + + private boolean usesContinuousRecordingSnapshot(AlertBreach alertBreach) { + return continuousProfilingEnabled && !alertBreach.isTargeted(); } private void startContinuousRecordingIfEnabled() { @@ -225,6 +239,7 @@ private void startContinuousRecordingIfEnabled() { if (continuousRecording != null) { return; } + Recording newRecording = null; try { // A continuous recording uses a circular buffer bounded by maxAge and no duration, so it // runs indefinitely while only retaining the most recent window of data on disk. @@ -236,16 +251,18 @@ private void startContinuousRecordingIfEnabled() { .maxSize(Long.toString(CONTINUOUS_PROFILING_MAX_SIZE_BYTES)) .disk("true") .build(); - continuousRecording = createRecording(recordingOptions, continuousRecordingConfiguration); - continuousRecording.start(); + newRecording = createRecording(recordingOptions, continuousRecordingConfiguration); + newRecording.start(); + continuousRecording = newRecording; continuousRecordingStart = timeSource.getNow(); logger.info( "Started continuous JFR recording with circular buffer maxAge of {} seconds and maxSize" + " of {} bytes", continuousProfilingMaxAge.getSeconds(), CONTINUOUS_PROFILING_MAX_SIZE_BYTES); - } catch (IOException | JfrConnectionException e) { + } catch (IOException | JfrConnectionException | IllegalStateException e) { logger.error("Failed to start continuous JFR recording", e); + closeRecordingAfterFailure(newRecording); continuousRecording = null; continuousRecordingStart = null; } @@ -266,7 +283,11 @@ public boolean isContinuousRecordingRunning() { @SuppressWarnings( "CatchingUnchecked") // catching unchecked exception is necessary for proper error handling private void captureContinuousRecording( - AlertBreach alertBreach, Instant recordingEnd, UploadListener uploadListener) { + AlertBreach alertBreach, + Instant recordingEnd, + Duration requestedDuration, + UploadListener uploadListener, + Runnable diagnosticAction) { File dumpFile; Instant bufferStart; synchronized (activeRecordingLock) { @@ -274,6 +295,11 @@ private void captureContinuousRecording( logger.warn("Profile requested but continuous recording is not running, ignoring request."); return; } + if (activeRecording != null) { + logger.warn( + "Profile requested, but an on-demand profile is already in progress, ignoring request."); + return; + } // Enforce global cooldown across all trigger sources if (globalCooldownSeconds > 0 && timeSource.getNow().isBefore(globalCooldownUntil)) { @@ -284,6 +310,8 @@ private void captureContinuousRecording( return; } + runDiagnosticAction(diagnosticAction); + // A live circular buffer can only be dumped in its entirety; the JFR connection only supports // streaming a sub-window from a stopped recording, so a shorter portal-/JMX-configured // profile @@ -297,6 +325,14 @@ private void captureContinuousRecording( (continuousRecordingStart != null && continuousRecordingStart.isAfter(maxAgeStart)) ? continuousRecordingStart : maxAgeStart; + Duration capturedDuration = Duration.between(bufferStart, recordingEnd); + if (!capturedDuration.equals(requestedDuration)) { + logger.info( + "Continuous profiling captures the retained buffer; requested duration was {} seconds," + + " actual captured duration is {} seconds", + requestedDuration.getSeconds(), + capturedDuration.getSeconds()); + } try { dumpFile = createJfrFile(bufferStart, recordingEnd); @@ -309,11 +345,9 @@ private void captureContinuousRecording( // Dump the current state of the circular buffer, capturing up to maxAge of data. The // continuous recording keeps running so future requests can be serviced immediately. continuousRecording.dump(dumpFile.getAbsolutePath()); - } catch (IOException | JfrConnectionException e) { + } catch (IOException | JfrConnectionException | IllegalStateException e) { logger.error("Failed to dump continuous recording", e); - if (dumpFile.exists() && !dumpFile.delete()) { - logger.error("Failed to remove file " + dumpFile.getAbsolutePath()); - } + deleteFileQuietly(dumpFile); return; } @@ -327,14 +361,8 @@ private void captureContinuousRecording( uploadService.upload(alertBreach, bufferStart.toEpochMilli(), dumpFile, uploadListener); } catch (Exception e) { logger.error("Failed to upload recording", e); - } catch (Error e) { - // rethrow errors - logger.error("Failed to upload recording", e); - throw e; } finally { - if (dumpFile.exists() && !dumpFile.delete()) { - logger.error("Failed to remove file " + dumpFile.getAbsolutePath()); - } + deleteFileQuietly(dumpFile); } } @@ -399,7 +427,10 @@ protected Recording createRecording( /** Perform a profile and notify the handler. */ private void executeProfile( - AlertMetricType alertType, Duration duration, Consumer handler) { + AlertMetricType alertType, + Duration duration, + Consumer handler, + Runnable diagnosticAction) { logger.info("Received " + alertType + " alert, Starting profile"); @@ -408,27 +439,47 @@ private void executeProfile( return; } - Recording newRecording = startRecording(alertType, duration); - - if (newRecording == null) { - return; - } - + Recording newRecording = null; try { + newRecording = startRecording(alertType, duration); + if (newRecording == null) { + return; + } + newRecording.start(); // schedule closing the recording + Recording startedRecording = newRecording; scheduledExecutorService.schedule( - () -> handler.accept(newRecording), duration.getSeconds(), TimeUnit.SECONDS); + () -> handler.accept(startedRecording), duration.getSeconds(), TimeUnit.SECONDS); + runDiagnosticAction(diagnosticAction); + + } catch (IOException + | JfrConnectionException + | IllegalStateException + | RejectedExecutionException e) { + logger.error("Failed to start or schedule JFR recording", e); + closeRecordingAfterFailure(newRecording); + clearActiveRecordingAfterFailure(); + } + } - } catch (IOException ioException) { - logger.error("Failed to start JFR recording", ioException); - CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(ioException); - } catch (JfrConnectionException internalError) { - logger.error("Internal JFR Error", internalError); - CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(internalError); + private static void runDiagnosticAction(Runnable diagnosticAction) { + try { + diagnosticAction.run(); + } catch (RuntimeException e) { + logger.error("Failed to emit profiler diagnostics", e); + } + } + + private static void closeRecordingAfterFailure(@Nullable Recording recording) { + if (recording == null) { + return; + } + try { + recording.close(); + } catch (IOException | JfrConnectionException e) { + logger.error("Failed to close JFR recording after startup failure", e); } } @@ -449,10 +500,6 @@ private Consumer uploadNewRecording( } catch (Exception e) { logger.error("Failed to upload recording", e); - } catch (Error e) { - // rethrow errors - logger.error("Failed to upload recording", e); - throw e; } finally { clearActiveRecording(); } @@ -502,19 +549,38 @@ private static void writeFileFromStream(Recording recording, File recordingFile) // visible for testing void clearActiveRecording() { + clearActiveRecording(true); + } + + private void clearActiveRecording(boolean startCooldown) { synchronized (activeRecordingLock) { activeRecording = null; - // Start global cooldown now that the recording is complete - startGlobalCooldown(); - - // delete uploaded profile - if (activeRecordingFile != null && activeRecordingFile.exists()) { - if (!activeRecordingFile.delete()) { - logger.error("Failed to remove file " + activeRecordingFile.getAbsolutePath()); - } + if (startCooldown) { + // Start global cooldown now that the recording is complete + startGlobalCooldown(); } + + File recordingFile = activeRecordingFile; activeRecordingFile = null; + deleteFileQuietly(recordingFile); + } + } + + private void clearActiveRecordingAfterFailure() { + clearActiveRecording(false); + } + + private static void deleteFileQuietly(@Nullable File file) { + if (file == null) { + return; + } + try { + if (file.exists() && !file.delete()) { + logger.error("Failed to remove file " + file.getAbsolutePath()); + } + } catch (RuntimeException e) { + logger.error("Failed to remove file " + file.getAbsolutePath(), e); } } @@ -580,6 +646,11 @@ private void performPeriodicProfile(UploadListener uploadListener) { /** Dispatch alert breach event to handler. */ // visible for tests public void accept(AlertBreach alertBreach, UploadListener uploadListener) { + accept(alertBreach, uploadListener, () -> {}); + } + + public void accept( + AlertBreach alertBreach, UploadListener uploadListener, Runnable diagnosticAction) { if (alertBreach.getType() == AlertMetricType.PERIODIC) { performPeriodicProfile(uploadListener); @@ -587,7 +658,8 @@ public void accept(AlertBreach alertBreach, UploadListener uploadListener) { profileAndUpload( alertBreach, Duration.ofSeconds(alertBreach.getAlertConfiguration().getProfileDurationSeconds()), - uploadListener); + uploadListener, + diagnosticAction); } } } diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/ProfilingInitializer.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/ProfilingInitializer.java index 4420a6c4a2c..5e5ed616bd1 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/ProfilingInitializer.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/ProfilingInitializer.java @@ -7,6 +7,7 @@ import com.azure.core.http.HttpPipeline; import com.azure.core.http.policy.DefaultRedirectStrategy; import com.azure.core.http.policy.RedirectPolicy; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.ContextTagKeys; import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.SystemInformation; import com.azure.monitor.opentelemetry.autoconfigure.implementation.utils.ThreadPoolUtils; import com.microsoft.applicationinsights.agent.internal.common.FriendlyException; @@ -23,8 +24,10 @@ import java.io.File; import java.net.MalformedURLException; import java.net.URL; +import java.time.Instant; import java.util.Arrays; import java.util.HashSet; +import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -90,6 +93,20 @@ public static ProfilingInitializer initialize( String roleInstance, TelemetryClient telemetryClient) { + Map telemetryTags = + telemetryClient.newMessageTelemetryBuilder().build().getTags(); + if (telemetryTags != null) { + String resolvedRoleName = telemetryTags.get(ContextTagKeys.AI_CLOUD_ROLE.toString()); + String resolvedRoleInstance = + telemetryTags.get(ContextTagKeys.AI_CLOUD_ROLE_INSTANCE.toString()); + if (resolvedRoleName != null) { + roleName = resolvedRoleName; + } + if (resolvedRoleInstance != null) { + roleInstance = resolvedRoleInstance; + } + } + ProfilingInitializer profilingInitializer = new ProfilingInitializer( SystemInformation.getProcessId(), @@ -189,7 +206,8 @@ synchronized void applyConfiguration(ProfilerConfiguration config) { boolean manualProfilingConfigured = configuration.manualTrigger.enabled || configuration.enableProfilerControlMBean; - if (alertingConfig.hasAnEnabledTrigger() || manualProfilingConfigured) { + if (alertingConfig.hasAnEnabledTrigger(roleName, machineName, Instant.now()) + || manualProfilingConfigured) { if (!currentlyEnabled.getAndSet(true)) { enableProfiler(); } diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/config/ProfilerConfiguration.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/config/ProfilerConfiguration.java index 2c5951c85ab..7fcf91c2db9 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/config/ProfilerConfiguration.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/config/ProfilerConfiguration.java @@ -42,6 +42,7 @@ public class ProfilerConfiguration implements JsonSerializable requestTriggerConfiguration; + @Nullable private TargetedCollectionPlan targetedCollectionPlan; public boolean hasBeenConfigured() { return getLastModified().compareTo(DEFAULT_DATE) != 0; @@ -134,6 +135,17 @@ public ProfilerConfiguration setRequestTriggerConfiguration( return this; } + @Nullable + public TargetedCollectionPlan getTargetedCollectionPlan() { + return targetedCollectionPlan; + } + + public ProfilerConfiguration setTargetedCollectionPlan( + @Nullable TargetedCollectionPlan targetedCollectionPlan) { + this.targetedCollectionPlan = targetedCollectionPlan; + return this; + } + @Override public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { jsonWriter.writeStartObject(); @@ -151,6 +163,7 @@ public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { trigger.toJson(jsonWriter); } jsonWriter.writeEndArray(); + jsonWriter.writeJsonField("targetedCollectionPlan", targetedCollectionPlan); jsonWriter.writeEndObject(); return jsonWriter; } @@ -194,6 +207,9 @@ public static ProfilerConfiguration fromJson(JsonReader jsonReader) throws IOExc } else if ("requestTriggerConfiguration".equals(fieldName)) { deserializedProfilerConfiguration.setRequestTriggerConfiguration( reader.readArray(AlertingConfig.RequestTrigger::fromJson)); + } else if ("targetedCollectionPlan".equals(fieldName)) { + deserializedProfilerConfiguration.setTargetedCollectionPlan( + TargetedCollectionPlan.fromJson(reader)); } else { reader.skipChildren(); } diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/config/TargetedCollectionPlan.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/config/TargetedCollectionPlan.java new file mode 100644 index 00000000000..f6c070447d3 --- /dev/null +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/config/TargetedCollectionPlan.java @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.agent.internal.profiler.config; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; +import java.io.IOException; +import java.util.List; +import javax.annotation.Nullable; + +public class TargetedCollectionPlan implements JsonSerializable { + + @Nullable private List roles; + @Nullable private List instances; + private int immediateProfilingDuration; + @Nullable private String expiration; + @Nullable private String settingsMoniker; + + @Nullable + public List getRoles() { + return roles; + } + + public TargetedCollectionPlan setRoles(@Nullable List roles) { + this.roles = roles; + return this; + } + + @Nullable + public List getInstances() { + return instances; + } + + public TargetedCollectionPlan setInstances(@Nullable List instances) { + this.instances = instances; + return this; + } + + public int getImmediateProfilingDuration() { + return immediateProfilingDuration; + } + + public TargetedCollectionPlan setImmediateProfilingDuration(int immediateProfilingDuration) { + this.immediateProfilingDuration = immediateProfilingDuration; + return this; + } + + @Nullable + public String getExpiration() { + return expiration; + } + + public TargetedCollectionPlan setExpiration(@Nullable String expiration) { + this.expiration = expiration; + return this; + } + + @Nullable + public String getSettingsMoniker() { + return settingsMoniker; + } + + public TargetedCollectionPlan setSettingsMoniker(@Nullable String settingsMoniker) { + this.settingsMoniker = settingsMoniker; + return this; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + jsonWriter.writeStartObject(); + if (roles != null) { + jsonWriter.writeArrayField("roles", roles, JsonWriter::writeString); + } + if (instances != null) { + jsonWriter.writeArrayField("instances", instances, JsonWriter::writeJson); + } + jsonWriter.writeIntField("immediateProfilingDuration", immediateProfilingDuration); + jsonWriter.writeStringField("expiration", expiration); + jsonWriter.writeStringField("settingsMoniker", settingsMoniker); + return jsonWriter.writeEndObject(); + } + + public static TargetedCollectionPlan fromJson(JsonReader jsonReader) throws IOException { + return jsonReader.readObject( + reader -> { + TargetedCollectionPlan plan = new TargetedCollectionPlan(); + while (reader.nextToken() != JsonToken.END_OBJECT) { + reader.nextToken(); + String fieldName = reader.getFieldName(); + if ("roles".equals(fieldName)) { + plan.setRoles(reader.readArray(JsonReader::getString)); + } else if ("instances".equals(fieldName)) { + plan.setInstances(reader.readArray(TargetedInstance::fromJson)); + } else if ("immediateProfilingDuration".equals(fieldName)) { + plan.setImmediateProfilingDuration(reader.getInt()); + } else if ("expiration".equals(fieldName)) { + plan.setExpiration(reader.getString()); + } else if ("settingsMoniker".equals(fieldName)) { + plan.setSettingsMoniker(reader.getString()); + } else { + reader.skipChildren(); + } + } + return plan; + }); + } +} diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/config/TargetedInstance.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/config/TargetedInstance.java new file mode 100644 index 00000000000..3e3f50e603e --- /dev/null +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/config/TargetedInstance.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.agent.internal.profiler.config; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; +import java.io.IOException; +import javax.annotation.Nullable; + +public class TargetedInstance implements JsonSerializable { + + @Nullable private String role; + @Nullable private String name; + + @Nullable + public String getRole() { + return role; + } + + public TargetedInstance setRole(@Nullable String role) { + this.role = role; + return this; + } + + @Nullable + public String getName() { + return name; + } + + public TargetedInstance setName(@Nullable String name) { + this.name = name; + return this; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter + .writeStartObject() + .writeStringField("role", role) + .writeStringField("name", name) + .writeEndObject(); + } + + public static TargetedInstance fromJson(JsonReader jsonReader) throws IOException { + return jsonReader.readObject( + reader -> { + TargetedInstance instance = new TargetedInstance(); + while (reader.nextToken() != JsonToken.END_OBJECT) { + reader.nextToken(); + String fieldName = reader.getFieldName(); + if ("role".equals(fieldName)) { + instance.setRole(reader.getString()); + } else if ("name".equals(fieldName)) { + instance.setName(reader.getString()); + } else { + reader.skipChildren(); + } + } + return instance; + }); + } +} diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/service/ServiceProfilerClient.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/service/ServiceProfilerClient.java index 829a165eaf4..2aa838afa22 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/service/ServiceProfilerClient.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/service/ServiceProfilerClient.java @@ -35,13 +35,16 @@ public class ServiceProfilerClient { private static final String SETTINGS_PATH = PROFILER_API_PREFIX + "/settings"; public static final String OLD_TIMESTAMP_PARAMETER = "oldTimestamp"; public static final String FEATURE_VERSION_PARAMETER = "featureVersion"; - public static final String FEATURE_VERSION = "1.0.0"; + public static final String FEATURE_VERSION = "2.0.0"; + private static final String LEGACY_FEATURE_VERSION = "1.0.0"; + private static final String EMPTY_GUID = "00000000-0000-0000-0000-000000000000"; public static final String API_FEATURE_VERSION = "2020-10-14-preview"; private final URL hostUrl; private final String instrumentationKey; private final HttpPipeline httpPipeline; @Nullable private final String userAgent; + private volatile String settingsFeatureVersion = FEATURE_VERSION; public ServiceProfilerClient( URL hostUrl, @@ -144,15 +147,39 @@ private static Mono reportUploadFinish(HttpResponse response) { /** Obtain current settings that have been configured within the UI. */ public Mono getSettings(Date oldTimeStamp) { + String featureVersion = settingsFeatureVersion; + return getSettings(oldTimeStamp, featureVersion) + .flatMap( + config -> { + if (FEATURE_VERSION.equals(featureVersion) + && isUnsupportedFeatureVersionResponse(config)) { + logger.info( + "Service Profiler settings protocol {} is not supported; falling back to {}", + FEATURE_VERSION, + LEGACY_FEATURE_VERSION); + settingsFeatureVersion = LEGACY_FEATURE_VERSION; + return getSettings(oldTimeStamp, LEGACY_FEATURE_VERSION); + } + return Mono.just(config); + }); + } - URL requestUrl = getSettingsPath(oldTimeStamp); - + private Mono getSettings(Date oldTimeStamp, String featureVersion) { + URL requestUrl = getSettingsPath(oldTimeStamp, featureVersion); HttpRequest request = new HttpRequest(HttpMethod.GET, requestUrl); - return httpPipeline.send(request).flatMap(response -> handle(response, requestUrl)); } + private static boolean isUnsupportedFeatureVersionResponse(ProfilerConfiguration config) { + String id = config.id(); + return !config.isEnabled() && (id == null || id.isEmpty() || EMPTY_GUID.equals(id)); + } + private static Mono handle(HttpResponse response, URL requestUrl) { + if (response.getStatusCode() == 304) { + response.close(); + return Mono.empty(); + } if (response.getStatusCode() >= 300) { // need to consume the body or close the response, otherwise get netty ByteBuf leak warnings: // io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before @@ -175,7 +202,7 @@ private static Mono handle(HttpResponse response, URL req } // api/profileragent/v4/settings?ikey=xyz&featureVersion=1.0.0&oldTimestamp=123 - private URL getSettingsPath(Date oldTimeStamp) { + private URL getSettingsPath(Date oldTimeStamp, String featureVersion) { String path = SETTINGS_PATH @@ -190,7 +217,7 @@ private URL getSettingsPath(Date oldTimeStamp) { + "&" + FEATURE_VERSION_PARAMETER + "=" - + FEATURE_VERSION; + + featureVersion; try { return new URL(hostUrl, path); diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/triggers/AlertConfigParser.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/triggers/AlertConfigParser.java index 55de44fd381..8bb61721c6c 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/triggers/AlertConfigParser.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/triggers/AlertConfigParser.java @@ -4,6 +4,8 @@ package com.microsoft.applicationinsights.agent.internal.profiler.triggers; import com.microsoft.applicationinsights.agent.internal.profiler.config.ProfilerConfiguration; +import com.microsoft.applicationinsights.agent.internal.profiler.config.TargetedCollectionPlan; +import com.microsoft.applicationinsights.agent.internal.profiler.config.TargetedInstance; import com.microsoft.applicationinsights.alerting.aiconfig.AlertingConfig; import com.microsoft.applicationinsights.alerting.config.AlertConfiguration; import com.microsoft.applicationinsights.alerting.config.AlertMetricType; @@ -11,19 +13,27 @@ import com.microsoft.applicationinsights.alerting.config.CollectionPlanConfiguration; import com.microsoft.applicationinsights.alerting.config.CollectionPlanConfiguration.EngineMode; import com.microsoft.applicationinsights.alerting.config.DefaultConfiguration; +import com.microsoft.applicationinsights.alerting.config.TargetedCollectionPlanConfiguration; +import com.microsoft.applicationinsights.alerting.config.TargetedInstanceConfiguration; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Parses the configuration from the service profiler endpoint. */ public class AlertConfigParser { + private static final Logger logger = LoggerFactory.getLogger(AlertConfigParser.class); + static AlertingConfiguration parse( String cpuConfig, String memoryConfig, @@ -61,13 +71,7 @@ private static List buildRequestTriggerConfiguration( // --settings-moniker Portal_b5bd7880-7406-4058-a6f8-3ea0102706b1 private static CollectionPlanConfiguration parseCollectionPlan(@Nullable String collectionPlan) { if (collectionPlan == null || collectionPlan.isEmpty()) { - return CollectionPlanConfiguration.builder() - .setSingle(false) - .setMode(EngineMode.immediate) - .setExpiration(Instant.ofEpochMilli(0)) - .setImmediateProfilingDurationSeconds(0) - .setSettingsMoniker("") - .build(); + return disabledCollectionPlan(); } String[] tokens = collectionPlan.split(" "); @@ -90,7 +94,22 @@ private static CollectionPlanConfiguration parseCollectionPlan(@Nullable String "settings-moniker", new ParseConfigValue<>(true, (config, arg) -> config.setSettingsMoniker(arg))); - return parseConfig(CollectionPlanConfiguration.builder(), tokens, parsers).build(); + try { + return parseConfig(CollectionPlanConfiguration.builder(), tokens, parsers).build(); + } catch (NumberFormatException | IllegalStateException e) { + logger.warn("Ignoring invalid profiler collection plan", e); + return disabledCollectionPlan(); + } + } + + private static CollectionPlanConfiguration disabledCollectionPlan() { + return CollectionPlanConfiguration.builder() + .setSingle(false) + .setMode(EngineMode.immediate) + .setExpiration(Instant.ofEpochMilli(0)) + .setImmediateProfilingDurationSeconds(0) + .setSettingsMoniker("") + .build(); } static DefaultConfiguration parseDefaultConfiguration(@Nullable String defaultConfig) { @@ -227,13 +246,57 @@ private static T parseConfig( public static AlertingConfiguration toAlertingConfig( ProfilerConfiguration profilerConfiguration) { + String legacyPlan = profilerConfiguration.getCollectionPlan(); + TargetedCollectionPlan targetedPlan = profilerConfiguration.getTargetedCollectionPlan(); + + return AlertingConfiguration.create( + parseFromCpu(profilerConfiguration.getCpuTriggerConfiguration()), + parseFromMemory(profilerConfiguration.getMemoryTriggerConfiguration()), + parseDefaultConfiguration(profilerConfiguration.getDefaultConfiguration()), + parseCollectionPlan(legacyPlan), + buildRequestTriggerConfiguration(profilerConfiguration.getRequestTriggerConfiguration()), + parseTargetedCollectionPlan(targetedPlan)); + } + + @Nullable + private static TargetedCollectionPlanConfiguration parseTargetedCollectionPlan( + @Nullable TargetedCollectionPlan plan) { + if (plan == null) { + return null; + } + + List instances = null; + if (plan.getInstances() != null) { + instances = new ArrayList<>(); + for (TargetedInstance instance : plan.getInstances()) { + instances.add( + instance == null + ? null + : TargetedInstanceConfiguration.create(instance.getRole(), instance.getName())); + } + } + + Instant expiration = null; + if (!isBlank(plan.getExpiration())) { + try { + expiration = + OffsetDateTime.parse(plan.getExpiration(), DateTimeFormatter.ISO_OFFSET_DATE_TIME) + .toInstant(); + } catch (DateTimeParseException e) { + logger.warn("Targeted profiler collection plan has invalid expiration"); + } + } + + return TargetedCollectionPlanConfiguration.create( + plan.getRoles(), + instances, + plan.getImmediateProfilingDuration(), + expiration, + plan.getSettingsMoniker()); + } - return AlertConfigParser.parse( - profilerConfiguration.getCpuTriggerConfiguration(), - profilerConfiguration.getMemoryTriggerConfiguration(), - profilerConfiguration.getDefaultConfiguration(), - profilerConfiguration.getCollectionPlan(), - profilerConfiguration.getRequestTriggerConfiguration()); + private static boolean isBlank(@Nullable String value) { + return value == null || value.trim().isEmpty(); } // visible for testing diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/triggers/AlertingSubsystemInit.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/triggers/AlertingSubsystemInit.java index e843b02bca6..be3692ad444 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/triggers/AlertingSubsystemInit.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/triggers/AlertingSubsystemInit.java @@ -25,7 +25,7 @@ import com.microsoft.applicationinsights.alerting.analysis.pipelines.AlertPipeline; import com.microsoft.applicationinsights.alerting.analysis.pipelines.AlertPipelineMultiplexer; import com.microsoft.applicationinsights.alerting.config.AlertMetricType; -import com.microsoft.applicationinsights.alerting.config.AlertingProfileFileTriggerConfiguration; +import com.microsoft.applicationinsights.alerting.config.AlertingSubsystemConfiguration; import com.microsoft.applicationinsights.diagnostics.DiagnosticEngine; import java.util.List; import java.util.Map; @@ -51,22 +51,20 @@ public static AlertingSubsystem create( TelemetryClient telemetryClient, DiagnosticEngine diagnosticEngine, ExecutorService executorService, - AlertingProfileFileTriggerConfiguration alertingProfileFileTriggerConfiguration) { + AlertingSubsystemConfiguration alertingSubsystemConfiguration) { // TODO (trask) delay creation of AlertingSubsystem until after Profiler is created and // initialized? Consumer alertAction = - alert -> - alertAction( - alert, - profiler, - diagnosticEngine, - telemetryClient, - configuration.enableContinuousProfiling); + alert -> alertAction(alert, profiler, diagnosticEngine, telemetryClient); alertingSubsystem = AlertingSubsystem.create( - alertAction, TimeSource.DEFAULT, alertingProfileFileTriggerConfiguration); + alertAction, + TimeSource.DEFAULT, + alertingSubsystemConfiguration.getRoleName(), + alertingSubsystemConfiguration.getRoleInstance(), + alertingSubsystemConfiguration.getProfileFileTriggerConfiguration()); if (configuration.enableRequestTriggering) { if (!configuration.requestTriggerEndpoints.isEmpty()) { @@ -133,30 +131,21 @@ private static void alertAction( AlertBreach alert, Profiler profiler, DiagnosticEngine diagnosticEngine, - TelemetryClient telemetryClient, - boolean continuousProfilingEnabled) { + TelemetryClient telemetryClient) { if (profiler != null) { // This is an event that the backend specifically looks for to track when a profile is // started sendMessageTelemetry(telemetryClient, "StartProfiler triggered."); - // With continuous profiling the profiler immediately dumps a backward-looking snapshot of the - // circular buffer, so the breach diagnostics (AlertBreach, CGroupData, MachineInfo) must be - // emitted before the dump in order to be captured in the recording. - if (continuousProfilingEnabled && diagnosticEngine != null) { - diagnosticEngine.performDiagnosis(alert); - } - profiler.accept( alert, - serviceProfilerIndex -> sendServiceProfilerIndex(serviceProfilerIndex, telemetryClient)); - - // With traditional profiling a new forward-looking recording is created, so diagnostics are - // emitted after the recording has started. - if (!continuousProfilingEnabled && diagnosticEngine != null) { - diagnosticEngine.performDiagnosis(alert); - } + serviceProfilerIndex -> sendServiceProfilerIndex(serviceProfilerIndex, telemetryClient), + () -> { + if (diagnosticEngine != null) { + diagnosticEngine.performDiagnosis(alert); + } + }); } } diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/upload/ServiceProfilerIndex.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/upload/ServiceProfilerIndex.java index 4b626385a5e..c9a5db78aeb 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/upload/ServiceProfilerIndex.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/upload/ServiceProfilerIndex.java @@ -43,6 +43,8 @@ public static class Builder { private static final String SERVICE_PROFILER_PROCESSID_PROPERTY_NAME = "ProcessId"; // visible for testing public static final String SERVICE_PROFILER_ETLFILESESSIONID_PROPERTY_NAME = "EtlFileSessionId"; + // visible for testing + public static final String SERVICE_PROFILER_SETTINGS_MONIKER_PROPERTY_NAME = "SettingsMoniker"; private static final String SERVICE_PROFILER_OPERATINGSYSTEM_PROPERTY_NAME = "OperatingSystem"; private static final String SERVICE_PROFILER_AVERAGECPUUSAGE_METRIC_NAME = "AverageCPUUsage"; private static final String SERVICE_PROFILER_AVERAGE_MEMORY_USAGE_METRIC_NAME = @@ -79,6 +81,11 @@ public Builder setTimeStamp(String timeStamp) { return this; } + public Builder setSettingsMoniker(String settingsMoniker) { + sampleEvent.put(SERVICE_PROFILER_SETTINGS_MONIKER_PROPERTY_NAME, settingsMoniker); + return this; + } + public Builder setMachineName(String machineName) { sampleEvent.put(SERVICE_PROFILER_MACHINENAME_PROPERTY_NAME, machineName); return this; diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/upload/UploadService.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/upload/UploadService.java index 35d4a6d3251..0d52d53d9f9 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/upload/UploadService.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/profiler/upload/UploadService.java @@ -100,7 +100,8 @@ public void upload( timestamp, file, alertBreach.getCpuMetric(), - alertBreach.getMemoryUsage()) + alertBreach.getMemoryUsage(), + alertBreach.getSettingsMoniker()) .subscribe(onUploadComplete(uploadListener), e -> logger.error("Failed to upload file", e)); } @@ -122,9 +123,29 @@ Mono uploadJfrFile( File file, double cpuUsage, double memoryUsage) { + return uploadJfrFile(profileId, triggerName, timestamp, file, cpuUsage, memoryUsage, null); + } + // visible for tests + Mono uploadJfrFile( + UUID profileId, + String triggerName, + long timestamp, + File file, + double cpuUsage, + double memoryUsage, + @Nullable String settingsMoniker) { return uploadFile( - triggerName, timestamp, profileId, file, cpuUsage, memoryUsage, "Profile", "jfr", "jfr"); + triggerName, + timestamp, + profileId, + file, + cpuUsage, + memoryUsage, + "Profile", + "jfr", + "jfr", + settingsMoniker); } @SuppressWarnings("TooManyParameters") // parameter count justified by method complexity @@ -138,6 +159,31 @@ public Mono uploadFile( String artifactKind, String extension, String fileFormat) { + return uploadFile( + triggerName, + timestamp, + profileId, + file, + cpuUsage, + memoryUsage, + artifactKind, + extension, + fileFormat, + null); + } + + @SuppressWarnings("TooManyParameters") // parameter count justified by method complexity + private Mono uploadFile( + String triggerName, + long timestamp, + UUID profileId, + File file, + double cpuUsage, + double memoryUsage, + String artifactKind, + String extension, + String fileFormat, + @Nullable String settingsMoniker) { String appId = appIdSupplier.get(); if (appId == null || appId.isEmpty()) { logger.error("Failed to upload due to lack of appId"); @@ -163,21 +209,25 @@ public Mono uploadFile( String fileId = createId(); String formattedTimestamp = TimestampContract.padNanos(done.getTimeStamp()); - return ServiceProfilerIndex.builder() - .setTriggeredBy(triggerName) - .setFileId(fileId) - .setStampId(done.getStampId()) - .setDataCubeId(UUID.fromString(appId)) - .setTimeStamp(formattedTimestamp) - .setMachineName(uploadContext.getMachineName()) - .setOs(OsPlatformProvider.getOsPlatformDescription()) - .setProcessId(processId) - .setArtifactKind(artifactKind) - .setArtifactId(profileId.toString()) - .setExtension(extension) - .setCpuUsage(cpuUsage) - .setMemoryUsage(memoryUsage) - .build(); + ServiceProfilerIndex.Builder indexBuilder = + ServiceProfilerIndex.builder() + .setTriggeredBy(triggerName) + .setFileId(fileId) + .setStampId(done.getStampId()) + .setDataCubeId(UUID.fromString(appId)) + .setTimeStamp(formattedTimestamp) + .setMachineName(uploadContext.getMachineName()) + .setOs(OsPlatformProvider.getOsPlatformDescription()) + .setProcessId(processId) + .setArtifactKind(artifactKind) + .setArtifactId(profileId.toString()) + .setExtension(extension) + .setCpuUsage(cpuUsage) + .setMemoryUsage(memoryUsage); + if (settingsMoniker != null && !settingsMoniker.trim().isEmpty()) { + indexBuilder.setSettingsMoniker(settingsMoniker.trim()); + } + return indexBuilder.build(); }); } diff --git a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/telemetry/TelemetryClient.java b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/telemetry/TelemetryClient.java index 3d96b05328a..c2733935be0 100644 --- a/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/telemetry/TelemetryClient.java +++ b/agent/agent-tooling/src/main/java/com/microsoft/applicationinsights/agent/internal/telemetry/TelemetryClient.java @@ -348,6 +348,14 @@ public void populateDefaults(AbstractTelemetryBuilder telemetryBuilder, Resource new ResourceParser().updateRoleNameAndInstance(telemetryBuilder, resource); } + public void populateDefaultsForHeartbeat( + AbstractTelemetryBuilder telemetryBuilder, Resource ignoredResource) { + // HeartbeatExporter supplies Resource.empty(), but targeting must use the same final resource + // identity as every other telemetry item. + Resource resource = otelResource; + populateDefaults(telemetryBuilder, resource == null ? ignoredResource : resource); + } + @Nullable public ConnectionString getConnectionString() { return connectionString; diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/ProfilerContinuousProfilingTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/ProfilerContinuousProfilingTest.java index 5f3b787b9cc..542a0a7f338 100644 --- a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/ProfilerContinuousProfilingTest.java +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/ProfilerContinuousProfilingTest.java @@ -4,10 +4,13 @@ package com.microsoft.applicationinsights.agent.internal.profiler; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockingDetails; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -27,10 +30,14 @@ import java.time.Instant; import java.util.UUID; import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; class ProfilerContinuousProfilingTest { @TempDir File tempDir; @@ -39,8 +46,9 @@ class ProfilerContinuousProfilingTest { private ScheduledExecutorService executor; @AfterEach + @SuppressWarnings("DirectInvocationOnMock") void tearDown() { - if (executor != null) { + if (executor != null && !mockingDetails(executor).isMock()) { executor.shutdownNow(); } } @@ -61,26 +69,35 @@ private static AlertBreach manualBreach(int profileDurationSeconds) { .build(); } + private static AlertBreach targetedBreach(int profileDurationSeconds) { + return manualBreach(profileDurationSeconds).toBuilder() + .setSettingsMoniker("Portal_test") + .setTargeted(true) + .build(); + } + @Test - void profileRequestAlwaysDumpsWholeBufferEvenForShorterRequestedDuration() throws Exception { + void targetedProfileUsesExactOneSecondOnDemandRecording() throws Exception { Configuration.ProfilerConfiguration config = new Configuration.ProfilerConfiguration(); config.enableContinuousProfiling = true; config.continuousProfilingMaxAgeSeconds = 60; config.globalCooldownSeconds = 0; Recording continuousRecording = mock(Recording.class); + Recording onDemandRecording = mock(Recording.class); + AtomicInteger recordingCount = new AtomicInteger(); Profiler profiler = new Profiler(config, tempDir, timeSource) { @Override protected Recording createRecording(RecordingOptions o, RecordingConfiguration c) { - return continuousRecording; + return recordingCount.getAndIncrement() == 0 ? continuousRecording : onDemandRecording; } }; UploadService uploadService = mock(UploadService.class); FlightRecorderConnection frc = mock(FlightRecorderConnection.class); - executor = Executors.newScheduledThreadPool(1); + executor = mock(ScheduledExecutorService.class); Instant now = Instant.parse("2025-01-01T00:00:00Z"); // The continuous recording has been running for longer than maxAge, so the circular buffer is @@ -94,39 +111,42 @@ protected Recording createRecording(RecordingOptions o, RecordingConfiguration c timeSource.setNow(now); UploadListener noOp = index -> {}; - // A live circular buffer can only be dumped in its entirety; a shorter portal-/JMX-configured - // duration (10s) cannot be honored by streaming a sub-window from the still-running recording, - // so the whole 60s buffer is dumped via the robust dump() path. - profiler.profileAndUpload(manualBreach(10), Duration.ofSeconds(10), noOp); + profiler.profileAndUpload(targetedBreach(1), Duration.ofSeconds(1), noOp); - verify(continuousRecording).dump(anyString()); + verify(continuousRecording, never()).dump(anyString()); verify(continuousRecording, never()).getStream(any(), any()); verify(continuousRecording, never()).stop(); - // The captured window is the whole 60s buffer, so the profile is timestamped at now - 60s. - verify(uploadService) - .upload(any(), eq(now.minusSeconds(60).toEpochMilli()), any(File.class), any()); - assertThat(profiler.isRecordingActive()).isFalse(); + verify(onDemandRecording).start(); + verify(executor).schedule(any(Runnable.class), eq(1L), eq(TimeUnit.SECONDS)); + assertThat(profiler.isRecordingActive()).isTrue(); + + Runnable rejectedDiagnostic = mock(Runnable.class); + profiler.accept(manualBreach(1), noOp, rejectedDiagnostic); + verify(continuousRecording, never()).dump(anyString()); + verify(rejectedDiagnostic, never()).run(); } @Test - void profileRequestDumpsWholeBufferWhenRequestedDurationExceedsMaxAge() throws Exception { + void targetedProfileUsesExactMaximumDurationOnDemandRecording() throws Exception { Configuration.ProfilerConfiguration config = new Configuration.ProfilerConfiguration(); config.enableContinuousProfiling = true; config.continuousProfilingMaxAgeSeconds = 60; config.globalCooldownSeconds = 0; Recording continuousRecording = mock(Recording.class); + Recording onDemandRecording = mock(Recording.class); + AtomicInteger recordingCount = new AtomicInteger(); Profiler profiler = new Profiler(config, tempDir, timeSource) { @Override protected Recording createRecording(RecordingOptions o, RecordingConfiguration c) { - return continuousRecording; + return recordingCount.getAndIncrement() == 0 ? continuousRecording : onDemandRecording; } }; UploadService uploadService = mock(UploadService.class); FlightRecorderConnection frc = mock(FlightRecorderConnection.class); - executor = Executors.newScheduledThreadPool(1); + executor = mock(ScheduledExecutorService.class); Instant now = Instant.parse("2025-01-01T00:00:00Z"); // The continuous recording has been running for longer than maxAge, so the buffer is full. @@ -138,26 +158,157 @@ protected Recording createRecording(RecordingOptions o, RecordingConfiguration c timeSource.setNow(now); UploadListener noOp = index -> {}; - // The requested duration (90s) exceeds the 60s buffer, so the whole circular buffer is dumped - // via the more robust dump() path. - profiler.profileAndUpload(manualBreach(90), Duration.ofSeconds(90), noOp); + profiler.profileAndUpload(targetedBreach(360), Duration.ofMinutes(6), noOp); - verify(continuousRecording).dump(anyString()); + verify(continuousRecording, never()).clone(true); + verify(continuousRecording, never()).dump(anyString()); verify(continuousRecording, never()).getStream(any(), any()); verify(continuousRecording, never()).stop(); - // The captured window is the whole 60s buffer, so the profile is timestamped at now - 60s. - verify(uploadService) - .upload(any(), eq(now.minusSeconds(60).toEpochMilli()), any(File.class), any()); + verify(onDemandRecording).start(); + verify(executor).schedule(any(Runnable.class), eq(360L), eq(TimeUnit.SECONDS)); + assertThat(profiler.isRecordingActive()).isTrue(); + } + + @Test + void targetedProfileUploadFailureDoesNotEscape() throws Exception { + Configuration.ProfilerConfiguration config = new Configuration.ProfilerConfiguration(); + config.enableContinuousProfiling = true; + config.continuousProfilingMaxAgeSeconds = 60; + config.globalCooldownSeconds = 0; + + Recording continuousRecording = mock(Recording.class); + Recording onDemandRecording = mock(Recording.class); + AtomicInteger recordingCount = new AtomicInteger(); + Profiler profiler = + new Profiler(config, tempDir, timeSource) { + @Override + protected Recording createRecording(RecordingOptions o, RecordingConfiguration c) { + return recordingCount.getAndIncrement() == 0 ? continuousRecording : onDemandRecording; + } + }; + + UploadService uploadService = mock(UploadService.class); + FlightRecorderConnection frc = mock(FlightRecorderConnection.class); + executor = mock(ScheduledExecutorService.class); + ArgumentCaptor scheduledUpload = ArgumentCaptor.forClass(Runnable.class); + + timeSource.setNow(Instant.parse("2025-01-01T00:00:00Z")); + profiler.initialize(uploadService, executor, frc); + profiler.profileAndUpload(targetedBreach(1), Duration.ofSeconds(1), index -> {}); + + verify(executor).schedule(scheduledUpload.capture(), eq(1L), eq(TimeUnit.SECONDS)); + doThrow(new IllegalStateException("simulated upload failure")) + .when(uploadService) + .upload(any(), any(Long.class), any(File.class), any()); + + assertThatCode(scheduledUpload.getValue()::run).doesNotThrowAnyException(); assertThat(profiler.isRecordingActive()).isFalse(); } @Test - void profileRequestSoonAfterStartupReportsActualCapturedWindow() throws Exception { + void targetedProfileCreationFailureDoesNotEscape() throws Exception { Configuration.ProfilerConfiguration config = new Configuration.ProfilerConfiguration(); config.enableContinuousProfiling = true; config.continuousProfilingMaxAgeSeconds = 60; config.globalCooldownSeconds = 0; + Recording continuousRecording = mock(Recording.class); + AtomicInteger recordingCount = new AtomicInteger(); + Profiler profiler = + new Profiler(config, tempDir, timeSource) { + @Override + protected Recording createRecording(RecordingOptions o, RecordingConfiguration c) { + if (recordingCount.getAndIncrement() == 0) { + return continuousRecording; + } + throw new IllegalStateException("simulated recording creation failure"); + } + }; + + UploadService uploadService = mock(UploadService.class); + FlightRecorderConnection frc = mock(FlightRecorderConnection.class); + executor = mock(ScheduledExecutorService.class); + + timeSource.setNow(Instant.parse("2025-01-01T00:00:00Z")); + profiler.initialize(uploadService, executor, frc); + + assertThatCode( + () -> profiler.profileAndUpload(targetedBreach(1), Duration.ofSeconds(1), index -> {})) + .doesNotThrowAnyException(); + assertThat(profiler.isRecordingActive()).isFalse(); + } + + @Test + void targetedProfileSchedulingFailureClosesRecordingAndDoesNotEscape() throws Exception { + Configuration.ProfilerConfiguration config = new Configuration.ProfilerConfiguration(); + config.enableContinuousProfiling = true; + config.continuousProfilingMaxAgeSeconds = 60; + config.globalCooldownSeconds = 120; + + Recording continuousRecording = mock(Recording.class); + Recording onDemandRecording = mock(Recording.class); + AtomicInteger recordingCount = new AtomicInteger(); + Profiler profiler = + new Profiler(config, tempDir, timeSource) { + @Override + protected Recording createRecording(RecordingOptions o, RecordingConfiguration c) { + return recordingCount.getAndIncrement() == 0 ? continuousRecording : onDemandRecording; + } + }; + + UploadService uploadService = mock(UploadService.class); + FlightRecorderConnection frc = mock(FlightRecorderConnection.class); + executor = mock(ScheduledExecutorService.class); + doThrow(new RejectedExecutionException("simulated scheduling failure")) + .when(executor) + .schedule(any(Runnable.class), eq(1L), eq(TimeUnit.SECONDS)); + + timeSource.setNow(Instant.parse("2025-01-01T00:00:00Z")); + profiler.initialize(uploadService, executor, frc); + + assertThatCode( + () -> profiler.profileAndUpload(targetedBreach(1), Duration.ofSeconds(1), index -> {})) + .doesNotThrowAnyException(); + verify(onDemandRecording).close(); + assertThat(profiler.isRecordingActive()).isFalse(); + assertThat(profiler.getGlobalCooldownUntil()).isEqualTo(Instant.MIN); + } + + @Test + void continuousRecordingStartupFailureClosesRecordingAndDoesNotEscape() throws Exception { + Configuration.ProfilerConfiguration config = new Configuration.ProfilerConfiguration(); + config.enableContinuousProfiling = true; + config.continuousProfilingMaxAgeSeconds = 60; + + Recording continuousRecording = mock(Recording.class); + doThrow(new IllegalStateException("simulated startup failure")) + .when(continuousRecording) + .start(); + Profiler profiler = + new Profiler(config, tempDir, timeSource) { + @Override + protected Recording createRecording(RecordingOptions o, RecordingConfiguration c) { + return continuousRecording; + } + }; + + assertThatCode( + () -> + profiler.initialize( + mock(UploadService.class), + mock(ScheduledExecutorService.class), + mock(FlightRecorderConnection.class))) + .doesNotThrowAnyException(); + verify(continuousRecording).close(); + assertThat(profiler.isContinuousRecordingRunning()).isFalse(); + } + + @Test + void profileRequestSoonAfterStartupReportsActualCapturedWindow() throws Exception { + Configuration.ProfilerConfiguration config = new Configuration.ProfilerConfiguration(); + config.enableContinuousProfiling = true; + config.continuousProfilingMaxAgeSeconds = 60; + config.globalCooldownSeconds = 0; Recording continuousRecording = mock(Recording.class); Profiler profiler = new Profiler(config, tempDir, timeSource) { diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/ProfilingInitializerTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/ProfilingInitializerTest.java index f1681842127..f3f673c02ee 100644 --- a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/ProfilingInitializerTest.java +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/ProfilingInitializerTest.java @@ -5,9 +5,12 @@ import com.azure.monitor.opentelemetry.autoconfigure.implementation.builders.MessageTelemetryBuilder; import com.azure.monitor.opentelemetry.autoconfigure.implementation.configuration.ConnectionString; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.ContextTagKeys; import com.microsoft.applicationinsights.agent.internal.configuration.Configuration; import com.microsoft.applicationinsights.agent.internal.configuration.GcReportingLevel; import com.microsoft.applicationinsights.agent.internal.profiler.config.ProfilerConfiguration; +import com.microsoft.applicationinsights.agent.internal.profiler.config.TargetedCollectionPlan; +import com.microsoft.applicationinsights.agent.internal.profiler.config.TargetedInstance; import com.microsoft.applicationinsights.agent.internal.telemetry.TelemetryClient; import java.io.File; import java.time.Duration; @@ -17,6 +20,7 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.function.Consumer; @@ -31,6 +35,8 @@ private static class ProfilingInitializerTestCaseBuilder { final String name; final List configurations = new ArrayList<>(); Configuration.ProfilerConfiguration localConfiguration = defaultLocalConfiguration(); + String telemetryRoleName; + String telemetryRoleInstance; private ProfilingInitializerTestCaseBuilder(String name) { this.name = name; @@ -47,8 +53,21 @@ ProfilingInitializerTestCaseBuilder withLocalConfiguration( return this; } + ProfilingInitializerTestCaseBuilder withTelemetryIdentity( + String roleName, String roleInstance) { + telemetryRoleName = roleName; + telemetryRoleInstance = roleInstance; + return this; + } + ProfilingInitializerTestCase assertThat(Consumer assertion) { - return new ProfilingInitializerTestCase(name, configurations, localConfiguration, assertion); + return new ProfilingInitializerTestCase( + name, + configurations, + localConfiguration, + telemetryRoleName, + telemetryRoleInstance, + assertion); } } @@ -56,16 +75,22 @@ private static class ProfilingInitializerTestCase { final String name; final List configurations; final Configuration.ProfilerConfiguration localConfiguration; + final String telemetryRoleName; + final String telemetryRoleInstance; final Consumer assertion; private ProfilingInitializerTestCase( String name, List configurations, Configuration.ProfilerConfiguration localConfiguration, + String telemetryRoleName, + String telemetryRoleInstance, Consumer assertion) { this.name = name; this.configurations = configurations; this.localConfiguration = localConfiguration; + this.telemetryRoleName = telemetryRoleName; + this.telemetryRoleInstance = telemetryRoleInstance; this.assertion = assertion; } } @@ -167,6 +192,29 @@ private ProfilingInitializerTestCase( .withLocalConfiguration(localConfiguration(false, true)) .then(userConfiguredTriggersState(false)) .assertThat(ENABLED)); + + tests.add( + new ProfilingInitializerTestCaseBuilder("Matching targeted plan enables profiler") + .then(targetedProfileState("test-role-name", "test-role-instance")) + .assertThat(ENABLED)); + + tests.add( + new ProfilingInitializerTestCaseBuilder("Unmatched targeted plan does not enable profiler") + .then(targetedProfileState("other-role", "test-role-instance")) + .assertThat(NOT_ENABLED)); + + tests.add( + new ProfilingInitializerTestCaseBuilder( + "Resource-derived service identity enables targeted plan") + .withTelemetryIdentity("[production]/orders", "pod-1") + .then(targetedProfileState("[production]/orders", "pod-1")) + .assertThat(ENABLED)); + + tests.add( + new ProfilingInitializerTestCaseBuilder("AKS-derived identity enables targeted plan") + .withTelemetryIdentity("orders-deployment", "orders-pod-1") + .then(targetedProfileState("orders-deployment", "orders-pod-1")) + .assertThat(ENABLED)); } @TestFactory @@ -178,7 +226,10 @@ public Collection runTests() { testCase.name, () -> { ProfilingInitializer profiler = - createProfilingInitializer(testCase.localConfiguration); + createProfilingInitializer( + testCase.localConfiguration, + testCase.telemetryRoleName, + testCase.telemetryRoleInstance); testCase.configurations.forEach(profiler::applyConfiguration); @@ -236,12 +287,33 @@ private static ProfilerConfiguration profileNowState( + triggersEnabled); } + private static ProfilerConfiguration targetedProfileState(String role, String instance) { + return userConfiguredTriggersState(false) + .setTargetedCollectionPlan( + new TargetedCollectionPlan() + .setInstances( + Collections.singletonList( + new TargetedInstance().setRole(role).setName(instance))) + .setImmediateProfilingDuration(120) + .setExpiration("2099-08-17T19:00:00.0000000Z") + .setSettingsMoniker("Portal_test")); + } + @SuppressWarnings( "DirectInvocationOnMock") // direct mock invocation is intentional for test setup private static ProfilingInitializer createProfilingInitializer( - Configuration.ProfilerConfiguration localConfiguration) { + Configuration.ProfilerConfiguration localConfiguration, + String telemetryRoleName, + String telemetryRoleInstance) { TelemetryClient client = Mockito.mock(TelemetryClient.class); MessageTelemetryBuilder messageTelemetryBuilder = MessageTelemetryBuilder.create(); + if (telemetryRoleName != null) { + messageTelemetryBuilder.addTag(ContextTagKeys.AI_CLOUD_ROLE.toString(), telemetryRoleName); + } + if (telemetryRoleInstance != null) { + messageTelemetryBuilder.addTag( + ContextTagKeys.AI_CLOUD_ROLE_INSTANCE.toString(), telemetryRoleInstance); + } Mockito.when(client.newMessageTelemetryBuilder()).thenReturn(messageTelemetryBuilder); Mockito.when(client.getConnectionString()) .thenReturn( diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/config/ProfilerConfigurationTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/config/ProfilerConfigurationTest.java index eb889fa33d0..62b3a308fb4 100644 --- a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/config/ProfilerConfigurationTest.java +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/config/ProfilerConfigurationTest.java @@ -4,6 +4,7 @@ package com.microsoft.applicationinsights.agent.internal.profiler.config; import com.azure.json.JsonOptions; +import com.azure.json.JsonProviders; import com.azure.json.JsonReader; import com.azure.json.implementation.DefaultJsonReader; import com.fasterxml.jackson.core.JsonProcessingException; @@ -79,4 +80,47 @@ public void testAlertDeserialization() { throw new RuntimeException(e); } } + + @Test + void parsesTargetedCollectionPlan() throws IOException { + String configStr = + "{\"id\":\"an-id\",\"lastModified\":\"2026-07-24T13:58:12.447Z\"," + + "\"enabledLastModified\":\"2026-07-24T13:58:12.447Z\",\"enabled\":true," + + "\"collectionPlan\":\"\",\"targetedCollectionPlan\":{" + + "\"instances\":[{\"role\":\"frontend\",\"name\":\"vm-1\",\"future\":true}]," + + "\"immediateProfilingDuration\":120," + + "\"expiration\":\"2026-08-17T19:00:00.0000000Z\"," + + "\"settingsMoniker\":\"Portal_test\"," + + "\"futureField\":\"ignored\"}}"; + + ProfilerConfiguration configuration; + try (JsonReader reader = JsonProviders.createReader(configStr)) { + configuration = ProfilerConfiguration.fromJson(reader); + } + + TargetedCollectionPlan plan = configuration.getTargetedCollectionPlan(); + Assertions.assertNotNull(plan); + Assertions.assertNull(plan.getRoles()); + Assertions.assertEquals(1, plan.getInstances().size()); + Assertions.assertEquals("frontend", plan.getInstances().get(0).getRole()); + Assertions.assertEquals("vm-1", plan.getInstances().get(0).getName()); + Assertions.assertEquals(120, plan.getImmediateProfilingDuration()); + Assertions.assertEquals("2026-08-17T19:00:00.0000000Z", plan.getExpiration()); + Assertions.assertEquals("Portal_test", plan.getSettingsMoniker()); + } + + @Test + void targetedCollectionPlanIsOptional() throws IOException { + String configStr = + "{\"id\":\"an-id\",\"lastModified\":\"2026-07-24T13:58:12.447Z\"," + + "\"enabledLastModified\":\"2026-07-24T13:58:12.447Z\",\"enabled\":true," + + "\"collectionPlan\":\"\"}"; + + ProfilerConfiguration configuration; + try (JsonReader reader = JsonProviders.createReader(configStr)) { + configuration = ProfilerConfiguration.fromJson(reader); + } + + Assertions.assertNull(configuration.getTargetedCollectionPlan()); + } } diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/service/ServiceProfilerClientTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/service/ServiceProfilerClientTest.java new file mode 100644 index 00000000000..5ffdd5a796c --- /dev/null +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/service/ServiceProfilerClientTest.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.agent.internal.profiler.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.azure.core.http.HttpClient; +import com.azure.core.http.HttpPipelineBuilder; +import com.azure.core.test.http.MockHttpResponse; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +class ServiceProfilerClientTest { + + @Test + void fallsBackToLegacyFeatureVersionAndCachesResult() throws MalformedURLException { + List requestUrls = new ArrayList<>(); + HttpClient httpClient = + request -> { + requestUrls.add(request.getUrl().toString()); + String body = + request.getUrl().getQuery().contains("featureVersion=2.0.0") + ? "{\"id\":\"00000000-0000-0000-0000-000000000000\",\"enabled\":false}" + : settingsJson(true); + return Mono.just( + new MockHttpResponse(request, 200, body.getBytes(StandardCharsets.UTF_8))); + }; + ServiceProfilerClient client = newServiceProfilerClient(httpClient); + + assertThat(client.getSettings(new Date(0)).block().isEnabled()).isTrue(); + assertThat(client.getSettings(new Date(0)).block().isEnabled()).isTrue(); + + assertThat(requestUrls) + .containsExactly( + "https://agent.azureserviceprofiler.net/api/profileragent/v4/settings?iKey=00000000-0000-0000-0000-000000000000&oldTimestamp=1970-01-01T00:00:00.0Z&featureVersion=2.0.0", + "https://agent.azureserviceprofiler.net/api/profileragent/v4/settings?iKey=00000000-0000-0000-0000-000000000000&oldTimestamp=1970-01-01T00:00:00.0Z&featureVersion=1.0.0", + "https://agent.azureserviceprofiler.net/api/profileragent/v4/settings?iKey=00000000-0000-0000-0000-000000000000&oldTimestamp=1970-01-01T00:00:00.0Z&featureVersion=1.0.0"); + } + + @Test + void keepsTargetedFeatureVersionWhenSupported() throws MalformedURLException { + List requestUrls = new ArrayList<>(); + HttpClient httpClient = + request -> { + requestUrls.add(request.getUrl().toString()); + return Mono.just( + new MockHttpResponse( + request, 200, settingsJson(false).getBytes(StandardCharsets.UTF_8))); + }; + ServiceProfilerClient client = newServiceProfilerClient(httpClient); + + assertThat(client.getSettings(new Date(0)).block().isEnabled()).isFalse(); + assertThat(client.getSettings(new Date(0)).block().isEnabled()).isFalse(); + + assertThat(requestUrls).allMatch(url -> url.contains("featureVersion=2.0.0")); + } + + private static ServiceProfilerClient newServiceProfilerClient(HttpClient httpClient) + throws MalformedURLException { + return new ServiceProfilerClient( + new URL("https://agent.azureserviceprofiler.net/"), + "00000000-0000-0000-0000-000000000000", + new HttpPipelineBuilder().httpClient(httpClient).build()); + } + + private static String settingsJson(boolean enabled) { + return "{\"id\":\"11111111-1111-1111-1111-111111111111\"," + + "\"lastModified\":\"2026-09-02T16:00:00Z\"," + + "\"enabledLastModified\":\"2026-09-02T16:00:00Z\"," + + "\"enabled\":" + + enabled + + "}"; + } +} diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/triggers/AlertConfigParserTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/triggers/AlertConfigParserTest.java index d26cfd63881..524e0a3ad1e 100644 --- a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/triggers/AlertConfigParserTest.java +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/triggers/AlertConfigParserTest.java @@ -5,6 +5,9 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.microsoft.applicationinsights.agent.internal.profiler.config.ProfilerConfiguration; +import com.microsoft.applicationinsights.agent.internal.profiler.config.TargetedCollectionPlan; +import com.microsoft.applicationinsights.agent.internal.profiler.config.TargetedInstance; import com.microsoft.applicationinsights.alerting.aiconfig.AlertingConfig; import com.microsoft.applicationinsights.alerting.config.AlertConfiguration; import com.microsoft.applicationinsights.alerting.config.AlertMetricType; @@ -12,7 +15,12 @@ import com.microsoft.applicationinsights.alerting.config.CollectionPlanConfiguration; import com.microsoft.applicationinsights.alerting.config.CollectionPlanConfiguration.EngineMode; import com.microsoft.applicationinsights.alerting.config.DefaultConfiguration; +import com.microsoft.applicationinsights.alerting.config.TargetedCollectionPlanConfiguration; +import com.microsoft.applicationinsights.alerting.config.TargetedInstanceConfiguration; +import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; @@ -127,4 +135,162 @@ void requestTriggerIsBuilt() { .setRequestTrigger(requestTrigger) .build()); } + + @Test + void targetedRolesAreParsedFaithfully() { + ProfilerConfiguration profilerConfiguration = + targetedConfiguration(targetedPlan().setRoles(Arrays.asList(" frontend ", "backend"))); + + TargetedCollectionPlanConfiguration plan = + AlertConfigParser.toAlertingConfig(profilerConfiguration) + .getTargetedCollectionPlanConfiguration(); + + assertThat(plan).isNotNull(); + assertThat(plan.getRoles()).containsExactly(" frontend ", "backend"); + assertThat(plan.getImmediateProfilingDurationSeconds()).isEqualTo(120); + assertThat(plan.getExpiration()).isEqualTo(Instant.parse("2099-08-17T19:00:00Z")); + assertThat(plan.getSettingsMoniker()).isEqualTo("Portal_test"); + } + + @Test + void targetedInstancesAreParsedFaithfully() { + ProfilerConfiguration profilerConfiguration = + targetedConfiguration( + targetedPlan() + .setInstances( + Collections.singletonList( + new TargetedInstance().setRole("frontend").setName("instance-1")))); + + TargetedCollectionPlanConfiguration plan = + AlertConfigParser.toAlertingConfig(profilerConfiguration) + .getTargetedCollectionPlanConfiguration(); + + assertThat(plan).isNotNull(); + assertThat(plan.getInstances()) + .containsExactly(TargetedInstanceConfiguration.create("frontend", "instance-1")); + } + + @Test + void malformedLegacyPlanDoesNotBlockTargetedPlan() { + ProfilerConfiguration profilerConfiguration = + targetedConfiguration(targetedPlan().setRoles(Collections.singletonList("frontend"))) + .setCollectionPlan( + "--single --mode immediate --immediate-profiling-duration invalid" + + " --expiration invalid --settings-moniker legacy"); + + AlertingConfiguration config = AlertConfigParser.toAlertingConfig(profilerConfiguration); + + assertThat(config.getCollectionPlanConfiguration().isSingle()).isFalse(); + assertThat(config.getTargetedCollectionPlanConfiguration()).isNotNull(); + assertThat( + config.hasAnEnabledTrigger( + "frontend", "instance-1", Instant.parse("2099-01-01T00:00:00Z"))) + .isTrue(); + } + + @Test + void invalidTargetedPlansFailClosed() { + TargetedCollectionPlan mixedPlan = + targetedPlan() + .setRoles(Collections.singletonList("frontend")) + .setInstances( + Collections.singletonList( + new TargetedInstance().setRole("frontend").setName("instance-1"))); + ProfilerConfiguration mixedConfiguration = targetedConfiguration(mixedPlan); + + AlertingConfiguration mixedAlertingConfig = + AlertConfigParser.toAlertingConfig(mixedConfiguration); + assertThat(mixedAlertingConfig.getTargetedCollectionPlanConfiguration()).isNotNull(); + assertThat( + mixedAlertingConfig.hasAnEnabledTrigger( + "frontend", "instance-1", Instant.parse("2099-01-01T00:00:00Z"))) + .isFalse(); + + ProfilerConfiguration mixedLegacyConfiguration = + targetedConfiguration(targetedPlan().setRoles(Collections.singletonList("frontend"))) + .setCollectionPlan( + "--single --mode immediate --immediate-profiling-duration 120" + + " --expiration 5249157885138288517 --settings-moniker legacy"); + + AlertingConfiguration combinedConfig = + AlertConfigParser.toAlertingConfig(mixedLegacyConfiguration); + assertThat(combinedConfig.getCollectionPlanConfiguration().isSingle()).isTrue(); + assertThat(combinedConfig.getTargetedCollectionPlanConfiguration()).isNotNull(); + assertThat( + combinedConfig.hasAnEnabledTrigger( + "frontend", "instance-1", Instant.parse("2099-01-01T00:00:00Z"))) + .isTrue(); + + ProfilerConfiguration invalidTargetedWithLegacy = + targetedConfiguration(mixedPlan) + .setCollectionPlan( + "--single --mode immediate --immediate-profiling-duration 120" + + " --expiration 5249157885138288517 --settings-moniker legacy"); + assertThat( + AlertConfigParser.toAlertingConfig(invalidTargetedWithLegacy) + .hasAnEnabledTrigger("frontend", "instance-1", Instant.EPOCH)) + .isFalse(); + } + + @Test + void targetedPlansWithNullValuesFailClosed() { + assertTargetedPlanInvalid(targetedPlan().setInstances(Collections.singletonList(null))); + assertTargetedPlanInvalid( + targetedPlan() + .setInstances( + Collections.singletonList(new TargetedInstance().setRole(null).setName(null)))); + assertTargetedPlanInvalid( + targetedPlan() + .setRoles(Collections.singletonList("frontend")) + .setExpiration(null) + .setSettingsMoniker(null)); + } + + @Test + void targetedPlanValidatesDurationExpirationAndIdentity() { + assertTargetedPlanInvalid( + targetedPlan() + .setRoles(Collections.singletonList("frontend")) + .setImmediateProfilingDuration(361)); + assertTargetedPlanInvalid( + targetedPlan() + .setRoles(Collections.singletonList("frontend")) + .setExpiration("not-a-timestamp")); + + TargetedCollectionPlanConfiguration plan = + AlertConfigParser.toAlertingConfig( + targetedConfiguration( + targetedPlan().setRoles(Collections.singletonList("frontend")))) + .getTargetedCollectionPlanConfiguration(); + assertThat(plan).isNotNull(); + assertThat( + AlertConfigParser.toAlertingConfig( + targetedConfiguration( + targetedPlan().setRoles(Collections.singletonList("frontend")))) + .hasAnEnabledTrigger(null, "instance-1", Instant.parse("2099-01-01T00:00:00Z"))) + .isFalse(); + } + + private static void assertTargetedPlanInvalid(TargetedCollectionPlan plan) { + TargetedCollectionPlanConfiguration parsedPlan = + AlertConfigParser.toAlertingConfig(targetedConfiguration(plan)) + .getTargetedCollectionPlanConfiguration(); + assertThat(parsedPlan).isNotNull(); + assertThat( + AlertConfigParser.toAlertingConfig(targetedConfiguration(plan)) + .hasAnEnabledTrigger( + "frontend", "instance-1", Instant.parse("2099-01-01T00:00:00Z"))) + .isFalse(); + } + + private static ProfilerConfiguration targetedConfiguration(TargetedCollectionPlan plan) { + return new ProfilerConfiguration().setCollectionPlan("").setTargetedCollectionPlan(plan); + } + + private static TargetedCollectionPlan targetedPlan() { + return new TargetedCollectionPlan() + .setImmediateProfilingDuration(120) + .setExpiration("2099-08-17T19:00:00.0000000Z") + .setSettingsMoniker("Portal_test"); + } } diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/upload/UploadServiceTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/upload/UploadServiceTest.java index 2f8dd7873a5..c8c605bc3ba 100644 --- a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/upload/UploadServiceTest.java +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/profiler/upload/UploadServiceTest.java @@ -46,7 +46,9 @@ void uploadFileGoodPathReturnsExpectedResponse() throws IOException { "a-role-name"); ServiceProfilerIndex serviceProfilerIndex = - uploadService.uploadJfrFile(profileId, "a-trigger", 321, tmpFile, 0.0, 0.0).block(); + uploadService + .uploadJfrFile(profileId, "a-trigger", 321, tmpFile, 0.0, 0.0, "Portal_test") + .block(); assertThat( serviceProfilerIndex @@ -71,6 +73,12 @@ void uploadFileGoodPathReturnsExpectedResponse() throws IOException { .getProperties() .get(ServiceProfilerIndex.Builder.SERVICE_PROFILER_DATACUBE_PROPERTY_NAME)) .isEqualTo(appId.toString()); + + assertThat( + serviceProfilerIndex + .getProperties() + .get(ServiceProfilerIndex.Builder.SERVICE_PROFILER_SETTINGS_MONIKER_PROPERTY_NAME)) + .isEqualTo("Portal_test"); } private static File createFakeJfrFile() throws IOException { diff --git a/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/telemetry/TelemetryClientTest.java b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/telemetry/TelemetryClientTest.java new file mode 100644 index 00000000000..e269cb48fb7 --- /dev/null +++ b/agent/agent-tooling/src/test/java/com/microsoft/applicationinsights/agent/internal/telemetry/TelemetryClientTest.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.applicationinsights.agent.internal.telemetry; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.azure.monitor.opentelemetry.autoconfigure.implementation.builders.MetricTelemetryBuilder; +import com.azure.monitor.opentelemetry.autoconfigure.implementation.models.ContextTagKeys; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.resources.Resource; +import org.junit.jupiter.api.Test; + +class TelemetryClientTest { + + @Test + void heartbeatUsesSameResourceDerivedIdentityAsOtherTelemetry() { + TelemetryClient telemetryClient = TelemetryClient.createForTest(); + telemetryClient.setOtelResource( + Resource.create( + Attributes.builder() + .put("service.namespace", "production") + .put("service.name", "orders") + .put("service.instance.id", "pod-1") + .build())); + + MetricTelemetryBuilder heartbeatBuilder = MetricTelemetryBuilder.create("HeartbeatState", 1); + telemetryClient.populateDefaultsForHeartbeat(heartbeatBuilder, Resource.empty()); + + assertThat(heartbeatBuilder.build().getTags()) + .containsEntry(ContextTagKeys.AI_CLOUD_ROLE.toString(), "[production]/orders") + .containsEntry(ContextTagKeys.AI_CLOUD_ROLE_INSTANCE.toString(), "pod-1") + .containsAllEntriesOf(telemetryClient.newMessageTelemetryBuilder().build().getTags()); + } +} diff --git a/agent/agent-tooling/src/test/resources/session-records/ConfigServiceTest.badServiceResponseDoesNotProvideReturn.json b/agent/agent-tooling/src/test/resources/session-records/ConfigServiceTest.badServiceResponseDoesNotProvideReturn.json index 60b61ce0ab5..caf084aaa04 100644 --- a/agent/agent-tooling/src/test/resources/session-records/ConfigServiceTest.badServiceResponseDoesNotProvideReturn.json +++ b/agent/agent-tooling/src/test/resources/session-records/ConfigServiceTest.badServiceResponseDoesNotProvideReturn.json @@ -1,7 +1,7 @@ { "networkCallRecords" : [ { "Method" : "GET", - "Uri" : "https://REDACTED.azureserviceprofiler.net/api/profileragent/v4/settings?iKey=00000000-0000-0000-0000-000000000000&oldTimestamp=1970-01-01T00:00:00.0Z&featureVersion=1.0.0", + "Uri" : "https://REDACTED.azureserviceprofiler.net/api/profileragent/v4/settings?iKey=00000000-0000-0000-0000-000000000000&oldTimestamp=1970-01-01T00:00:00.0Z&featureVersion=2.0.0", "Headers" : { }, "Response" : { "Transfer-Encoding" : "chunked", diff --git a/agent/agent-tooling/src/test/resources/session-records/ConfigServiceTest.pullSettings.json b/agent/agent-tooling/src/test/resources/session-records/ConfigServiceTest.pullSettings.json index 7fea40eddc1..35d0c65ac52 100644 --- a/agent/agent-tooling/src/test/resources/session-records/ConfigServiceTest.pullSettings.json +++ b/agent/agent-tooling/src/test/resources/session-records/ConfigServiceTest.pullSettings.json @@ -1,7 +1,7 @@ { "networkCallRecords" : [ { "Method" : "GET", - "Uri" : "https://REDACTED.azureserviceprofiler.net/api/profileragent/v4/settings?iKey=00000000-0000-0000-0000-000000000000&oldTimestamp=1970-01-01T00:00:00.0Z&featureVersion=1.0.0", + "Uri" : "https://REDACTED.azureserviceprofiler.net/api/profileragent/v4/settings?iKey=00000000-0000-0000-0000-000000000000&oldTimestamp=1970-01-01T00:00:00.0Z&featureVersion=2.0.0", "Headers" : { }, "Response" : { "Transfer-Encoding" : "chunked", diff --git a/docs/README.md b/docs/README.md index 56615a8d9af..2dc8963eb0b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -146,10 +146,10 @@ captured window. Note the following limitations while this feature is in preview - The continuous recording uses the `cpuTriggeredSettings` JFC for all trigger types, so `memoryTriggeredSettings` and `manualTriggeredSettings` are not applied to continuous captures. -- A requested profile duration (from the portal, JMX, or a file trigger) is ignored: each request - dumps the whole retained circular buffer (up to `continuousProfilingMaxAgeSeconds`), because a - live JFR recording can only be dumped in its entirety and cannot be streamed for a sub-window - without being stopped. +- A targeted portal request uses a separate on-demand recording so that its requested duration is + honored. Other requests, including legacy Profile Now, JMX, and file triggers, dump the whole + retained circular buffer (up to `continuousProfilingMaxAgeSeconds`), because a live JFR recording + can only be dumped in its entirety and cannot be streamed for a sub-window without being stopped. - Because JFR runs for the lifetime of the JVM rather than in short bursts, expect a steady-state increase in CPU, memory and disk I/O compared to on-demand profiling. diff --git a/smoke-tests/apps/DiagnosticExtension/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/JavaProfileConfigTest.java b/smoke-tests/apps/DiagnosticExtension/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/JavaProfileConfigTest.java index 05af416d969..8e10ed8e242 100644 --- a/smoke-tests/apps/DiagnosticExtension/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/JavaProfileConfigTest.java +++ b/smoke-tests/apps/DiagnosticExtension/src/smokeTest/java/com/microsoft/applicationinsights/smoketest/JavaProfileConfigTest.java @@ -79,4 +79,26 @@ static class JavaProfilerManualProfileTest extends JavaProfileConfigTest { super(testing, true); } } + + @Environment(JAVA_11) + static class JavaProfilerTargetedMatchingTest extends JavaProfileConfigTest { + @RegisterExtension + static final SmokeTestExtension testing = + BASE_BUILDER.setProfilerEndpoint(ProfilerState.targetedMatching).build(); + + JavaProfilerTargetedMatchingTest() { + super(testing, true); + } + } + + @Environment(JAVA_11) + static class JavaProfilerTargetedUnmatchedTest extends JavaProfileConfigTest { + @RegisterExtension + static final SmokeTestExtension testing = + BASE_BUILDER.setProfilerEndpoint(ProfilerState.targetedUnmatched).build(); + + JavaProfilerTargetedUnmatchedTest() { + super(testing, false); + } + } } diff --git a/smoke-tests/framework/src/main/java/com/microsoft/applicationinsights/smoketest/fakeingestion/MockedProfilerSettingsServlet.java b/smoke-tests/framework/src/main/java/com/microsoft/applicationinsights/smoketest/fakeingestion/MockedProfilerSettingsServlet.java index 8225b48507c..aed2a8f9c41 100644 --- a/smoke-tests/framework/src/main/java/com/microsoft/applicationinsights/smoketest/fakeingestion/MockedProfilerSettingsServlet.java +++ b/smoke-tests/framework/src/main/java/com/microsoft/applicationinsights/smoketest/fakeingestion/MockedProfilerSettingsServlet.java @@ -99,6 +99,44 @@ public class MockedProfilerSettingsServlet extends HttpServlet { + "\",\n" + " \"memoryTriggerConfiguration\" : \"--memory-threshold 80 --memory-trigger-profilingDuration 120 --memory-trigger-cooldown 14400 --memory-trigger-enabled true\"\n" + "}\n"); + + CONFIGS.put( + ProfilerState.targetedMatching, + targetedConfig(now, Instant.now().plusSeconds(3600), "testrolename", "testroleinstance")); + CONFIGS.put( + ProfilerState.targetedUnmatched, + targetedConfig(now, Instant.now().plusSeconds(3600), "other-role", "testroleinstance")); + } + + private static String targetedConfig( + String now, Instant expiration, String roleName, String roleInstance) { + return "{\n" + + " \"agentConcurrency\" : 0,\n" + + " \"collectionPlan\" : \"\",\n" + + " \"cpuTriggerConfiguration\" : \"--cpu-threshold 80 --cpu-trigger-profilingDuration 120 --cpu-trigger-cooldown 14400 --cpu-trigger-enabled false\",\n" + + " \"defaultConfiguration\" : null,\n" + + " \"enabled\" : true,\n" + + " \"enabledLastModified\" : \"" + + now + + "\",\n" + + " \"id\" : \"an-id\",\n" + + " \"lastModified\" : \"" + + now + + "\",\n" + + " \"memoryTriggerConfiguration\" : \"--memory-threshold 80 --memory-trigger-profilingDuration 120 --memory-trigger-cooldown 14400 --memory-trigger-enabled false\",\n" + + " \"targetedCollectionPlan\" : {\n" + + " \"instances\" : [{ \"role\" : \"" + + roleName + + "\", \"name\" : \"" + + roleInstance + + "\" }],\n" + + " \"immediateProfilingDuration\" : 1,\n" + + " \"expiration\" : \"" + + expiration + + "\",\n" + + " \"settingsMoniker\" : \"Portal_targeted-smoke\"\n" + + " }\n" + + "}\n"; } private static long toSeconds(Instant time) { diff --git a/smoke-tests/framework/src/main/java/com/microsoft/applicationinsights/smoketest/fakeingestion/ProfilerState.java b/smoke-tests/framework/src/main/java/com/microsoft/applicationinsights/smoketest/fakeingestion/ProfilerState.java index 94d38ac46e1..efdaee76f6d 100644 --- a/smoke-tests/framework/src/main/java/com/microsoft/applicationinsights/smoketest/fakeingestion/ProfilerState.java +++ b/smoke-tests/framework/src/main/java/com/microsoft/applicationinsights/smoketest/fakeingestion/ProfilerState.java @@ -7,5 +7,7 @@ public enum ProfilerState { unconfigured, configuredEnabled, configuredDisabled, - manualprofile + manualprofile, + targetedMatching, + targetedUnmatched }