From 8052f2616362108867c660c1f7e3b22b955d665f Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 15 Aug 2025 02:33:52 +0800 Subject: [PATCH 1/3] feat: Create log stream if it doesn't exist --- lib/cadet/logger/cloudwatch_logger.ex | 62 ++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/lib/cadet/logger/cloudwatch_logger.ex b/lib/cadet/logger/cloudwatch_logger.ex index 6c828582c..6c1d15df6 100644 --- a/lib/cadet/logger/cloudwatch_logger.ex +++ b/lib/cadet/logger/cloudwatch_logger.ex @@ -151,7 +151,8 @@ defmodule Cadet.Logger.CloudWatchLogger do defp send_to_cloudwatch(log_stream, log_group, buffer) do # Ensure that the already have ExAws authentication configured - with :ok <- check_exaws_config() do + with :ok <- check_exaws_config(), + :ok <- ensure_log_stream_exists(log_group, log_stream) do operation = build_log_operation(log_stream, log_group, buffer) operation @@ -159,6 +160,65 @@ defmodule Cadet.Logger.CloudWatchLogger do end end + # Ensures the log stream exists, creates it if not + # Returns :ok or :error + # Uses ExAws.Logs.describe_log_streams and ExAws.Logs.create_log_stream + # Assumes ExAws.Logs is available + + defp ensure_log_stream_exists(log_group, log_stream) do + describe_op = %ExAws.Operation.JSON{ + http_method: :post, + service: :logs, + headers: [ + {"x-amz-target", "Logs_20140328.DescribeLogStreams"}, + {"content-type", "application/x-amz-json-1.1"} + ], + data: %{ + "logGroupName" => log_group, + "logStreamNamePrefix" => log_stream + } + } + + client = Application.get_env(:ex_aws, :ex_aws_mock, ExAws) + + case client.request(describe_op) do + {:ok, %{"logStreams" => streams}} -> + if Enum.any?(streams, fn s -> s["logStreamName"] == log_stream end) do + :ok + else + create_log_stream(log_group, log_stream, client) + end + + {:error, reason} -> + Logger.error("Failed to describe log streams: #{inspect(reason)}") + :error + end + end + + defp create_log_stream(log_group, log_stream, client) do + create_op = %ExAws.Operation.JSON{ + http_method: :post, + service: :logs, + headers: [ + {"x-amz-target", "Logs_20140328.CreateLogStream"}, + {"content-type", "application/x-amz-json-1.1"} + ], + data: %{ + "logGroupName" => log_group, + "logStreamName" => log_stream + } + } + + case client.request(create_op) do + {:ok, _} -> + :ok + + {:error, reason} -> + Logger.error("Failed to create log stream: #{inspect(reason)}") + :error + end + end + defp build_log_operation(log_stream, log_group, buffer) do # The headers and body structure can be found in the AWS API documentation: # https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html From 626d42501ceff32b1b2ef095772ab2d2d074297c Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 15 Aug 2025 03:09:18 +0800 Subject: [PATCH 2/3] refactor: Upsert log stream only at initialization --- lib/cadet/logger/cloudwatch_logger.ex | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/cadet/logger/cloudwatch_logger.ex b/lib/cadet/logger/cloudwatch_logger.ex index 6c1d15df6..d77c89a31 100644 --- a/lib/cadet/logger/cloudwatch_logger.ex +++ b/lib/cadet/logger/cloudwatch_logger.ex @@ -27,13 +27,17 @@ defmodule Cadet.Logger.CloudWatchLogger do @impl true def init({__MODULE__, opts}) when is_list(opts) do config = configure_merge(read_env(), opts) - {:ok, init(config, %__MODULE__{})} + state = init(config, %__MODULE__{}) + ensure_log_stream_exists(state.log_group, state.log_stream) + {:ok, state} end @impl true def init({__MODULE__, name}) when is_atom(name) do config = read_env() - {:ok, init(config, %__MODULE__{})} + state = init(config, %__MODULE__{}) + ensure_log_stream_exists(state.log_group, state.log_stream) + {:ok, state} end @impl true @@ -151,8 +155,7 @@ defmodule Cadet.Logger.CloudWatchLogger do defp send_to_cloudwatch(log_stream, log_group, buffer) do # Ensure that the already have ExAws authentication configured - with :ok <- check_exaws_config(), - :ok <- ensure_log_stream_exists(log_group, log_stream) do + with :ok <- check_exaws_config() do operation = build_log_operation(log_stream, log_group, buffer) operation From f3c95de662600cea436fa08ed3ecbc11ef901763 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 15 Aug 2025 03:37:11 +0800 Subject: [PATCH 3/3] fix(tests): Account for initialization logic --- test/cadet/logger/cloudwatch_logger_test.exs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/cadet/logger/cloudwatch_logger_test.exs b/test/cadet/logger/cloudwatch_logger_test.exs index a19b64c7a..24c792e88 100644 --- a/test/cadet/logger/cloudwatch_logger_test.exs +++ b/test/cadet/logger/cloudwatch_logger_test.exs @@ -21,6 +21,26 @@ defmodule Cadet.Logger.CloudWatchLoggerTest do metadata: [:request_id] ) + # Mock the DescribeLogStreams request during initialization + expect(ExAwsMock, :request, fn %ExAws.Operation.JSON{} = op + when op.headers == [ + {"x-amz-target", "Logs_20140328.DescribeLogStreams"}, + {"content-type", "application/x-amz-json-1.1"} + ] -> + # Simulate no existing log streams + {:ok, %{"logStreams" => []}} + end) + + # Mock the CreateLogStream request during initialization + expect(ExAwsMock, :request, fn %ExAws.Operation.JSON{} = op + when op.headers == [ + {"x-amz-target", "Logs_20140328.CreateLogStream"}, + {"content-type", "application/x-amz-json-1.1"} + ] -> + # Simulate successful log stream creation + {:ok, %{}} + end) + LoggerBackends.add({CloudWatchLogger, :cloudwatch_logger}) Logger.configure_backend(:console, level: :error, format: "$metadata[$level] $message\n")