Skip to content

Datadog Transport

Go ReferenceVersionSourceChangelog

Sends log entries to Datadog's Logs HTTP intake API. Built on the HTTP transport with a Datadog-specific encoder, site-aware URL, and DD-API-KEY header.

sh
go get go.loglayer.dev/transports/datadog/v2

Getting an API Key and Site

Datadog identifies your account with an API Key and a Site code (the region your account lives in). You need both.

To get the API key:

  1. Sign in at the URL that matches your Datadog account (see the Site table below).
  2. Bottom-left Personal menu (the avatar) → Organization SettingsAPI Keys. Direct link: https://app.<your-site>/organization-settings/api-keys.
  3. Either copy an existing key or click + New Key and name it. Datadog hides the value after creation, so save it immediately.

To pick the Site:

Site codeSign-in URLAPI hostname
SiteUS1 (default)app.datadoghq.comapi.datadoghq.com
SiteUS3us3.datadoghq.comapi.us3.datadoghq.com
SiteUS5us5.datadoghq.comapi.us5.datadoghq.com
SiteEUapp.datadoghq.euapi.datadoghq.eu
SiteAP1ap1.datadoghq.comapi.ap1.datadoghq.com

If you signed up at the bare datadoghq.com, you're on SiteUS1. If you had to pick a region during signup, match that to the Site code above.

The API key is a secret. Treat it like a password: load it from an environment variable or secret manager rather than hard-coding it in source.

Basic Usage

go
import (
    "go.loglayer.dev/v2"
    "go.loglayer.dev/transports/datadog/v2"
)

tr := datadog.New(datadog.Config{
    APIKey:   os.Getenv("DD_API_KEY"),
    Site:     datadog.SiteUS1, // or SiteEU, SiteUS3, SiteUS5, SiteAP1
    Source:   "go",
    Service:  "checkout-api",
    Hostname: hostname,
    Tags:     "env:prod,team:platform",
})
defer tr.Close()

log := loglayer.New(loglayer.Config{Transport: tr})
log = log.WithFields(loglayer.Fields{"requestId": "abc"})
log.WithMetadata(loglayer.Metadata{"durationMs": 42}).Info("served request")

The transport is async and batched (inherited from the HTTP transport, default 100 entries / 5 seconds). Always call Close() on shutdown to flush pending entries.

Sites

Site controls the intake URL. Pick the one that matches your Datadog account:

SiteIntake URL
SiteUS1 (default)https://http-intake.logs.datadoghq.com/api/v2/logs
SiteUS3https://http-intake.logs.us3.datadoghq.com/api/v2/logs
SiteUS5https://http-intake.logs.us5.datadoghq.com/api/v2/logs
SiteEUhttps://http-intake.logs.datadoghq.eu/api/v2/logs
SiteAP1https://http-intake.logs.ap1.datadoghq.com/api/v2/logs

On-prem / custom URL

For Datadog on-prem deployments or when testing against a mock endpoint, set Config.URL directly. The override wins over Site:

go
datadog.New(datadog.Config{
    APIKey: "...",
    URL:    "https://datadog.internal.acme.com/api/v2/logs",
    // Site is ignored when URL is set.
})

The transport rejects non-HTTPS URLs by default. To point at an httptest.Server or a local plain-HTTP proxy, also set AllowInsecureURL: true:

go
srv := httptest.NewServer(http.HandlerFunc(...))
defer srv.Close()

tr := datadog.New(datadog.Config{
    APIKey:           "fake-for-tests",
    URL:              srv.URL,        // http:// from httptest
    AllowInsecureURL: true,           // required for non-HTTPS URLs
})

AllowInsecureURL is a test/debug ergonomic; leave it off for any URL that leaves your machine.

Config

go
type Config struct {
    transport.BaseConfig

    APIKey           string  // required
    Site             Site    // default SiteUS1; ignored when URL is set
    URL              string  // overrides the Site-derived intake URL (on-prem / mock)
    AllowInsecureURL bool    // permit non-HTTPS URLs (httptest, local proxies)

    Source   string  // ddsource (e.g. "go")
    Service  string  // service name
    Hostname string  // host name
    Tags     string  // ddtags, comma-separated key:value

    HTTP httptransport.Config // batching/client/error handling overrides
}

APIKey

Required. Set as the DD-API-KEY header on every request. datadog.New panics with datadog.ErrAPIKeyRequired when this is empty; use datadog.Build(cfg) (*Transport, error) if you load the key from an environment variable and want to handle the missing-config case explicitly.

Source, Service, Hostname, Tags

Optional Datadog reserved attributes. Empty values are omitted from the payload.

FieldDatadog fieldRecommended value
Sourceddsource"go" (or your framework name)
ServiceserviceYour service/application name
HostnamehostnameResolved hostname from os.Hostname()
Tagsddtags"env:prod,team:platform,version:1.2.3"

HTTP

Embedded httptransport.Config for batching, client timeout, error handling, and other HTTP-layer concerns. The URL, Encoder, and DD-API-KEY header are set by the Datadog wrapper and cannot be overridden via this field.

go
tr := datadog.New(datadog.Config{
    APIKey: key,
    HTTP: httptransport.Config{
        BatchSize:     500,
        BatchInterval: 2 * time.Second,
        Client:        &http.Client{Timeout: 10 * time.Second},
        OnError: func(err error, entries []httptransport.Entry) {
            metrics.Counter("datadog.send.failed").Add(int64(len(entries)))
        },
    },
})

See the HTTP transport docs for the full HTTP config surface.

Encoded Body Shape

Each log entry becomes one object in a JSON array:

json
[
  {
    "ddsource":  "go",
    "service":   "checkout-api",
    "hostname":  "ip-10-0-0-1",
    "ddtags":    "env:prod,team:platform",
    "status":    "info",
    "message":   "served request",
    "date":      "2026-04-26T12:00:00.123Z",
    "requestId": "abc",
    "durationMs": 42
  }
]

Persistent fields (WithFields) and metadata (WithMetadata) follow the core placement rules: when FieldsKey is empty, fields merge at the root of each Datadog log object; when MetadataFieldName is empty, map metadata merges at the root and non-map metadata nests under metadata. Set either knob on loglayer.Config to nest under a configured key instead.

Level → Status Mapping

Datadog uses a status string per entry. The transport maps loglayer levels:

LogLayer LevelDatadog status
LogLevelTracedebug
LogLevelDebugdebug
LogLevelInfoinfo
LogLevelWarnwarning
LogLevelErrorerror
LogLevelFatalcritical
LogLevelPanicemergency

Datadog has no native trace status, so Trace folds into debug (the closest below-info bucket). Panic uses emergency, the highest-severity status in Datadog's reserved set, so it stays distinguishable from Fatal.

API Limits

Datadog's intake has these limits (reference):

  • 5MB max body size per request
  • 1MB max single log entry
  • 1,000 max log entries per array

The default BatchSize of 100 stays well under all of these. If you bump BatchSize for higher throughput, keep it under 1,000 and watch the body-size limit if your entries are large.

Closing

Datadog.Transport embeds *httptransport.Transport, so it has the same Close() error method. Always call it on shutdown so the in-flight batch is flushed:

go
tr := datadog.New(...)
defer tr.Close()

After Close, subsequent log calls drop the entry and invoke the underlying HTTP transport's OnError with httptransport.ErrClosed.

Reaching the Underlying HTTP Transport

datadog.Transport embeds *httptransport.Transport, so any HTTP-transport method works on it directly:

go
tr := datadog.New(...)
tr.Close()                      // from httptransport.Transport
tr.GetLoggerInstance()          // from httptransport.Transport (returns nil)

Fatal Behavior

This transport writes fatal entries normally; whether the process actually exits is the core's decision via Config.DisableFatalExit (default: exit). See Fatal Exits the Process.

Same async caveat as the underlying HTTP transport: set DisableFatalExit: true and call tr.Close() before os.Exit(1) if you need guaranteed delivery of the fatal entry.