Skip to content

chore: add support for multiple dimensions #884

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,10 @@ void PushSingleMetric(string name, double value, MetricUnit unit, string nameSpa
/// </summary>
/// <param name="context"></param>
void CaptureColdStartMetric(ILambdaContext context);

/// <summary>
/// Adds multiple dimensions at once.
/// </summary>
/// <param name="dimensions">Array of key-value tuples representing dimensions.</param>
void AddDimensions(params (string key, string value)[] dimensions);
}
60 changes: 59 additions & 1 deletion libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ void IMetrics.ClearDefaultDimensions()
}

/// <inheritdoc />
public void SetService(string service)
void IMetrics.SetService(string service)
{
// this needs to check if service is set through code or env variables
// the default value service_undefined has to be ignored and return null so it is not added as default
Expand Down Expand Up @@ -433,6 +433,15 @@ public static void SetNamespace(string nameSpace)
{
Instance.SetNamespace(nameSpace);
}

/// <summary>
/// Sets the service name for the metrics.
/// </summary>
/// <param name="service">The service name.</param>
public static void SetService(string service)
{
Instance.SetService(service);
}

/// <summary>
/// Retrieves namespace identifier.
Expand Down Expand Up @@ -576,6 +585,55 @@ void IMetrics.CaptureColdStartMetric(ILambdaContext context)
dimensions
);
}

/// <inheritdoc />
void IMetrics.AddDimensions(params (string key, string value)[] dimensions)
{
if (dimensions == null || dimensions.Length == 0)
return;

// Validate all dimensions first
foreach (var (key, value) in dimensions)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentNullException(nameof(dimensions),
"'AddDimensions' method requires valid dimension keys. 'Null' or empty values are not allowed.");

if (string.IsNullOrWhiteSpace(value))
throw new ArgumentNullException(nameof(dimensions),
"'AddDimensions' method requires valid dimension values. 'Null' or empty values are not allowed.");
}

// Create a new dimension set with all dimensions
var dimensionSet = new DimensionSet(dimensions[0].key, dimensions[0].value);

// Add remaining dimensions to the same set
for (var i = 1; i < dimensions.Length; i++)
{
dimensionSet.Dimensions.Add(dimensions[i].key, dimensions[i].value);
}

// Add the dimensionSet to a list and pass it to AddDimensions
_context.AddDimensions([dimensionSet]);
}

/// <summary>
/// Adds multiple dimensions at once.
/// </summary>
/// <param name="dimensions">Array of key-value tuples representing dimensions.</param>
public static void AddDimensions(params (string key, string value)[] dimensions)
{
Instance.AddDimensions(dimensions);
}

/// <summary>
/// Flushes the metrics.
/// </summary>
/// <param name="metricsOverflow">If set to <c>true</c>, indicates a metrics overflow.</param>
public static void Flush(bool metricsOverflow = false)
{
Instance.Flush(metricsOverflow);
}

/// <summary>
/// Helper method for testing purposes. Clears static instance between test execution
Expand Down
11 changes: 10 additions & 1 deletion libraries/src/AWS.Lambda.Powertools.Metrics/Model/Metadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,19 @@ internal string GetService()
/// Adds new Dimension
/// </summary>
/// <param name="dimension">Dimension to add</param>
internal void AddDimensionSet(DimensionSet dimension)
internal void AddDimension(DimensionSet dimension)
{
_metricDirective.AddDimension(dimension);
}

/// <summary>
/// Adds new List of Dimensions
/// </summary>
/// <param name="dimension">Dimensions to add</param>
internal void AddDimensionSet(List<DimensionSet> dimension)
{
_metricDirective.AddDimensionSet(dimension);
}

/// <summary>
/// Sets default dimensions list
Expand Down
103 changes: 80 additions & 23 deletions libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricDirective.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,22 +109,35 @@
{
get
{
var defaultKeys = DefaultDimensions
.Where(d => d.DimensionKeys.Any())
.SelectMany(s => s.DimensionKeys)
.ToList();
var result = new List<List<string>>();
var allDimKeys = new List<string>();

var keys = Dimensions
.Where(d => d.DimensionKeys.Any())
.SelectMany(s => s.DimensionKeys)
.ToList();
// Add default dimensions keys
if (DefaultDimensions.Any())
{
foreach (var dimensionSet in DefaultDimensions)
{
foreach (var key in dimensionSet.DimensionKeys.Where(key => !allDimKeys.Contains(key)))
{
allDimKeys.Add(key);
}
}
}

defaultKeys.AddRange(keys);
// Add all regular dimensions to the same array
foreach (var dimensionSet in Dimensions)
{
foreach (var key in dimensionSet.DimensionKeys.Where(key => !allDimKeys.Contains(key)))
{
allDimKeys.Add(key);
}
}

if (defaultKeys.Count == 0) defaultKeys = new List<string>();
// Add non-empty dimension arrays
// When no dimensions exist, add an empty array
result.Add(allDimKeys.Any() ? allDimKeys : []);

// Wrap the list of strings in another list
return new List<List<string>> { defaultKeys };
return result;
}
}

Expand Down Expand Up @@ -192,19 +205,37 @@
/// <exception cref="System.ArgumentOutOfRangeException">Dimensions - Cannot add more than 9 dimensions at the same time.</exception>
internal void AddDimension(DimensionSet dimension)
{
if (Dimensions.Count < PowertoolsConfigurations.MaxDimensions)
// Check if we already have any dimensions
if (Dimensions.Count > 0)
{
var matchingKeys = AllDimensionKeys.Where(x => x.Contains(dimension.DimensionKeys[0]));
if (!matchingKeys.Any())
Dimensions.Add(dimension);
else
Console.WriteLine(
$"##WARNING##: Failed to Add dimension '{dimension.DimensionKeys[0]}'. Dimension already exists.");
// Get the first dimension set where we now store all dimensions
var firstDimensionSet = Dimensions[0];

// Check the actual dimension count inside the first dimension set
if (firstDimensionSet.Dimensions.Count >= PowertoolsConfigurations.MaxDimensions)
{
throw new ArgumentOutOfRangeException(nameof(dimension),
$"Cannot add more than {PowertoolsConfigurations.MaxDimensions} dimensions at the same time.");
}

// Add to the first dimension set instead of creating a new one
foreach (var pair in dimension.Dimensions)
{
if (!firstDimensionSet.Dimensions.ContainsKey(pair.Key))
{
firstDimensionSet.Dimensions.Add(pair.Key, pair.Value);
}
else
{
Console.WriteLine(
$"##WARNING##: Failed to Add dimension '{pair.Key}'. Dimension already exists.");
}

Check warning on line 232 in libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricDirective.cs

View check run for this annotation

Codecov / codecov/patch

libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricDirective.cs#L229-L232

Added lines #L229 - L232 were not covered by tests
}
}
else
{
throw new ArgumentOutOfRangeException(nameof(Dimensions),
$"Cannot add more than {PowertoolsConfigurations.MaxDimensions} dimensions at the same time.");
// No dimensions yet, add the new one
Dimensions.Add(dimension);
}
}

Expand All @@ -228,18 +259,44 @@
/// <returns>Dictionary with dimension and default dimension list appended</returns>
internal Dictionary<string, string> ExpandAllDimensionSets()
{
// if a key appears multiple times, the last value will be the one that's used in the output.
var dimensions = new Dictionary<string, string>();

foreach (var dimensionSet in DefaultDimensions)
foreach (var (key, value) in dimensionSet.Dimensions)
dimensions.TryAdd(key, value);
dimensions[key] = value;

foreach (var dimensionSet in Dimensions)
foreach (var (key, value) in dimensionSet.Dimensions)
dimensions.TryAdd(key, value);
dimensions[key] = value;

return dimensions;
}

/// <summary>
/// Adds multiple dimensions as a complete dimension set to memory.
/// </summary>
/// <param name="dimensionSets">List of dimension sets to add</param>
internal void AddDimensionSet(List<DimensionSet> dimensionSets)
{
if (dimensionSets == null || !dimensionSets.Any())
return;

Check warning on line 283 in libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricDirective.cs

View check run for this annotation

Codecov / codecov/patch

libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricDirective.cs#L283

Added line #L283 was not covered by tests

if (Dimensions.Count + dimensionSets.Count <= PowertoolsConfigurations.MaxDimensions)
{
// Simply add the dimension sets without checking for existing keys
// This ensures dimensions added together stay together
foreach (var dimensionSet in dimensionSets.Where(dimensionSet => dimensionSet.DimensionKeys.Any()))
{
Dimensions.Add(dimensionSet);
}
}
else
{
throw new ArgumentOutOfRangeException(nameof(Dimensions),
$"Cannot add more than {PowertoolsConfigurations.MaxDimensions} dimensions at the same time.");

Check warning on line 297 in libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricDirective.cs

View check run for this annotation

Codecov / codecov/patch

libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricDirective.cs#L295-L297

Added lines #L295 - L297 were not covered by tests
}
}

/// <summary>
/// Clears both default dimensions and dimensions lists
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ internal string GetService()
/// <param name="value">Dimension value</param>
public void AddDimension(string key, string value)
{
_rootNode.AWS.AddDimensionSet(new DimensionSet(key, value));
_rootNode.AWS.AddDimension(new DimensionSet(key, value));
}

/// <summary>
Expand All @@ -141,10 +141,8 @@ public void AddDimension(string key, string value)
/// <param name="dimensions">List of dimensions</param>
public void AddDimensions(List<DimensionSet> dimensions)
{
foreach (var dimension in dimensions)
{
_rootNode.AWS.AddDimensionSet(dimension);
}
// Call the AddDimensionSet method on the MetricDirective to add as a set
_rootNode.AWS.AddDimensionSet(dimensions);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public void WhenMaxDataPointsAreAddedToTheSameMetric_FlushAutomatically()

[Trait("Category", "EMFLimits")]
[Fact]
public void WhenMoreThan9DimensionsAdded_ThrowArgumentOutOfRangeException()
public void WhenMoreThan29DimensionsAdded_ThrowArgumentOutOfRangeException()
{
// Act
var act = () => { _handler.MaxDimensions(29); };
Expand Down Expand Up @@ -400,6 +400,96 @@ public async Task WhenMetricsAsyncRaceConditionItemSameKeyExists_ValidateLock()
"{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Metric Name\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\"]]",
metricsOutput);
}

[Trait("Category", "MetricsImplementation")]
[Fact]
public void AddDimensions_WithMultipleValues_AddsDimensionsToSameDimensionSet()
{
// Act
_handler.AddMultipleDimensionsInSameSet();

var result = _consoleOut.ToString();

// Assert
Assert.Contains("\"Dimensions\":[[\"Service\",\"Environment\",\"Region\"]]", result);
Assert.Contains("\"Service\":\"testService\",\"Environment\":\"test\",\"Region\":\"us-west-2\"", result);
}

[Trait("Category", "MetricsImplementation")]
[Fact]
public void AddDimensions_WithEmptyArray_DoesNotAddAnyDimensions()
{
// Act
_handler.AddEmptyDimensions();

var result = _consoleOut.ToString();

// Assert
Assert.Contains("\"Dimensions\":[[\"Service\"]]", result);
Assert.DoesNotContain("\"Environment\":", result);
}

[Trait("Category", "MetricsImplementation")]
[Fact]
public void AddDimensions_WithNullOrEmptyKey_ThrowsArgumentNullException()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => _handler.AddDimensionsWithInvalidKey());
}

[Trait("Category", "MetricsImplementation")]
[Fact]
public void AddDimensions_WithNullOrEmptyValue_ThrowsArgumentNullException()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => _handler.AddDimensionsWithInvalidValue());
}

[Trait("Category", "MetricsImplementation")]
[Fact]
public void AddDimensions_OverwritesExistingDimensions_LastValueWins()
{
// Act
_handler.AddDimensionsWithOverwrite();

var result = _consoleOut.ToString();

// Assert
Assert.Contains("\"Service\":\"testService\",\"dimension1\":\"B\",\"dimension2\":\"2\"", result);
Assert.DoesNotContain("\"dimension1\":\"A\"", result);
}

[Trait("Category", "MetricsImplementation")]
[Fact]
public void AddDimensions_IncludesDefaultDimensions()
{
// Act
_handler.AddDimensionsWithDefaultDimensions();

var result = _consoleOut.ToString();

// Assert
Assert.Contains("\"Dimensions\":[[\"Service\",\"environment\",\"dimension1\",\"dimension2\"]]", result);
Assert.Contains("\"Service\":\"testService\",\"environment\":\"prod\",\"dimension1\":\"1\",\"dimension2\":\"2\"", result);
}

[Trait("Category", "MetricsImplementation")]
[Fact]
public void AddDefaultDimensionsAtRuntime_OnlyAppliedToNewDimensionSets()
{
// Act
_handler.AddDefaultDimensionsAtRuntime();

var result = _consoleOut.ToString();

// First metric output should have original default dimensions
Assert.Contains("\"Metrics\":[{\"Name\":\"FirstMetric\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"environment\",\"dimension1\",\"dimension2\"]]", result);
Assert.Contains("\"Service\":\"testService\",\"environment\":\"prod\",\"dimension1\":\"1\",\"dimension2\":\"2\",\"FirstMetric\":1", result);

// Second metric output should have additional default dimensions
Assert.Contains("\"Metrics\":[{\"Name\":\"SecondMetric\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"environment\",\"tenantId\",\"foo\",\"bar\"]]", result);
Assert.Contains("\"Service\":\"testService\",\"environment\":\"prod\",\"tenantId\":\"1\",\"foo\":\"1\",\"bar\":\"2\",\"SecondMetric\":1", result);
}


#region Helpers
Expand Down
Loading
Loading