Cap the Time Window of Datadog Searches

Keeps Datadog searches to a recent window. The agent can search logs, spans, RUM, and events as much as it likes, but each call looks back at most 7 days .

On this page
  1. What it does
  2. Compliance alignment
  3. Why ingress
  4. Time formats understood
  5. Configuration
  6. Examples
  7. Window trimmed
  8. Unchanged
  9. Composition
  10. Known limitations
  11. Policy source (Rego)
  12. Related policies
Direction
ingress (tool_pre_invoke)
Default
allow (transform-only, never denies)
Rego package
datadog.ingress.cap_search_window
App
Datadog
Bundles
Engineering Stack JiraEngineering Stack Linear
Published
Minimum gateway
1.0.0b24
Schema version
1.0.0
Checksum
sha256:a8f154d80315f9efd7a4b0cc90442f00bb67ba763249244240bfa336ffcac0cf

What it does

Keeps Datadog searches to a recent window. The agent can search logs, spans, RUM, and events as much as it likes, but each call looks back at most 7 days. One broad prompt like "find every log for this user" can't quietly turn into a scan of months of data.

This is a transform, not a block: when to minus from is longer than 7 days, from is moved forward so the window is exactly 7 days. When to is missing or now, the new from is written as now-7d; otherwise it's an RFC 3339 timestamp 7 days before to. Windows of 7 days or less are left alone, and every other argument passes through unchanged.

The number of records returned isn't capped here. Datadog already bounds each response with max_tokens (hard cap 20,000), and an agent reading a burst of errors around a deploy should be able to see them.

Applies to search_datadog_logs, analyze_datadog_logs, search_datadog_spans, aggregate_spans, search_datadog_rum_events, aggregate_rum_events, search_datadog_events, and aggregate_events.

Compliance alignment

  • GDPR Art. 5(1)(c) — data minimisation: logs often carry personal data, and each agent read covers a bounded period.
  • SOC 2 CC6.1 — limits how much history the agent channel can reach per call.

Why ingress

The time range is a request argument; trimming it before the query runs means Datadog never runs the oversized query.

Time formats understood

now, relative now-<n><unit> with units s, m, h, d, w; RFC 3339 timestamps (2026-10-01T00:00:00Z); dates (2026-10-01); and Unix timestamps in seconds (10 digits) or milliseconds (13 digits). A from the policy can't parse is left unchanged.

Configuration

max_window_days at the top of the policy. Raise the window to 14 or 30 days if your incident reviews routinely look further back.

Examples

Window trimmed

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "datadog-search_datadog_logs", "type": "tool" },
    "payload": { "args": { "query": "service:checkout-api status:error", "from": "now-30d" } }
  }
}

allow = true, transform.transformed_payload has from: "now-7d".

Unchanged

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "datadog-aggregate_spans", "type": "tool" },
    "payload": { "args": { "query": "service:checkout-api", "from": "now-4h" } }
  }
}

allow = true, no transform.

Composition

Pairs with datadog/read-only-except-notebooks and datadog/redact-secrets-egress.

Known limitations

  • Server name prefix. Scope is tools whose name starts with datadog. Change _is_datadog if your server is registered under another name.
  • Unparseable times pass. Formats outside the list above are not rewritten.
  • Tool defaults. When from is omitted, Datadog's default applies (1 hour for most tools, 7 days for RUM), which is already within the cap.

Compliance note. This policy supports alignment with the cited framework controls on the MCP path only. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it.

Policy source (Rego)

package datadog.ingress.cap_search_window

# Transform-only policy — never denies, only bounds how far back a search looks.
default allow := true

max_window_days := 7

_day_ns := 86400000000000

_max_window_ns := max_window_days * _day_ns

_unit_ns := {
    "s": 1000000000,
    "m": 60000000000,
    "h": 3600000000000,
    "d": 86400000000000,
    "w": 604800000000000,
}

# Normalize separators so `search_datadog_logs` and `search-datadog-logs` match alike.
_tool := replace(lower(object.get(input.resource, "name", "")), "-", "_")

_args := object.get(object.get(input, "payload", {}), "args", {})

_is_datadog if startswith(_tool, "datadog")

_windowed_suffixes := {
    "search_datadog_logs", "analyze_datadog_logs",
    "search_datadog_spans", "aggregate_spans",
    "search_datadog_rum_events", "aggregate_rum_events",
    "search_datadog_events", "aggregate_events",
}

_is_windowed if {
    _is_datadog
    some s in _windowed_suffixes
    endswith(_tool, s)
}

_now := time.now_ns()

# --- Time parsing: returns nanoseconds since epoch, undefined if unparseable ---

_parse_time(s) := _now if {
    is_string(s)
    lower(trim_space(s)) == "now"
}

_parse_time(s) := ns if {
    is_string(s)
    m := regex.find_all_string_submatch_n(`^now-(\d+)([smhdw])$`, lower(trim_space(s)), 1)
    count(m) == 1
    ns := _now - (to_number(m[0][1]) * _unit_ns[m[0][2]])
}

_parse_time(s) := ns if {
    is_string(s)
    regex.match(`^\d{4}-\d{2}-\d{2}T`, trim_space(s))
    ns := time.parse_rfc3339_ns(trim_space(s))
}

_parse_time(s) := ns if {
    is_string(s)
    regex.match(`^\d{4}-\d{2}-\d{2}$`, trim_space(s))
    ns := time.parse_ns("2006-01-02", trim_space(s))
}

_parse_time(s) := ns if {
    is_string(s)
    regex.match(`^\d{10}$`, trim_space(s))
    ns := to_number(trim_space(s)) * 1000000000
}

_parse_time(s) := ns if {
    is_string(s)
    regex.match(`^\d{13}$`, trim_space(s))
    ns := to_number(trim_space(s)) * 1000000
}

# --- Window ---

_raw_to := object.get(_args, "to", "now")

_to_is_now if lower(trim_space(_raw_to)) == "now"

_to_ns := _parse_time(_raw_to)

_from_ns := _parse_time(object.get(_args, "from", null))

_window_too_wide if {
    _is_windowed
    _to_ns - _from_ns > _max_window_ns
}

_new_from := sprintf("now-%dd", [max_window_days]) if _to_is_now

_new_from := time.format([_to_ns - _max_window_ns, "UTC", "2006-01-02T15:04:05Z07:00"]) if not _to_is_now

# --- Transform ---

transform := {"transformed_payload": object.union(_args, {"from": _new_from})} if {
    input.action == "tool_pre_invoke"
    _window_too_wide
}

Canonical source: policy.md on GitHub · raw · raw on this site (.md)

↑↓ to navigate ↵ to open

Get in touch

Tell us what you need to govern.

A few lines is plenty — what you're deploying, and what you need control over. We read every one.