Skip to content

Commit 95523fb

Browse files
authored
Merge pull request #226 from hjgraca/feat_support_high_resolution_metrics
feat: Support for High resolution metrics
2 parents 01de762 + 6a3b39e commit 95523fb

File tree

11 files changed

+199
-23
lines changed

11 files changed

+199
-23
lines changed

docs/core/metrics.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,26 @@ These metrics can be visualized through [Amazon CloudWatch Console](https://aws.
1010
## Key features
1111

1212
* Aggregate up to 100 metrics using a single CloudWatch EMF object (large JSON blob)
13-
* Validate against common metric definitions mistakes (metric unit, values, max dimensions, max metrics, etc)
14-
* Metrics are created asynchronously by CloudWatch service, no custom stacks needed
13+
* Validating your metrics against common metric definitions mistakes (for example, metric unit, values, max dimensions, max metrics)
14+
* Metrics are created asynchronously by the CloudWatch service. You do not need any custom stacks, and there is no impact to Lambda function latency
1515
* Context manager to create a one off metric with a different dimension
1616

17+
<br />
18+
19+
<figure>
20+
<img src="../../media/metrics_utility_showcase.png" loading="lazy" alt="Screenshot of the Amazon CloudWatch Console showing an example of business metrics in the Metrics Explorer" />
21+
<figcaption>Metrics showcase - Metrics Explorer</figcaption>
22+
</figure>
23+
1724
## Terminologies
1825

1926
If you're new to Amazon CloudWatch, there are two terminologies you must be aware of before using this utility:
2027

21-
* **Namespace**. It's the highest level container that will group multiple metrics from multiple services for a given application, for example `MyCompanyEcommerce`.
28+
* **Namespace**. It's the highest level container that will group multiple metrics from multiple services for a given application, for example `ServerlessEcommerce`.
2229
* **Dimensions**. Metrics metadata in key-value format. They help you slice and dice metrics visualization, for example `ColdStart` metric by Payment `service`.
30+
* **Metric**. It's the name of the metric, for example: SuccessfulBooking or UpdatedBooking.
31+
* **Unit**. It's a value representing the unit of measure for the corresponding metric, for example: Count or Seconds.
32+
* **Resolution**. It's a value representing the storage resolution for the corresponding metric. Metrics can be either Standard or High resolution. Read more [here](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Resolution_definition).
2333

2434
Visit the AWS documentation for a complete explanation for [Amazon CloudWatch concepts](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html).
2535

@@ -130,6 +140,38 @@ You can create metrics using **`AddMetric`**, and you can create dimensions for
130140
!!! warning "Do not create metrics or dimensions outside the handler"
131141
Metrics or dimensions added in the global scope will only be added during cold start. Disregard if that's the intended behavior.
132142

143+
### Adding high-resolution metrics
144+
145+
You can create [high-resolution metrics](https://aws.amazon.com/about-aws/whats-new/2023/02/amazon-cloudwatch-high-resolution-metric-extraction-structured-logs/) passing `MetricResolution` as parameter to `AddMetric`.
146+
147+
!!! tip "When is it useful?"
148+
High-resolution metrics are data with a granularity of one second and are very useful in several situations such as telemetry, time series, real-time incident management, and others.
149+
150+
=== "Metrics with high resolution"
151+
152+
```csharp hl_lines="9 12 15"
153+
using AWS.Lambda.Powertools.Metrics;
154+
155+
public class Function {
156+
157+
[Metrics(Namespace = "ExampleApplication", Service = "Booking")]
158+
public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
159+
{
160+
// Publish a metric with standard resolution i.e. StorageResolution = 60
161+
Metrics.AddMetric("SuccessfulBooking", 1, MetricUnit.Count, MetricResolution.Standard);
162+
163+
// Publish a metric with high resolution i.e. StorageResolution = 1
164+
Metrics.AddMetric("FailedBooking", 1, MetricUnit.Count, MetricResolution.High);
165+
166+
// The last parameter (storage resolution) is optional
167+
Metrics.AddMetric("SuccessfulUpgrade", 1, MetricUnit.Count);
168+
}
169+
}
170+
```
171+
172+
!!! tip "Autocomplete Metric Resolutions"
173+
Use the `MetricResolution` enum to easily find a supported metric resolution by CloudWatch.
174+
133175
### Adding default dimensions
134176

135177
You can use **`SetDefaultDimensions`** method to persist dimensions across Lambda invocations.
165 KB
Loading

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ public interface IMetrics : IDisposable
3131
/// <param name="key">Metric key</param>
3232
/// <param name="value">Metric value</param>
3333
/// <param name="unit">Metric unit</param>
34-
void AddMetric(string key, double value, MetricUnit unit);
34+
/// <param name="metricResolution"></param>
35+
void AddMetric(string key, double value, MetricUnit unit, MetricResolution metricResolution);
3536

3637
/// <summary>
3738
/// Adds a dimension
@@ -62,8 +63,9 @@ public interface IMetrics : IDisposable
6263
/// <param name="nameSpace">Metric namespace</param>
6364
/// <param name="service">Metric service</param>
6465
/// <param name="defaultDimensions">Metric default dimensions</param>
66+
/// <param name="metricResolution">Metrics resolution</param>
6567
void PushSingleMetric(string metricName, double value, MetricUnit unit, string nameSpace = null,
66-
string service = null, Dictionary<string, string> defaultDimensions = null);
68+
string service = null, Dictionary<string, string> defaultDimensions = null, MetricResolution metricResolution = MetricResolution.Default);
6769

6870
/// <summary>
6971
/// Sets the namespace

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,12 @@ internal Metrics(IPowertoolsConfigurations powertoolsConfigurations, string name
8181
/// <param name="key">Metric Key</param>
8282
/// <param name="value">Metric Value</param>
8383
/// <param name="unit">Metric Unit</param>
84+
/// <param name="metricResolution">Metric resolution</param>
8485
/// <exception cref="System.ArgumentNullException">
8586
/// 'AddMetric' method requires a valid metrics key. 'Null' or empty values
8687
/// are not allowed.
8788
/// </exception>
88-
void IMetrics.AddMetric(string key, double value, MetricUnit unit)
89+
void IMetrics.AddMetric(string key, double value, MetricUnit unit, MetricResolution metricResolution)
8990
{
9091
if (string.IsNullOrWhiteSpace(key))
9192
throw new ArgumentNullException(
@@ -98,7 +99,7 @@ void IMetrics.AddMetric(string key, double value, MetricUnit unit)
9899

99100
if (_context.GetMetrics().Count == 100) _instance.Flush(true);
100101

101-
_context.AddMetric(key, value, unit);
102+
_context.AddMetric(key, value, unit, metricResolution);
102103
}
103104

104105
/// <summary>
@@ -231,19 +232,20 @@ public string Serialize()
231232
/// <param name="nameSpace">Metric Namespace</param>
232233
/// <param name="service">Service Name</param>
233234
/// <param name="defaultDimensions">Default dimensions list</param>
235+
/// <param name="metricResolution">Metrics resolution</param>
234236
/// <exception cref="System.ArgumentNullException">
235237
/// 'PushSingleMetric' method requires a valid metrics key. 'Null' or empty
236238
/// values are not allowed.
237239
/// </exception>
238240
void IMetrics.PushSingleMetric(string metricName, double value, MetricUnit unit, string nameSpace, string service,
239-
Dictionary<string, string> defaultDimensions)
241+
Dictionary<string, string> defaultDimensions, MetricResolution metricResolution)
240242
{
241243
if (string.IsNullOrWhiteSpace(metricName))
242244
throw new ArgumentNullException(
243245
$"'PushSingleMetric' method requires a valid metrics key. 'Null' or empty values are not allowed.");
244246

245247
using var context = InitializeContext(nameSpace, service, defaultDimensions);
246-
context.AddMetric(metricName, value, unit);
248+
context.AddMetric(metricName, value, unit, metricResolution);
247249

248250
Flush(context);
249251
}
@@ -262,9 +264,11 @@ public void Dispose()
262264
/// <param name="key">Metric Key. Must not be null, empty or whitespace</param>
263265
/// <param name="value">Metric Value</param>
264266
/// <param name="unit">Metric Unit</param>
265-
public static void AddMetric(string key, double value, MetricUnit unit = MetricUnit.None)
267+
/// <param name="metricResolution"></param>
268+
public static void AddMetric(string key, double value, MetricUnit unit = MetricUnit.None,
269+
MetricResolution metricResolution = MetricResolution.Default)
266270
{
267-
_instance.AddMetric(key, value, unit);
271+
_instance.AddMetric(key, value, unit, metricResolution);
268272
}
269273

270274
/// <summary>
@@ -336,10 +340,11 @@ private void Flush(MetricsContext context)
336340
/// <param name="nameSpace">Metric Namespace</param>
337341
/// <param name="service">Service Name</param>
338342
/// <param name="defaultDimensions">Default dimensions list</param>
343+
/// <param name="metricResolution">Metrics resolution</param>
339344
public static void PushSingleMetric(string metricName, double value, MetricUnit unit, string nameSpace = null,
340-
string service = null, Dictionary<string, string> defaultDimensions = null)
345+
string service = null, Dictionary<string, string> defaultDimensions = null, MetricResolution metricResolution = MetricResolution.Default)
341346
{
342-
_instance.PushSingleMetric(metricName, value, unit, nameSpace, service, defaultDimensions);
347+
_instance.PushSingleMetric(metricName, value, unit, nameSpace, service, defaultDimensions, metricResolution);
343348
}
344349

345350
/// <summary>

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,10 @@ internal void ClearNonDefaultDimensions()
8282
/// <param name="key">Metric key. Cannot be null, empty or whitespace</param>
8383
/// <param name="value">Metric value</param>
8484
/// <param name="unit">Metric Unit</param>
85-
internal void AddMetric(string key, double value, MetricUnit unit)
85+
/// <param name="metricResolution">Metric Resolution, Standard (default), High</param>
86+
internal void AddMetric(string key, double value, MetricUnit unit, MetricResolution metricResolution)
8687
{
87-
_metricDirective.AddMetric(key, value, unit);
88+
_metricDirective.AddMetric(key, value, unit, metricResolution);
8889
}
8990

9091
/// <summary>

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class MetricDefinition
2828
/// </summary>
2929
/// <param name="name">Metric name</param>
3030
/// <param name="value">Metric value</param>
31-
public MetricDefinition(string name, double value) : this(name, MetricUnit.None, new List<double> {value})
31+
public MetricDefinition(string name, double value) : this(name, MetricUnit.None, new List<double> {value}, MetricResolution.Default)
3232
{
3333
}
3434

@@ -38,7 +38,8 @@ public class MetricDefinition
3838
/// <param name="name">Metric name</param>
3939
/// <param name="unit">Metric unit</param>
4040
/// <param name="value">Metric value</param>
41-
public MetricDefinition(string name, MetricUnit unit, double value) : this(name, unit, new List<double> {value})
41+
/// <param name="metricResolution">Metric resolution</param>
42+
public MetricDefinition(string name, MetricUnit unit, double value, MetricResolution metricResolution) : this(name, unit, new List<double> {value}, metricResolution)
4243
{
4344
}
4445

@@ -48,11 +49,13 @@ public class MetricDefinition
4849
/// <param name="name">Metric name</param>
4950
/// <param name="unit">Metric unit</param>
5051
/// <param name="values">List of metric values</param>
51-
public MetricDefinition(string name, MetricUnit unit, List<double> values)
52+
/// <param name="metricResolution">Metric resolution</param>
53+
public MetricDefinition(string name, MetricUnit unit, List<double> values, MetricResolution metricResolution)
5254
{
5355
Name = name;
5456
Unit = unit;
5557
Values = values;
58+
StorageResolution = metricResolution;
5659
}
5760

5861
/// <summary>
@@ -75,6 +78,14 @@ public MetricDefinition(string name, MetricUnit unit, List<double> values)
7578
/// <value>The unit.</value>
7679
[JsonPropertyName(nameof(Unit))]
7780
public MetricUnit Unit { get; set; }
81+
82+
/// <summary>
83+
/// Gets or sets the StorageResolution.
84+
/// </summary>
85+
/// <value>The unit.</value>
86+
[JsonPropertyName(nameof(StorageResolution))]
87+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
88+
public MetricResolution StorageResolution { get; set; }
7889

7990
/// <summary>
8091
/// Adds value to existing metric with same key

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,17 @@ public List<List<string>> AllDimensionKeys
133133
/// <param name="name">Metric name. Cannot be null, empty or whitespace</param>
134134
/// <param name="value">Metric value</param>
135135
/// <param name="unit">Metric unit</param>
136+
/// <param name="metricResolution">Metric Resolution, Standard (default), High</param>
136137
/// <exception cref="System.ArgumentOutOfRangeException">Metrics - Cannot add more than 100 metrics at the same time.</exception>
137-
public void AddMetric(string name, double value, MetricUnit unit)
138+
public void AddMetric(string name, double value, MetricUnit unit, MetricResolution metricResolution)
138139
{
139140
if (Metrics.Count < PowertoolsConfigurations.MaxMetrics)
140141
{
141142
var metric = Metrics.FirstOrDefault(m => m.Name == name);
142143
if (metric != null)
143144
metric.AddValue(value);
144145
else
145-
Metrics.Add(new MetricDefinition(name, unit, value));
146+
Metrics.Add(new MetricDefinition(name, unit, value, metricResolution));
146147
}
147148
else
148149
{
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Runtime.Serialization;
2+
using System.Text.Json.Serialization;
3+
4+
namespace AWS.Lambda.Powertools.Metrics;
5+
6+
/// <summary>
7+
/// Enum MetricResolution
8+
/// </summary>
9+
// [JsonConverter(typeof(StringEnumConverter))]
10+
public enum MetricResolution
11+
{
12+
/// <summary>
13+
/// Standard resolution, with data having a one-minute granularity
14+
/// </summary>
15+
Standard = 60,
16+
17+
/// <summary>
18+
/// High resolution, with data at a granularity of one second
19+
/// </summary>
20+
High = 1,
21+
22+
/// <summary>
23+
/// When not set default to not sending metric resolution
24+
/// </summary>
25+
Default = 0
26+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ internal void ClearNonDefaultDimensions()
8383
/// <param name="key">Metric key. Cannot be null, empty or whitespace</param>
8484
/// <param name="value">Metric value</param>
8585
/// <param name="unit">Metric unit</param>
86-
public void AddMetric(string key, double value, MetricUnit unit)
86+
/// <param name="metricResolution">Metric Resolution, Standard (default), High</param>
87+
public void AddMetric(string key, double value, MetricUnit unit, MetricResolution metricResolution)
8788
{
88-
_rootNode.AWS.AddMetric(key, value, unit);
89+
_rootNode.AWS.AddMetric(key, value, unit, metricResolution);
8990
}
9091

9192
/// <summary>

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,90 @@ public void WhenMetricsWithSameNameAdded_ValidateMetricArray()
539539
// Reset
540540
handler.ResetForTest();
541541
}
542+
543+
[Trait("Category", "MetricsImplementation")]
544+
[Fact]
545+
public void WhenMetricsWithStandardResolutionAdded_ValidateMetricArray()
546+
{
547+
// Arrange
548+
var methodName = Guid.NewGuid().ToString();
549+
var consoleOut = new StringWriter();
550+
Console.SetOut(consoleOut);
551+
552+
var configurations = new Mock<IPowertoolsConfigurations>();
553+
554+
var logger = new Metrics(
555+
configurations.Object,
556+
nameSpace: "dotnet-powertools-test",
557+
service: "testService"
558+
);
559+
560+
var handler = new MetricsAspectHandler(
561+
logger,
562+
false
563+
);
564+
565+
var eventArgs = new AspectEventArgs { Name = methodName };
566+
567+
// Act
568+
handler.OnEntry(eventArgs);
569+
Metrics.AddDimension("functionVersion", "$LATEST");
570+
Metrics.AddMetric("Time", 100.5, MetricUnit.Milliseconds, MetricResolution.Standard);
571+
handler.OnExit(eventArgs);
572+
573+
var result = consoleOut.ToString();
574+
575+
// Assert
576+
Assert.Contains("\"Metrics\":[{\"Name\":\"Time\",\"Unit\":\"Milliseconds\",\"StorageResolution\":60}]"
577+
, result);
578+
Assert.Contains("\"Time\":100.5"
579+
, result);
580+
581+
// Reset
582+
handler.ResetForTest();
583+
}
584+
585+
[Trait("Category", "MetricsImplementation")]
586+
[Fact]
587+
public void WhenMetricsWithHighResolutionAdded_ValidateMetricArray()
588+
{
589+
// Arrange
590+
var methodName = Guid.NewGuid().ToString();
591+
var consoleOut = new StringWriter();
592+
Console.SetOut(consoleOut);
593+
594+
var configurations = new Mock<IPowertoolsConfigurations>();
595+
596+
var logger = new Metrics(
597+
configurations.Object,
598+
nameSpace: "dotnet-powertools-test",
599+
service: "testService"
600+
);
601+
602+
var handler = new MetricsAspectHandler(
603+
logger,
604+
false
605+
);
606+
607+
var eventArgs = new AspectEventArgs { Name = methodName };
608+
609+
// Act
610+
handler.OnEntry(eventArgs);
611+
Metrics.AddDimension("functionVersion", "$LATEST");
612+
Metrics.AddMetric("Time", 100.5, MetricUnit.Milliseconds, MetricResolution.High);
613+
handler.OnExit(eventArgs);
614+
615+
var result = consoleOut.ToString();
616+
617+
// Assert
618+
Assert.Contains("\"Metrics\":[{\"Name\":\"Time\",\"Unit\":\"Milliseconds\",\"StorageResolution\":1}]"
619+
, result);
620+
Assert.Contains("\"Time\":100.5"
621+
, result);
622+
623+
// Reset
624+
handler.ResetForTest();
625+
}
542626

543627
#region Helpers
544628

0 commit comments

Comments
 (0)