Skip to content

Commit fa2243e

Browse files
authored
Merge pull request #884 from aws-powertools/feature/metrics-multiple-dimensions
chore: add support for multiple dimensions
2 parents 108140a + 72d35e8 commit fa2243e

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
@@ -104,4 +104,10 @@ void PushSingleMetric(string name, double value, MetricUnit unit, string nameSpa
104104
/// </summary>
105105
/// <param name="context"></param>
106106
void CaptureColdStartMetric(ILambdaContext context);
107+
108+
/// <summary>
109+
/// Adds multiple dimensions at once.
110+
/// </summary>
111+
/// <param name="dimensions">Array of key-value tuples representing dimensions.</param>
112+
void AddDimensions(params (string key, string value)[] dimensions);
107113
}

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

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

304304
/// <inheritdoc />
305-
public void SetService(string service)
305+
void IMetrics.SetService(string service)
306306
{
307307
// this needs to check if service is set through code or env variables
308308
// the default value service_undefined has to be ignored and return null so it is not added as default
@@ -418,6 +418,15 @@ public static void SetNamespace(string nameSpace)
418418
{
419419
Instance.SetNamespace(nameSpace);
420420
}
421+
422+
/// <summary>
423+
/// Sets the service name for the metrics.
424+
/// </summary>
425+
/// <param name="service">The service name.</param>
426+
public static void SetService(string service)
427+
{
428+
Instance.SetService(service);
429+
}
421430

422431
/// <summary>
423432
/// Retrieves namespace identifier.
@@ -561,6 +570,55 @@ void IMetrics.CaptureColdStartMetric(ILambdaContext context)
561570
dimensions
562571
);
563572
}
573+
574+
/// <inheritdoc />
575+
void IMetrics.AddDimensions(params (string key, string value)[] dimensions)
576+
{
577+
if (dimensions == null || dimensions.Length == 0)
578+
return;
579+
580+
// Validate all dimensions first
581+
foreach (var (key, value) in dimensions)
582+
{
583+
if (string.IsNullOrWhiteSpace(key))
584+
throw new ArgumentNullException(nameof(dimensions),
585+
"'AddDimensions' method requires valid dimension keys. 'Null' or empty values are not allowed.");
586+
587+
if (string.IsNullOrWhiteSpace(value))
588+
throw new ArgumentNullException(nameof(dimensions),
589+
"'AddDimensions' method requires valid dimension values. 'Null' or empty values are not allowed.");
590+
}
591+
592+
// Create a new dimension set with all dimensions
593+
var dimensionSet = new DimensionSet(dimensions[0].key, dimensions[0].value);
594+
595+
// Add remaining dimensions to the same set
596+
for (var i = 1; i < dimensions.Length; i++)
597+
{
598+
dimensionSet.Dimensions.Add(dimensions[i].key, dimensions[i].value);
599+
}
600+
601+
// Add the dimensionSet to a list and pass it to AddDimensions
602+
_context.AddDimensions([dimensionSet]);
603+
}
604+
605+
/// <summary>
606+
/// Adds multiple dimensions at once.
607+
/// </summary>
608+
/// <param name="dimensions">Array of key-value tuples representing dimensions.</param>
609+
public static void AddDimensions(params (string key, string value)[] dimensions)
610+
{
611+
Instance.AddDimensions(dimensions);
612+
}
613+
614+
/// <summary>
615+
/// Flushes the metrics.
616+
/// </summary>
617+
/// <param name="metricsOverflow">If set to <c>true</c>, indicates a metrics overflow.</param>
618+
public static void Flush(bool metricsOverflow = false)
619+
{
620+
Instance.Flush(metricsOverflow);
621+
}
564622

565623
/// <summary>
566624
/// 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
@@ -114,10 +114,19 @@ internal string GetService()
114114
/// Adds new Dimension
115115
/// </summary>
116116
/// <param name="dimension">Dimension to add</param>
117-
internal void AddDimensionSet(DimensionSet dimension)
117+
internal void AddDimension(DimensionSet dimension)
118118
{
119119
_metricDirective.AddDimension(dimension);
120120
}
121+
122+
/// <summary>
123+
/// Adds new List of Dimensions
124+
/// </summary>
125+
/// <param name="dimension">Dimensions to add</param>
126+
internal void AddDimensionSet(List<DimensionSet> dimension)
127+
{
128+
_metricDirective.AddDimensionSet(dimension);
129+
}
121130

122131
/// <summary>
123132
/// 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(dimension),
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
@@ -95,7 +95,7 @@ public void WhenMaxDataPointsAreAddedToTheSameMetric_FlushAutomatically()
9595

9696
[Trait("Category", "EMFLimits")]
9797
[Fact]
98-
public void WhenMoreThan9DimensionsAdded_ThrowArgumentOutOfRangeException()
98+
public void WhenMoreThan29DimensionsAdded_ThrowArgumentOutOfRangeException()
9999
{
100100
// Act
101101
var act = () => { _handler.MaxDimensions(29); };
@@ -385,6 +385,96 @@ public async Task WhenMetricsAsyncRaceConditionItemSameKeyExists_ValidateLock()
385385
"{\"Namespace\":\"dotnet-powertools-test\",\"Metrics\":[{\"Name\":\"Metric Name\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\"]]",
386386
metricsOutput);
387387
}
388+
389+
[Trait("Category", "MetricsImplementation")]
390+
[Fact]
391+
public void AddDimensions_WithMultipleValues_AddsDimensionsToSameDimensionSet()
392+
{
393+
// Act
394+
_handler.AddMultipleDimensionsInSameSet();
395+
396+
var result = _consoleOut.ToString();
397+
398+
// Assert
399+
Assert.Contains("\"Dimensions\":[[\"Service\",\"Environment\",\"Region\"]]", result);
400+
Assert.Contains("\"Service\":\"testService\",\"Environment\":\"test\",\"Region\":\"us-west-2\"", result);
401+
}
402+
403+
[Trait("Category", "MetricsImplementation")]
404+
[Fact]
405+
public void AddDimensions_WithEmptyArray_DoesNotAddAnyDimensions()
406+
{
407+
// Act
408+
_handler.AddEmptyDimensions();
409+
410+
var result = _consoleOut.ToString();
411+
412+
// Assert
413+
Assert.Contains("\"Dimensions\":[[\"Service\"]]", result);
414+
Assert.DoesNotContain("\"Environment\":", result);
415+
}
416+
417+
[Trait("Category", "MetricsImplementation")]
418+
[Fact]
419+
public void AddDimensions_WithNullOrEmptyKey_ThrowsArgumentNullException()
420+
{
421+
// Act & Assert
422+
Assert.Throws<ArgumentNullException>(() => _handler.AddDimensionsWithInvalidKey());
423+
}
424+
425+
[Trait("Category", "MetricsImplementation")]
426+
[Fact]
427+
public void AddDimensions_WithNullOrEmptyValue_ThrowsArgumentNullException()
428+
{
429+
// Act & Assert
430+
Assert.Throws<ArgumentNullException>(() => _handler.AddDimensionsWithInvalidValue());
431+
}
432+
433+
[Trait("Category", "MetricsImplementation")]
434+
[Fact]
435+
public void AddDimensions_OverwritesExistingDimensions_LastValueWins()
436+
{
437+
// Act
438+
_handler.AddDimensionsWithOverwrite();
439+
440+
var result = _consoleOut.ToString();
441+
442+
// Assert
443+
Assert.Contains("\"Service\":\"testService\",\"dimension1\":\"B\",\"dimension2\":\"2\"", result);
444+
Assert.DoesNotContain("\"dimension1\":\"A\"", result);
445+
}
446+
447+
[Trait("Category", "MetricsImplementation")]
448+
[Fact]
449+
public void AddDimensions_IncludesDefaultDimensions()
450+
{
451+
// Act
452+
_handler.AddDimensionsWithDefaultDimensions();
453+
454+
var result = _consoleOut.ToString();
455+
456+
// Assert
457+
Assert.Contains("\"Dimensions\":[[\"Service\",\"environment\",\"dimension1\",\"dimension2\"]]", result);
458+
Assert.Contains("\"Service\":\"testService\",\"environment\":\"prod\",\"dimension1\":\"1\",\"dimension2\":\"2\"", result);
459+
}
460+
461+
[Trait("Category", "MetricsImplementation")]
462+
[Fact]
463+
public void AddDefaultDimensionsAtRuntime_OnlyAppliedToNewDimensionSets()
464+
{
465+
// Act
466+
_handler.AddDefaultDimensionsAtRuntime();
467+
468+
var result = _consoleOut.ToString();
469+
470+
// First metric output should have original default dimensions
471+
Assert.Contains("\"Metrics\":[{\"Name\":\"FirstMetric\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"environment\",\"dimension1\",\"dimension2\"]]", result);
472+
Assert.Contains("\"Service\":\"testService\",\"environment\":\"prod\",\"dimension1\":\"1\",\"dimension2\":\"2\",\"FirstMetric\":1", result);
473+
474+
// Second metric output should have additional default dimensions
475+
Assert.Contains("\"Metrics\":[{\"Name\":\"SecondMetric\",\"Unit\":\"Count\"}],\"Dimensions\":[[\"Service\",\"environment\",\"tenantId\",\"foo\",\"bar\"]]", result);
476+
Assert.Contains("\"Service\":\"testService\",\"environment\":\"prod\",\"tenantId\":\"1\",\"foo\":\"1\",\"bar\":\"2\",\"SecondMetric\":1", result);
477+
}
388478

389479

390480
#region Helpers

0 commit comments

Comments
 (0)