Datadog Transport
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.
go get go.loglayer.dev/transports/datadog/v2Getting 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:
- Sign in at the URL that matches your Datadog account (see the Site table below).
- Bottom-left Personal menu (the avatar) → Organization Settings → API Keys. Direct link:
https://app.<your-site>/organization-settings/api-keys. - 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 code | Sign-in URL | API hostname |
|---|---|---|
SiteUS1 (default) | app.datadoghq.com | api.datadoghq.com |
SiteUS3 | us3.datadoghq.com | api.us3.datadoghq.com |
SiteUS5 | us5.datadoghq.com | api.us5.datadoghq.com |
SiteEU | app.datadoghq.eu | api.datadoghq.eu |
SiteAP1 | ap1.datadoghq.com | api.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
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:
| Site | Intake URL |
|---|---|
SiteUS1 (default) | https://http-intake.logs.datadoghq.com/api/v2/logs |
SiteUS3 | https://http-intake.logs.us3.datadoghq.com/api/v2/logs |
SiteUS5 | https://http-intake.logs.us5.datadoghq.com/api/v2/logs |
SiteEU | https://http-intake.logs.datadoghq.eu/api/v2/logs |
SiteAP1 | https://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:
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:
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
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.
| Field | Datadog field | Recommended value |
|---|---|---|
Source | ddsource | "go" (or your framework name) |
Service | service | Your service/application name |
Hostname | hostname | Resolved hostname from os.Hostname() |
Tags | ddtags | "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.
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:
[
{
"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 Level | Datadog status |
|---|---|
LogLevelTrace | debug |
LogLevelDebug | debug |
LogLevelInfo | info |
LogLevelWarn | warning |
LogLevelError | error |
LogLevelFatal | critical |
LogLevelPanic | emergency |
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:
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:
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.
