/// <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());
                }
            }
        }