diff --git a/lib/cadet/logger/cloudwatch_logger.ex b/lib/cadet/logger/cloudwatch_logger.ex index 6c828582c..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 @@ -159,6 +163,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 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")