Skip to content

Commit fa5f7c5

Browse files
committed
feat(metrics): add support for multiple dimensions and improve dimension management
1 parent dfb8ae4 commit fa5f7c5

File tree

7 files changed

+359
-31
lines changed

7 files changed

+359
-31
lines changed

libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,10 @@ void PushSingleMetric(string name, double value, MetricUnit unit, string nameSpa
119119
/// </summary>
120120
/// <param name="context"></param>
121121
void CaptureColdStartMetric(ILambdaContext context);
122+
123+
/// <summary>
124+
/// Adds multiple dimensions at once.
125+
/// </summary>
126+
/// <param name="dimensions">Array of key-value tuples representing dimensions.</param>
127+
void AddDimensions(params (string key, string value)[] dimensions);
122128
}

libraries/src/AWS.Lambda.Powertools.Metrics/Metrics.cs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ void IMetrics.ClearDefaultDimensions()
317317
}
318318

319319
/// <inheritdoc />
320-
public void SetService(string service)
320+
void IMetrics.SetService(string service)
321321
{
322322
// this needs to check if service is set through code or env variables
323323
// the default value service_undefined has to be ignored and return null so it is not added as default
@@ -433,6 +433,15 @@ public static void SetNamespace(string nameSpace)
433433
{
434434
Instance.SetNamespace(nameSpace);
435435
}
436+
437+
/// <summary>
438+
/// Sets the service name for the metrics.
439+
/// </summary>
440+
/// <param name="service">The service name.</param>
441+
public static void SetService(string service)
442+
{
443+
Instance.SetService(service);
444+
}
436445

437446
/// <summary>
438447
/// Retrieves namespace identifier.
@@ -576,6 +585,55 @@ void IMetrics.CaptureColdStartMetric(ILambdaContext context)
576585
dimensions
577586
);
578587
}
588+
589+
/// <inheritdoc />
590+
void IMetrics.AddDimensions(params (string key, string value)[] dimensions)
591+
{
592+
if (dimensions == null || dimensions.Length == 0)
593+
return;
594+
595+
// Validate all dimensions first
596+
foreach (var (key, value) in dimensions)
597+
{
598+
if (string.IsNullOrWhiteSpace(key))
599+
throw new ArgumentNullException(nameof(key),
600+
"'AddDimensions' method requires valid dimension keys. 'Null' or empty values are not allowed.");
601+
602+
if (string.IsNullOrWhiteSpace(value))
603+
throw new ArgumentNullException(nameof(value),
604+
"'AddDimensions' method requires valid dimension values. 'Null' or empty values are not allowed.");
605+
}
606+
607+
// Create a new dimension set with all dimensions
608+
var dimensionSet = new DimensionSet(dimensions[0].key, dimensions[0].value);
609+
610+
// Add remaining dimensions to the same set
611+
for (var i = 1; i < dimensions.Length; i++)
612+
{
613+
dimensionSet.Dimensions.Add(dimensions[i].key, dimensions[i].value);
614+
}
615+
616+
// Add the dimensionSet to a list and pass it to AddDimensions
617+
_context.AddDimensions([dimensionSet]);
618+
}
619+
620+
/// <summary>
621+
/// Adds multiple dimensions at once.
622+
/// </summary>
623+
/// <param name="dimensions">Array of key-value tuples representing dimensions.</param>
624+
public static void AddDimensions(params (string key, string value)[] dimensions)
625+
{
626+
Instance.AddDimensions(dimensions);
627+
}
628+
629+
/// <summary>
630+
/// Flushes the metrics.
631+
/// </summary>
632+
/// <param name="metricsOverflow">If set to <c>true</c>, indicates a metrics overflow.</param>
633+
public static void Flush(bool metricsOverflow = false)
634+
{
635+
Instance.Flush(metricsOverflow);
636+
}
579637

580638
/// <summary>
581639
/// Helper method for testing purposes. Clears static instance between test execution

libraries/src/AWS.Lambda.Powertools.Metrics/Model/Metadata.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,19 @@ internal string GetService()
129129
/// Adds new Dimension
130130
/// </summary>
131131
/// <param name="dimension">Dimension to add</param>
132-
internal void AddDimensionSet(DimensionSet dimension)
132+
internal void AddDimension(DimensionSet dimension)
133133
{
134134
_metricDirective.AddDimension(dimension);
135135
}
136+
137+
/// <summary>
138+
/// Adds new List of Dimensions
139+
/// </summary>
140+
/// <param name="dimension">Dimensions to add</param>
141+
internal void AddDimensionSet(List<DimensionSet> dimension)
142+
{
143+
_metricDirective.AddDimensionSet(dimension);
144+
}
136145

137146
/// <summary>
138147
/// Sets default dimensions list

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

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -109,22 +109,35 @@ public List<List<string>> AllDimensionKeys
109109
{
110110
get
111111
{
112-
var defaultKeys = DefaultDimensions
113-
.Where(d => d.DimensionKeys.Any())
114-
.SelectMany(s => s.DimensionKeys)
115-
.ToList();
112+
var result = new List<List<string>>();
113+
var allDimKeys = new List<string>();
116114

117-
var keys = Dimensions
118-
.Where(d => d.DimensionKeys.Any())
119-
.SelectMany(s => s.DimensionKeys)
120-
.ToList();
115+
// Add default dimensions keys
116+
if (DefaultDimensions.Any())
117+
{
118+
foreach (var dimensionSet in DefaultDimensions)
119+
{
120+
foreach (var key in dimensionSet.DimensionKeys.Where(key => !allDimKeys.Contains(key)))
121+
{
122+
allDimKeys.Add(key);
123+
}
124+
}
125+
}
121126

122-
defaultKeys.AddRange(keys);
127+
// Add all regular dimensions to the same array
128+
foreach (var dimensionSet in Dimensions)
129+
{
130+
foreach (var key in dimensionSet.DimensionKeys.Where(key => !allDimKeys.Contains(key)))
131+
{
132+
allDimKeys.Add(key);
133+
}
134+
}
123135

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

126-
// Wrap the list of strings in another list
127-
return new List<List<string>> { defaultKeys };
140+
return result;
128141
}
129142
}
130143

@@ -192,19 +205,37 @@ internal void SetService(string service)
192205
/// <exception cref="System.ArgumentOutOfRangeException">Dimensions - Cannot add more than 9 dimensions at the same time.</exception>
193206
internal void AddDimension(DimensionSet dimension)
194207
{
195-
if (Dimensions.Count < PowertoolsConfigurations.MaxDimensions)
208+
// Check if we already have any dimensions
209+
if (Dimensions.Count > 0)
196210
{
197-
var matchingKeys = AllDimensionKeys.Where(x => x.Contains(dimension.DimensionKeys[0]));
198-
if (!matchingKeys.Any())
199-
Dimensions.Add(dimension);
200-
else
201-
Console.WriteLine(
202-
$"##WARNING##: Failed to Add dimension '{dimension.DimensionKeys[0]}'. Dimension already exists.");
211+
// Get the first dimension set where we now store all dimensions
212+
var firstDimensionSet = Dimensions[0];
213+
214+
// Check the actual dimension count inside the first dimension set
215+
if (firstDimensionSet.Dimensions.Count >= PowertoolsConfigurations.MaxDimensions)
216+
{
217+
throw new ArgumentOutOfRangeException(nameof(Dimensions),
218+
$"Cannot add more than {PowertoolsConfigurations.MaxDimensions} dimensions at the same time.");
219+
}
220+
221+
// Add to the first dimension set instead of creating a new one
222+
foreach (var pair in dimension.Dimensions)
223+
{
224+
if (!firstDimensionSet.Dimensions.ContainsKey(pair.Key))
225+
{
226+
firstDimensionSet.Dimensions.Add(pair.Key, pair.Value);
227+
}
228+
else
229+
{
230+
Console.WriteLine(
231+
$"##WARNING##: Failed to Add dimension '{pair.Key}'. Dimension already exists.");
232+
}
233+
}
203234
}
204235
else
205236
{
206-
throw new ArgumentOutOfRangeException(nameof(Dimensions),
207-
$"Cannot add more than {PowertoolsConfigurations.MaxDimensions} dimensions at the same time.");
237+
// No dimensions yet, add the new one
238+
Dimensions.Add(dimension);
208239
}
209240
}
210241

@@ -228,18 +259,44 @@ internal void SetDefaultDimensions(List<DimensionSet> defaultDimensions)
228259
/// <returns>Dictionary with dimension and default dimension list appended</returns>
229260
internal Dictionary<string, string> ExpandAllDimensionSets()
230261
{
262+
// if a key appears multiple times, the last value will be the one that's used in the output.
231263
var dimensions = new Dictionary<string, string>();
232264

233265
foreach (var dimensionSet in DefaultDimensions)
234266
foreach (var (key, value) in dimensionSet.Dimensions)
235-
dimensions.TryAdd(key, value);
267+
dimensions[key] = value;
236268

237269
foreach (var dimensionSet in Dimensions)
238270
foreach (var (key, value) in dimensionSet.Dimensions)
239-
dimensions.TryAdd(key, value);
271+
dimensions[key] = value;
240272

241273
return dimensions;
242274
}
275+
276+
/// <summary>
277+
/// Adds multiple dimensions as a complete dimension set to memory.
278+
/// </summary>
279+
/// <param name="dimensionSets">List of dimension sets to add</param>
280+
internal void AddDimensionSet(List<DimensionSet> dimensionSets)
281+
{
282+
if (dimensionSets == null || !dimensionSets.Any())
283+
return;
284+
285+
if (Dimensions.Count + dimensionSets.Count <= PowertoolsConfigurations.MaxDimensions)
286+
{
287+
// Simply add the dimension sets without checking for existing keys
288+
// This ensures dimensions added together stay together
289+
foreach (var dimensionSet in dimensionSets.Where(dimensionSet => dimensionSet.DimensionKeys.Any()))
290+
{
291+
Dimensions.Add(dimensionSet);
292+
}
293+
}
294+
else
295+
{
296+
throw new ArgumentOutOfRangeException(nameof(Dimensions),
297+
$"Cannot add more than {PowertoolsConfigurations.MaxDimensions} dimensions at the same time.");
298+
}
299+
}
243300

244301
/// <summary>
245302
/// Clears both default dimensions and dimensions lists

libraries/src/AWS.Lambda.Powertools.Metrics/Model/MetricsContext.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ internal string GetService()
132132
/// <param name="value">Dimension value</param>
133133
public void AddDimension(string key, string value)
134134
{
135-
_rootNode.AWS.AddDimensionSet(new DimensionSet(key, value));
135+
_rootNode.AWS.AddDimension(new DimensionSet(key, value));
136136
}
137137

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

150148
/// <summary>

libraries/tests/AWS.Lambda.Powertools.Metrics.Tests/EMFValidationTests.cs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public void WhenMaxDataPointsAreAddedToTheSameMetric_FlushAutomatically()
110110

111111
[Trait("Category", "EMFLimits")]
112112
[Fact]
113-
public void WhenMoreThan9DimensionsAdded_ThrowArgumentOutOfRangeException()
113+
public void WhenMoreThan29DimensionsAdded_ThrowArgumentOutOfRangeException()
114114
{
115115
// Act
116116
var act = () => { _handler.MaxDimensions(29); };
@@ -400,6 +400,96 @@ public async Task WhenMetricsAsyncRaceConditionItemSameKeyExists_ValidateLock()
400400
"{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Metric Name\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\"]]",
401401
metricsOutput);
402402
}
403+
404+
[Trait("Category", "MetricsImplementation")]
405+
[Fact]
406+
public void AddDimensions_WithMultipleValues_AddsDimensionsToSameDimensionSet()
407+
{
408+
// Act
409+
_handler.AddMultipleDimensionsInSameSet();
410+
411+
var result = _consoleOut.ToString();
412+
413+
// Assert
414+
Assert.Contains("\"Dimensions\":[[\"Service\",\"Environment\",\"Region\"]]", result);
415+
Assert.Contains("\"Service\":\"testService\",\"Environment\":\"test\",\"Region\":\"us-west-2\"", result);
416+
}
417+
418+
[Trait("Category", "MetricsImplementation")]
419+
[Fact]
420+
public void AddDimensions_WithEmptyArray_DoesNotAddAnyDimensions()
421+
{
422+
// Act
423+
_handler.AddEmptyDimensions();
424+
425+
var result = _consoleOut.ToString();
426+
427+
// Assert
428+
Assert.Contains("\"Dimensions\":[[\"Service\"]]", result);
429+
Assert.DoesNotContain("\"Environment\":", result);
430+
}
431+
432+
[Trait("Category", "MetricsImplementation")]
433+
[Fact]
434+
public void AddDimensions_WithNullOrEmptyKey_ThrowsArgumentNullException()
435+
{
436+
// Act & Assert
437+
Assert.Throws<ArgumentNullException>(() => _handler.AddDimensionsWithInvalidKey());
438+
}
439+
440+
[Trait("Category", "MetricsImplementation")]
441+
[Fact]
442+
public void AddDimensions_WithNullOrEmptyValue_ThrowsArgumentNullException()
443+
{
444+
// Act & Assert
445+
Assert.Throws<ArgumentNullException>(() => _handler.AddDimensionsWithInvalidValue());
446+
}
447+
448+
[Trait("Category", "MetricsImplementation")]
449+
[Fact]
450+
public void AddDimensions_OverwritesExistingDimensions_LastValueWins()
451+
{
452+
// Act
453+
_handler.AddDimensionsWithOverwrite();
454+
455+
var result = _consoleOut.ToString();
456+
457+
// Assert
458+
Assert.Contains("\"Service\":\"testService\",\"dimension1\":\"B\",\"dimension2\":\"2\"", result);
459+
Assert.DoesNotContain("\"dimension1\":\"A\"", result);
460+
}
461+
462+
[Trait("Category", "MetricsImplementation")]
463+
[Fact]
464+
public void AddDimensions_IncludesDefaultDimensions()
465+
{
466+
// Act
467+
_handler.AddDimensionsWithDefaultDimensions();
468+
469+
var result = _consoleOut.ToString();
470+
471+
// Assert
472+
Assert.Contains("\"Dimensions\":[[\"Service\",\"environment\",\"dimension1\",\"dimension2\"]]", result);
473+
Assert.Contains("\"Service\":\"testService\",\"environment\":\"prod\",\"dimension1\":\"1\",\"dimension2\":\"2\"", result);
474+
}
475+
476+
[Trait("Category", "MetricsImplementation")]
477+
[Fact]
478+
public void AddDefaultDimensionsAtRuntime_OnlyAppliedToNewDimensionSets()
479+
{
480+
// Act
481+
_handler.AddDefaultDimensionsAtRuntime();
482+
483+
var result = _consoleOut.ToString();
484+
485+
// First metric output should have original default dimensions
486+
Assert.Contains("\"Metrics\":[{\"Name\":\"FirstMetric\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"environment\",\"dimension1\",\"dimension2\"]]", result);
487+
Assert.Contains("\"Service\":\"testService\",\"environment\":\"prod\",\"dimension1\":\"1\",\"dimension2\":\"2\",\"FirstMetric\":1", result);
488+
489+
// Second metric output should have additional default dimensions
490+
Assert.Contains("\"Metrics\":[{\"Name\":\"SecondMetric\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"environment\",\"tenantId\",\"foo\",\"bar\"]]", result);
491+
Assert.Contains("\"Service\":\"testService\",\"environment\":\"prod\",\"tenantId\":\"1\",\"foo\":\"1\",\"bar\":\"2\",\"SecondMetric\":1", result);
492+
}
403493

404494

405495
#region Helpers

0 commit comments

Comments
 (0)