mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-21 13:00:04 +08:00
feat(datadog): extend to 40 tools and align every operation with the published OpenAPI specs (#6745)
* feat(datadog): add incidents, SLOs, dashboards, synthetics, Cloud SIEM, and APM tools
Extends the Datadog block from 12 to 39 operations, all verified against
Datadog's published OpenAPI specs:
- Incidents (v2, public beta): list, get, create, update, add todo
- SLOs (v1): list, get, create, update, delete, history
- Dashboards (v1): list, get, create, delete
- Synthetics (v1): list tests, get test, latest results, trigger, pause/resume
- Cloud SIEM (v2): search signals, get signal, update triage state, assign,
list detection rules
- APM: search spans (v2), list Service Catalog definitions (v2)
Adds tools/datadog/utils.ts so every tool builds its URL from the configured
site/region and shares the JSON:API-aware error extraction, and handles the
v1 flat vs v2 envelope shapes and cursor pagination per endpoint.
* fix(datadog): align every operation with the published OpenAPI specs
Validated all 39 shipped operations (plus the 12 pre-existing ones that had
never been spec-checked) against the DataDog v1 and v2 OpenAPI schemas.
- `POST /api/v2/downtime` requires `monitor_identifier`, so a downtime created
without a monitor id was rejected. Default to the `*` monitor tag.
- A one-time downtime schedule declares `additionalProperties: false` and
accepts only `start`/`end`; the timezone moves to `display_timezone`.
- `GET /api/v2/downtime` has no `monitor_id` filter, and the response carries
no `disabled` attribute. Downtime ids are UUID strings, not numbers.
- Drop scaffold types for operations that do not exist (metric metadata, event
query, monitor update/delete/unmute, host listing) along with their fields.
- Note that monitor mute is no longer published in the v1 specification.
- Add browser Synthetic test results, which the browser-specific endpoint
returns with its own camelCase step-count shape.
- Replace every `any` with a spec-derived interface, keeping the polymorphic
service-definition schema opaque.
* fix(datadog): remove remaining any types and declare every returned output field
Replace the six surviving `Record<string, any>` request-body and response-cast
sites with concrete spec-derived shapes, and declare the output fields that
transformResponse already returned but outputs omitted:
- create_downtime / list_downtimes: timezone, created, modified
- create_monitor / get_monitor: options, creator
- list_monitors: message, priority, options, created, modified, creator
- query_logs: content.attributes, content.tags
- update_security_signal_state / _assignee: type; assignee also gained the
archiveReason/archiveComment pair its sibling already declared
- query_timeseries: series gained the items shape it never described
* fix(datadog): stop dropping downtime targeting inputs in the block mapping
create_downtime accepts monitorTags, timezone and muteFirstRecoveryNotification,
but the block exposed no inputs for them and never forwarded them. Monitor-tag
targeting silently fell back to the `*` tag, so a downtime meant for one team's
monitors muted every monitor in scope. Adds the three advanced sub-blocks and
wires them through.
Also routes list_downtimes' currentOnly through toSwitchBoolean. A switch yields
the strings 'true'/'false', and 'false' is truthy, so turning the toggle off
still sent current_only=true. Every other switch in the block already used the
helper; this was the last raw one.
* fix(datadog): correct metric type codes, stop SLO update data loss, drop unpublished mute
Independent re-validation of all 39 operations against the DataDog/datadog-api-client-go
generator specs (v1 and v2 openapi.yaml) rather than the client-rendered docs site.
Correctness:
- submit_metrics sent inverted MetricIntakeType codes (gauge as 0/unspecified, rate as 1/count,
count as 2/rate), silently changing how Datadog aggregated every submitted series. The spec
enum is 0 unspecified, 1 count, 2 rate, 3 gauge; an unrecognized type is now omitted so
Datadog infers it. Also stops stamping an invented `resources: [{name:'host'}]` default and
now forwards `interval`, which Datadog requires for count and rate metrics.
- update_slo replaced the whole SLO with only the fields the caller filled in, so editing one
field erased description, tags, query, monitor_ids, groups, thresholds, and timeframe.
PUT /api/v1/slo/{slo_id} is a full replacement, so the stored SLO is now read first and the
supplied edits are overlaid onto it, with the read-only fields stripped.
- update_incident admitted empty strings, so a blank input could blank a stored incident title
or fail as an invalid date-time.
- query_timeseries reported a failed query as success: Datadog returns 200 with a non-ok
`status` and the reason in `error`.
- create_monitor swallowed malformed options JSON and created a monitor with no thresholds.
- send_logs rebuilt each entry from a fixed field list, discarding the custom attributes
Datadog accepts as additionalProperties, and padded absent optional fields with empty strings.
Removed:
- mute_monitor. /api/v1/monitor/{monitor_id}/mute is absent from the v1 spec entirely, there is
no unmute counterpart to reverse it, and downtimes are the supported mechanism.
Contract accuracy:
- Security signal search advertised relative times ("now-1h"); the spec types filter.from/to as
format: date-time. Descriptions, placeholders, and wand prompts now produce ISO-8601.
- list_incidents advertised an `include` value ("integrations") that is not in the spec enum,
and neither incident tool trimmed the comma-separated list, so "users, attachments" 400d.
- Invalid "ok" group state dropped from both monitor descriptions.
- time_slice removed from SLO create input, which cannot build one without an SLI specification.
- DatadogSite gains ap2, uk1, and us2.ddog-gov.com.
Pagination and errors:
- list_downtimes silently truncated at Datadog's default 30 with no way to page; adds
page[limit]/page[offset] and surfaces totalCount.
- query_logs returned a cursor it had no way to accept back.
- Error extraction consolidated onto datadogErrorMessage, which now also reads the
dictionary-shaped errors of the SLO delete conflict. Ten tools were reading `.detail` off
plain strings or the raw entry off objects, degrading every failure to a bare status line.
- Debug logging removed from list_monitors.
Adds 29 regression tests, each verified to fail when its fix is reverted.
* fix(datadog): add SEV-0, document page-size caps, drop unsourced output defaults
- The severity dropdown omitted SEV-0, which IncidentSeverity allows and both incident
tool descriptions already advertised.
- Page-size descriptions now state Datadog's documented default of 10 and cap of 100
instead of an arbitrary example, so an agent does not request an out-of-range page.
- trigger_synthetics_tests emitted an explicit null for a string-typed optional output,
and update_synthetics_status reported 'live' on the error path regardless of what the
caller actually requested.
* fix(datadog): keep mute_monitor and add the missing unmute counterpart
Reverses the removal in the previous commit. Absence from the datadog-api-client-go
generator spec showed the endpoint is unpublished there, not that it is retired:
Datadog's official Python client still implements it on master as
`Monitor.mute(id, scope=, end=)` and `Monitor.unmute(id, scope=, all_scopes=)`
(datadogpy datadog/api/monitors.py), which `_trigger_class_action` resolves to
`POST /api/v1/monitor/{id}/mute` and `/unmute` with exactly those body fields.
mute_monitor has also been in the block since #2175 in December, so dropping it would
have broken existing workflows for an endpoint that two independent sources agree is live.
The genuine defect was that muting was a one-way trapdoor: Sim could mute a monitor but
had no way to reverse it. Adds datadog_unmute_monitor, sharing the monitor ID and scope
inputs with mute, so the operation is recoverable from the same block.
Also: mute no longer discards the response body (it now reports the monitor id, name, and
state), routes errors through datadogErrorMessage, encodes the monitor ID in the path, and
stops dropping an explicit `end` of 0.
* fix(datadog): make downtime targeting explicit and reach downtime pagination from the block
Addresses the review findings on the previous round.
- create_downtime accepted both a monitor ID and monitor tags but `monitor_identifier` is a
oneOf, so it silently kept the ID and dropped the tags, muting a different set of monitors
than the caller asked for. It now rejects the ambiguous combination.
- create_downtime ran Number.parseInt on the monitor ID with no validation, so a non-numeric
value became NaN and serialized as null inside monitor_identifier. It now uses the same
parseMonitorIds guard the SLO path already had, naming the offending value.
- list_downtimes gained limit/offset in the tool but the block exposed neither, so no
block-driven call could page past Datadog's default. Adds the two sub-blocks and wires them
through the params mapper.
- The block did not declare the totalCount the tool now returns, so nothing downstream could
bind to it.
* fix(datadog): tolerate non-string list inputs and keep the shipped mute subblock ids
Both defects were introduced by this branch.
- splitCommaList called .split on its argument, so routing create_downtime's monitorId
through it turned a legitimate numeric input into a TypeError before the request was
built. A <Block.output> reference to get_monitor or list_monitors resolves to a number,
and an LLM tool call can pass a number or an array, so the helper now normalizes all
three shapes. The previous Number.parseInt path had accepted a number by coercion.
- Adding the unmute operation renamed the mute subblock ids scope/end to muteScope/muteEnd.
Workflow state is persisted by subblock id, so every existing Mute Monitor block would
have kept the old keys and silently lost its scope and end time. Restored the shipped ids;
both are still unique block-wide and no operation reads another operation's value.
* fix(datadog): compare downtime targets after parsing, not before
A whitespace-only Monitor ID is truthy as a raw string but parses to no monitor, so
the oneOf conflict guard rejected a valid tag-targeted downtime whenever the untouched
Monitor ID field carried blank text. Both sides are now compared after parsing.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+1678
-28
File diff suppressed because it is too large
Load Diff
@@ -4933,7 +4933,11 @@
|
||||
},
|
||||
{
|
||||
"name": "Mute Monitor",
|
||||
"description": "Mute a monitor to temporarily suppress notifications."
|
||||
"description": "Mute a monitor to temporarily suppress its notifications. Use Unmute Monitor to reverse it, or schedule a downtime instead when you want a planned, auditable maintenance window."
|
||||
},
|
||||
{
|
||||
"name": "Unmute Monitor",
|
||||
"description": "Unmute a monitor so it resumes sending notifications. Reverses Mute Monitor, either for one scope or for every scope at once."
|
||||
},
|
||||
{
|
||||
"name": "Query Logs",
|
||||
@@ -4954,9 +4958,121 @@
|
||||
{
|
||||
"name": "Cancel Downtime",
|
||||
"description": "Cancel a scheduled downtime."
|
||||
},
|
||||
{
|
||||
"name": "List Incidents",
|
||||
"description": "List incidents for the organization. Requires the Incident Management `incident_read` permission; the Incidents API is in public beta."
|
||||
},
|
||||
{
|
||||
"name": "Get Incident",
|
||||
"description": "Get the details of a single incident by ID. Requires the Incident Management `incident_read` permission; the Incidents API is in public beta."
|
||||
},
|
||||
{
|
||||
"name": "Create Incident",
|
||||
"description": "Declare a new incident. Requires the Incident Management `incident_write` permission; the Incidents API is in public beta."
|
||||
},
|
||||
{
|
||||
"name": "Update Incident",
|
||||
"description": "Partially update an existing incident. Requires the Incident Management `incident_write` permission; the Incidents API is in public beta."
|
||||
},
|
||||
{
|
||||
"name": "Add Incident Todo",
|
||||
"description": "Add a follow-up task (todo) to an incident. Requires the Incident Management `incident_write` permission; the Incidents API is in public beta."
|
||||
},
|
||||
{
|
||||
"name": "List SLOs",
|
||||
"description": "List service level objectives, optionally filtered by IDs, name, tags, or underlying metrics query."
|
||||
},
|
||||
{
|
||||
"name": "Get SLO",
|
||||
"description": "Get the configuration of a single service level objective by ID."
|
||||
},
|
||||
{
|
||||
"name": "Create SLO",
|
||||
"description": "Create a service level objective from a metric query, monitors, or a time-slice condition."
|
||||
},
|
||||
{
|
||||
"name": "Update SLO",
|
||||
"description": "Update a service level objective. Reads the current SLO first and applies only the fields you supply, so anything left blank keeps its stored value."
|
||||
},
|
||||
{
|
||||
"name": "Delete SLO",
|
||||
"description": "Permanently delete a service level objective. Datadog returns a conflict when the SLO is still referenced by a dashboard."
|
||||
},
|
||||
{
|
||||
"name": "Get SLO History",
|
||||
"description": "Get an SLO’s history over a time window, including the overall SLI value and remaining error budget."
|
||||
},
|
||||
{
|
||||
"name": "List Dashboards",
|
||||
"description": "List custom created or cloned dashboards. Datadog preset dashboards are not returned."
|
||||
},
|
||||
{
|
||||
"name": "Get Dashboard",
|
||||
"description": "Get the full definition of a dashboard, including its widgets."
|
||||
},
|
||||
{
|
||||
"name": "Create Dashboard",
|
||||
"description": "Create a dashboard from a title, layout type, and widget definitions."
|
||||
},
|
||||
{
|
||||
"name": "Delete Dashboard",
|
||||
"description": "Delete a dashboard by ID."
|
||||
},
|
||||
{
|
||||
"name": "List Synthetic Tests",
|
||||
"description": "List all Synthetic tests (API, browser, and mobile) with their current status."
|
||||
},
|
||||
{
|
||||
"name": "Get Synthetic Test",
|
||||
"description": "Get the configuration of a Synthetic test by public ID. Browser test steps are not included by this type-agnostic endpoint."
|
||||
},
|
||||
{
|
||||
"name": "Get Synthetic Test Results",
|
||||
"description": "Get the latest result summaries (up to the last 150 runs) for a Synthetic API test."
|
||||
},
|
||||
{
|
||||
"name": "Get Browser Synthetic Test Results",
|
||||
"description": "Get the latest result summaries (up to the last 150 runs) for a Synthetic browser test, including step counts and errors."
|
||||
},
|
||||
{
|
||||
"name": "Trigger Synthetic Tests",
|
||||
"description": "Trigger an immediate run of one or more Synthetic tests by public ID."
|
||||
},
|
||||
{
|
||||
"name": "Pause Or Start Synthetic Test",
|
||||
"description": "Pause or resume a Synthetic test by setting its status to \"paused\" or \"live\"."
|
||||
},
|
||||
{
|
||||
"name": "List Security Signals",
|
||||
"description": "Search Cloud SIEM security signals by query and time range. Requires the `security_monitoring_signals_read` permission."
|
||||
},
|
||||
{
|
||||
"name": "Get Security Signal",
|
||||
"description": "Get the details of a single Cloud SIEM security signal. Requires the `security_monitoring_signals_read` permission."
|
||||
},
|
||||
{
|
||||
"name": "Update Security Signal State",
|
||||
"description": "Change the triage state of a Cloud SIEM security signal to open, under_review, or archived. Requires the `security_monitoring_signals_write` permission."
|
||||
},
|
||||
{
|
||||
"name": "Assign Security Signal",
|
||||
"description": "Assign a Cloud SIEM security signal to a Datadog user by UUID. Requires the `security_monitoring_signals_write` permission."
|
||||
},
|
||||
{
|
||||
"name": "List Security Rules",
|
||||
"description": "List Cloud SIEM detection rules. Requires the `security_monitoring_rules_read` permission."
|
||||
},
|
||||
{
|
||||
"name": "Search Spans",
|
||||
"description": "Search indexed APM spans using the span query syntax, with cursor pagination."
|
||||
},
|
||||
{
|
||||
"name": "List Services",
|
||||
"description": "List service definitions from the Datadog Service Catalog. Requires the `apm_service_catalog_read` permission."
|
||||
}
|
||||
],
|
||||
"operationCount": 12,
|
||||
"operationCount": 41,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "api-key",
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { AddIncidentTodoParams, AddIncidentTodoResponse } from '@/tools/datadog/types'
|
||||
import {
|
||||
datadogApiUrl,
|
||||
datadogErrorMessage,
|
||||
datadogHeaders,
|
||||
splitCommaList,
|
||||
} from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const addIncidentTodoTool: ToolConfig<AddIncidentTodoParams, AddIncidentTodoResponse> = {
|
||||
id: 'datadog_add_incident_todo',
|
||||
name: 'Datadog Add Incident Todo',
|
||||
description:
|
||||
'Add a follow-up task (todo) to an incident. Requires the Incident Management `incident_write` permission; the Incidents API is in public beta.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
incidentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the incident the todo belongs to',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The follow-up task content (e.g., "Restore lost data")',
|
||||
},
|
||||
assignees: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated assignee handles (e.g., "@jane@example.com,@on-call"). Datadog requires at least one assignee',
|
||||
},
|
||||
dueDate: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO-8601 timestamp for when the todo should be completed',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v2/incidents/${encodeURIComponent(params.incidentId)}/relationships/todos`
|
||||
),
|
||||
method: 'POST',
|
||||
headers: datadogHeaders,
|
||||
body: (params) => {
|
||||
const attributes: Record<string, unknown> = {
|
||||
content: params.content,
|
||||
assignees: splitCommaList(params.assignees) ?? [],
|
||||
incident_id: params.incidentId,
|
||||
}
|
||||
if (params.dueDate) attributes.due_date = params.dueDate
|
||||
|
||||
return { data: { type: 'incident_todos', attributes } }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { todo: { attributes: {} } },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
todo: {
|
||||
id: data.data?.id,
|
||||
type: data.data?.type,
|
||||
attributes: data.data?.attributes ?? {},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
todo: {
|
||||
type: 'object',
|
||||
description: 'The created incident todo',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Todo UUID' },
|
||||
type: { type: 'string', description: 'Resource type (incident_todos)' },
|
||||
attributes: {
|
||||
type: 'object',
|
||||
description: 'Todo attributes',
|
||||
properties: {
|
||||
content: { type: 'string', description: 'Task content' },
|
||||
assignees: { type: 'array', description: 'Assignee handles' },
|
||||
due_date: { type: 'string', description: 'Due date' },
|
||||
completed: { type: 'string', description: 'Completion timestamp' },
|
||||
incident_id: { type: 'string', description: 'UUID of the parent incident' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CancelDowntimeParams, CancelDowntimeResponse } from '@/tools/datadog/types'
|
||||
import { datadogErrorMessage } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const cancelDowntimeTool: ToolConfig<CancelDowntimeParams, CancelDowntimeResponse> = {
|
||||
@@ -49,13 +50,13 @@ export const cancelDowntimeTool: ToolConfig<CancelDowntimeParams, CancelDowntime
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok && response.status !== 204) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const message = await datadogErrorMessage(response)
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
success: false,
|
||||
},
|
||||
error: errorData.errors?.[0]?.detail || `HTTP ${response.status}: ${response.statusText}`,
|
||||
error: message,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import type { CreateDashboardParams, CreateDashboardResponse } from '@/tools/datadog/types'
|
||||
import {
|
||||
datadogApiUrl,
|
||||
datadogErrorMessage,
|
||||
datadogHeaders,
|
||||
parseJsonParam,
|
||||
splitCommaList,
|
||||
} from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createDashboardTool: ToolConfig<CreateDashboardParams, CreateDashboardResponse> = {
|
||||
id: 'datadog_create_dashboard',
|
||||
name: 'Datadog Create Dashboard',
|
||||
description: 'Create a dashboard from a title, layout type, and widget definitions.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Title of the dashboard',
|
||||
},
|
||||
layoutType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Layout type: "ordered" or "free"',
|
||||
},
|
||||
widgets: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of widget definitions, e.g. [{"definition": {"type": "timeseries", "requests": [{"q": "avg:system.cpu.user{*}"}]}}]',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Description of the dashboard',
|
||||
},
|
||||
notifyList: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated user handles to notify on dashboard changes',
|
||||
},
|
||||
templateVariables: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of template variable definitions, e.g. [{"name": "env", "prefix": "env", "available_values": ["prod"]}]',
|
||||
},
|
||||
tags: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated dashboard tags in the form "team:<name>" (max 5)',
|
||||
},
|
||||
reflowType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Reflow type for ordered layouts: "auto" or "fixed"',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => datadogApiUrl(params.site, '/api/v1/dashboard'),
|
||||
method: 'POST',
|
||||
headers: datadogHeaders,
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
title: params.title,
|
||||
layout_type: params.layoutType,
|
||||
widgets: parseJsonParam<unknown[]>(params.widgets, 'widgets parameter') ?? [],
|
||||
}
|
||||
|
||||
if (params.description) body.description = params.description
|
||||
if (params.reflowType) body.reflow_type = params.reflowType
|
||||
|
||||
const notifyList = splitCommaList(params.notifyList)
|
||||
if (notifyList) body.notify_list = notifyList
|
||||
|
||||
const tags = splitCommaList(params.tags)
|
||||
if (tags) body.tags = tags
|
||||
|
||||
const templateVariables = parseJsonParam<unknown[]>(
|
||||
params.templateVariables,
|
||||
'templateVariables parameter'
|
||||
)
|
||||
if (templateVariables) body.template_variables = templateVariables
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { dashboard: {} },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { dashboard: await response.json() },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
dashboard: {
|
||||
type: 'object',
|
||||
description: 'The created dashboard',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Dashboard ID' },
|
||||
title: { type: 'string', description: 'Dashboard title' },
|
||||
layout_type: { type: 'string', description: 'Layout type: ordered or free' },
|
||||
url: { type: 'string', description: 'Dashboard URL path' },
|
||||
author_handle: { type: 'string', description: 'Handle of the dashboard author' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
modified_at: { type: 'string', description: 'Modification timestamp' },
|
||||
widgets: { type: 'array', description: 'Widget definitions' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { CreateDowntimeParams, CreateDowntimeResponse } from '@/tools/datadog/types'
|
||||
import type {
|
||||
CreateDowntimeParams,
|
||||
CreateDowntimeResponse,
|
||||
DowntimeAttributes,
|
||||
} from '@/tools/datadog/types'
|
||||
import { datadogErrorMessage, parseMonitorIds, splitCommaList } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createDowntimeTool: ToolConfig<CreateDowntimeParams, CreateDowntimeResponse> = {
|
||||
@@ -90,57 +95,62 @@ export const createDowntimeTool: ToolConfig<CreateDowntimeParams, CreateDowntime
|
||||
'DD-APPLICATION-KEY': params.applicationKey,
|
||||
}),
|
||||
body: (params) => {
|
||||
const schedule: Record<string, any> = {}
|
||||
// A one-time schedule accepts only `start` and `end` (`additionalProperties: false`);
|
||||
// the timezone is a display-only attribute on the downtime itself.
|
||||
const schedule: { start?: string; end?: string } = {}
|
||||
if (params.start) schedule.start = new Date(params.start * 1000).toISOString()
|
||||
if (params.end) schedule.end = new Date(params.end * 1000).toISOString()
|
||||
if (params.timezone) schedule.timezone = params.timezone
|
||||
|
||||
const body: Record<string, any> = {
|
||||
data: {
|
||||
type: 'downtime',
|
||||
attributes: {
|
||||
scope: params.scope,
|
||||
schedule: Object.keys(schedule).length > 0 ? schedule : undefined,
|
||||
},
|
||||
},
|
||||
const monitorTags = splitCommaList(params.monitorTags)
|
||||
const monitorId = parseMonitorIds(params.monitorId)?.[0]
|
||||
|
||||
/**
|
||||
* `monitor_identifier` is required and is a `oneOf`: a downtime targets either a
|
||||
* single monitor or a tag set, never both. Accepting both silently would drop one
|
||||
* of them and mute a different set of monitors than the caller asked for. Both
|
||||
* sides are compared after parsing so a blank or whitespace-only input, which is
|
||||
* how an untouched field arrives, does not read as a chosen target.
|
||||
*/
|
||||
if (monitorId !== undefined && monitorTags) {
|
||||
throw new Error(
|
||||
'Supply either a monitor ID or monitor tags, not both — a downtime targets one or the other'
|
||||
)
|
||||
}
|
||||
|
||||
if (params.message) body.data.attributes.message = params.message
|
||||
// Datadog expresses "every monitor in scope" as the `*` monitor tag, which is the
|
||||
// fallback when no monitor is named.
|
||||
const monitorIdentifier =
|
||||
monitorId !== undefined ? { monitor_id: monitorId } : { monitor_tags: monitorTags ?? ['*'] }
|
||||
|
||||
const attributes: Record<string, unknown> = {
|
||||
scope: params.scope,
|
||||
monitor_identifier: monitorIdentifier,
|
||||
}
|
||||
if (Object.keys(schedule).length > 0) attributes.schedule = schedule
|
||||
if (params.timezone) attributes.display_timezone = params.timezone
|
||||
if (params.message) attributes.message = params.message
|
||||
if (params.muteFirstRecoveryNotification !== undefined) {
|
||||
body.data.attributes.mute_first_recovery_notification = params.muteFirstRecoveryNotification
|
||||
attributes.mute_first_recovery_notification = params.muteFirstRecoveryNotification
|
||||
}
|
||||
|
||||
if (params.monitorId) {
|
||||
body.data.attributes.monitor_identifier = {
|
||||
monitor_id: Number.parseInt(params.monitorId, 10),
|
||||
}
|
||||
} else if (params.monitorTags) {
|
||||
body.data.attributes.monitor_identifier = {
|
||||
monitor_tags: params.monitorTags
|
||||
.split(',')
|
||||
.map((t: string) => t.trim())
|
||||
.filter((t: string) => t.length > 0),
|
||||
}
|
||||
}
|
||||
|
||||
return body
|
||||
return { data: { type: 'downtime', attributes } }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const message = await datadogErrorMessage(response)
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
downtime: {} as any,
|
||||
downtime: { scope: [] },
|
||||
},
|
||||
error: errorData.errors?.[0]?.detail || `HTTP ${response.status}: ${response.statusText}`,
|
||||
error: message,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const attrs = data.data?.attributes || {}
|
||||
const attrs: DowntimeAttributes = data.data?.attributes || {}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -152,8 +162,7 @@ export const createDowntimeTool: ToolConfig<CreateDowntimeParams, CreateDowntime
|
||||
? new Date(attrs.schedule.start).getTime() / 1000
|
||||
: undefined,
|
||||
end: attrs.schedule?.end ? new Date(attrs.schedule.end).getTime() / 1000 : undefined,
|
||||
timezone: attrs.schedule?.timezone,
|
||||
disabled: attrs.disabled,
|
||||
timezone: attrs.display_timezone ?? undefined,
|
||||
active: attrs.status === 'active',
|
||||
created: attrs.created ? new Date(attrs.created).getTime() / 1000 : undefined,
|
||||
modified: attrs.modified ? new Date(attrs.modified).getTime() / 1000 : undefined,
|
||||
@@ -167,12 +176,15 @@ export const createDowntimeTool: ToolConfig<CreateDowntimeParams, CreateDowntime
|
||||
type: 'object',
|
||||
description: 'The created downtime details',
|
||||
properties: {
|
||||
id: { type: 'number', description: 'Downtime ID' },
|
||||
id: { type: 'string', description: 'Downtime UUID' },
|
||||
scope: { type: 'array', description: 'Downtime scope' },
|
||||
message: { type: 'string', description: 'Downtime message' },
|
||||
start: { type: 'number', description: 'Start time (Unix timestamp)' },
|
||||
end: { type: 'number', description: 'End time (Unix timestamp)' },
|
||||
timezone: { type: 'string', description: 'Display timezone for the downtime' },
|
||||
active: { type: 'boolean', description: 'Whether downtime is currently active' },
|
||||
created: { type: 'number', description: 'Creation time (Unix timestamp)' },
|
||||
modified: { type: 'number', description: 'Last modification time (Unix timestamp)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { CreateEventParams, CreateEventResponse } from '@/tools/datadog/types'
|
||||
import type {
|
||||
CreateEventParams,
|
||||
CreateEventResponse,
|
||||
EventAlertType,
|
||||
EventPriority,
|
||||
} from '@/tools/datadog/types'
|
||||
import { datadogErrorMessage } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createEventTool: ToolConfig<CreateEventParams, CreateEventResponse> = {
|
||||
@@ -91,7 +97,17 @@ export const createEventTool: ToolConfig<CreateEventParams, CreateEventResponse>
|
||||
'DD-API-KEY': params.apiKey,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {
|
||||
const body: {
|
||||
title: string
|
||||
text: string
|
||||
alert_type?: EventAlertType
|
||||
priority?: EventPriority
|
||||
host?: string
|
||||
aggregation_key?: string
|
||||
source_type_name?: string
|
||||
date_happened?: number
|
||||
tags?: string[]
|
||||
} = {
|
||||
title: params.title,
|
||||
text: params.text,
|
||||
}
|
||||
@@ -116,13 +132,13 @@ export const createEventTool: ToolConfig<CreateEventParams, CreateEventResponse>
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const message = await datadogErrorMessage(response)
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
event: {} as any,
|
||||
event: {},
|
||||
},
|
||||
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
|
||||
error: message,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import type { CreateIncidentParams, CreateIncidentResponse } from '@/tools/datadog/types'
|
||||
import {
|
||||
datadogApiUrl,
|
||||
datadogErrorMessage,
|
||||
datadogHeaders,
|
||||
parseJsonParam,
|
||||
splitCommaList,
|
||||
} from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createIncidentTool: ToolConfig<CreateIncidentParams, CreateIncidentResponse> = {
|
||||
id: 'datadog_create_incident',
|
||||
name: 'Datadog Create Incident',
|
||||
description:
|
||||
'Declare a new incident. Requires the Incident Management `incident_write` permission; the Incidents API is in public beta.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Title of the incident summarizing what happened',
|
||||
},
|
||||
customerImpacted: {
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the incident caused customer impact',
|
||||
},
|
||||
severity: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Incident severity: UNKNOWN, SEV-0, SEV-1, SEV-2, SEV-3, SEV-4, or SEV-5',
|
||||
},
|
||||
customerImpactScope: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Summary of the customer impact. Required when customerImpacted is true',
|
||||
},
|
||||
incidentTypeUuid: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UUID of the incident type. The default incident type is used when omitted',
|
||||
},
|
||||
isTest: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether this is a test incident',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON object of user-defined incident fields, e.g. {"severity": {"type": "dropdown", "value": "SEV-2"}}',
|
||||
},
|
||||
notificationHandles: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated handles to notify on creation (e.g., "@slack-incidents,@user@example.com")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => datadogApiUrl(params.site, '/api/v2/incidents'),
|
||||
method: 'POST',
|
||||
headers: datadogHeaders,
|
||||
body: (params) => {
|
||||
const fields =
|
||||
parseJsonParam<Record<string, unknown>>(params.fields, 'fields parameter') ?? {}
|
||||
if (params.severity) {
|
||||
fields.severity = { type: 'dropdown', value: params.severity }
|
||||
}
|
||||
|
||||
const attributes: Record<string, unknown> = {
|
||||
title: params.title,
|
||||
customer_impacted: params.customerImpacted,
|
||||
}
|
||||
if (params.customerImpactScope) attributes.customer_impact_scope = params.customerImpactScope
|
||||
if (params.incidentTypeUuid) attributes.incident_type_uuid = params.incidentTypeUuid
|
||||
if (params.isTest !== undefined) attributes.is_test = params.isTest
|
||||
if (Object.keys(fields).length > 0) attributes.fields = fields
|
||||
|
||||
const handles = splitCommaList(params.notificationHandles)
|
||||
if (handles) {
|
||||
attributes.notification_handles = handles.map((handle) => ({ handle }))
|
||||
}
|
||||
|
||||
return { data: { type: 'incidents', attributes } }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { incident: { id: '', attributes: {} } },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
incident: {
|
||||
id: data.data?.id,
|
||||
type: data.data?.type,
|
||||
attributes: data.data?.attributes ?? {},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
incident: {
|
||||
type: 'object',
|
||||
description: 'The created incident',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Incident UUID' },
|
||||
type: { type: 'string', description: 'Resource type (incidents)' },
|
||||
attributes: {
|
||||
type: 'object',
|
||||
description: 'Incident attributes',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'Incident title' },
|
||||
public_id: { type: 'number', description: 'Incremental public incident ID' },
|
||||
customer_impacted: { type: 'boolean', description: 'Whether customers were impacted' },
|
||||
created: { type: 'string', description: 'Creation timestamp' },
|
||||
modified: { type: 'string', description: 'Last modification timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CreateMonitorParams, CreateMonitorResponse } from '@/tools/datadog/types'
|
||||
import type { CreateMonitorParams, CreateMonitorResponse, MonitorType } from '@/tools/datadog/types'
|
||||
import { datadogErrorMessage, parseJsonParam } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createMonitorTool: ToolConfig<CreateMonitorParams, CreateMonitorResponse> = {
|
||||
@@ -86,7 +87,15 @@ export const createMonitorTool: ToolConfig<CreateMonitorParams, CreateMonitorRes
|
||||
'DD-APPLICATION-KEY': params.applicationKey,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {
|
||||
const body: {
|
||||
name: string
|
||||
type: MonitorType
|
||||
query: string
|
||||
message?: string
|
||||
priority?: number
|
||||
tags?: string[]
|
||||
options?: unknown
|
||||
} = {
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
query: params.query,
|
||||
@@ -102,14 +111,8 @@ export const createMonitorTool: ToolConfig<CreateMonitorParams, CreateMonitorRes
|
||||
.filter((t: string) => t.length > 0)
|
||||
}
|
||||
|
||||
if (params.options) {
|
||||
try {
|
||||
body.options =
|
||||
typeof params.options === 'string' ? JSON.parse(params.options) : params.options
|
||||
} catch {
|
||||
// If options parsing fails, skip it
|
||||
}
|
||||
}
|
||||
const options = parseJsonParam<Record<string, unknown>>(params.options, 'options parameter')
|
||||
if (options) body.options = options
|
||||
|
||||
return body
|
||||
},
|
||||
@@ -117,13 +120,13 @@ export const createMonitorTool: ToolConfig<CreateMonitorParams, CreateMonitorRes
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const message = await datadogErrorMessage(response)
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
monitor: {} as any,
|
||||
monitor: {},
|
||||
},
|
||||
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
|
||||
error: message,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +167,11 @@ export const createMonitorTool: ToolConfig<CreateMonitorParams, CreateMonitorRes
|
||||
overall_state: { type: 'string', description: 'Current monitor state' },
|
||||
created: { type: 'string', description: 'Creation timestamp' },
|
||||
modified: { type: 'string', description: 'Last modification timestamp' },
|
||||
options: {
|
||||
type: 'json',
|
||||
description: 'Monitor options (thresholds, notification settings)',
|
||||
},
|
||||
creator: { type: 'json', description: 'Monitor creator (email, handle, name)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import type { CreateSloParams, CreateSloResponse } from '@/tools/datadog/types'
|
||||
import {
|
||||
buildSloPayload,
|
||||
datadogApiUrl,
|
||||
datadogErrorMessage,
|
||||
datadogHeaders,
|
||||
} from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createSloTool: ToolConfig<CreateSloParams, CreateSloResponse> = {
|
||||
id: 'datadog_create_slo',
|
||||
name: 'Datadog Create SLO',
|
||||
description:
|
||||
'Create a service level objective from a metric query, monitors, or a time-slice condition.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the SLO (e.g., "Checkout API availability")',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'SLO type: "metric" (supply query) or "monitor" (supply monitorIds). Time-slice SLOs are not supported here because they need an SLI specification this tool does not send.',
|
||||
},
|
||||
thresholds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of thresholds, e.g. [{"timeframe": "30d", "target": 99.9, "warning": 99.95}]',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Description of the SLO',
|
||||
},
|
||||
tags: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated tags (e.g., "env:prod,team:core")',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'For metric SLOs, JSON with numerator and denominator, e.g. {"numerator": "sum:requests{status:ok}.as_count()", "denominator": "sum:requests{*}.as_count()"}',
|
||||
},
|
||||
monitorIds: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'For monitor SLOs, comma-separated monitor IDs (e.g., "123,456")',
|
||||
},
|
||||
groups: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'For monitor SLOs with a single monitor, comma-separated monitor groups (e.g., "env:prod,role:mysql")',
|
||||
},
|
||||
targetThreshold: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Primary target threshold (e.g., 99.9)',
|
||||
},
|
||||
warningThreshold: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Primary warning threshold, must be greater than the target (e.g., 99.95)',
|
||||
},
|
||||
timeframe: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Primary timeframe: "7d", "30d", or "90d"',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => datadogApiUrl(params.site, '/api/v1/slo'),
|
||||
method: 'POST',
|
||||
headers: datadogHeaders,
|
||||
body: buildSloPayload,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { slo: { id: '', name: '', type: '' } },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { slo: data.data?.[0] ?? { id: '', name: '', type: '' } },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
slo: {
|
||||
type: 'object',
|
||||
description: 'The created service level objective',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'SLO ID' },
|
||||
name: { type: 'string', description: 'SLO name' },
|
||||
type: { type: 'string', description: 'SLO type' },
|
||||
description: { type: 'string', description: 'SLO description' },
|
||||
tags: { type: 'array', description: 'SLO tags' },
|
||||
thresholds: { type: 'array', description: 'Timeframe targets and warnings' },
|
||||
created_at: { type: 'number', description: 'Creation timestamp (Unix seconds)' },
|
||||
modified_at: { type: 'number', description: 'Modification timestamp (Unix seconds)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createDowntimeTool } from '@/tools/datadog/create_downtime'
|
||||
import { createEventTool } from '@/tools/datadog/create_event'
|
||||
import { createMonitorTool } from '@/tools/datadog/create_monitor'
|
||||
import { getIncidentTool } from '@/tools/datadog/get_incident'
|
||||
import { listDowntimesTool } from '@/tools/datadog/list_downtimes'
|
||||
import { listIncidentsTool } from '@/tools/datadog/list_incidents'
|
||||
import { muteMonitorTool } from '@/tools/datadog/mute_monitor'
|
||||
import { queryLogsTool } from '@/tools/datadog/query_logs'
|
||||
import { queryTimeseriesTool } from '@/tools/datadog/query_timeseries'
|
||||
import { sendLogsTool } from '@/tools/datadog/send_logs'
|
||||
import { submitMetricsTool } from '@/tools/datadog/submit_metrics'
|
||||
import { unmuteMonitorTool } from '@/tools/datadog/unmute_monitor'
|
||||
import { updateIncidentTool } from '@/tools/datadog/update_incident'
|
||||
import { updateSloTool } from '@/tools/datadog/update_slo'
|
||||
import {
|
||||
buildSloPayload,
|
||||
datadogErrorMessage,
|
||||
mergeSloUpdatePayload,
|
||||
splitCommaList,
|
||||
} from '@/tools/datadog/utils'
|
||||
|
||||
const auth = { apiKey: 'key', applicationKey: 'app-key' } as const
|
||||
|
||||
function jsonResponse(body: unknown, init?: { status?: number; statusText?: string }): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: init?.status ?? 200,
|
||||
statusText: init?.statusText ?? 'OK',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
function callBody<TParams>(
|
||||
tool: { request: { body?: (params: TParams) => unknown } },
|
||||
params: TParams
|
||||
): any {
|
||||
return tool.request.body?.(params)
|
||||
}
|
||||
|
||||
function callUrl<TParams>(tool: { request: { url: unknown } }, params: TParams): string {
|
||||
const url = tool.request.url
|
||||
return typeof url === 'function' ? url(params) : (url as string)
|
||||
}
|
||||
|
||||
describe('submit_metrics metric intake types', () => {
|
||||
/**
|
||||
* Datadog `MetricIntakeType` is 0 unspecified, 1 count, 2 rate, 3 gauge. Swapping
|
||||
* these silently changes how Datadog aggregates the series.
|
||||
*/
|
||||
it.each([
|
||||
['count', 1],
|
||||
['rate', 2],
|
||||
['gauge', 3],
|
||||
])('encodes %s as %i', (type, code) => {
|
||||
const body = callBody(submitMetricsTool, {
|
||||
...auth,
|
||||
series: JSON.stringify([{ metric: 'm', type, points: [{ timestamp: 1, value: 2 }] }]),
|
||||
} as any)
|
||||
expect(body.series[0].type).toBe(code)
|
||||
})
|
||||
|
||||
it('omits type when the caller did not supply one', () => {
|
||||
const body = callBody(submitMetricsTool, {
|
||||
...auth,
|
||||
series: JSON.stringify([{ metric: 'm', points: [{ timestamp: 1, value: 2 }] }]),
|
||||
} as any)
|
||||
expect(body.series[0]).not.toHaveProperty('type')
|
||||
})
|
||||
|
||||
it('does not invent a resources entry', () => {
|
||||
const body = callBody(submitMetricsTool, {
|
||||
...auth,
|
||||
series: JSON.stringify([{ metric: 'm', points: [{ timestamp: 1, value: 2 }] }]),
|
||||
} as any)
|
||||
expect(body.series[0]).not.toHaveProperty('resources')
|
||||
})
|
||||
|
||||
it('forwards interval, which Datadog requires for count and rate metrics', () => {
|
||||
const body = callBody(submitMetricsTool, {
|
||||
...auth,
|
||||
series: JSON.stringify([
|
||||
{ metric: 'm', type: 'count', interval: 20, points: [{ timestamp: 1, value: 2 }] },
|
||||
]),
|
||||
} as any)
|
||||
expect(body.series[0].interval).toBe(20)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SLO payloads', () => {
|
||||
it('rejects blank thresholds instead of sending an empty array', () => {
|
||||
expect(() =>
|
||||
buildSloPayload({ ...auth, name: 'n', type: 'metric', thresholds: '' } as any)
|
||||
).toThrow(/thresholds must be a non-empty JSON array/)
|
||||
})
|
||||
|
||||
it('rejects a non-numeric monitor id instead of sending null', () => {
|
||||
expect(() =>
|
||||
buildSloPayload({
|
||||
...auth,
|
||||
name: 'n',
|
||||
type: 'monitor',
|
||||
thresholds: '[{"timeframe":"30d","target":99.9}]',
|
||||
monitorIds: '123,abc',
|
||||
} as any)
|
||||
).toThrow(/monitorIds must be a comma-separated list of whole numbers/)
|
||||
})
|
||||
|
||||
/**
|
||||
* `PUT /api/v1/slo/{slo_id}` is a full replacement, so every stored field the user
|
||||
* did not edit has to be replayed or Datadog erases it.
|
||||
*/
|
||||
it('preserves stored fields the caller left blank', () => {
|
||||
const stored = {
|
||||
id: 'slo-1',
|
||||
created_at: 1,
|
||||
modified_at: 2,
|
||||
creator: { email: 'a@b.c' },
|
||||
monitor_tags: ['env:prod'],
|
||||
name: 'Old name',
|
||||
type: 'monitor',
|
||||
thresholds: [{ timeframe: '30d', target: 99.9 }],
|
||||
description: 'keep me',
|
||||
tags: ['team:core'],
|
||||
monitor_ids: [123],
|
||||
groups: ['env:prod'],
|
||||
target_threshold: 99.9,
|
||||
timeframe: '30d',
|
||||
}
|
||||
|
||||
const body = mergeSloUpdatePayload(stored, { ...auth, sloId: 'slo-1', name: 'New name' } as any)
|
||||
|
||||
expect(body.name).toBe('New name')
|
||||
expect(body.description).toBe('keep me')
|
||||
expect(body.tags).toEqual(['team:core'])
|
||||
expect(body.monitor_ids).toEqual([123])
|
||||
expect(body.groups).toEqual(['env:prod'])
|
||||
expect(body.thresholds).toEqual([{ timeframe: '30d', target: 99.9 }])
|
||||
expect(body.target_threshold).toBe(99.9)
|
||||
expect(body.timeframe).toBe('30d')
|
||||
})
|
||||
|
||||
it('strips fields Datadog computes and rejects on update', () => {
|
||||
const body = mergeSloUpdatePayload(
|
||||
{ id: 'slo-1', created_at: 1, modified_at: 2, creator: {}, monitor_tags: [], name: 'n' },
|
||||
{ ...auth, sloId: 'slo-1' } as any
|
||||
)
|
||||
for (const field of ['id', 'created_at', 'modified_at', 'creator', 'monitor_tags']) {
|
||||
expect(body).not.toHaveProperty(field)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('update_slo read-modify-write', () => {
|
||||
beforeEach(() => vi.restoreAllMocks())
|
||||
|
||||
it('reads the stored SLO before replacing it', async () => {
|
||||
const fetchMock = vi
|
||||
.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse({ data: { id: 'slo-1', name: 'Old', type: 'metric', description: 'keep' } })
|
||||
)
|
||||
.mockResolvedValueOnce(jsonResponse({ data: [{ id: 'slo-1', name: 'New' }] }))
|
||||
|
||||
const result = await updateSloTool.directExecution!(
|
||||
{ ...auth, sloId: 'slo-1', name: 'New' } as any,
|
||||
undefined
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||||
expect(fetchMock.mock.calls[0][1]?.method).toBe('GET')
|
||||
|
||||
const putBody = JSON.parse(String(fetchMock.mock.calls[1][1]?.body))
|
||||
expect(putBody.name).toBe('New')
|
||||
expect(putBody.description).toBe('keep')
|
||||
})
|
||||
|
||||
it('does not write when the stored SLO cannot be read', async () => {
|
||||
const fetchMock = vi
|
||||
.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValueOnce(jsonResponse({ errors: ['SLO not found'] }, { status: 404 }))
|
||||
|
||||
const result = await updateSloTool.directExecution!(
|
||||
{ ...auth, sloId: 'missing', name: 'New' } as any,
|
||||
undefined
|
||||
)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toContain('SLO not found')
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('datadogErrorMessage', () => {
|
||||
it('reads v1 string errors', async () => {
|
||||
await expect(
|
||||
datadogErrorMessage(jsonResponse({ errors: ['bad query'] }, { status: 400 }))
|
||||
).resolves.toBe('bad query')
|
||||
})
|
||||
|
||||
it('reads v2 JSON:API error objects', async () => {
|
||||
await expect(
|
||||
datadogErrorMessage(jsonResponse({ errors: [{ detail: 'forbidden' }] }, { status: 403 }))
|
||||
).resolves.toBe('forbidden')
|
||||
})
|
||||
|
||||
/** The SLO delete conflict returns errors as a map of resource id to reason. */
|
||||
it('reads dictionary-shaped errors', async () => {
|
||||
await expect(
|
||||
datadogErrorMessage(
|
||||
jsonResponse({ errors: { 'slo-1': 'still referenced by a dashboard' } }, { status: 409 })
|
||||
)
|
||||
).resolves.toBe('still referenced by a dashboard')
|
||||
})
|
||||
|
||||
it('falls back to the status line when no message is present', async () => {
|
||||
await expect(
|
||||
datadogErrorMessage(jsonResponse({}, { status: 500, statusText: 'Server Error' }))
|
||||
).resolves.toBe('HTTP 500: Server Error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('send_logs', () => {
|
||||
/** Datadog treats unrecognized keys as the log's structured attributes. */
|
||||
it('preserves custom attributes', () => {
|
||||
const body = callBody(sendLogsTool, {
|
||||
...auth,
|
||||
logs: JSON.stringify([{ message: 'boom', status: 'error', orderId: 42 }]),
|
||||
} as any)
|
||||
expect(body[0].status).toBe('error')
|
||||
expect(body[0].orderId).toBe(42)
|
||||
})
|
||||
|
||||
it('does not pad absent optional fields with empty strings', () => {
|
||||
const body = callBody(sendLogsTool, {
|
||||
...auth,
|
||||
logs: JSON.stringify([{ message: 'boom' }]),
|
||||
} as any)
|
||||
expect(body[0]).not.toHaveProperty('hostname')
|
||||
expect(body[0]).not.toHaveProperty('service')
|
||||
expect(body[0]).not.toHaveProperty('ddtags')
|
||||
})
|
||||
})
|
||||
|
||||
describe('incident include parameter', () => {
|
||||
/** The spec enum is exact, so a space from a comma-separated input 400s. */
|
||||
it('trims spaces in get_incident', () => {
|
||||
const url = callUrl(getIncidentTool, {
|
||||
...auth,
|
||||
incidentId: 'abc',
|
||||
include: 'users, attachments',
|
||||
} as any)
|
||||
expect(url).toContain('include=users%2Cattachments')
|
||||
expect(url).not.toContain('%20')
|
||||
})
|
||||
|
||||
it('trims spaces in list_incidents', () => {
|
||||
const url = callUrl(listIncidentsTool, { ...auth, include: 'users, attachments' } as any)
|
||||
expect(url).toContain('include=users%2Cattachments')
|
||||
expect(url).not.toContain('%20')
|
||||
})
|
||||
})
|
||||
|
||||
describe('update_incident blank handling', () => {
|
||||
/** A blank input must not blank the stored incident under a partial update. */
|
||||
it('omits empty strings rather than overwriting stored values', () => {
|
||||
const body = callBody(updateIncidentTool, {
|
||||
...auth,
|
||||
incidentId: 'abc',
|
||||
title: '',
|
||||
customerImpactScope: '',
|
||||
detected: '',
|
||||
} as any)
|
||||
expect(body.data.attributes).not.toHaveProperty('title')
|
||||
expect(body.data.attributes).not.toHaveProperty('customer_impact_scope')
|
||||
expect(body.data.attributes).not.toHaveProperty('detected')
|
||||
})
|
||||
|
||||
it('still sends supplied values', () => {
|
||||
const body = callBody(updateIncidentTool, {
|
||||
...auth,
|
||||
incidentId: 'abc',
|
||||
title: 'Real title',
|
||||
} as any)
|
||||
expect(body.data.id).toBe('abc')
|
||||
expect(body.data.type).toBe('incidents')
|
||||
expect(body.data.attributes.title).toBe('Real title')
|
||||
})
|
||||
})
|
||||
|
||||
describe('create_monitor options', () => {
|
||||
/** Silently dropping malformed options created monitors with no thresholds. */
|
||||
it('fails loudly on malformed options JSON', () => {
|
||||
expect(() =>
|
||||
callBody(createMonitorTool, {
|
||||
...auth,
|
||||
name: 'n',
|
||||
type: 'metric alert',
|
||||
query: 'q',
|
||||
options: '{not json',
|
||||
} as any)
|
||||
).toThrow(/options parameter must be valid JSON/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('query_timeseries', () => {
|
||||
/** Datadog reports query failures with a 200 and a non-ok status. */
|
||||
it('surfaces a non-ok status as a failure', async () => {
|
||||
const result = await queryTimeseriesTool.transformResponse!(
|
||||
jsonResponse({ status: 'error', error: 'invalid query', series: [] })
|
||||
)
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toContain('invalid query')
|
||||
})
|
||||
|
||||
it('passes through a successful query', async () => {
|
||||
const result = await queryTimeseriesTool.transformResponse!(
|
||||
jsonResponse({ status: 'ok', series: [{ metric: 'm', tag_set: [], pointlist: [[1000, 5]] }] })
|
||||
)
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output.series[0].points[0]).toEqual({ timestamp: 1, value: 5 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('pagination wiring', () => {
|
||||
it('sends downtime page params', () => {
|
||||
const url = callUrl(listDowntimesTool, { ...auth, limit: 50, offset: 100 } as any)
|
||||
expect(url).toContain('page%5Blimit%5D=50')
|
||||
expect(url).toContain('page%5Boffset%5D=100')
|
||||
})
|
||||
|
||||
it('feeds a log search cursor back into the request', () => {
|
||||
const body = callBody(queryLogsTool, {
|
||||
...auth,
|
||||
query: '*',
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
cursor: 'abc123',
|
||||
} as any)
|
||||
expect(body.page.cursor).toBe('abc123')
|
||||
})
|
||||
})
|
||||
|
||||
describe('monitor mute and unmute', () => {
|
||||
it('mutes with the scope and end datadogpy documents', () => {
|
||||
const body = callBody(muteMonitorTool, {
|
||||
...auth,
|
||||
monitorId: '123',
|
||||
scope: 'host:web-1',
|
||||
end: 1705323600,
|
||||
} as any)
|
||||
expect(body).toEqual({ scope: 'host:web-1', end: 1705323600 })
|
||||
expect(callUrl(muteMonitorTool, { ...auth, monitorId: '123' } as any)).toContain(
|
||||
'/api/v1/monitor/123/mute'
|
||||
)
|
||||
})
|
||||
|
||||
/** An indefinite mute sends no `end`, so the monitor stays muted until unmuted. */
|
||||
it('omits end when the caller wants an indefinite mute', () => {
|
||||
const body = callBody(muteMonitorTool, { ...auth, monitorId: '123' } as any)
|
||||
expect(body).not.toHaveProperty('end')
|
||||
})
|
||||
|
||||
/** Muting is only safe to ship because it can be reversed from Sim. */
|
||||
it('ships an unmute counterpart that can clear every scope', () => {
|
||||
const body = callBody(unmuteMonitorTool, {
|
||||
...auth,
|
||||
monitorId: '123',
|
||||
allScopes: true,
|
||||
} as any)
|
||||
expect(body).toEqual({ all_scopes: true })
|
||||
expect(callUrl(unmuteMonitorTool, { ...auth, monitorId: '123' } as any)).toContain(
|
||||
'/api/v1/monitor/123/unmute'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('create_downtime monitor targeting', () => {
|
||||
/** monitor_identifier is a oneOf, so accepting both would silently drop one. */
|
||||
it('rejects a monitor ID and monitor tags together', () => {
|
||||
expect(() =>
|
||||
callBody(createDowntimeTool, {
|
||||
...auth,
|
||||
scope: '*',
|
||||
monitorId: '123',
|
||||
monitorTags: 'team:backend',
|
||||
} as any)
|
||||
).toThrow(/either a monitor ID or monitor tags, not both/)
|
||||
})
|
||||
|
||||
it('rejects a non-numeric monitor ID instead of sending null', () => {
|
||||
expect(() =>
|
||||
callBody(createDowntimeTool, { ...auth, scope: '*', monitorId: 'abc' } as any)
|
||||
).toThrow(/monitorIds must be a comma-separated list of whole numbers/)
|
||||
})
|
||||
|
||||
it('falls back to the wildcard monitor tag when no monitor is named', () => {
|
||||
const body = callBody(createDowntimeTool, { ...auth, scope: '*' } as any)
|
||||
expect(body.data.attributes.monitor_identifier).toEqual({ monitor_tags: ['*'] })
|
||||
})
|
||||
|
||||
it('targets a single monitor by numeric id', () => {
|
||||
const body = callBody(createDowntimeTool, { ...auth, scope: '*', monitorId: '123' } as any)
|
||||
expect(body.data.attributes.monitor_identifier).toEqual({ monitor_id: 123 })
|
||||
})
|
||||
|
||||
/**
|
||||
* A `<Block.output>` reference to get_monitor resolves to a number, and an LLM tool
|
||||
* call can pass one too, so the parser must not assume a string.
|
||||
*/
|
||||
it('accepts a monitor id that arrives as a number', () => {
|
||||
const body = callBody(createDowntimeTool, { ...auth, scope: '*', monitorId: 123 } as any)
|
||||
expect(body.data.attributes.monitor_identifier).toEqual({ monitor_id: 123 })
|
||||
})
|
||||
|
||||
/** A blank monitor ID is how an untouched field arrives; it is not a chosen target. */
|
||||
it('does not treat a whitespace-only monitor id as a conflicting target', () => {
|
||||
const body = callBody(createDowntimeTool, {
|
||||
...auth,
|
||||
scope: '*',
|
||||
monitorId: ' ',
|
||||
monitorTags: 'team:backend',
|
||||
} as any)
|
||||
expect(body.data.attributes.monitor_identifier).toEqual({ monitor_tags: ['team:backend'] })
|
||||
})
|
||||
|
||||
it('accepts monitor tags that arrive as an array', () => {
|
||||
const body = callBody(createDowntimeTool, {
|
||||
...auth,
|
||||
scope: '*',
|
||||
monitorTags: ['team:backend', 'priority:high'],
|
||||
} as any)
|
||||
expect(body.data.attributes.monitor_identifier).toEqual({
|
||||
monitor_tags: ['team:backend', 'priority:high'],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('splitCommaList input tolerance', () => {
|
||||
it('handles strings, numbers, and arrays without throwing', () => {
|
||||
expect(splitCommaList('a, b')).toEqual(['a', 'b'])
|
||||
expect(splitCommaList(123)).toEqual(['123'])
|
||||
expect(splitCommaList([1, 2])).toEqual(['1', '2'])
|
||||
expect(splitCommaList(undefined)).toBeUndefined()
|
||||
expect(splitCommaList('')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('registry surface', () => {
|
||||
it('keeps create_event on api-key-only auth', () => {
|
||||
expect(createEventTool.params.applicationKey).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { DeleteDashboardParams, DeleteDashboardResponse } from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteDashboardTool: ToolConfig<DeleteDashboardParams, DeleteDashboardResponse> = {
|
||||
id: 'datadog_delete_dashboard',
|
||||
name: 'Datadog Delete Dashboard',
|
||||
description: 'Delete a dashboard by ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
dashboardId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the dashboard to delete',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
datadogApiUrl(params.site, `/api/v1/dashboard/${encodeURIComponent(params.dashboardId)}`),
|
||||
method: 'DELETE',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { success: false },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => ({}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
deletedDashboardId: data.deleted_dashboard_id,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the dashboard was deleted',
|
||||
},
|
||||
deletedDashboardId: {
|
||||
type: 'string',
|
||||
description: 'ID of the deleted dashboard',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { DeleteSloParams, DeleteSloResponse } from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteSloTool: ToolConfig<DeleteSloParams, DeleteSloResponse> = {
|
||||
id: 'datadog_delete_slo',
|
||||
name: 'Datadog Delete SLO',
|
||||
description:
|
||||
'Permanently delete a service level objective. Datadog returns a conflict when the SLO is still referenced by a dashboard.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
sloId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the service level objective to delete',
|
||||
},
|
||||
force: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Delete even when the SLO is referenced by other resources',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryString = params.force ? '?force=true' : ''
|
||||
return datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v1/slo/${encodeURIComponent(params.sloId)}${queryString}`
|
||||
)
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { success: false, deletedIds: [] },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => ({}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
deletedIds: data.data ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the SLO was deleted',
|
||||
},
|
||||
deletedIds: {
|
||||
type: 'array',
|
||||
description: 'IDs of the deleted service level objectives',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import type {
|
||||
GetBrowserSyntheticsResultsParams,
|
||||
GetBrowserSyntheticsResultsResponse,
|
||||
} from '@/tools/datadog/types'
|
||||
import {
|
||||
datadogApiUrl,
|
||||
datadogErrorMessage,
|
||||
datadogHeaders,
|
||||
splitCommaList,
|
||||
} from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getBrowserSyntheticsResultsTool: ToolConfig<
|
||||
GetBrowserSyntheticsResultsParams,
|
||||
GetBrowserSyntheticsResultsResponse
|
||||
> = {
|
||||
id: 'datadog_get_browser_synthetics_results',
|
||||
name: 'Datadog Get Browser Synthetic Test Results',
|
||||
description:
|
||||
'Get the latest result summaries (up to the last 150 runs) for a Synthetic browser test, including step counts and errors.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
publicId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The public ID of the Synthetic browser test',
|
||||
},
|
||||
fromTs: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Timestamp in milliseconds from which to start querying results',
|
||||
},
|
||||
toTs: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Timestamp in milliseconds up to which to query results',
|
||||
},
|
||||
probeDc: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated locations to query results for (e.g., "aws:eu-west-3")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.fromTs !== undefined) queryParams.set('from_ts', String(params.fromTs))
|
||||
if (params.toTs !== undefined) queryParams.set('to_ts', String(params.toTs))
|
||||
for (const location of splitCommaList(params.probeDc) ?? []) {
|
||||
queryParams.append('probe_dc', location)
|
||||
}
|
||||
const queryString = queryParams.toString()
|
||||
return datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v1/synthetics/tests/browser/${encodeURIComponent(params.publicId)}/results${
|
||||
queryString ? `?${queryString}` : ''
|
||||
}`
|
||||
)
|
||||
},
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { results: [] },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results: data.results ?? [],
|
||||
lastTimestampFetched: data.last_timestamp_fetched,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Latest browser test result summaries',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result_id: { type: 'string', description: 'ID of the browser test result' },
|
||||
check_time: { type: 'number', description: 'Time the browser test ran' },
|
||||
probe_dc: { type: 'string', description: 'Location the browser test ran from' },
|
||||
status: {
|
||||
type: 'number',
|
||||
description: 'Monitor status: 0 not triggered, 1 triggered, 2 no data',
|
||||
},
|
||||
result: {
|
||||
type: 'object',
|
||||
description: 'Run outcome',
|
||||
properties: {
|
||||
duration: { type: 'number', description: 'Length of the run in milliseconds' },
|
||||
errorCount: { type: 'number', description: 'Number of errors collected in the run' },
|
||||
stepCountCompleted: {
|
||||
type: 'number',
|
||||
description: 'Steps completed before failing',
|
||||
},
|
||||
stepCountTotal: { type: 'number', description: 'Total number of steps' },
|
||||
device: { type: 'object', description: 'Device the run was performed on' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
lastTimestampFetched: {
|
||||
type: 'number',
|
||||
description: 'Timestamp of the latest browser test run',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { GetDashboardParams, GetDashboardResponse } from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getDashboardTool: ToolConfig<GetDashboardParams, GetDashboardResponse> = {
|
||||
id: 'datadog_get_dashboard',
|
||||
name: 'Datadog Get Dashboard',
|
||||
description: 'Get the full definition of a dashboard, including its widgets.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
dashboardId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the dashboard (e.g., "abc-def-ghi")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
datadogApiUrl(params.site, `/api/v1/dashboard/${encodeURIComponent(params.dashboardId)}`),
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { dashboard: {} },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { dashboard: await response.json() },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
dashboard: {
|
||||
type: 'object',
|
||||
description: 'The dashboard definition',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Dashboard ID' },
|
||||
title: { type: 'string', description: 'Dashboard title' },
|
||||
description: { type: 'string', description: 'Dashboard description' },
|
||||
layout_type: { type: 'string', description: 'Layout type: ordered or free' },
|
||||
url: { type: 'string', description: 'Dashboard URL path' },
|
||||
author_handle: { type: 'string', description: 'Handle of the dashboard author' },
|
||||
author_name: { type: 'string', description: 'Name of the dashboard author' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
modified_at: { type: 'string', description: 'Modification timestamp' },
|
||||
tags: { type: 'array', description: 'Dashboard tags' },
|
||||
notify_list: { type: 'array', description: 'Handles notified on dashboard changes' },
|
||||
template_variables: { type: 'array', description: 'Template variable definitions' },
|
||||
widgets: { type: 'array', description: 'Widget definitions' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { GetIncidentParams, GetIncidentResponse } from '@/tools/datadog/types'
|
||||
import {
|
||||
datadogApiUrl,
|
||||
datadogErrorMessage,
|
||||
datadogHeaders,
|
||||
splitCommaList,
|
||||
} from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getIncidentTool: ToolConfig<GetIncidentParams, GetIncidentResponse> = {
|
||||
id: 'datadog_get_incident',
|
||||
name: 'Datadog Get Incident',
|
||||
description:
|
||||
'Get the details of a single incident by ID. Requires the Incident Management `incident_read` permission; the Incidents API is in public beta.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
incidentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the incident (e.g., "00000000-0000-0000-1234-000000000000")',
|
||||
},
|
||||
include: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated related resources to include (e.g., "users", "attachments")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const include = splitCommaList(params.include)?.join(',')
|
||||
const queryString = include ? `?${new URLSearchParams({ include }).toString()}` : ''
|
||||
return datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v2/incidents/${encodeURIComponent(params.incidentId)}${queryString}`
|
||||
)
|
||||
},
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { incident: { id: '', attributes: {} } },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
incident: {
|
||||
id: data.data?.id,
|
||||
type: data.data?.type,
|
||||
attributes: data.data?.attributes ?? {},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
incident: {
|
||||
type: 'object',
|
||||
description: 'The incident',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Incident UUID' },
|
||||
type: { type: 'string', description: 'Resource type (incidents)' },
|
||||
attributes: {
|
||||
type: 'object',
|
||||
description: 'Incident attributes',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'Incident title' },
|
||||
state: { type: 'string', description: 'Incident state' },
|
||||
severity: { type: 'string', description: 'Incident severity' },
|
||||
public_id: { type: 'number', description: 'Incremental public incident ID' },
|
||||
customer_impacted: { type: 'boolean', description: 'Whether customers were impacted' },
|
||||
customer_impact_scope: {
|
||||
type: 'string',
|
||||
description: 'Summary of the customer impact',
|
||||
},
|
||||
created: { type: 'string', description: 'Creation timestamp' },
|
||||
modified: { type: 'string', description: 'Last modification timestamp' },
|
||||
resolved: { type: 'string', description: 'Resolution timestamp' },
|
||||
time_to_resolve: { type: 'number', description: 'Seconds from creation to resolution' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { GetMonitorParams, GetMonitorResponse } from '@/tools/datadog/types'
|
||||
import { datadogErrorMessage } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getMonitorTool: ToolConfig<GetMonitorParams, GetMonitorResponse> = {
|
||||
@@ -19,7 +20,7 @@ export const getMonitorTool: ToolConfig<GetMonitorParams, GetMonitorResponse> =
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated group states to include (e.g., "alert,warn", "alert,warn,no data,ok")',
|
||||
'Comma-separated group states to include. Valid values are "all", "alert", "warn", and "no data" (e.g., "alert,warn").',
|
||||
},
|
||||
withDowntimes: {
|
||||
type: 'boolean',
|
||||
@@ -68,13 +69,13 @@ export const getMonitorTool: ToolConfig<GetMonitorParams, GetMonitorResponse> =
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const message = await datadogErrorMessage(response)
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
monitor: {} as any,
|
||||
monitor: {},
|
||||
},
|
||||
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
|
||||
error: message,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +116,11 @@ export const getMonitorTool: ToolConfig<GetMonitorParams, GetMonitorResponse> =
|
||||
overall_state: { type: 'string', description: 'Current monitor state' },
|
||||
created: { type: 'string', description: 'Creation timestamp' },
|
||||
modified: { type: 'string', description: 'Last modification timestamp' },
|
||||
options: {
|
||||
type: 'json',
|
||||
description: 'Monitor options (thresholds, notification settings)',
|
||||
},
|
||||
creator: { type: 'json', description: 'Monitor creator (email, handle, name)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { GetSecuritySignalParams, GetSecuritySignalResponse } from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getSecuritySignalTool: ToolConfig<GetSecuritySignalParams, GetSecuritySignalResponse> =
|
||||
{
|
||||
id: 'datadog_get_security_signal',
|
||||
name: 'Datadog Get Security Signal',
|
||||
description:
|
||||
'Get the details of a single Cloud SIEM security signal. Requires the `security_monitoring_signals_read` permission.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
signalId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the security signal',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v2/security_monitoring/signals/${encodeURIComponent(params.signalId)}`
|
||||
),
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { signal: { attributes: {} } },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
signal: {
|
||||
id: data.data?.id,
|
||||
type: data.data?.type,
|
||||
attributes: data.data?.attributes ?? {},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
signal: {
|
||||
type: 'object',
|
||||
description: 'The security signal',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Signal ID' },
|
||||
type: { type: 'string', description: 'Resource type (signal)' },
|
||||
attributes: {
|
||||
type: 'object',
|
||||
description: 'Signal attributes',
|
||||
properties: {
|
||||
message: { type: 'string', description: 'Message from the detection rule' },
|
||||
timestamp: { type: 'string', description: 'Signal timestamp' },
|
||||
tags: { type: 'array', description: 'Tags on the signal' },
|
||||
custom: { type: 'object', description: 'Signal-specific attributes' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { GetSloParams, GetSloResponse } from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getSloTool: ToolConfig<GetSloParams, GetSloResponse> = {
|
||||
id: 'datadog_get_slo',
|
||||
name: 'Datadog Get SLO',
|
||||
description: 'Get the configuration of a single service level objective by ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
sloId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the service level objective',
|
||||
},
|
||||
withConfiguredAlertIds: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Include the IDs of SLO monitors that reference this SLO',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryString = params.withConfiguredAlertIds ? '?with_configured_alert_ids=true' : ''
|
||||
return datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v1/slo/${encodeURIComponent(params.sloId)}${queryString}`
|
||||
)
|
||||
},
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { slo: { id: '', name: '', type: '' } },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { slo: data.data ?? { id: '', name: '', type: '' } },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
slo: {
|
||||
type: 'object',
|
||||
description: 'The service level objective',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'SLO ID' },
|
||||
name: { type: 'string', description: 'SLO name' },
|
||||
type: { type: 'string', description: 'SLO type: metric, monitor, or time_slice' },
|
||||
description: { type: 'string', description: 'SLO description' },
|
||||
tags: { type: 'array', description: 'SLO tags' },
|
||||
thresholds: { type: 'array', description: 'Timeframe targets and warnings' },
|
||||
target_threshold: { type: 'number', description: 'Primary target threshold' },
|
||||
warning_threshold: { type: 'number', description: 'Primary warning threshold' },
|
||||
timeframe: { type: 'string', description: 'Primary timeframe' },
|
||||
monitor_ids: { type: 'array', description: 'Monitor IDs for monitor-based SLOs' },
|
||||
groups: { type: 'array', description: 'Monitor groups narrowing the SLO scope' },
|
||||
configured_alert_ids: {
|
||||
type: 'array',
|
||||
description: 'SLO monitor IDs referencing this SLO',
|
||||
optional: true,
|
||||
},
|
||||
created_at: { type: 'number', description: 'Creation timestamp (Unix seconds)' },
|
||||
modified_at: { type: 'number', description: 'Modification timestamp (Unix seconds)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import type { GetSloHistoryParams, GetSloHistoryResponse } from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getSloHistoryTool: ToolConfig<GetSloHistoryParams, GetSloHistoryResponse> = {
|
||||
id: 'datadog_get_slo_history',
|
||||
name: 'Datadog Get SLO History',
|
||||
description:
|
||||
'Get an SLO’s history over a time window, including the overall SLI value and remaining error budget.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
sloId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the service level objective',
|
||||
},
|
||||
fromTs: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Start of the query window as a Unix timestamp in seconds',
|
||||
},
|
||||
toTs: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End of the query window as a Unix timestamp in seconds',
|
||||
},
|
||||
target: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'SLO target between 0 and 100. When supplied, the response includes the remaining error budget for a custom timeframe',
|
||||
},
|
||||
applyCorrection: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Whether to apply SLO corrections (defaults to true)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams({
|
||||
from_ts: String(params.fromTs),
|
||||
to_ts: String(params.toTs),
|
||||
})
|
||||
if (params.target !== undefined) queryParams.set('target', String(params.target))
|
||||
if (params.applyCorrection !== undefined)
|
||||
queryParams.set('apply_correction', String(params.applyCorrection))
|
||||
return datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v1/slo/${encodeURIComponent(params.sloId)}/history?${queryParams.toString()}`
|
||||
)
|
||||
},
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { history: {} },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const history = data.data ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
history,
|
||||
sliValue: history.overall?.sli_value ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
history: {
|
||||
type: 'object',
|
||||
description: 'SLO history for the requested window',
|
||||
properties: {
|
||||
from_ts: { type: 'number', description: 'Window start (Unix seconds)' },
|
||||
to_ts: { type: 'number', description: 'Window end (Unix seconds)' },
|
||||
type: { type: 'string', description: 'SLO type' },
|
||||
overall: {
|
||||
type: 'object',
|
||||
description: 'Overall SLI data for the window',
|
||||
properties: {
|
||||
sli_value: { type: 'number', description: 'SLI value over the window' },
|
||||
span_precision: { type: 'number', description: 'Decimal precision of the SLI value' },
|
||||
error_budget_remaining: {
|
||||
type: 'object',
|
||||
description: 'Remaining error budget keyed by timeframe',
|
||||
},
|
||||
},
|
||||
},
|
||||
groups: { type: 'array', description: 'Per-group SLI data for grouped SLOs' },
|
||||
monitors: { type: 'array', description: 'Per-monitor SLI data for multi-monitor SLOs' },
|
||||
thresholds: { type: 'object', description: 'Thresholds keyed by timeframe' },
|
||||
},
|
||||
},
|
||||
sliValue: {
|
||||
type: 'number',
|
||||
description: 'Overall SLI value over the window',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import type {
|
||||
GetSyntheticsResultsParams,
|
||||
GetSyntheticsResultsResponse,
|
||||
} from '@/tools/datadog/types'
|
||||
import {
|
||||
datadogApiUrl,
|
||||
datadogErrorMessage,
|
||||
datadogHeaders,
|
||||
splitCommaList,
|
||||
} from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getSyntheticsResultsTool: ToolConfig<
|
||||
GetSyntheticsResultsParams,
|
||||
GetSyntheticsResultsResponse
|
||||
> = {
|
||||
id: 'datadog_get_synthetics_results',
|
||||
name: 'Datadog Get Synthetic Test Results',
|
||||
description:
|
||||
'Get the latest result summaries (up to the last 150 runs) for a Synthetic API test.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
publicId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The public ID of the Synthetic API test',
|
||||
},
|
||||
fromTs: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Timestamp in milliseconds from which to start querying results',
|
||||
},
|
||||
toTs: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Timestamp in milliseconds up to which to query results',
|
||||
},
|
||||
probeDc: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated locations to query results for (e.g., "aws:eu-west-3")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.fromTs !== undefined) queryParams.set('from_ts', String(params.fromTs))
|
||||
if (params.toTs !== undefined) queryParams.set('to_ts', String(params.toTs))
|
||||
for (const location of splitCommaList(params.probeDc) ?? []) {
|
||||
queryParams.append('probe_dc', location)
|
||||
}
|
||||
const queryString = queryParams.toString()
|
||||
return datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v1/synthetics/tests/${encodeURIComponent(params.publicId)}/results${
|
||||
queryString ? `?${queryString}` : ''
|
||||
}`
|
||||
)
|
||||
},
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { results: [] },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results: data.results ?? [],
|
||||
lastTimestampFetched: data.last_timestamp_fetched,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Latest test result summaries',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result_id: { type: 'string', description: 'ID of the test result' },
|
||||
check_time: { type: 'number', description: 'Time the test ran' },
|
||||
probe_dc: { type: 'string', description: 'Location the test ran from' },
|
||||
status: {
|
||||
type: 'number',
|
||||
description: 'Monitor status: 0 not triggered, 1 triggered, 2 no data',
|
||||
},
|
||||
result: {
|
||||
type: 'object',
|
||||
description: 'Run outcome',
|
||||
properties: {
|
||||
passed: { type: 'boolean', description: 'Whether the run passed' },
|
||||
timings: { type: 'object', description: 'Request timing breakdown' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
lastTimestampFetched: {
|
||||
type: 'number',
|
||||
description: 'Timestamp of the latest test run',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { GetSyntheticsTestParams, GetSyntheticsTestResponse } from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getSyntheticsTestTool: ToolConfig<GetSyntheticsTestParams, GetSyntheticsTestResponse> =
|
||||
{
|
||||
id: 'datadog_get_synthetics_test',
|
||||
name: 'Datadog Get Synthetic Test',
|
||||
description:
|
||||
'Get the configuration of a Synthetic test by public ID. Browser test steps are not included by this type-agnostic endpoint.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
publicId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The public ID of the Synthetic test (e.g., "abc-def-ghi")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v1/synthetics/tests/${encodeURIComponent(params.publicId)}`
|
||||
),
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { test: {} },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { test: await response.json() },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
test: {
|
||||
type: 'object',
|
||||
description: 'The Synthetic test configuration',
|
||||
properties: {
|
||||
public_id: { type: 'string', description: 'Public ID of the test' },
|
||||
name: { type: 'string', description: 'Test name' },
|
||||
status: { type: 'string', description: 'Pause status: live or paused' },
|
||||
type: { type: 'string', description: 'Test type: api, browser, mobile, or network' },
|
||||
subtype: { type: 'string', description: 'Test subtype, such as http or ssl' },
|
||||
message: { type: 'string', description: 'Notification message' },
|
||||
monitor_id: { type: 'number', description: 'Associated monitor ID' },
|
||||
tags: { type: 'array', description: 'Tags attached to the test' },
|
||||
locations: { type: 'array', description: 'Locations the test runs from' },
|
||||
config: { type: 'object', description: 'Test request, assertions, and variables' },
|
||||
options: { type: 'object', description: 'Scheduling, retry, and monitor options' },
|
||||
creator: { type: 'object', description: 'User who created the test' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,15 +1,44 @@
|
||||
import { addIncidentTodoTool } from '@/tools/datadog/add_incident_todo'
|
||||
import { cancelDowntimeTool } from '@/tools/datadog/cancel_downtime'
|
||||
import { createDashboardTool } from '@/tools/datadog/create_dashboard'
|
||||
import { createDowntimeTool } from '@/tools/datadog/create_downtime'
|
||||
import { createEventTool } from '@/tools/datadog/create_event'
|
||||
import { createIncidentTool } from '@/tools/datadog/create_incident'
|
||||
import { createMonitorTool } from '@/tools/datadog/create_monitor'
|
||||
import { createSloTool } from '@/tools/datadog/create_slo'
|
||||
import { deleteDashboardTool } from '@/tools/datadog/delete_dashboard'
|
||||
import { deleteSloTool } from '@/tools/datadog/delete_slo'
|
||||
import { getBrowserSyntheticsResultsTool } from '@/tools/datadog/get_browser_synthetics_results'
|
||||
import { getDashboardTool } from '@/tools/datadog/get_dashboard'
|
||||
import { getIncidentTool } from '@/tools/datadog/get_incident'
|
||||
import { getMonitorTool } from '@/tools/datadog/get_monitor'
|
||||
import { getSecuritySignalTool } from '@/tools/datadog/get_security_signal'
|
||||
import { getSloTool } from '@/tools/datadog/get_slo'
|
||||
import { getSloHistoryTool } from '@/tools/datadog/get_slo_history'
|
||||
import { getSyntheticsResultsTool } from '@/tools/datadog/get_synthetics_results'
|
||||
import { getSyntheticsTestTool } from '@/tools/datadog/get_synthetics_test'
|
||||
import { listDashboardsTool } from '@/tools/datadog/list_dashboards'
|
||||
import { listDowntimesTool } from '@/tools/datadog/list_downtimes'
|
||||
import { listIncidentsTool } from '@/tools/datadog/list_incidents'
|
||||
import { listMonitorsTool } from '@/tools/datadog/list_monitors'
|
||||
import { listSecurityRulesTool } from '@/tools/datadog/list_security_rules'
|
||||
import { listSecuritySignalsTool } from '@/tools/datadog/list_security_signals'
|
||||
import { listServicesTool } from '@/tools/datadog/list_services'
|
||||
import { listSlosTool } from '@/tools/datadog/list_slos'
|
||||
import { listSyntheticsTestsTool } from '@/tools/datadog/list_synthetics_tests'
|
||||
import { muteMonitorTool } from '@/tools/datadog/mute_monitor'
|
||||
import { queryLogsTool } from '@/tools/datadog/query_logs'
|
||||
import { queryTimeseriesTool } from '@/tools/datadog/query_timeseries'
|
||||
import { searchSpansTool } from '@/tools/datadog/search_spans'
|
||||
import { sendLogsTool } from '@/tools/datadog/send_logs'
|
||||
import { submitMetricsTool } from '@/tools/datadog/submit_metrics'
|
||||
import { triggerSyntheticsTestsTool } from '@/tools/datadog/trigger_synthetics_tests'
|
||||
import { unmuteMonitorTool } from '@/tools/datadog/unmute_monitor'
|
||||
import { updateIncidentTool } from '@/tools/datadog/update_incident'
|
||||
import { updateSecuritySignalAssigneeTool } from '@/tools/datadog/update_security_signal_assignee'
|
||||
import { updateSecuritySignalStateTool } from '@/tools/datadog/update_security_signal_state'
|
||||
import { updateSloTool } from '@/tools/datadog/update_slo'
|
||||
import { updateSyntheticsStatusTool } from '@/tools/datadog/update_synthetics_status'
|
||||
|
||||
export const datadogSubmitMetricsTool = submitMetricsTool
|
||||
export const datadogQueryTimeseriesTool = queryTimeseriesTool
|
||||
@@ -18,8 +47,37 @@ export const datadogCreateMonitorTool = createMonitorTool
|
||||
export const datadogGetMonitorTool = getMonitorTool
|
||||
export const datadogListMonitorsTool = listMonitorsTool
|
||||
export const datadogMuteMonitorTool = muteMonitorTool
|
||||
export const datadogUnmuteMonitorTool = unmuteMonitorTool
|
||||
export const datadogQueryLogsTool = queryLogsTool
|
||||
export const datadogSendLogsTool = sendLogsTool
|
||||
export const datadogCreateDowntimeTool = createDowntimeTool
|
||||
export const datadogListDowntimesTool = listDowntimesTool
|
||||
export const datadogCancelDowntimeTool = cancelDowntimeTool
|
||||
export const datadogListIncidentsTool = listIncidentsTool
|
||||
export const datadogGetIncidentTool = getIncidentTool
|
||||
export const datadogCreateIncidentTool = createIncidentTool
|
||||
export const datadogUpdateIncidentTool = updateIncidentTool
|
||||
export const datadogAddIncidentTodoTool = addIncidentTodoTool
|
||||
export const datadogListSlosTool = listSlosTool
|
||||
export const datadogGetSloTool = getSloTool
|
||||
export const datadogCreateSloTool = createSloTool
|
||||
export const datadogUpdateSloTool = updateSloTool
|
||||
export const datadogDeleteSloTool = deleteSloTool
|
||||
export const datadogGetSloHistoryTool = getSloHistoryTool
|
||||
export const datadogListDashboardsTool = listDashboardsTool
|
||||
export const datadogGetDashboardTool = getDashboardTool
|
||||
export const datadogCreateDashboardTool = createDashboardTool
|
||||
export const datadogDeleteDashboardTool = deleteDashboardTool
|
||||
export const datadogListSyntheticsTestsTool = listSyntheticsTestsTool
|
||||
export const datadogGetSyntheticsTestTool = getSyntheticsTestTool
|
||||
export const datadogGetSyntheticsResultsTool = getSyntheticsResultsTool
|
||||
export const datadogGetBrowserSyntheticsResultsTool = getBrowserSyntheticsResultsTool
|
||||
export const datadogTriggerSyntheticsTestsTool = triggerSyntheticsTestsTool
|
||||
export const datadogUpdateSyntheticsStatusTool = updateSyntheticsStatusTool
|
||||
export const datadogListSecuritySignalsTool = listSecuritySignalsTool
|
||||
export const datadogGetSecuritySignalTool = getSecuritySignalTool
|
||||
export const datadogUpdateSecuritySignalStateTool = updateSecuritySignalStateTool
|
||||
export const datadogUpdateSecuritySignalAssigneeTool = updateSecuritySignalAssigneeTool
|
||||
export const datadogListSecurityRulesTool = listSecurityRulesTool
|
||||
export const datadogSearchSpansTool = searchSpansTool
|
||||
export const datadogListServicesTool = listServicesTool
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { ListDashboardsParams, ListDashboardsResponse } from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listDashboardsTool: ToolConfig<ListDashboardsParams, ListDashboardsResponse> = {
|
||||
id: 'datadog_list_dashboards',
|
||||
name: 'Datadog List Dashboards',
|
||||
description:
|
||||
'List custom created or cloned dashboards. Datadog preset dashboards are not returned.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
filterShared: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return only shared dashboards',
|
||||
},
|
||||
filterDeleted: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Return only deleted dashboards. Incompatible with filterShared',
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of dashboards to return (default: 100)',
|
||||
},
|
||||
start: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Offset of the first dashboard returned (e.g., 0, 100)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.filterShared !== undefined)
|
||||
queryParams.set('filter[shared]', String(params.filterShared))
|
||||
if (params.filterDeleted !== undefined)
|
||||
queryParams.set('filter[deleted]', String(params.filterDeleted))
|
||||
if (params.count !== undefined) queryParams.set('count', String(params.count))
|
||||
if (params.start !== undefined) queryParams.set('start', String(params.start))
|
||||
const queryString = queryParams.toString()
|
||||
return datadogApiUrl(params.site, `/api/v1/dashboard${queryString ? `?${queryString}` : ''}`)
|
||||
},
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { dashboards: [] },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { dashboards: data.dashboards ?? [] },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
dashboards: {
|
||||
type: 'array',
|
||||
description: 'List of dashboard summaries',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Dashboard ID' },
|
||||
title: { type: 'string', description: 'Dashboard title' },
|
||||
description: { type: 'string', description: 'Dashboard description' },
|
||||
layout_type: { type: 'string', description: 'Layout type: ordered or free' },
|
||||
url: { type: 'string', description: 'Dashboard URL path' },
|
||||
author_handle: { type: 'string', description: 'Handle of the dashboard author' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
modified_at: { type: 'string', description: 'Modification timestamp' },
|
||||
is_read_only: { type: 'boolean', description: 'Whether the dashboard is read-only' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { ListDowntimesParams, ListDowntimesResponse } from '@/tools/datadog/types'
|
||||
import type {
|
||||
DatadogV2Resource,
|
||||
DowntimeAttributes,
|
||||
ListDowntimesParams,
|
||||
ListDowntimesResponse,
|
||||
} from '@/tools/datadog/types'
|
||||
import { datadogErrorMessage } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listDowntimesTool: ToolConfig<ListDowntimesParams, ListDowntimesResponse> = {
|
||||
@@ -14,11 +20,17 @@ export const listDowntimesTool: ToolConfig<ListDowntimesParams, ListDowntimesRes
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only return currently active downtimes',
|
||||
},
|
||||
monitorId: {
|
||||
type: 'string',
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by monitor ID (e.g., "12345678")',
|
||||
description: 'Number of downtimes to return per page (default: 30, max: 100)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index of the first downtime to return (e.g., 0, 30, 60)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
@@ -46,7 +58,8 @@ export const listDowntimesTool: ToolConfig<ListDowntimesParams, ListDowntimesRes
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.currentOnly) queryParams.set('current_only', 'true')
|
||||
if (params.monitorId) queryParams.set('monitor_id', params.monitorId)
|
||||
if (params.limit !== undefined) queryParams.set('page[limit]', String(params.limit))
|
||||
if (params.offset !== undefined) queryParams.set('page[offset]', String(params.offset))
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
return `https://api.${site}/api/v2/downtime${queryString ? `?${queryString}` : ''}`
|
||||
@@ -61,27 +74,26 @@ export const listDowntimesTool: ToolConfig<ListDowntimesParams, ListDowntimesRes
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const message = await datadogErrorMessage(response)
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
downtimes: [],
|
||||
},
|
||||
error: errorData.errors?.[0]?.detail || `HTTP ${response.status}: ${response.statusText}`,
|
||||
error: message,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const downtimes = (data.data || []).map((d: any) => {
|
||||
const attrs = d.attributes || {}
|
||||
const downtimes = (data.data || []).map((d: DatadogV2Resource<DowntimeAttributes>) => {
|
||||
const attrs: DowntimeAttributes = d.attributes || {}
|
||||
return {
|
||||
id: d.id,
|
||||
scope: attrs.scope ? [attrs.scope] : [],
|
||||
message: attrs.message,
|
||||
start: attrs.schedule?.start ? new Date(attrs.schedule.start).getTime() / 1000 : undefined,
|
||||
end: attrs.schedule?.end ? new Date(attrs.schedule.end).getTime() / 1000 : undefined,
|
||||
timezone: attrs.schedule?.timezone,
|
||||
disabled: attrs.disabled,
|
||||
timezone: attrs.display_timezone ?? undefined,
|
||||
active: attrs.status === 'active',
|
||||
created: attrs.created ? new Date(attrs.created).getTime() / 1000 : undefined,
|
||||
modified: attrs.modified ? new Date(attrs.modified).getTime() / 1000 : undefined,
|
||||
@@ -92,23 +104,32 @@ export const listDowntimesTool: ToolConfig<ListDowntimesParams, ListDowntimesRes
|
||||
success: true,
|
||||
output: {
|
||||
downtimes,
|
||||
totalCount: data.meta?.page?.total_filtered_count,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total number of downtimes matching the filter, across all pages',
|
||||
optional: true,
|
||||
},
|
||||
downtimes: {
|
||||
type: 'array',
|
||||
description: 'List of downtimes',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'number', description: 'Downtime ID' },
|
||||
id: { type: 'string', description: 'Downtime UUID' },
|
||||
scope: { type: 'array', description: 'Downtime scope' },
|
||||
message: { type: 'string', description: 'Downtime message' },
|
||||
start: { type: 'number', description: 'Start time (Unix timestamp)' },
|
||||
end: { type: 'number', description: 'End time (Unix timestamp)' },
|
||||
timezone: { type: 'string', description: 'Display timezone for the downtime' },
|
||||
active: { type: 'boolean', description: 'Whether downtime is currently active' },
|
||||
created: { type: 'number', description: 'Creation time (Unix timestamp)' },
|
||||
modified: { type: 'number', description: 'Last modification time (Unix timestamp)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import type {
|
||||
DatadogV2Resource,
|
||||
IncidentAttributes,
|
||||
ListIncidentsParams,
|
||||
ListIncidentsResponse,
|
||||
} from '@/tools/datadog/types'
|
||||
import {
|
||||
datadogApiUrl,
|
||||
datadogErrorMessage,
|
||||
datadogHeaders,
|
||||
splitCommaList,
|
||||
} from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listIncidentsTool: ToolConfig<ListIncidentsParams, ListIncidentsResponse> = {
|
||||
id: 'datadog_list_incidents',
|
||||
name: 'Datadog List Incidents',
|
||||
description:
|
||||
'List incidents for the organization. Requires the Incident Management `incident_read` permission; the Incidents API is in public beta.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
include: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated related resources to include: "users" and/or "attachments"',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of incidents to return per page (default: 10, max: 100)',
|
||||
},
|
||||
pageOffset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index of the first incident to return (e.g., 0, 10, 20)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
const include = splitCommaList(params.include)?.join(',')
|
||||
if (include) queryParams.set('include', include)
|
||||
if (params.pageSize !== undefined) queryParams.set('page[size]', String(params.pageSize))
|
||||
if (params.pageOffset !== undefined)
|
||||
queryParams.set('page[offset]', String(params.pageOffset))
|
||||
const queryString = queryParams.toString()
|
||||
return datadogApiUrl(params.site, `/api/v2/incidents${queryString ? `?${queryString}` : ''}`)
|
||||
},
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { incidents: [] },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
incidents: (data.data ?? []).map((incident: DatadogV2Resource<IncidentAttributes>) => ({
|
||||
id: incident.id,
|
||||
type: incident.type,
|
||||
attributes: incident.attributes ?? {},
|
||||
})),
|
||||
nextOffset: data.meta?.pagination?.next_offset,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
incidents: {
|
||||
type: 'array',
|
||||
description: 'List of incidents',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Incident UUID' },
|
||||
type: { type: 'string', description: 'Resource type (incidents)' },
|
||||
attributes: {
|
||||
type: 'object',
|
||||
description: 'Incident attributes',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'Incident title' },
|
||||
state: { type: 'string', description: 'Incident state' },
|
||||
severity: { type: 'string', description: 'Incident severity' },
|
||||
public_id: { type: 'number', description: 'Incremental public incident ID' },
|
||||
customer_impacted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether customers were impacted',
|
||||
},
|
||||
created: { type: 'string', description: 'Creation timestamp' },
|
||||
modified: { type: 'string', description: 'Last modification timestamp' },
|
||||
resolved: { type: 'string', description: 'Resolution timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nextOffset: {
|
||||
type: 'number',
|
||||
description: 'Offset to use for the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ListMonitorsParams, ListMonitorsResponse } from '@/tools/datadog/types'
|
||||
import type { ListMonitorsParams, ListMonitorsResponse, MonitorData } from '@/tools/datadog/types'
|
||||
import { datadogErrorMessage } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DatadogListMonitors')
|
||||
|
||||
export const listMonitorsTool: ToolConfig<ListMonitorsParams, ListMonitorsResponse> = {
|
||||
id: 'datadog_list_monitors',
|
||||
name: 'Datadog List Monitors',
|
||||
@@ -16,7 +14,7 @@ export const listMonitorsTool: ToolConfig<ListMonitorsParams, ListMonitorsRespon
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated group states to filter by (e.g., "alert,warn", "alert,warn,no data,ok")',
|
||||
'Comma-separated group states to filter by. Valid values are "all", "alert", "warn", and "no data" (e.g., "alert,warn").',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
@@ -89,18 +87,7 @@ export const listMonitorsTool: ToolConfig<ListMonitorsParams, ListMonitorsRespon
|
||||
if (params.pageSize) queryParams.set('page_size', String(params.pageSize))
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
const url = `https://api.${site}/api/v1/monitor${queryString ? `?${queryString}` : ''}`
|
||||
logger.info(
|
||||
'[Datadog List Monitors] URL:',
|
||||
url,
|
||||
'Site param:',
|
||||
params.site,
|
||||
'API Key present:',
|
||||
!!params.apiKey,
|
||||
'App Key present:',
|
||||
!!params.applicationKey
|
||||
)
|
||||
return url
|
||||
return `https://api.${site}/api/v1/monitor${queryString ? `?${queryString}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
@@ -112,18 +99,18 @@ export const listMonitorsTool: ToolConfig<ListMonitorsParams, ListMonitorsRespon
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const message = await datadogErrorMessage(response)
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
monitors: [],
|
||||
},
|
||||
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
|
||||
error: message,
|
||||
}
|
||||
}
|
||||
|
||||
const text = await response.text()
|
||||
let data: any
|
||||
let data: unknown
|
||||
try {
|
||||
data = JSON.parse(text)
|
||||
} catch (e) {
|
||||
@@ -142,7 +129,7 @@ export const listMonitorsTool: ToolConfig<ListMonitorsParams, ListMonitorsRespon
|
||||
}
|
||||
}
|
||||
|
||||
const monitors = data.map((m: any) => ({
|
||||
const monitors = (data as MonitorData[]).map((m) => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
type: m.type,
|
||||
@@ -176,8 +163,17 @@ export const listMonitorsTool: ToolConfig<ListMonitorsParams, ListMonitorsRespon
|
||||
name: { type: 'string', description: 'Monitor name' },
|
||||
type: { type: 'string', description: 'Monitor type' },
|
||||
query: { type: 'string', description: 'Monitor query' },
|
||||
message: { type: 'string', description: 'Notification message' },
|
||||
overall_state: { type: 'string', description: 'Current state' },
|
||||
tags: { type: 'array', description: 'Tags' },
|
||||
priority: { type: 'number', description: 'Monitor priority' },
|
||||
options: {
|
||||
type: 'json',
|
||||
description: 'Monitor options (thresholds, notification settings)',
|
||||
},
|
||||
created: { type: 'string', description: 'Creation timestamp' },
|
||||
modified: { type: 'string', description: 'Last modification timestamp' },
|
||||
creator: { type: 'json', description: 'Monitor creator (email, handle, name)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import type {
|
||||
ListSecurityRulesParams,
|
||||
ListSecurityRulesResponse,
|
||||
SecurityRuleData,
|
||||
} from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listSecurityRulesTool: ToolConfig<ListSecurityRulesParams, ListSecurityRulesResponse> =
|
||||
{
|
||||
id: 'datadog_list_security_rules',
|
||||
name: 'Datadog List Security Rules',
|
||||
description:
|
||||
'List Cloud SIEM detection rules. Requires the `security_monitoring_rules_read` permission.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Search query filtering rules by attributes such as type, source, or tags (e.g., "type:log_detection source:cloudtrail")',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Sort attribute, prefix with "-" for descending: name, creation_date, update_date, enabled, type, highest_severity, or source',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of rules per page (default: 10, max: 100)',
|
||||
},
|
||||
pageNumber: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page to retrieve, starting at zero',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.query) queryParams.set('query', params.query)
|
||||
if (params.sort) queryParams.set('sort', params.sort)
|
||||
if (params.pageSize !== undefined) queryParams.set('page[size]', String(params.pageSize))
|
||||
if (params.pageNumber !== undefined)
|
||||
queryParams.set('page[number]', String(params.pageNumber))
|
||||
const queryString = queryParams.toString()
|
||||
return datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v2/security_monitoring/rules${queryString ? `?${queryString}` : ''}`
|
||||
)
|
||||
},
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { rules: [] },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
rules: (data.data ?? []).map((rule: SecurityRuleData) => ({
|
||||
id: rule.id,
|
||||
name: rule.name,
|
||||
type: rule.type,
|
||||
message: rule.message,
|
||||
tags: rule.tags ?? [],
|
||||
isEnabled: rule.isEnabled,
|
||||
isDefault: rule.isDefault,
|
||||
createdAt: rule.createdAt,
|
||||
version: rule.version,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
rules: {
|
||||
type: 'array',
|
||||
description: 'List of detection rules',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Rule ID' },
|
||||
name: { type: 'string', description: 'Rule name' },
|
||||
type: { type: 'string', description: 'Rule type' },
|
||||
message: { type: 'string', description: 'Message attached to generated signals' },
|
||||
tags: { type: 'array', description: 'Rule tags' },
|
||||
isEnabled: { type: 'boolean', description: 'Whether the rule is enabled' },
|
||||
isDefault: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the rule is a Datadog default rule',
|
||||
},
|
||||
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
|
||||
version: { type: 'number', description: 'Rule version' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import type {
|
||||
DatadogV2Resource,
|
||||
ListSecuritySignalsParams,
|
||||
ListSecuritySignalsResponse,
|
||||
SecuritySignalAttributes,
|
||||
} from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listSecuritySignalsTool: ToolConfig<
|
||||
ListSecuritySignalsParams,
|
||||
ListSecuritySignalsResponse
|
||||
> = {
|
||||
id: 'datadog_list_security_signals',
|
||||
name: 'Datadog List Security Signals',
|
||||
description:
|
||||
'Search Cloud SIEM security signals by query and time range. Requires the `security_monitoring_signals_read` permission.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Signal search query (e.g., "security:attack status:high")',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Minimum timestamp as an ISO-8601 date-time (e.g., "2026-01-02T09:42:36.320Z"). Signal search does not accept relative expressions like "now-1h".',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Maximum timestamp as an ISO-8601 date-time (e.g., "2026-01-03T09:42:36.320Z"). Signal search does not accept relative expressions like "now".',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort order: "timestamp" for oldest first, "-timestamp" for newest first',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor returned as nextCursor by a previous call',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of signals to return (default: 10, max: 1000)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => datadogApiUrl(params.site, '/api/v2/security_monitoring/signals/search'),
|
||||
method: 'POST',
|
||||
headers: datadogHeaders,
|
||||
body: (params) => {
|
||||
const filter: Record<string, string> = {}
|
||||
if (params.query) filter.query = params.query
|
||||
if (params.from) filter.from = params.from
|
||||
if (params.to) filter.to = params.to
|
||||
|
||||
const page: Record<string, string | number> = {}
|
||||
if (params.cursor) page.cursor = params.cursor
|
||||
if (params.limit !== undefined) page.limit = params.limit
|
||||
|
||||
const body: Record<string, unknown> = {}
|
||||
if (Object.keys(filter).length > 0) body.filter = filter
|
||||
if (Object.keys(page).length > 0) body.page = page
|
||||
if (params.sort) body.sort = params.sort
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { signals: [] },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
signals: (data.data ?? []).map((signal: DatadogV2Resource<SecuritySignalAttributes>) => ({
|
||||
id: signal.id,
|
||||
type: signal.type,
|
||||
attributes: signal.attributes ?? {},
|
||||
})),
|
||||
nextCursor: data.meta?.page?.after,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
signals: {
|
||||
type: 'array',
|
||||
description: 'List of security signals',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Signal ID' },
|
||||
type: { type: 'string', description: 'Resource type (signal)' },
|
||||
attributes: {
|
||||
type: 'object',
|
||||
description: 'Signal attributes',
|
||||
properties: {
|
||||
message: { type: 'string', description: 'Message from the detection rule' },
|
||||
timestamp: { type: 'string', description: 'Signal timestamp' },
|
||||
tags: { type: 'array', description: 'Tags on the signal' },
|
||||
custom: { type: 'object', description: 'Signal-specific attributes' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for the next page of signals',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type {
|
||||
DatadogV2Resource,
|
||||
ListServicesParams,
|
||||
ListServicesResponse,
|
||||
ServiceDefinitionAttributes,
|
||||
} from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listServicesTool: ToolConfig<ListServicesParams, ListServicesResponse> = {
|
||||
id: 'datadog_list_services',
|
||||
name: 'Datadog List Services',
|
||||
description:
|
||||
'List service definitions from the Datadog Service Catalog. Requires the `apm_service_catalog_read` permission.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of service definitions per page (default: 10, max: 100)',
|
||||
},
|
||||
pageNumber: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page to retrieve, starting at zero',
|
||||
},
|
||||
schemaVersion: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Schema version to return (e.g., "v2", "v2.1", "v2.2")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.pageSize !== undefined) queryParams.set('page[size]', String(params.pageSize))
|
||||
if (params.pageNumber !== undefined)
|
||||
queryParams.set('page[number]', String(params.pageNumber))
|
||||
if (params.schemaVersion) queryParams.set('schema_version', params.schemaVersion)
|
||||
const queryString = queryParams.toString()
|
||||
return datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v2/services/definitions${queryString ? `?${queryString}` : ''}`
|
||||
)
|
||||
},
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { services: [] },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
services: (data.data ?? []).map(
|
||||
(service: DatadogV2Resource<ServiceDefinitionAttributes>) => ({
|
||||
id: service.id,
|
||||
type: service.type,
|
||||
schema: service.attributes?.schema ?? {},
|
||||
meta: service.attributes?.meta ?? {},
|
||||
})
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
services: {
|
||||
type: 'array',
|
||||
description: 'List of service definitions',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Service definition ID' },
|
||||
type: { type: 'string', description: 'Resource type (service_definitions)' },
|
||||
schema: {
|
||||
type: 'object',
|
||||
description:
|
||||
'The service definition schema. Its shape depends on the requested schema version',
|
||||
},
|
||||
meta: {
|
||||
type: 'object',
|
||||
description: 'Ingestion metadata such as origin and last modified time',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { ListSlosParams, ListSlosResponse } from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listSlosTool: ToolConfig<ListSlosParams, ListSlosResponse> = {
|
||||
id: 'datadog_list_slos',
|
||||
name: 'Datadog List SLOs',
|
||||
description:
|
||||
'List service level objectives, optionally filtered by IDs, name, tags, or underlying metrics query.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
ids: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated SLO IDs to fetch (e.g., "id1,id2")',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter results by SLO name (e.g., "checkout latency")',
|
||||
},
|
||||
tagsQuery: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter results by a single SLO tag (e.g., "env:prod")',
|
||||
},
|
||||
metricsQuery: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Filter results by SLO numerator and denominator (e.g., "aws.elb.request_count")',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of SLOs to return (default: 1000)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Offset of the first SLO returned (e.g., 0, 50)',
|
||||
},
|
||||
isDeleted: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Return only deleted SLOs',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.ids) queryParams.set('ids', params.ids)
|
||||
if (params.query) queryParams.set('query', params.query)
|
||||
if (params.tagsQuery) queryParams.set('tags_query', params.tagsQuery)
|
||||
if (params.metricsQuery) queryParams.set('metrics_query', params.metricsQuery)
|
||||
if (params.limit !== undefined) queryParams.set('limit', String(params.limit))
|
||||
if (params.offset !== undefined) queryParams.set('offset', String(params.offset))
|
||||
if (params.isDeleted) queryParams.set('is_deleted', 'true')
|
||||
const queryString = queryParams.toString()
|
||||
return datadogApiUrl(params.site, `/api/v1/slo${queryString ? `?${queryString}` : ''}`)
|
||||
},
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { slos: [] },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
slos: data.data ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
slos: {
|
||||
type: 'array',
|
||||
description: 'List of service level objectives',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'SLO ID' },
|
||||
name: { type: 'string', description: 'SLO name' },
|
||||
type: { type: 'string', description: 'SLO type: metric, monitor, or time_slice' },
|
||||
description: { type: 'string', description: 'SLO description' },
|
||||
tags: { type: 'array', description: 'SLO tags' },
|
||||
thresholds: { type: 'array', description: 'Timeframe targets and warnings' },
|
||||
target_threshold: { type: 'number', description: 'Primary target threshold' },
|
||||
warning_threshold: { type: 'number', description: 'Primary warning threshold' },
|
||||
timeframe: { type: 'string', description: 'Primary timeframe' },
|
||||
monitor_ids: { type: 'array', description: 'Monitor IDs for monitor-based SLOs' },
|
||||
created_at: { type: 'number', description: 'Creation timestamp (Unix seconds)' },
|
||||
modified_at: { type: 'number', description: 'Modification timestamp (Unix seconds)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type {
|
||||
ListSyntheticsTestsParams,
|
||||
ListSyntheticsTestsResponse,
|
||||
SyntheticsTestData,
|
||||
} from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listSyntheticsTestsTool: ToolConfig<
|
||||
ListSyntheticsTestsParams,
|
||||
ListSyntheticsTestsResponse
|
||||
> = {
|
||||
id: 'datadog_list_synthetics_tests',
|
||||
name: 'Datadog List Synthetic Tests',
|
||||
description: 'List all Synthetic tests (API, browser, and mobile) with their current status.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of tests returned per page (default: 100)',
|
||||
},
|
||||
pageNumber: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page to retrieve, starting at zero',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.pageSize !== undefined) queryParams.set('page_size', String(params.pageSize))
|
||||
if (params.pageNumber !== undefined) queryParams.set('page_number', String(params.pageNumber))
|
||||
const queryString = queryParams.toString()
|
||||
return datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v1/synthetics/tests${queryString ? `?${queryString}` : ''}`
|
||||
)
|
||||
},
|
||||
method: 'GET',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { tests: [] },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tests: (data.tests ?? []).map((test: SyntheticsTestData) => ({
|
||||
public_id: test.public_id,
|
||||
name: test.name,
|
||||
status: test.status,
|
||||
type: test.type,
|
||||
subtype: test.subtype,
|
||||
message: test.message,
|
||||
monitor_id: test.monitor_id,
|
||||
tags: test.tags ?? [],
|
||||
locations: test.locations ?? [],
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tests: {
|
||||
type: 'array',
|
||||
description: 'List of Synthetic tests',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
public_id: { type: 'string', description: 'Public ID of the test' },
|
||||
name: { type: 'string', description: 'Test name' },
|
||||
status: { type: 'string', description: 'Pause status: live or paused' },
|
||||
type: { type: 'string', description: 'Test type: api, browser, mobile, or network' },
|
||||
subtype: { type: 'string', description: 'Test subtype, such as http or ssl' },
|
||||
message: { type: 'string', description: 'Notification message' },
|
||||
monitor_id: { type: 'number', description: 'Associated monitor ID' },
|
||||
tags: { type: 'array', description: 'Tags attached to the test' },
|
||||
locations: { type: 'array', description: 'Locations the test runs from' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { MuteMonitorParams, MuteMonitorResponse } from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const muteMonitorTool: ToolConfig<MuteMonitorParams, MuteMonitorResponse> = {
|
||||
id: 'datadog_mute_monitor',
|
||||
name: 'Datadog Mute Monitor',
|
||||
description: 'Mute a monitor to temporarily suppress notifications.',
|
||||
description:
|
||||
'Mute a monitor to temporarily suppress its notifications. Use Unmute Monitor to reverse it, or schedule a downtime instead when you want a planned, auditable maintenance window.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
@@ -26,7 +28,7 @@ export const muteMonitorTool: ToolConfig<MuteMonitorParams, MuteMonitorResponse>
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Unix timestamp in seconds when the mute should end (e.g., 1705323600). If not specified, mutes indefinitely.',
|
||||
'Unix timestamp in seconds when the mute should end (e.g., 1705323600). If not specified, the monitor stays muted until it is unmuted.',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
@@ -49,40 +51,36 @@ export const muteMonitorTool: ToolConfig<MuteMonitorParams, MuteMonitorResponse>
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const site = params.site || 'datadoghq.com'
|
||||
return `https://api.${site}/api/v1/monitor/${params.monitorId}/mute`
|
||||
},
|
||||
url: (params) =>
|
||||
datadogApiUrl(params.site, `/api/v1/monitor/${encodeURIComponent(params.monitorId)}/mute`),
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'DD-API-KEY': params.apiKey,
|
||||
'DD-APPLICATION-KEY': params.applicationKey,
|
||||
}),
|
||||
headers: datadogHeaders,
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {}
|
||||
const body: { scope?: string; end?: number } = {}
|
||||
if (params.scope) body.scope = params.scope
|
||||
if (params.end) body.end = params.end
|
||||
if (params.end !== undefined) body.end = params.end
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
success: false,
|
||||
},
|
||||
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
|
||||
output: { success: false },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => ({}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
monitorId: data.id,
|
||||
name: data.name,
|
||||
overallState: data.overall_state,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -92,5 +90,20 @@ export const muteMonitorTool: ToolConfig<MuteMonitorParams, MuteMonitorResponse>
|
||||
type: 'boolean',
|
||||
description: 'Whether the monitor was successfully muted',
|
||||
},
|
||||
monitorId: {
|
||||
type: 'number',
|
||||
description: 'ID of the muted monitor',
|
||||
optional: true,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name of the muted monitor',
|
||||
optional: true,
|
||||
},
|
||||
overallState: {
|
||||
type: 'string',
|
||||
description: 'Monitor state after muting',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { QueryLogsParams, QueryLogsResponse } from '@/tools/datadog/types'
|
||||
import type {
|
||||
DatadogV2Resource,
|
||||
LogAttributes,
|
||||
QueryLogsParams,
|
||||
QueryLogsResponse,
|
||||
} from '@/tools/datadog/types'
|
||||
import { datadogErrorMessage } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const queryLogsTool: ToolConfig<QueryLogsParams, QueryLogsResponse> = {
|
||||
@@ -36,6 +42,13 @@ export const queryLogsTool: ToolConfig<QueryLogsParams, QueryLogsResponse> = {
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of logs to return (e.g., 50, 100, max: 1000)',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Pagination cursor from a previous call, taken from its nextLogId output. Omit for the first page.',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
@@ -80,7 +93,11 @@ export const queryLogsTool: ToolConfig<QueryLogsParams, QueryLogsResponse> = {
|
||||
'DD-APPLICATION-KEY': params.applicationKey,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {
|
||||
const body: {
|
||||
filter: { query: string; from: string; to: string; indexes?: string[] }
|
||||
page: { limit: number; cursor?: string }
|
||||
sort?: 'timestamp' | '-timestamp'
|
||||
} = {
|
||||
filter: {
|
||||
query: params.query,
|
||||
from: params.from,
|
||||
@@ -91,6 +108,10 @@ export const queryLogsTool: ToolConfig<QueryLogsParams, QueryLogsResponse> = {
|
||||
},
|
||||
}
|
||||
|
||||
if (params.cursor) {
|
||||
body.page.cursor = params.cursor
|
||||
}
|
||||
|
||||
if (params.sort) {
|
||||
body.sort = params.sort
|
||||
}
|
||||
@@ -108,18 +129,18 @@ export const queryLogsTool: ToolConfig<QueryLogsParams, QueryLogsResponse> = {
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const message = await datadogErrorMessage(response)
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
logs: [],
|
||||
},
|
||||
error: errorData.errors?.[0]?.detail || `HTTP ${response.status}: ${response.statusText}`,
|
||||
error: message,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const logs = (data.data || []).map((log: any) => ({
|
||||
const logs = (data.data || []).map((log: DatadogV2Resource<LogAttributes>) => ({
|
||||
id: log.id,
|
||||
content: {
|
||||
timestamp: log.attributes?.timestamp,
|
||||
@@ -158,6 +179,8 @@ export const queryLogsTool: ToolConfig<QueryLogsParams, QueryLogsResponse> = {
|
||||
service: { type: 'string', description: 'Service name' },
|
||||
message: { type: 'string', description: 'Log message' },
|
||||
status: { type: 'string', description: 'Log status/level' },
|
||||
attributes: { type: 'json', description: 'Free-form log attributes' },
|
||||
tags: { type: 'array', description: 'Log tags' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { QueryTimeseriesParams, QueryTimeseriesResponse } from '@/tools/datadog/types'
|
||||
import type {
|
||||
MetricsQuerySeries,
|
||||
QueryTimeseriesParams,
|
||||
QueryTimeseriesResponse,
|
||||
} from '@/tools/datadog/types'
|
||||
import { datadogErrorMessage } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const queryTimeseriesTool: ToolConfig<QueryTimeseriesParams, QueryTimeseriesResponse> = {
|
||||
@@ -68,19 +73,33 @@ export const queryTimeseriesTool: ToolConfig<QueryTimeseriesParams, QueryTimeser
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const message = await datadogErrorMessage(response)
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
series: [],
|
||||
status: 'error',
|
||||
},
|
||||
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
|
||||
error: message,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const series = (data.series || []).map((s: any) => ({
|
||||
|
||||
/**
|
||||
* A failed metrics query still returns 200; Datadog signals it with a non-`ok`
|
||||
* `status` and puts the reason in `error`. Without this the caller gets an empty
|
||||
* series and no indication anything went wrong.
|
||||
*/
|
||||
if (data.status && data.status !== 'ok') {
|
||||
return {
|
||||
success: false,
|
||||
output: { series: [], status: data.status },
|
||||
error: data.error || `Datadog returned query status "${data.status}"`,
|
||||
}
|
||||
}
|
||||
|
||||
const series = (data.series || []).map((s: MetricsQuerySeries) => ({
|
||||
metric: s.metric || s.expression,
|
||||
tags: s.tag_set || [],
|
||||
points: (s.pointlist || []).map((p: [number, number]) => ({
|
||||
@@ -93,7 +112,7 @@ export const queryTimeseriesTool: ToolConfig<QueryTimeseriesParams, QueryTimeser
|
||||
success: true,
|
||||
output: {
|
||||
series,
|
||||
status: data.status || 'ok',
|
||||
status: data.status,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -102,6 +121,24 @@ export const queryTimeseriesTool: ToolConfig<QueryTimeseriesParams, QueryTimeser
|
||||
series: {
|
||||
type: 'array',
|
||||
description: 'Array of timeseries data with metric name, tags, and data points',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
metric: { type: 'string', description: 'Metric name' },
|
||||
tags: { type: 'array', description: 'Tags attached to the series' },
|
||||
points: {
|
||||
type: 'array',
|
||||
description: 'Data points',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
timestamp: { type: 'number', description: 'Point timestamp (Unix seconds)' },
|
||||
value: { type: 'number', description: 'Point value' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import type {
|
||||
DatadogV2Resource,
|
||||
SearchSpansParams,
|
||||
SearchSpansResponse,
|
||||
SpanAttributes,
|
||||
} from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const searchSpansTool: ToolConfig<SearchSpansParams, SearchSpansResponse> = {
|
||||
id: 'datadog_search_spans',
|
||||
name: 'Datadog Search Spans',
|
||||
description: 'Search indexed APM spans using the span query syntax, with cursor pagination.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Span search query (e.g., "service:web* AND @http.status_code:[500 TO 599]"). Defaults to "*"',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Minimum time, ISO-8601, date math, or milliseconds (default: "now-15m")',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum time, ISO-8601, date math, or milliseconds (default: "now")',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort order: "timestamp" for oldest first, "-timestamp" for newest first',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor returned as nextCursor by a previous call',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of spans to return (default: 10, max: 1000)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => datadogApiUrl(params.site, '/api/v2/spans/events/search'),
|
||||
method: 'POST',
|
||||
headers: datadogHeaders,
|
||||
body: (params) => {
|
||||
const filter: Record<string, string> = {}
|
||||
if (params.query) filter.query = params.query
|
||||
if (params.from) filter.from = params.from
|
||||
if (params.to) filter.to = params.to
|
||||
|
||||
const page: Record<string, string | number> = {}
|
||||
if (params.cursor) page.cursor = params.cursor
|
||||
if (params.limit !== undefined) page.limit = params.limit
|
||||
|
||||
const attributes: Record<string, unknown> = {}
|
||||
if (Object.keys(filter).length > 0) attributes.filter = filter
|
||||
if (Object.keys(page).length > 0) attributes.page = page
|
||||
if (params.sort) attributes.sort = params.sort
|
||||
|
||||
return { data: { type: 'search_request', attributes } }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { spans: [] },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
spans: (data.data ?? []).map((span: DatadogV2Resource<SpanAttributes>) => ({
|
||||
id: span.id,
|
||||
type: span.type,
|
||||
attributes: span.attributes ?? {},
|
||||
})),
|
||||
nextCursor: data.meta?.page?.after,
|
||||
elapsed: data.meta?.elapsed,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
spans: {
|
||||
type: 'array',
|
||||
description: 'List of matching spans',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Unique span event ID' },
|
||||
type: { type: 'string', description: 'Resource type (spans)' },
|
||||
attributes: {
|
||||
type: 'object',
|
||||
description: 'Span attributes',
|
||||
properties: {
|
||||
service: { type: 'string', description: 'Service that emitted the span' },
|
||||
resource_name: { type: 'string', description: 'Resource name' },
|
||||
env: { type: 'string', description: 'Environment' },
|
||||
host: { type: 'string', description: 'Host that emitted the span' },
|
||||
type: { type: 'string', description: 'Span type, such as web or db' },
|
||||
trace_id: { type: 'string', description: 'Trace ID' },
|
||||
span_id: { type: 'string', description: 'Span ID' },
|
||||
parent_id: { type: 'string', description: 'Parent span ID' },
|
||||
start_timestamp: { type: 'string', description: 'Span start timestamp' },
|
||||
end_timestamp: { type: 'string', description: 'Span end timestamp' },
|
||||
tags: { type: 'array', description: 'Tags on the span' },
|
||||
custom: { type: 'object', description: 'Custom span data' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for the next page of spans',
|
||||
optional: true,
|
||||
},
|
||||
elapsed: {
|
||||
type: 'number',
|
||||
description: 'Query time in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { SendLogsParams, SendLogsResponse } from '@/tools/datadog/types'
|
||||
import { filterUndefined } from '@sim/utils/object'
|
||||
import type { LogEntry, SendLogsParams, SendLogsResponse } from '@/tools/datadog/types'
|
||||
import { datadogErrorMessage, parseJsonParam } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const sendLogsTool: ToolConfig<SendLogsParams, SendLogsResponse> = {
|
||||
@@ -47,33 +49,31 @@ export const sendLogsTool: ToolConfig<SendLogsParams, SendLogsResponse> = {
|
||||
'DD-API-KEY': params.apiKey,
|
||||
}),
|
||||
body: (params) => {
|
||||
let logs: any[]
|
||||
try {
|
||||
logs = typeof params.logs === 'string' ? JSON.parse(params.logs) : params.logs
|
||||
} catch {
|
||||
throw new Error('Invalid JSON in logs parameter')
|
||||
const logs = parseJsonParam<LogEntry[]>(params.logs, 'logs parameter')
|
||||
if (!Array.isArray(logs) || logs.length === 0) {
|
||||
throw new Error('logs must be a non-empty JSON array of log entries')
|
||||
}
|
||||
|
||||
// Ensure each log entry has the required format
|
||||
return logs.map((log: any) => ({
|
||||
ddsource: log.ddsource || 'custom',
|
||||
ddtags: log.ddtags || '',
|
||||
hostname: log.hostname || '',
|
||||
message: log.message,
|
||||
service: log.service || '',
|
||||
}))
|
||||
/**
|
||||
* Every extra key is preserved: Datadog's log intake accepts arbitrary
|
||||
* additional properties as the log's structured attributes, so rebuilding each
|
||||
* entry from a fixed field list would silently discard them. Empty optional
|
||||
* fields are dropped rather than sent blank, which would otherwise suppress
|
||||
* Datadog's own host inference and write an empty reserved `service` attribute.
|
||||
*/
|
||||
return logs.map((log) => filterUndefined({ ...log, ddsource: log.ddsource || 'custom' }))
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const message = await datadogErrorMessage(response)
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
success: false,
|
||||
},
|
||||
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
|
||||
error: message,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
import type { SubmitMetricsParams, SubmitMetricsResponse } from '@/tools/datadog/types'
|
||||
import { filterUndefined } from '@sim/utils/object'
|
||||
import type {
|
||||
MetricSeries,
|
||||
SubmitMetricsParams,
|
||||
SubmitMetricsResponse,
|
||||
} from '@/tools/datadog/types'
|
||||
import { datadogErrorMessage, parseJsonParam } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Datadog `MetricIntakeType` codes: 0 unspecified, 1 count, 2 rate, 3 gauge.
|
||||
* A metric with no recognized type is submitted without a `type`, letting Datadog
|
||||
* infer it rather than silently mislabeling the series.
|
||||
*/
|
||||
const METRIC_INTAKE_TYPE = {
|
||||
count: 1,
|
||||
rate: 2,
|
||||
gauge: 3,
|
||||
} as const
|
||||
|
||||
export const submitMetricsTool: ToolConfig<SubmitMetricsParams, SubmitMetricsResponse> = {
|
||||
id: 'datadog_submit_metrics',
|
||||
name: 'Datadog Submit Metrics',
|
||||
@@ -14,7 +31,7 @@ export const submitMetricsTool: ToolConfig<SubmitMetricsParams, SubmitMetricsRes
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of metric series to submit. Each series should include metric name, type (gauge/rate/count), points (timestamp/value pairs), and optional tags.',
|
||||
'JSON array of metric series to submit. Each entry needs "metric" and "points" (objects with "timestamp" in POSIX seconds and a numeric "value"); timestamps cannot be more than 10 minutes in the future or 1 hour in the past. Optional per entry: "type" ("count", "rate", or "gauge"; omit to let Datadog infer), "interval" in seconds (required by Datadog for count and rate), "tags", "unit", "sourceTypeName", and "resources".',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
@@ -41,25 +58,23 @@ export const submitMetricsTool: ToolConfig<SubmitMetricsParams, SubmitMetricsRes
|
||||
'DD-API-KEY': params.apiKey,
|
||||
}),
|
||||
body: (params) => {
|
||||
let series: any[]
|
||||
try {
|
||||
series = typeof params.series === 'string' ? JSON.parse(params.series) : params.series
|
||||
} catch {
|
||||
throw new Error('Invalid JSON in series parameter')
|
||||
const series = parseJsonParam<MetricSeries[]>(params.series, 'series parameter')
|
||||
if (!Array.isArray(series) || series.length === 0) {
|
||||
throw new Error('series must be a non-empty JSON array of metric series')
|
||||
}
|
||||
|
||||
// Transform to Datadog API v2 format
|
||||
const formattedSeries = series.map((s: any) => ({
|
||||
metric: s.metric,
|
||||
type: s.type === 'gauge' ? 0 : s.type === 'rate' ? 1 : s.type === 'count' ? 2 : 3,
|
||||
points: s.points.map((p: any) => ({
|
||||
timestamp: p.timestamp,
|
||||
value: p.value,
|
||||
})),
|
||||
tags: s.tags || [],
|
||||
unit: s.unit,
|
||||
resources: s.resources || [{ name: 'host', type: 'host' }],
|
||||
}))
|
||||
const formattedSeries = series.map((s) =>
|
||||
filterUndefined({
|
||||
metric: s.metric,
|
||||
type: METRIC_INTAKE_TYPE[s.type as keyof typeof METRIC_INTAKE_TYPE],
|
||||
points: s.points?.map((p) => ({ timestamp: p.timestamp, value: p.value })),
|
||||
tags: s.tags,
|
||||
unit: s.unit,
|
||||
interval: s.interval,
|
||||
source_type_name: s.sourceTypeName,
|
||||
resources: s.resources,
|
||||
})
|
||||
)
|
||||
|
||||
return { series: formattedSeries }
|
||||
},
|
||||
@@ -67,14 +82,11 @@ export const submitMetricsTool: ToolConfig<SubmitMetricsParams, SubmitMetricsRes
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const message = await datadogErrorMessage(response)
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
success: false,
|
||||
errors: [errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`],
|
||||
},
|
||||
error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`,
|
||||
output: { success: false, errors: [message] },
|
||||
error: message,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import type {
|
||||
TriggerSyntheticsTestsParams,
|
||||
TriggerSyntheticsTestsResponse,
|
||||
} from '@/tools/datadog/types'
|
||||
import {
|
||||
datadogApiUrl,
|
||||
datadogErrorMessage,
|
||||
datadogHeaders,
|
||||
splitCommaList,
|
||||
} from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const triggerSyntheticsTestsTool: ToolConfig<
|
||||
TriggerSyntheticsTestsParams,
|
||||
TriggerSyntheticsTestsResponse
|
||||
> = {
|
||||
id: 'datadog_trigger_synthetics_tests',
|
||||
name: 'Datadog Trigger Synthetic Tests',
|
||||
description: 'Trigger an immediate run of one or more Synthetic tests by public ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
publicIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated public IDs of the Synthetic tests to trigger',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => datadogApiUrl(params.site, '/api/v1/synthetics/tests/trigger'),
|
||||
method: 'POST',
|
||||
headers: datadogHeaders,
|
||||
body: (params) => ({
|
||||
tests: (splitCommaList(params.publicIds) ?? []).map((publicId) => ({
|
||||
public_id: publicId,
|
||||
})),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { triggeredCheckIds: [], results: [], locations: [] },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
batchId: data.batch_id,
|
||||
triggeredCheckIds: data.triggered_check_ids ?? [],
|
||||
results: data.results ?? [],
|
||||
locations: data.locations ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
batchId: {
|
||||
type: 'string',
|
||||
description: 'Public ID of the triggered batch',
|
||||
optional: true,
|
||||
},
|
||||
triggeredCheckIds: {
|
||||
type: 'array',
|
||||
description: 'Public IDs of the triggered Synthetic tests',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Information about each triggered test run',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
public_id: { type: 'string', description: 'Public ID of the test' },
|
||||
result_id: { type: 'string', description: 'ID of the run result' },
|
||||
location: { type: 'number', description: 'Location ID of the run' },
|
||||
device: { type: 'string', description: 'Device ID used for browser tests' },
|
||||
},
|
||||
},
|
||||
},
|
||||
locations: {
|
||||
type: 'array',
|
||||
description: 'Locations the tests were triggered from',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'number', description: 'Location ID' },
|
||||
name: { type: 'string', description: 'Location name' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
+747
-318
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
||||
import type { UnmuteMonitorParams, UnmuteMonitorResponse } from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const unmuteMonitorTool: ToolConfig<UnmuteMonitorParams, UnmuteMonitorResponse> = {
|
||||
id: 'datadog_unmute_monitor',
|
||||
name: 'Datadog Unmute Monitor',
|
||||
description:
|
||||
'Unmute a monitor so it resumes sending notifications. Reverses Mute Monitor, either for one scope or for every scope at once.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
monitorId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the monitor to unmute (e.g., "12345678")',
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Scope to unmute (e.g., "host:myhost"). Leave blank to unmute the monitor itself rather than a single scope.',
|
||||
},
|
||||
allScopes: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Clear the mute settings for every scope on this monitor',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
datadogApiUrl(params.site, `/api/v1/monitor/${encodeURIComponent(params.monitorId)}/unmute`),
|
||||
method: 'POST',
|
||||
headers: datadogHeaders,
|
||||
body: (params) => {
|
||||
const body: { scope?: string; all_scopes?: boolean } = {}
|
||||
if (params.scope) body.scope = params.scope
|
||||
if (params.allScopes !== undefined) body.all_scopes = params.allScopes
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { success: false },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => ({}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
monitorId: data.id,
|
||||
name: data.name,
|
||||
overallState: data.overall_state,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the monitor was successfully unmuted',
|
||||
},
|
||||
monitorId: {
|
||||
type: 'number',
|
||||
description: 'ID of the unmuted monitor',
|
||||
optional: true,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name of the unmuted monitor',
|
||||
optional: true,
|
||||
},
|
||||
overallState: {
|
||||
type: 'string',
|
||||
description: 'Monitor state after unmuting',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import type { UpdateIncidentParams, UpdateIncidentResponse } from '@/tools/datadog/types'
|
||||
import {
|
||||
datadogApiUrl,
|
||||
datadogErrorMessage,
|
||||
datadogHeaders,
|
||||
parseJsonParam,
|
||||
splitCommaList,
|
||||
} from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateIncidentTool: ToolConfig<UpdateIncidentParams, UpdateIncidentResponse> = {
|
||||
id: 'datadog_update_incident',
|
||||
name: 'Datadog Update Incident',
|
||||
description:
|
||||
'Partially update an existing incident. Requires the Incident Management `incident_write` permission; the Incidents API is in public beta.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
incidentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the incident to update',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New title for the incident',
|
||||
},
|
||||
severity: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Incident severity: UNKNOWN, SEV-0, SEV-1, SEV-2, SEV-3, SEV-4, or SEV-5',
|
||||
},
|
||||
customerImpacted: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the incident caused customer impact',
|
||||
},
|
||||
customerImpactScope: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Summary of the customer impact',
|
||||
},
|
||||
customerImpactStart: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO-8601 timestamp when customers began being impacted',
|
||||
},
|
||||
customerImpactEnd: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO-8601 timestamp when customers were no longer impacted',
|
||||
},
|
||||
detected: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ISO-8601 timestamp when the incident was detected',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON object of user-defined incident fields to update, e.g. {"state": {"type": "dropdown", "value": "resolved"}}',
|
||||
},
|
||||
notificationHandles: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated handles to notify about the update',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
datadogApiUrl(params.site, `/api/v2/incidents/${encodeURIComponent(params.incidentId)}`),
|
||||
method: 'PATCH',
|
||||
headers: datadogHeaders,
|
||||
body: (params) => {
|
||||
const fields =
|
||||
parseJsonParam<Record<string, unknown>>(params.fields, 'fields parameter') ?? {}
|
||||
if (params.severity) {
|
||||
fields.severity = { type: 'dropdown', value: params.severity }
|
||||
}
|
||||
|
||||
/**
|
||||
* Only non-empty values are sent. An empty string is how an untouched input
|
||||
* arrives, and Datadog would take it literally — blanking the stored title or
|
||||
* rejecting the request as an invalid `date-time`.
|
||||
*/
|
||||
const attributes: Record<string, unknown> = {}
|
||||
if (params.title) attributes.title = params.title
|
||||
if (params.customerImpacted !== undefined)
|
||||
attributes.customer_impacted = params.customerImpacted
|
||||
if (params.customerImpactScope) attributes.customer_impact_scope = params.customerImpactScope
|
||||
if (params.customerImpactStart) attributes.customer_impact_start = params.customerImpactStart
|
||||
if (params.customerImpactEnd) attributes.customer_impact_end = params.customerImpactEnd
|
||||
if (params.detected) attributes.detected = params.detected
|
||||
if (Object.keys(fields).length > 0) attributes.fields = fields
|
||||
|
||||
const handles = splitCommaList(params.notificationHandles)
|
||||
if (handles) {
|
||||
attributes.notification_handles = handles.map((handle) => ({ handle }))
|
||||
}
|
||||
|
||||
return { data: { type: 'incidents', id: params.incidentId, attributes } }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { incident: { id: '', attributes: {} } },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
incident: {
|
||||
id: data.data?.id,
|
||||
type: data.data?.type,
|
||||
attributes: data.data?.attributes ?? {},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
incident: {
|
||||
type: 'object',
|
||||
description: 'The updated incident',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Incident UUID' },
|
||||
type: { type: 'string', description: 'Resource type (incidents)' },
|
||||
attributes: {
|
||||
type: 'object',
|
||||
description: 'Incident attributes',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'Incident title' },
|
||||
state: { type: 'string', description: 'Incident state' },
|
||||
severity: { type: 'string', description: 'Incident severity' },
|
||||
modified: { type: 'string', description: 'Last modification timestamp' },
|
||||
resolved: { type: 'string', description: 'Resolution timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import type {
|
||||
UpdateSecuritySignalAssigneeParams,
|
||||
UpdateSecuritySignalAssigneeResponse,
|
||||
} from '@/tools/datadog/types'
|
||||
import {
|
||||
datadogApiUrl,
|
||||
datadogErrorMessage,
|
||||
datadogHeaders,
|
||||
mapSignalTriageData,
|
||||
} from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateSecuritySignalAssigneeTool: ToolConfig<
|
||||
UpdateSecuritySignalAssigneeParams,
|
||||
UpdateSecuritySignalAssigneeResponse
|
||||
> = {
|
||||
id: 'datadog_update_security_signal_assignee',
|
||||
name: 'Datadog Assign Security Signal',
|
||||
description:
|
||||
'Assign a Cloud SIEM security signal to a Datadog user by UUID. Requires the `security_monitoring_signals_write` permission.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
signalId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the security signal',
|
||||
},
|
||||
assigneeUuid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'UUID of the Datadog user to assign the signal to (e.g., "773b045d-ccf8-4808-bd3b-955ef6a8c940")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v2/security_monitoring/signals/${encodeURIComponent(params.signalId)}/assignee`
|
||||
),
|
||||
method: 'PATCH',
|
||||
headers: datadogHeaders,
|
||||
body: (params) => ({
|
||||
data: { attributes: { assignee: { uuid: params.assigneeUuid } } },
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { signal: {} },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { signal: mapSignalTriageData(await response.json()) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
signal: {
|
||||
type: 'object',
|
||||
description: 'The updated signal triage data',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Signal ID' },
|
||||
type: { type: 'string', description: 'Resource type of the signal' },
|
||||
state: { type: 'string', description: 'Current triage state' },
|
||||
assignee: { type: 'object', description: 'User the signal is assigned to' },
|
||||
incidentIds: { type: 'array', description: 'IDs of incidents linked to the signal' },
|
||||
archiveReason: { type: 'string', description: 'Archive reason, when archived' },
|
||||
archiveComment: { type: 'string', description: 'Archive comment, when archived' },
|
||||
stateUpdateTimestamp: {
|
||||
type: 'number',
|
||||
description: 'Timestamp of the last state update',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type {
|
||||
UpdateSecuritySignalStateParams,
|
||||
UpdateSecuritySignalStateResponse,
|
||||
} from '@/tools/datadog/types'
|
||||
import {
|
||||
datadogApiUrl,
|
||||
datadogErrorMessage,
|
||||
datadogHeaders,
|
||||
mapSignalTriageData,
|
||||
} from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateSecuritySignalStateTool: ToolConfig<
|
||||
UpdateSecuritySignalStateParams,
|
||||
UpdateSecuritySignalStateResponse
|
||||
> = {
|
||||
id: 'datadog_update_security_signal_state',
|
||||
name: 'Datadog Update Security Signal State',
|
||||
description:
|
||||
'Change the triage state of a Cloud SIEM security signal to open, under_review, or archived. Requires the `security_monitoring_signals_write` permission.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
signalId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the security signal',
|
||||
},
|
||||
state: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New triage state: "open", "under_review", or "archived"',
|
||||
},
|
||||
archiveReason: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Reason when archiving: none, false_positive, testing_or_maintenance, remediated, investigated_case_opened, true_positive_benign, true_positive_malicious, or other',
|
||||
},
|
||||
archiveComment: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comment explaining why the signal was archived',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v2/security_monitoring/signals/${encodeURIComponent(params.signalId)}/state`
|
||||
),
|
||||
method: 'PATCH',
|
||||
headers: datadogHeaders,
|
||||
body: (params) => {
|
||||
const attributes: Record<string, unknown> = { state: params.state }
|
||||
if (params.archiveReason) attributes.archive_reason = params.archiveReason
|
||||
if (params.archiveComment) attributes.archive_comment = params.archiveComment
|
||||
return { data: { attributes } }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { signal: {} },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { signal: mapSignalTriageData(await response.json()) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
signal: {
|
||||
type: 'object',
|
||||
description: 'The updated signal triage data',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Signal ID' },
|
||||
type: { type: 'string', description: 'Resource type of the signal' },
|
||||
state: { type: 'string', description: 'Current triage state' },
|
||||
assignee: { type: 'object', description: 'User the signal is assigned to' },
|
||||
incidentIds: { type: 'array', description: 'IDs of incidents linked to the signal' },
|
||||
archiveReason: { type: 'string', description: 'Archive reason, when archived' },
|
||||
archiveComment: { type: 'string', description: 'Archive comment, when archived' },
|
||||
stateUpdateTimestamp: {
|
||||
type: 'number',
|
||||
description: 'Timestamp of the last state update',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import type { UpdateSloParams, UpdateSloResponse } from '@/tools/datadog/types'
|
||||
import {
|
||||
datadogApiUrl,
|
||||
datadogErrorMessage,
|
||||
datadogHeaders,
|
||||
mergeSloUpdatePayload,
|
||||
} from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateSloTool: ToolConfig<UpdateSloParams, UpdateSloResponse> = {
|
||||
id: 'datadog_update_slo',
|
||||
name: 'Datadog Update SLO',
|
||||
description:
|
||||
'Update a service level objective. Reads the current SLO first and applies only the fields you supply, so anything left blank keeps its stored value.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
sloId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the service level objective to update',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New name for the SLO. Leave blank to keep the current name.',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'SLO type: "metric" or "monitor". Leave blank to keep the current type. Changing type requires supplying the matching query or monitorIds.',
|
||||
},
|
||||
thresholds: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of thresholds replacing the stored ones, e.g. [{"timeframe": "30d", "target": 99.9, "warning": 99.95}]. Leave blank to keep the current thresholds.',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Description of the SLO',
|
||||
},
|
||||
tags: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated tags (e.g., "env:prod,team:core")',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'For metric SLOs, JSON with numerator and denominator, e.g. {"numerator": "sum:requests{status:ok}.as_count()", "denominator": "sum:requests{*}.as_count()"}',
|
||||
},
|
||||
monitorIds: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'For monitor SLOs, comma-separated monitor IDs (e.g., "123,456")',
|
||||
},
|
||||
groups: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated monitor groups (e.g., "env:prod,role:mysql")',
|
||||
},
|
||||
targetThreshold: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Primary target threshold (e.g., 99.9)',
|
||||
},
|
||||
warningThreshold: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Primary warning threshold, must be greater than the target (e.g., 99.95)',
|
||||
},
|
||||
timeframe: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Primary timeframe: "7d", "30d", or "90d"',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => datadogApiUrl(params.site, `/api/v1/slo/${encodeURIComponent(params.sloId)}`),
|
||||
method: 'PUT',
|
||||
headers: datadogHeaders,
|
||||
},
|
||||
|
||||
/**
|
||||
* Datadog's SLO update is a full replacement, so the stored SLO is read first and
|
||||
* the supplied edits are overlaid onto it. Sending only the filled-in fields would
|
||||
* erase every field the caller left blank.
|
||||
*/
|
||||
directExecution: async (params, signal) => {
|
||||
const url = datadogApiUrl(params.site, `/api/v1/slo/${encodeURIComponent(params.sloId)}`)
|
||||
const headers = datadogHeaders(params)
|
||||
|
||||
const existingResponse = await fetch(url, { method: 'GET', headers, signal })
|
||||
if (!existingResponse.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { slo: { id: '', name: '', type: '' } },
|
||||
error: `Could not load SLO ${params.sloId} before updating it: ${await datadogErrorMessage(existingResponse)}`,
|
||||
}
|
||||
}
|
||||
|
||||
const existing = await existingResponse.json()
|
||||
const stored = existing.data
|
||||
if (!stored || typeof stored !== 'object') {
|
||||
return {
|
||||
success: false,
|
||||
output: { slo: { id: '', name: '', type: '' } },
|
||||
error: `Datadog returned no SLO for id ${params.sloId}`,
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(mergeSloUpdatePayload(stored, params)),
|
||||
signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { slo: { id: '', name: '', type: '' } },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { slo: data.data?.[0] ?? { id: '', name: '', type: '' } },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
slo: {
|
||||
type: 'object',
|
||||
description: 'The updated service level objective',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'SLO ID' },
|
||||
name: { type: 'string', description: 'SLO name' },
|
||||
type: { type: 'string', description: 'SLO type' },
|
||||
description: { type: 'string', description: 'SLO description' },
|
||||
tags: { type: 'array', description: 'SLO tags' },
|
||||
thresholds: { type: 'array', description: 'Timeframe targets and warnings' },
|
||||
modified_at: { type: 'number', description: 'Modification timestamp (Unix seconds)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type {
|
||||
UpdateSyntheticsStatusParams,
|
||||
UpdateSyntheticsStatusResponse,
|
||||
} from '@/tools/datadog/types'
|
||||
import { datadogApiUrl, datadogErrorMessage, datadogHeaders } from '@/tools/datadog/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateSyntheticsStatusTool: ToolConfig<
|
||||
UpdateSyntheticsStatusParams,
|
||||
UpdateSyntheticsStatusResponse
|
||||
> = {
|
||||
id: 'datadog_update_synthetics_status',
|
||||
name: 'Datadog Pause Or Start Synthetic Test',
|
||||
description: 'Pause or resume a Synthetic test by setting its status to "paused" or "live".',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
publicId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The public ID of the Synthetic test to update',
|
||||
},
|
||||
newStatus: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New status: "live" to start the test or "paused" to pause it',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog API key',
|
||||
},
|
||||
applicationKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog Application key',
|
||||
},
|
||||
site: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Datadog site/region (default: datadoghq.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
datadogApiUrl(
|
||||
params.site,
|
||||
`/api/v1/synthetics/tests/${encodeURIComponent(params.publicId)}/status`
|
||||
),
|
||||
method: 'PUT',
|
||||
headers: datadogHeaders,
|
||||
body: (params) => ({ new_status: params.newStatus }),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const status = params?.newStatus
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
output: { success: false, status },
|
||||
error: await datadogErrorMessage(response),
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await response.json().catch(() => false)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: updated === true,
|
||||
status,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether Datadog reported the status update as successful',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'The status that was requested: live or paused',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import type {
|
||||
CreateSloParams,
|
||||
DatadogSite,
|
||||
SecuritySignalTriageData,
|
||||
UpdateSloParams,
|
||||
} from '@/tools/datadog/types'
|
||||
|
||||
/**
|
||||
* Builds a fully-qualified Datadog API URL for the caller's site/region.
|
||||
* Datadog serves each region from its own host (`datadoghq.com`, `datadoghq.eu`,
|
||||
* `ddog-gov.com`, ...), so every request must be built from the configured site.
|
||||
*/
|
||||
export function datadogApiUrl(site: DatadogSite | undefined, path: string): string {
|
||||
return `https://api.${site || 'datadoghq.com'}${path}`
|
||||
}
|
||||
|
||||
/** Standard Datadog authentication headers for API + application key auth. */
|
||||
export function datadogHeaders(params: {
|
||||
apiKey: string
|
||||
applicationKey: string
|
||||
}): Record<string, string> {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'DD-API-KEY': params.apiKey,
|
||||
'DD-APPLICATION-KEY': params.applicationKey,
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads one Datadog error entry, which is a plain string (v1) or a JSON:API object (v2). */
|
||||
function errorEntryMessage(entry: unknown): string | null {
|
||||
if (typeof entry === 'string') return entry
|
||||
if (entry && typeof entry === 'object') {
|
||||
const record = entry as { detail?: unknown; title?: unknown }
|
||||
if (typeof record.detail === 'string') return record.detail
|
||||
if (typeof record.title === 'string') return record.title
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a human-readable message from a failed Datadog response.
|
||||
*
|
||||
* Datadog returns `{ errors: ... }` in three shapes: an array of plain strings (v1),
|
||||
* an array of JSON:API error objects carrying `detail`/`title` (v2), and — on the SLO
|
||||
* delete conflict — a dictionary keyed by resource ID whose values are the reasons.
|
||||
*/
|
||||
export async function datadogErrorMessage(response: Response): Promise<string> {
|
||||
const fallback = `HTTP ${response.status}: ${response.statusText}`
|
||||
const body = await response.json().catch(() => null)
|
||||
const errors = (body as { errors?: unknown })?.errors
|
||||
|
||||
const entries = Array.isArray(errors)
|
||||
? errors
|
||||
: errors && typeof errors === 'object'
|
||||
? Object.values(errors as Record<string, unknown>)
|
||||
: []
|
||||
|
||||
const messages = entries
|
||||
.map(errorEntryMessage)
|
||||
.filter((message): message is string => Boolean(message))
|
||||
|
||||
return messages.length > 0 ? messages.join('; ') : fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a comma-separated user input into a trimmed, non-empty list.
|
||||
*
|
||||
* Accepts `unknown` because a `<Block.output>` reference can resolve to a non-string:
|
||||
* a monitor ID read from `get_monitor` arrives as a number, and an LLM tool call can
|
||||
* pass an array. Calling `.split` on those would throw before the request is built.
|
||||
*/
|
||||
export function splitCommaList(value: unknown): string[] | undefined {
|
||||
if (value === undefined || value === null || value === '') return undefined
|
||||
const items = (Array.isArray(value) ? value : String(value).split(','))
|
||||
.map((item) => String(item).trim())
|
||||
.filter((item) => item.length > 0)
|
||||
return items.length > 0 ? items : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a JSON param, throwing a descriptive error when it is malformed.
|
||||
* Block inputs arrive as strings, but an upstream block reference can resolve to
|
||||
* an already-parsed object, so both shapes are accepted.
|
||||
*/
|
||||
export function parseJsonParam<T>(value: unknown, fieldName: string): T | undefined {
|
||||
if (value === undefined || value === null || value === '') return undefined
|
||||
if (typeof value !== 'string') return value as T
|
||||
try {
|
||||
return JSON.parse(value) as T
|
||||
} catch {
|
||||
throw new Error(`${fieldName} must be valid JSON`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a comma-separated monitor ID list into the integers Datadog expects.
|
||||
* `Number('abc')` yields `NaN`, which `JSON.stringify` writes as `null` and Datadog
|
||||
* rejects with a message that names nothing the user typed, so bad input is rejected here.
|
||||
*/
|
||||
export function parseMonitorIds(value: unknown): number[] | undefined {
|
||||
const items = splitCommaList(value)
|
||||
if (!items) return undefined
|
||||
return items.map((id) => {
|
||||
const parsed = Number(id)
|
||||
if (!Number.isInteger(parsed)) {
|
||||
throw new Error(`monitorIds must be a comma-separated list of whole numbers (got "${id}")`)
|
||||
}
|
||||
return parsed
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the `ServiceLevelObjective` request body for SLO creation.
|
||||
*/
|
||||
export function buildSloPayload(params: CreateSloParams): Record<string, unknown> {
|
||||
const thresholds = parseJsonParam<unknown[]>(params.thresholds, 'thresholds parameter')
|
||||
if (!Array.isArray(thresholds) || thresholds.length === 0) {
|
||||
throw new Error('thresholds must be a non-empty JSON array')
|
||||
}
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
thresholds,
|
||||
}
|
||||
|
||||
if (params.description) body.description = params.description
|
||||
|
||||
const tags = splitCommaList(params.tags)
|
||||
if (tags) body.tags = tags
|
||||
|
||||
const query = parseJsonParam<Record<string, unknown>>(params.query, 'query parameter')
|
||||
if (query) body.query = query
|
||||
|
||||
const monitorIds = parseMonitorIds(params.monitorIds)
|
||||
if (monitorIds) body.monitor_ids = monitorIds
|
||||
|
||||
const groups = splitCommaList(params.groups)
|
||||
if (groups) body.groups = groups
|
||||
|
||||
if (params.targetThreshold !== undefined) body.target_threshold = params.targetThreshold
|
||||
if (params.warningThreshold !== undefined) body.warning_threshold = params.warningThreshold
|
||||
if (params.timeframe) body.timeframe = params.timeframe
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
/**
|
||||
* Fields Datadog computes and rejects or ignores on an SLO update request.
|
||||
* They must be stripped from a stored SLO before it is replayed into `PUT /api/v1/slo/{slo_id}`.
|
||||
*/
|
||||
const SLO_READ_ONLY_FIELDS = ['id', 'created_at', 'modified_at', 'creator', 'monitor_tags'] as const
|
||||
|
||||
/**
|
||||
* Merges user-supplied SLO edits onto the SLO Datadog currently stores.
|
||||
*
|
||||
* `PUT /api/v1/slo/{slo_id}` is a full replacement, not a patch, so a request built
|
||||
* only from the fields the user filled in silently erases every field they left
|
||||
* blank. Reading the stored SLO first and overlaying only the supplied fields keeps
|
||||
* an edit to one field from destroying the rest.
|
||||
*/
|
||||
export function mergeSloUpdatePayload(
|
||||
stored: Record<string, unknown>,
|
||||
params: UpdateSloParams
|
||||
): Record<string, unknown> {
|
||||
const body: Record<string, unknown> = { ...stored }
|
||||
for (const field of SLO_READ_ONLY_FIELDS) delete body[field]
|
||||
|
||||
const thresholds = parseJsonParam<unknown[]>(params.thresholds, 'thresholds parameter')
|
||||
if (thresholds !== undefined) {
|
||||
if (!Array.isArray(thresholds) || thresholds.length === 0) {
|
||||
throw new Error('thresholds must be a non-empty JSON array')
|
||||
}
|
||||
body.thresholds = thresholds
|
||||
}
|
||||
|
||||
if (params.name) body.name = params.name
|
||||
if (params.type) body.type = params.type
|
||||
if (params.description !== undefined && params.description !== '') {
|
||||
body.description = params.description
|
||||
}
|
||||
|
||||
const tags = splitCommaList(params.tags)
|
||||
if (tags) body.tags = tags
|
||||
|
||||
const query = parseJsonParam<Record<string, unknown>>(params.query, 'query parameter')
|
||||
if (query) body.query = query
|
||||
|
||||
const monitorIds = parseMonitorIds(params.monitorIds)
|
||||
if (monitorIds) body.monitor_ids = monitorIds
|
||||
|
||||
const groups = splitCommaList(params.groups)
|
||||
if (groups) body.groups = groups
|
||||
|
||||
if (params.targetThreshold !== undefined) body.target_threshold = params.targetThreshold
|
||||
if (params.warningThreshold !== undefined) body.warning_threshold = params.warningThreshold
|
||||
if (params.timeframe) body.timeframe = params.timeframe
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
/**
|
||||
* Projects a security signal triage response (`PATCH .../state` and `.../assignee`
|
||||
* both return `SecurityMonitoringSignalTriageUpdateResponse`) onto a flat shape.
|
||||
*/
|
||||
export function mapSignalTriageData(data: unknown): SecuritySignalTriageData {
|
||||
const payload =
|
||||
(
|
||||
data as {
|
||||
data?: {
|
||||
id?: string
|
||||
type?: string
|
||||
attributes?: {
|
||||
state?: string
|
||||
assignee?: SecuritySignalTriageData['assignee']
|
||||
incident_ids?: number[]
|
||||
archive_reason?: string
|
||||
archive_comment?: string
|
||||
state_update_timestamp?: number
|
||||
}
|
||||
}
|
||||
}
|
||||
)?.data ?? {}
|
||||
const attributes = payload.attributes ?? {}
|
||||
return {
|
||||
id: payload.id,
|
||||
type: payload.type,
|
||||
state: attributes.state,
|
||||
assignee: attributes.assignee,
|
||||
incidentIds: attributes.incident_ids,
|
||||
archiveReason: attributes.archive_reason,
|
||||
archiveComment: attributes.archive_comment,
|
||||
stateUpdateTimestamp: attributes.state_update_timestamp,
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -805,18 +805,47 @@ import {
|
||||
databricksRunJobTool,
|
||||
} from '@/tools/databricks'
|
||||
import {
|
||||
datadogAddIncidentTodoTool,
|
||||
datadogCancelDowntimeTool,
|
||||
datadogCreateDashboardTool,
|
||||
datadogCreateDowntimeTool,
|
||||
datadogCreateEventTool,
|
||||
datadogCreateIncidentTool,
|
||||
datadogCreateMonitorTool,
|
||||
datadogCreateSloTool,
|
||||
datadogDeleteDashboardTool,
|
||||
datadogDeleteSloTool,
|
||||
datadogGetBrowserSyntheticsResultsTool,
|
||||
datadogGetDashboardTool,
|
||||
datadogGetIncidentTool,
|
||||
datadogGetMonitorTool,
|
||||
datadogGetSecuritySignalTool,
|
||||
datadogGetSloHistoryTool,
|
||||
datadogGetSloTool,
|
||||
datadogGetSyntheticsResultsTool,
|
||||
datadogGetSyntheticsTestTool,
|
||||
datadogListDashboardsTool,
|
||||
datadogListDowntimesTool,
|
||||
datadogListIncidentsTool,
|
||||
datadogListMonitorsTool,
|
||||
datadogListSecurityRulesTool,
|
||||
datadogListSecuritySignalsTool,
|
||||
datadogListServicesTool,
|
||||
datadogListSlosTool,
|
||||
datadogListSyntheticsTestsTool,
|
||||
datadogMuteMonitorTool,
|
||||
datadogQueryLogsTool,
|
||||
datadogQueryTimeseriesTool,
|
||||
datadogSearchSpansTool,
|
||||
datadogSendLogsTool,
|
||||
datadogSubmitMetricsTool,
|
||||
datadogTriggerSyntheticsTestsTool,
|
||||
datadogUnmuteMonitorTool,
|
||||
datadogUpdateIncidentTool,
|
||||
datadogUpdateSecuritySignalAssigneeTool,
|
||||
datadogUpdateSecuritySignalStateTool,
|
||||
datadogUpdateSloTool,
|
||||
datadogUpdateSyntheticsStatusTool,
|
||||
} from '@/tools/datadog'
|
||||
import {
|
||||
datagmaEnrichCompanyTool,
|
||||
@@ -8661,6 +8690,35 @@ export const tools: Record<string, ToolConfig> = {
|
||||
datadog_create_downtime: datadogCreateDowntimeTool,
|
||||
datadog_list_downtimes: datadogListDowntimesTool,
|
||||
datadog_cancel_downtime: datadogCancelDowntimeTool,
|
||||
datadog_list_incidents: datadogListIncidentsTool,
|
||||
datadog_get_incident: datadogGetIncidentTool,
|
||||
datadog_create_incident: datadogCreateIncidentTool,
|
||||
datadog_update_incident: datadogUpdateIncidentTool,
|
||||
datadog_add_incident_todo: datadogAddIncidentTodoTool,
|
||||
datadog_list_slos: datadogListSlosTool,
|
||||
datadog_get_slo: datadogGetSloTool,
|
||||
datadog_create_slo: datadogCreateSloTool,
|
||||
datadog_unmute_monitor: datadogUnmuteMonitorTool,
|
||||
datadog_update_slo: datadogUpdateSloTool,
|
||||
datadog_delete_slo: datadogDeleteSloTool,
|
||||
datadog_get_slo_history: datadogGetSloHistoryTool,
|
||||
datadog_list_dashboards: datadogListDashboardsTool,
|
||||
datadog_get_dashboard: datadogGetDashboardTool,
|
||||
datadog_create_dashboard: datadogCreateDashboardTool,
|
||||
datadog_delete_dashboard: datadogDeleteDashboardTool,
|
||||
datadog_list_synthetics_tests: datadogListSyntheticsTestsTool,
|
||||
datadog_get_synthetics_test: datadogGetSyntheticsTestTool,
|
||||
datadog_get_synthetics_results: datadogGetSyntheticsResultsTool,
|
||||
datadog_get_browser_synthetics_results: datadogGetBrowserSyntheticsResultsTool,
|
||||
datadog_trigger_synthetics_tests: datadogTriggerSyntheticsTestsTool,
|
||||
datadog_update_synthetics_status: datadogUpdateSyntheticsStatusTool,
|
||||
datadog_list_security_signals: datadogListSecuritySignalsTool,
|
||||
datadog_get_security_signal: datadogGetSecuritySignalTool,
|
||||
datadog_update_security_signal_state: datadogUpdateSecuritySignalStateTool,
|
||||
datadog_update_security_signal_assignee: datadogUpdateSecuritySignalAssigneeTool,
|
||||
datadog_list_security_rules: datadogListSecurityRulesTool,
|
||||
datadog_search_spans: datadogSearchSpansTool,
|
||||
datadog_list_services: datadogListServicesTool,
|
||||
image_generate: imageGenerateTool,
|
||||
openai_image: openAIImageTool,
|
||||
microsoft_ad_list_users: microsoftAdListUsersTool,
|
||||
|
||||
Reference in New Issue
Block a user