Skip to main content

src/datadog_client.gleam

import datadog_client/metric.{type Metric}
import gleam/http
import gleam/http/request.{type Request}
import gleam/http/response.{type Response}
import gleam/json
import gleam/string

@target(erlang)
import gleam/httpc
@target(erlang)
import gleam/result

@target(javascript)
import gleam/fetch
@target(javascript)
import gleam/javascript/promise.{type Promise}

/// HTTP host for the Datadog API. Default `datadoghq.com` (US1).
/// Use `datadoghq.eu`, `us3.datadoghq.com`, `us5.datadoghq.com`, `ap1.datadoghq.com`, etc.
pub type Client {
  Client(api_key: String, site: String)
}

/// Failure modes returned by `send`.
pub type SendError {
  /// Transport-level failure (DNS, TCP, TLS, etc.).
  HttpError(String)
  /// Datadog returned a non-2xx status.
  ApiError(status: Int, body: String)
}

// --- Client -----------------------------------------------------------------

/// Build a client for the default site (`datadoghq.com`).
pub fn new(api_key: String) -> Client {
  Client(api_key: api_key, site: "datadoghq.com")
}

/// Override the Datadog site (e.g. `"datadoghq.eu"`).
pub fn with_site(client: Client, to site: String) -> Client {
  Client(..client, site: site)
}

// --- Request building (transport-agnostic) ----------------------------------

/// Build the HTTPS request body for `/api/v1/series` without sending it.
/// Use this if you want to send via your own HTTP backend.
pub fn to_request(client: Client, metrics: List(Metric)) -> Request(String) {
  request.new()
  |> request.set_method(http.Post)
  |> request.set_scheme(http.Https)
  |> request.set_host("api." <> client.site)
  |> request.set_path("/api/v1/series")
  |> request.set_header("dd-api-key", client.api_key)
  |> request.set_header("content-type", "application/json")
  |> request.set_body(encode_to_json(metrics))
}

// --- Sending (Erlang via httpc) ---------------------------------------------

@target(erlang)
/// POST one or more metrics to `/api/v1/series`. Erlang target.
pub fn send(
  client: Client,
  metrics: List(Metric),
) -> Result(Response(String), SendError) {
  use resp <- result.try(
    httpc.send(to_request(client, metrics))
    |> result.map_error(fn(e) { HttpError(string.inspect(e)) }),
  )

  case resp.status {
    s if s >= 200 && s < 300 -> Ok(resp)
    s -> Error(ApiError(status: s, body: resp.body))
  }
}

@target(erlang)
/// Send a single metric. Convenience over `send`. Erlang target.
pub fn send_one(
  client: Client,
  m: Metric,
) -> Result(Response(String), SendError) {
  send(client, [m])
}

// --- Sending (JS via fetch) -------------------------------------------------

@target(javascript)
/// POST one or more metrics to `/api/v1/series`. JavaScript target.
pub fn send(
  client: Client,
  metrics: List(Metric),
) -> Promise(Result(Response(String), SendError)) {
  to_request(client, metrics)
  |> fetch.send
  |> promise.try_await(fetch.read_text_body)
  |> promise.map(fn(res) {
    case res {
      Error(e) -> Error(HttpError(string.inspect(e)))
      Ok(resp) ->
        case resp.status {
          s if s >= 200 && s < 300 -> Ok(resp)
          s -> Error(ApiError(status: s, body: resp.body))
        }
    }
  })
}

@target(javascript)
/// Send a single metric. Convenience over `send`. JavaScript target.
pub fn send_one(
  client: Client,
  m: Metric,
) -> Promise(Result(Response(String), SendError)) {
  send(client, [m])
}

// --- JSON encoding ----------------------------------------------------------

/// Serialize a list of metrics to the exact JSON body sent to `/api/v1/series`.
/// Exposed for tests and for buffering payloads for later submission.
@internal
pub fn encode_to_json(metrics: List(Metric)) -> String {
  json.object([#("series", json.array(metrics, of: metric.to_json))])
  |> json.to_string
}