Skip to content
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

Added support to set custom timestamp #55

Merged
merged 3 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,21 @@ Example:
SetNamespace("MyApplication")
```

- MetricsLogger **SetTimestamp**(DateTime dateTime)

Sets the timestamp of the metrics. If not set, current time of the client will be used.

Timestamp must meet CloudWatch requirements, otherwise a `InvalidTimestampException` will be thrown. See [Timestamps](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#about_timestamp) for valid values.

Example:

```c#
logger.SetTimestamp(DateTime.Now);
```




- **Flush**()

Flushes the current MetricsContext to the configured sink and resets all properties and metric values. The namespace and default dimensions will be preserved across flushes. Custom dimensions are preserved by default, but this behavior can be changed by setting `flushPreserveDimensions = false` on the metrics logger.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ private static void EmitMetrics(ILogger logger, IMetricsLogger metrics)
{

var dimensionSet = new DimensionSet();
metrics.SetTimestamp(DateTime.Now);
dimensionSet.AddDimension("Service", "Aggregator");
dimensionSet.AddDimension("Region", "us-west-2");
metrics.SetDimensions(dimensionSet);
Expand Down
3 changes: 2 additions & 1 deletion examples/Amazon.CloudWatch.EMF.Examples.Lambda/Function.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Amazon.CloudWatch.EMF.Model;
using Amazon.Lambda.Core;
using System.Text;

using System;
// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

Expand All @@ -27,6 +27,7 @@ public string FunctionHandler(string input, ILambdaContext context)
var dimensionSet = new DimensionSet();
dimensionSet.AddDimension("Service", "Aggregator");
dimensionSet.AddDimension("Region", "us-west-2");
logger.SetTimestamp(DateTime.Now);
logger.PutDimensions(dimensionSet);
logger.SetNamespace("EMFLambda");
logger.PutMetric("ProcessingLatency", 101, Unit.MILLISECONDS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public IEnumerable<WeatherForecast> Get()
var rng = new Random();
var temperature = rng.Next(-20, 55);
var WindSpeed = rng.Next(10, 100);

_metrics.SetTimestamp(DateTime.Now);
_metrics.PutMetric("Temperature", temperature, Unit.NONE);
_metrics.PutMetric("WindSpeed", WindSpeed, Unit.NONE, StorageResolution.HIGH);

Expand Down
4 changes: 4 additions & 0 deletions src/Amazon.CloudWatch.EMF/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@ public class Constants
public const int MaxMetricsPerEvent = 100;

public const string DefaultNamespace = "aws-embedded-metrics";

public const long MaxTimestampPastAgeDays = 14;

public const long MaxTimestampFutureAgeHours = 2;
}
}
12 changes: 12 additions & 0 deletions src/Amazon.CloudWatch.EMF/Exception/InvalidTimestampException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace Amazon.CloudWatch.EMF
{
public class InvalidTimestampException : Exception
{
public InvalidTimestampException(string message)
: base(message)
{
}
}
}
5 changes: 4 additions & 1 deletion src/Amazon.CloudWatch.EMF/Logger/IMetricsLogger.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Amazon.CloudWatch.EMF.Model;

namespace Amazon.CloudWatch.EMF.Logger
Expand All @@ -22,5 +23,7 @@ public interface IMetricsLogger
public MetricsLogger PutMetadata(string key, object value);

public MetricsLogger SetNamespace(string logNamespace);

public MetricsLogger SetTimestamp(DateTime dateTime);
}
}
12 changes: 12 additions & 0 deletions src/Amazon.CloudWatch.EMF/Logger/MetricsLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,18 @@ public void Dispose()
this.Flush();
}

/// <summary>
/// Sets the timestamp of the metrics. If not set, current time of the client will be used.
/// </summary>
/// <param name="dateTime">Date and Time</param>
/// <returns>the current logger.</returns>
public MetricsLogger SetTimestamp(DateTime dateTime)
{
Validator.ValidateTimestamp(dateTime);
_context.SetTimestamp(dateTime);
return this;
}

/// <summary>
/// Shutdown the associated environment's sink.
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions src/Amazon.CloudWatch.EMF/Model/MetaData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,15 @@ internal MetaData DeepCloneWithNewMetrics(List<MetricDefinition> metrics)
clone.Timestamp = Timestamp;
return clone;
}

internal void SetTimestamp(DateTime dateTime)
{
Timestamp = dateTime;
}

internal DateTime GetTimeStamp()
{
return Timestamp;
}
}
}
18 changes: 18 additions & 0 deletions src/Amazon.CloudWatch.EMF/Model/MetricsContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,24 @@ public void PutMetadata(string key, object value)
_rootNode.AWS.CustomMetadata.Add(key, value);
}

/// <summary>
/// Sets the timestamp of the metrics. If not set, current time of the client will be used.
/// </summary>
/// <param name="dateTime">Date and Time</param>
public void SetTimestamp(DateTime dateTime)
{
_rootNode.AWS.SetTimestamp(dateTime);
}

/// <summary>
/// Get metrics timestamp
/// </summary>
/// <returns>DateTime object</returns>
public DateTime GetTimestamp()
{
return _rootNode.AWS.GetTimeStamp();
}

/// <summary>
/// Serializes the metrics in this context to strings.
/// The EMF backend requires no more than 100 metrics in one log event.
Expand Down
39 changes: 39 additions & 0 deletions src/Amazon.CloudWatch.EMF/Utils/Validator.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.Text;
using Amazon.CloudWatch.EMF.Model;
using static System.Net.WebRequestMethods;

namespace Amazon.CloudWatch.EMF.Utils
{
Expand Down Expand Up @@ -103,6 +105,43 @@ internal static void ValidateNamespace(in string @namespace)
}
}

/// <summary>
/// Validates a given timestamp based on CloudWatch Timestamp guidelines.
/// Timestamp must meet CloudWatch requirements, otherwise a InvalidTimestampException will be thrown.
/// See [Timestamps] (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#about_timestamp) for valid values.
/// </summary>
/// <param name="timestamp">Datetime object representing the timestamp to validate.</param>
/// <exception cref="InvalidTimestampException">If the timestamp is either too old, or too far in the future.</exception>
internal static void ValidateTimestamp(DateTime timestamp)
{
DateTime currentTimeSpan;

if (timestamp.Kind == DateTimeKind.Utc)
{
currentTimeSpan = DateTime.UtcNow;
}
else
{
currentTimeSpan = DateTime.Now;
}

if (DateTime.Compare(timestamp, currentTimeSpan.Add(TimeSpan.FromHours(Constants.MaxTimestampFutureAgeHours))) > 0)
{
throw new InvalidTimestampException(
"Timestamp must not be newer than "
+ Constants.MaxTimestampFutureAgeHours
+ " hours");
}

if (DateTime.Compare(timestamp, currentTimeSpan.Subtract(TimeSpan.FromDays(Constants.MaxTimestampPastAgeDays))) < 0)
{
throw new InvalidTimestampException(
"Timestamp must not be older than "
+ Constants.MaxTimestampPastAgeDays
+ " days");
}
}

/// <summary>
/// Checks if given string is only ASCII.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions tests/Amazon.CloudWatch.EMF.Canary/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ static void Main(string[] args)
using (var logger = new MetricsLogger(loggerFactory))
{
logger.SetNamespace("Canary");
logger.SetTimestamp(DateTime.Now);

var dimensionSet = new DimensionSet();
dimensionSet.AddDimension("Runtime", "Dotnet");
Expand Down
56 changes: 56 additions & 0 deletions tests/Amazon.CloudWatch.EMF.Tests/Logger/MetricsLoggerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,27 @@ public void TestPutMetaData()
Assert.Equal(expectedValue, actualValue);
}


[Theory]
[MemberData(nameof(setTimestampValidInputs))]
public void SetTimestamp_WhenValidTimeStamp_SetTimestamp(DateTime timestamp)
{
MetricsContext metricsContext = new MetricsContext();
_metricsLogger = new MetricsLogger(_environment, metricsContext, _logger);
_metricsLogger.SetTimestamp(timestamp);
Assert.True(DateTime.Compare(metricsContext.GetTimestamp(), timestamp) == 0);
}


[Theory]
[MemberData(nameof(setTimestampInValidInputs))]
public void SetTimestamp_WhenInValidTimestamp_ThrowException(DateTime timestamp, string message)
{
Exception ex = Assert.Throws<InvalidTimestampException>(() => _metricsLogger.SetTimestamp(timestamp));
Assert.Equal(message, ex.Message);
}


private void ExpectDimension(string dimension, string value)
{
List<DimensionSet> dimensions = _sink.MetricsContext.GetAllDimensionSets();
Expand All @@ -446,5 +467,40 @@ private void ExpectDimension(string dimension, string value)
else
Assert.Null(dimensions[0].GetDimensionValue(dimension));
}


public static IEnumerable<object[]> setTimestampValidInputs()
{
yield return new object[] { DateTime.Now };

yield return new object[] { DateTime.UtcNow };

TimeSpan currentTimespan = TimeSpan.FromTicks(DateTime.Now.Ticks);
TimeSpan futureTimespan = currentTimespan.Add(TimeSpan.FromHours(Constants.MaxTimestampFutureAgeHours - 1));
yield return new object[] { new DateTime(futureTimespan.Ticks) };


TimeSpan pastTimespan = currentTimespan.Subtract(TimeSpan.FromHours((Constants.MaxTimestampPastAgeDays * 24) - 1));
yield return new object[] { new DateTime(pastTimespan.Ticks) };
}

public static IEnumerable<Object[]> setTimestampInValidInputs()
{
TimeSpan currentTimespan = TimeSpan.FromTicks(DateTime.Now.Ticks);
TimeSpan pastTimespan = currentTimespan.Subtract(TimeSpan.FromDays(Constants.MaxTimestampPastAgeDays) + TimeSpan.FromMinutes(1));
TimeSpan futureTimespan = currentTimespan.Add(TimeSpan.FromHours(Constants.MaxTimestampFutureAgeHours) + TimeSpan.FromMinutes(1));

yield return new object[] { DateTime.MinValue, "Timestamp must not be older than "
+ Constants.MaxTimestampPastAgeDays
+ " days" };

yield return new object[] { new DateTime(pastTimespan.Ticks), "Timestamp must not be older than "
+ Constants.MaxTimestampPastAgeDays
+ " days" };

yield return new object[] { new DateTime(futureTimespan.Ticks), "Timestamp must not be newer than "
+ Constants.MaxTimestampFutureAgeHours
+ " hours" };
}
}
}