defmodule LogDot do @moduledoc """ LogDot SDK for Elixir - Cloud logging and metrics. ## Configuration config :logdot, api_key: "ilog_live_YOUR_API_KEY", hostname: "my-app", entity_name: "my-service", entity_description: "My Service Description", timeout: 5000, retry_attempts: 3, debug: false ## Usage # Logging LogDot.info("User logged in", %{user_id: 123}) LogDot.error("Database connection failed", %{host: "db.example.com"}) # Context-aware logging logger = LogDot.with_context(%{user_id: 123, request_id: "abc"}) LogDot.info(logger, "User action") # Includes user_id and request_id # Metrics LogDot.init_metrics() LogDot.metric("response_time", 123.45, "ms", %{endpoint: "/api/users"}) # Batch logging LogDot.begin_batch() LogDot.info("Message 1") LogDot.info("Message 2") LogDot.send_batch() """ @type log_level :: :debug | :info | :warn | :error @type tags :: map() @type metadata :: map() # Context struct for context-aware logging defmodule Context do @moduledoc """ A context struct for context-aware logging. Create a context with `LogDot.with_context/1` and use it with logging functions. """ defstruct context: %{} @type t :: %__MODULE__{context: map()} end @doc """ Create a new context logger with the given context map. The context will be merged with any tags provided to log functions. ## Example logger = LogDot.with_context(%{user_id: 123, request_id: "abc-123"}) LogDot.info(logger, "Processing request") # Includes user_id and request_id # Chain contexts detailed = LogDot.with_context(logger, %{operation: "checkout"}) LogDot.info(detailed, "Starting checkout") # Includes all three fields """ @spec with_context(map()) :: Context.t() def with_context(context) when is_map(context) do %Context{context: context} end @doc """ Create a new context logger by merging additional context into an existing context. """ @spec with_context(Context.t(), map()) :: Context.t() def with_context(%Context{context: existing}, new_context) when is_map(new_context) do %Context{context: Map.merge(existing, new_context)} end @doc """ Get the context map from a context logger. """ @spec get_context(Context.t()) :: map() def get_context(%Context{context: context}), do: context # Helper to merge context with tags defp merge_tags(%Context{context: context}, tags) do Map.merge(context, tags) end defp merge_tags(nil, tags), do: tags # Logging functions @doc """ Send a debug level log. """ @spec debug(String.t(), tags()) :: :ok | {:error, term()} def debug(message), do: debug(message, %{}) def debug(%Context{} = ctx, message), do: debug(ctx, message, %{}) def debug(message, tags) when is_binary(message), do: LogDot.Logger.debug(message, tags) @doc """ Send a debug level log with context. """ @spec debug(Context.t(), String.t(), tags()) :: :ok | {:error, term()} def debug(%Context{} = ctx, message, tags) do LogDot.Logger.debug(message, merge_tags(ctx, tags)) end @doc """ Send an info level log. """ @spec info(String.t(), tags()) :: :ok | {:error, term()} def info(message), do: info(message, %{}) def info(%Context{} = ctx, message), do: info(ctx, message, %{}) def info(message, tags) when is_binary(message), do: LogDot.Logger.info(message, tags) @doc """ Send an info level log with context. """ @spec info(Context.t(), String.t(), tags()) :: :ok | {:error, term()} def info(%Context{} = ctx, message, tags) do LogDot.Logger.info(message, merge_tags(ctx, tags)) end @doc """ Send a warning level log. """ @spec warn(String.t(), tags()) :: :ok | {:error, term()} def warn(message), do: warn(message, %{}) def warn(%Context{} = ctx, message), do: warn(ctx, message, %{}) def warn(message, tags) when is_binary(message), do: LogDot.Logger.warn(message, tags) @doc """ Send a warning level log with context. """ @spec warn(Context.t(), String.t(), tags()) :: :ok | {:error, term()} def warn(%Context{} = ctx, message, tags) do LogDot.Logger.warn(message, merge_tags(ctx, tags)) end @doc """ Send an error level log. """ @spec error(String.t(), tags()) :: :ok | {:error, term()} def error(message), do: error(message, %{}) def error(%Context{} = ctx, message), do: error(ctx, message, %{}) def error(message, tags) when is_binary(message), do: LogDot.Logger.error(message, tags) @doc """ Send an error level log with context. """ @spec error(Context.t(), String.t(), tags()) :: :ok | {:error, term()} def error(%Context{} = ctx, message, tags) do LogDot.Logger.error(message, merge_tags(ctx, tags)) end @doc """ Send a log at the specified level. """ @spec log(log_level(), String.t(), tags()) :: :ok | {:error, term()} def log(level, message), do: log(level, message, %{}) def log(%Context{} = ctx, level, message), do: log(ctx, level, message, %{}) def log(level, message, tags) when is_atom(level) and is_binary(message), do: LogDot.Logger.log(level, message, tags) @doc """ Send a log at the specified level with context. """ @spec log(Context.t(), log_level(), String.t(), tags()) :: :ok | {:error, term()} def log(%Context{} = ctx, level, message, tags) do LogDot.Logger.log(level, message, merge_tags(ctx, tags)) end # Batch operations for logging @doc """ Begin batch mode for logs. """ @spec begin_batch() :: :ok def begin_batch, do: LogDot.Logger.begin_batch() @doc """ Send all queued logs in batch mode. """ @spec send_batch() :: :ok | {:error, term()} def send_batch, do: LogDot.Logger.send_batch() @doc """ End batch mode for logs. """ @spec end_batch() :: :ok def end_batch, do: LogDot.Logger.end_batch() @doc """ Clear the log batch queue. """ @spec clear_batch() :: :ok def clear_batch, do: LogDot.Logger.clear_batch() @doc """ Get the current log batch size. """ @spec batch_size() :: non_neg_integer() def batch_size, do: LogDot.Logger.batch_size() # Metrics functions @doc """ Initialize the metrics entity. """ @spec init_metrics(metadata()) :: :ok | {:error, term()} def init_metrics(metadata \\ %{}), do: LogDot.Metrics.init_entity(metadata) @doc """ Send a single metric. """ @spec metric(String.t(), number(), String.t(), tags()) :: :ok | {:error, term()} def metric(name, value, unit, tags \\ %{}) do LogDot.Metrics.send(name, value, unit, tags) end @doc """ Begin batch mode for a single metric type. """ @spec begin_metric_batch(String.t(), String.t()) :: :ok def begin_metric_batch(name, unit), do: LogDot.Metrics.begin_batch(name, unit) @doc """ Add a value to the current metric batch. """ @spec add_metric(number(), tags()) :: :ok | {:error, term()} def add_metric(value, tags \\ %{}), do: LogDot.Metrics.add(value, tags) @doc """ Begin multi-metric batch mode. """ @spec begin_multi_batch() :: :ok def begin_multi_batch, do: LogDot.Metrics.begin_multi_batch() @doc """ Add a metric to the multi-metric batch. """ @spec add_multi_metric(String.t(), number(), String.t(), tags()) :: :ok | {:error, term()} def add_multi_metric(name, value, unit, tags \\ %{}) do LogDot.Metrics.add_metric(name, value, unit, tags) end @doc """ Send all queued metrics. """ @spec send_metric_batch() :: :ok | {:error, term()} def send_metric_batch, do: LogDot.Metrics.send_batch() @doc """ End metric batch mode. """ @spec end_metric_batch() :: :ok def end_metric_batch, do: LogDot.Metrics.end_batch() @doc """ Get the current metric batch size. """ @spec metric_batch_size() :: non_neg_integer() def metric_batch_size, do: LogDot.Metrics.batch_size() @doc """ Get the entity ID. """ @spec entity_id() :: String.t() | nil def entity_id, do: LogDot.Metrics.entity_id() @doc """ Check if metrics are initialized. """ @spec metrics_ready?() :: boolean() def metrics_ready?, do: LogDot.Metrics.is_ready?() # Log capture @doc """ Start capturing Elixir `Logger` calls and forwarding them to LogDot. This installs an Erlang `:logger` handler that intercepts all log messages (`Logger.info/1`, `Logger.error/1`, etc.) and sends them to LogDot as log entries. The original logging behavior is preserved — messages still appear in your console/file backends as usual. A re-entrancy guard prevents infinite loops: when LogDot's own HTTP client triggers Logger events during delivery, those events are silently skipped. ## Example LogDot.capture_logging() # Now all Logger calls are also sent to LogDot require Logger Logger.info("This goes to LogDot automatically") # Stop capturing LogDot.stop_capture_logging() This is the same handler used by `LogDot.Phoenix.attach(capture_logging: true)`, exposed here for use outside Phoenix. """ @spec capture_logging() :: :ok | {:error, term()} def capture_logging do case :logger.add_handler(:logdot_backend, LogDot.LoggerBackend, %{}) do :ok -> :ok {:error, _} = err -> err end end @doc """ Stop capturing Elixir `Logger` calls. Removes the LogDot logger handler installed by `capture_logging/0`. """ @spec stop_capture_logging() :: :ok def stop_capture_logging do :logger.remove_handler(:logdot_backend) :ok end end