/// <summary> /// Send a CloudWatch event with optional event details and resources it applies to. This event is forwarded to the configured EventBridge. /// </summary> /// <param name="logger">The <see cref="ILambdaSharpLogger"/> instance to use.</param> /// <param name="source">Name of the event source.</param> /// <param name="detailType">Free-form string used to decide what fields to expect in the event detail.</param> /// <param name="detail">Data-structure to serialize as a JSON string. If value is already a <code>string</code>, it is sent as-is. There is no other schema imposed. The data-structure may contain fields and nested subobjects.</param> /// <param name="resources">Optional AWS or custom resources, identified by unique identifier (e.g. ARN), which the event primarily concerns. Any number, including zero, may be present.</param> public static void LogEvent <T>(this ILambdaSharpLogger logger, string source, string detailType, T detail, IEnumerable <string> resources = null) { // augment event resources with LambdaSharp specific resources var lambdaResources = new List <string>(); if (resources != null) { lambdaResources.AddRange(resources); } if (logger.Info.ModuleId != null) { lambdaResources.Add($"lambdasharp:stack:{logger.Info.ModuleId}"); } var moduleFullName = logger.Info.GetModuleFullName(); if (moduleFullName != null) { lambdaResources.Add($"lambdasharp:module:{moduleFullName}"); } if (logger.Info.DeploymentTier != null) { lambdaResources.Add($"lambdasharp:tier:{logger.Info.DeploymentTier}"); } if (logger.Info.ModuleInfo != null) { lambdaResources.Add($"lambdasharp:moduleinfo:{logger.Info.ModuleInfo}"); } var moduleOrigin = logger.Info.GetModuleOrigin(); if (moduleOrigin != null) { lambdaResources.Add($"lambdasharp:origin:{moduleOrigin}"); } if (logger.Info.AppId != null) { lambdaResources.Add($"lambdasharp:app:{logger.Info.AppId}"); } // create event record for logging var eventRecord = new LambdaEventRecord { EventBus = "default", Source = source, DetailType = detailType, Detail = (detail is string detailText) ? detailText : JsonSerializer.Serialize(detail, JsonSerializerOptions), Resources = lambdaResources }; eventRecord.SetTime(DateTimeOffset.UtcNow); logger.LogRecord(eventRecord); } }
/// <summary> /// Log a CloudWatch metric. The metric is picked up by CloudWatch logs and automatically ingested as a CloudWatch metric. /// </summary> /// <param name="logger">The <see cref="ILambdaSharpLogger"/> instance to use.</param> /// <param name="namespace">Metric namespace.</param> /// <param name="metrics">Enumeration of metrics, including their name, value, and unit.</param> /// <param name="dimensionNames">Metric dimensions as comma-separated list (e.g. [ "A", "A,B" ]).</param> /// <param name="dimensionValues">Dictionary of dimesion name-value pairs.</param> public static void LogMetric( this ILambdaSharpLogger logger, string @namespace, IEnumerable <LambdaMetric> metrics, IEnumerable <string> dimensionNames, Dictionary <string, string> dimensionValues ) { if (!metrics.Any()) { // nothing to do return; } if (dimensionNames.Count() > 9) { throw new ArgumentException("metric cannot exceed 9 dimensions", nameof(dimensionNames)); } var targets = new Dictionary <string, object>(); // validate and process metrics var index = 0; foreach (var metric in metrics) { if (string.IsNullOrEmpty(metric.Name)) { throw new ArgumentException($"metric name cannot be empty (index {index})"); } AssertNotReservedName(metric.Name, "metric"); if (double.IsNaN(metric.Value) || double.IsNegativeInfinity(metric.Value) || double.IsPositiveInfinity(metric.Value)) { // these values are rejected by CloudWatch metrics throw new ArgumentException($"metric '{metric.Name}' has an out-of-range value"); } if (!targets.TryAdd(metric.Name, metric.Value)) { throw new ArgumentException($"conflicting metric name: {metric.Name}"); } ++index; } // validate and process dimension values index = 0; foreach (var dimensionValue in dimensionValues) { if (string.IsNullOrEmpty(dimensionValue.Key)) { throw new ArgumentException($"dimension name cannot be empty (index {index})"); } if (string.IsNullOrEmpty(dimensionValue.Value)) { throw new ArgumentException($"dimension value cannot be empty (index {index})"); } AssertNotReservedName(dimensionValue.Key, "dimension"); if (!targets.TryAdd(dimensionValue.Key, dimensionValue.Value)) { throw new ArgumentException($"conflicting dimension name: {dimensionValue.Key}"); } ++index; } // validate and process metric dimensions var metricDimensions = dimensionNames .Select(dimension => dimension .Split(',') .Select(dim => dim.Trim()) .ToList() ) .ToList(); foreach (var metricDimension in metricDimensions.SelectMany(dimension => dimension)) { if (!dimensionValues.ContainsKey(metricDimension)) { throw new ArgumentException($"missing dimension value: {metricDimension}"); } } // create embedded metrics data-structure: var record = new LambdaMetricsRecord { Aws = new EmbeddedCloudWatchMetrics { CloudWatchMetrics = { new CloudWatchMetrics { Namespace = @namespace, Dimensions = metricDimensions, Metrics = metrics.Select(metric => new CloudWatchMetricValue { Name = metric.Name, Unit = ConvertUnit(metric.Unit) }).ToList() } } }, TargetMembers = targets }; // log metric record logger.LogRecord(record); // local functions void AssertNotReservedName(string name, string parameterType) { switch (name) { case "_aws": case "Source": case "Version": throw new ArgumentException($"{parameterType} name cannot be named '{name}'"); default: break; } } string ConvertUnit(LambdaMetricUnit unit) { switch (unit) { // these enum names need to be mapped to their correct CloudWatch metrics unit counterpart case LambdaMetricUnit.BytesPerSecond: return("Bytes/Second"); case LambdaMetricUnit.KilobytesPerSecond: return("Kilobytes/Second"); case LambdaMetricUnit.MegabytesPerSecond: return("Megabytes/Second"); case LambdaMetricUnit.GigabytesPerSecond: return("Gigabytes/Second"); case LambdaMetricUnit.TerabytesPerSecond: return("Terabytes/Second"); case LambdaMetricUnit.BitsPerSecond: return("Bits/Second"); case LambdaMetricUnit.KilobitsPerSecond: return("Kilobits/Second"); case LambdaMetricUnit.MegabitsPerSecond: return("Megabits/Second"); case LambdaMetricUnit.GigabitsPerSecond: return("Gigabits/Second"); case LambdaMetricUnit.TerabitsPerSecond: return("Terabits/Second"); case LambdaMetricUnit.CountPerSecond: return("Count/Second"); // the remaining enums are good as is default: return(unit.ToString()); } } }