From a3a9287498381f7155a2ff7b7a94b1b17994251f Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Wed, 5 Aug 2026 03:12:25 +0100
Subject: [PATCH 1/3] perf: skip logging scans without secrets
---
.../Console/ModuleOutputBuffer.cs | 9 ++++++--
.../Engine/ISecretObfuscator.cs | 11 ++++++++-
.../Engine/SecretObfuscator.cs | 3 +++
.../Logging/FormattedLogValuesObfuscator.cs | 10 ++++++--
.../Logging/ObfuscatedLogException.cs | 16 +++++++------
.../Console/ModuleOutputBufferTests.cs | 3 +++
.../FormattedLogValuesObfuscatorTests.cs | 23 +++++++++++++++++++
.../Logging/PipelineLevelLoggerTests.cs | 21 +++++++++++++++++
.../Logging/SecretObfuscatorCachingTests.cs | 19 +++++++++++++++
9 files changed, 103 insertions(+), 12 deletions(-)
diff --git a/src/ModularPipelines/Console/ModuleOutputBuffer.cs b/src/ModularPipelines/Console/ModuleOutputBuffer.cs
index df990b54f9c..31e80969ce2 100644
--- a/src/ModularPipelines/Console/ModuleOutputBuffer.cs
+++ b/src/ModularPipelines/Console/ModuleOutputBuffer.cs
@@ -813,10 +813,15 @@ public void WriteTo(ILogger logger)
public string? FormatException()
=> _obfuscatedException is null
? null
- : secretObfuscator.Obfuscate(_obfuscatedException.ToString(), null);
+ : Obfuscate(_obfuscatedException.ToString());
private string Format(object? state, Exception? logException)
- => secretObfuscator.Obfuscate(_rawFormattedMessage.Value, null) ?? string.Empty;
+ => Obfuscate(_rawFormattedMessage.Value);
+
+ private string Obfuscate(string value)
+ => secretObfuscator.HasSecrets
+ ? secretObfuscator.Obfuscate(value, null)
+ : value;
private string FormatTyped(TState state, Exception? logException)
=> Format(state!, logException);
diff --git a/src/ModularPipelines/Engine/ISecretObfuscator.cs b/src/ModularPipelines/Engine/ISecretObfuscator.cs
index 06dcbd92acb..5cda1ec1bb4 100644
--- a/src/ModularPipelines/Engine/ISecretObfuscator.cs
+++ b/src/ModularPipelines/Engine/ISecretObfuscator.cs
@@ -5,6 +5,15 @@ namespace ModularPipelines.Engine;
///
public interface ISecretObfuscator
{
+ ///
+ /// Gets whether any secrets are currently registered for global masking.
+ ///
+ ///
+ /// The conservative default preserves masking for custom implementations that do not
+ /// expose their registration state.
+ ///
+ bool HasSecrets => true;
+
///
/// Obfuscates sensitive information in the provided input.
///
@@ -12,4 +21,4 @@ public interface ISecretObfuscator
/// An options object that may contain sensitive properties.
/// The input with sensitive information obfuscated.
string Obfuscate(string? input, object? optionsObject);
-}
\ No newline at end of file
+}
diff --git a/src/ModularPipelines/Engine/SecretObfuscator.cs b/src/ModularPipelines/Engine/SecretObfuscator.cs
index 1092cb0c714..e1ef91cf2d0 100644
--- a/src/ModularPipelines/Engine/SecretObfuscator.cs
+++ b/src/ModularPipelines/Engine/SecretObfuscator.cs
@@ -34,6 +34,9 @@ internal class SecretObfuscator : ISecretObfuscator, IInitializer
public int Order => int.MaxValue;
+ public bool HasSecrets =>
+ GetRegisteredSecretCache(_maskingOptions.Value.CaseInsensitive).SearchValues is not null;
+
public SecretObfuscator(
ISecretProvider secretProvider,
IOptions maskingOptions)
diff --git a/src/ModularPipelines/Logging/FormattedLogValuesObfuscator.cs b/src/ModularPipelines/Logging/FormattedLogValuesObfuscator.cs
index 74e9d64be60..f934313acf9 100644
--- a/src/ModularPipelines/Logging/FormattedLogValuesObfuscator.cs
+++ b/src/ModularPipelines/Logging/FormattedLogValuesObfuscator.cs
@@ -31,6 +31,11 @@ public FormattedLogValuesObfuscator(ISecretObfuscator secretObfuscator)
public object TryObfuscateValues(object state)
{
+ if (!_secretObfuscator.HasSecrets)
+ {
+ return state;
+ }
+
if (state is not IReadOnlyList> values)
{
return ObfuscateValue(state);
@@ -69,7 +74,7 @@ private object ObfuscateValue(object value)
string originalValue;
try
{
- originalValue = value.ToString() ?? string.Empty;
+ originalValue = value as string ?? value.ToString() ?? string.Empty;
}
catch (Exception)
{
@@ -77,7 +82,8 @@ private object ObfuscateValue(object value)
}
var obfuscatedValue = _secretObfuscator.Obfuscate(originalValue, null);
- return obfuscatedValue.Equals(originalValue, StringComparison.Ordinal)
+ return ReferenceEquals(obfuscatedValue, originalValue)
+ || obfuscatedValue.Equals(originalValue, StringComparison.Ordinal)
? value
: obfuscatedValue;
}
diff --git a/src/ModularPipelines/Logging/ObfuscatedLogException.cs b/src/ModularPipelines/Logging/ObfuscatedLogException.cs
index 8a2e6173e4c..bc676c3dbe9 100644
--- a/src/ModularPipelines/Logging/ObfuscatedLogException.cs
+++ b/src/ModularPipelines/Logging/ObfuscatedLogException.cs
@@ -31,13 +31,15 @@ private ObfuscatedLogException(Exception exception, ISecretObfuscator secretObfu
}
public static Exception? Create(Exception? exception, ISecretObfuscator secretObfuscator)
- => exception switch
- {
- null => null,
- AggregateException aggregateException =>
- new ObfuscatedAggregateLogException(aggregateException, secretObfuscator),
- _ => new ObfuscatedLogException(exception, secretObfuscator),
- };
+ => exception is null || !secretObfuscator.HasSecrets
+ ? exception
+ : exception switch
+ {
+ null => null,
+ AggregateException aggregateException =>
+ new ObfuscatedAggregateLogException(aggregateException, secretObfuscator),
+ _ => new ObfuscatedLogException(exception, secretObfuscator),
+ };
public override string? StackTrace => _obfuscatedStackTrace;
diff --git a/test/ModularPipelines.UnitTests/Console/ModuleOutputBufferTests.cs b/test/ModularPipelines.UnitTests/Console/ModuleOutputBufferTests.cs
index c3751aae665..47ade244ffe 100644
--- a/test/ModularPipelines.UnitTests/Console/ModuleOutputBufferTests.cs
+++ b/test/ModularPipelines.UnitTests/Console/ModuleOutputBufferTests.cs
@@ -203,6 +203,7 @@ public async Task BufferedLogEvent_FormatsOnceAndObfuscatesEveryTime()
{
var formatterCalls = 0;
var secretObfuscator = new Mock();
+ secretObfuscator.SetupGet(x => x.HasSecrets).Returns(true);
secretObfuscator
.Setup(x => x.Obfuscate(It.IsAny(), null))
.Returns((string? value, object? _) => value?.Replace("secret", "***") ?? string.Empty);
@@ -236,6 +237,7 @@ public async Task BufferedLogEvent_ReobfuscatesMessageWithCurrentSecrets()
const string secret = "late-registered-secret";
var redactSecret = false;
var secretObfuscator = new Mock();
+ secretObfuscator.SetupGet(x => x.HasSecrets).Returns(() => redactSecret);
secretObfuscator
.Setup(x => x.Obfuscate(It.IsAny(), null))
.Returns((string? value, object? _) => redactSecret
@@ -268,6 +270,7 @@ public async Task BufferedLogEvent_ReobfuscatesExceptionWithCurrentSecrets()
const string secret = "late-registered-secret";
var redactSecret = false;
var secretObfuscator = new Mock();
+ secretObfuscator.SetupGet(x => x.HasSecrets).Returns(() => redactSecret);
secretObfuscator
.Setup(x => x.Obfuscate(It.IsAny(), null))
.Returns((string? value, object? _) => redactSecret
diff --git a/test/ModularPipelines.UnitTests/Logging/FormattedLogValuesObfuscatorTests.cs b/test/ModularPipelines.UnitTests/Logging/FormattedLogValuesObfuscatorTests.cs
index 893db56a1c1..c8340c82c17 100644
--- a/test/ModularPipelines.UnitTests/Logging/FormattedLogValuesObfuscatorTests.cs
+++ b/test/ModularPipelines.UnitTests/Logging/FormattedLogValuesObfuscatorTests.cs
@@ -7,6 +7,22 @@ namespace ModularPipelines.UnitTests.Logging;
public class FormattedLogValuesObfuscatorTests
{
+ [Test]
+ public async Task TryObfuscateValues_DoesNotInspectStateWhenNoSecretsAreRegistered()
+ {
+ var state = new ThrowingToStringState();
+ var secretObfuscator = new Mock();
+ secretObfuscator.SetupGet(x => x.HasSecrets).Returns(false);
+
+ var obfuscatedState = new FormattedLogValuesObfuscator(secretObfuscator.Object)
+ .TryObfuscateValues(state);
+
+ await Assert.That(obfuscatedState).IsSameReferenceAs(state);
+ secretObfuscator.Verify(
+ x => x.Obfuscate(It.IsAny(), It.IsAny