mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-31 03:47:12 +08:00
feat(dynatrace): add the Dynatrace integration (#6393)
* feat(dynatrace): add the Dynatrace integration
Adds a Dynatrace block backed by 22 Environment API v2 tools, covering the
surfaces an observability workflow actually reaches for:
- Problems: list, get, close, list comments, add comment
- Metrics: query data points, list and get descriptors, ingest line protocol
- Entities: list, get, list entity types
- Events: list, get, ingest
- Logs: search, ingest
- SLOs: list, get
- Application Security: list and get security problems
- Audit log: read
Every request path, query parameter, and response mapping is taken from the
published Dynatrace API reference — no inferred fields. Auth is an access
token sent as `Authorization: Api-Token ...` against a user-supplied
environment URL, so SaaS, Managed, and environment ActiveGate all work.
Two details worth knowing:
`ingest_event` exposes Dynatrace's event timeout as `eventTimeout`, not
`timeout`. The tool transport reserves `params.timeout` for the HTTP request
deadline, so the obvious name would have silently retargeted the wrong knob.
`get_metric` encodes its path segment with `encodeDynatracePathSegment`
rather than `encodeURIComponent`, which leaves the `:` separators in metric
keys and transformation operators intact, matching the docs' own examples.
* fix(dynatrace): close the gaps a validation pass turned up
Three real defects and one usability gap, all found by auditing the tools
against the Dynatrace API reference a second time.
`ingest_logs` double-encoded its payload. `logs` is a `json` param, and a
`json` param arrives as a *string* whenever it comes from a long-input field
or an LLM tool call — only a block-to-block reference hands over a parsed
value. `JSON.stringify` on that string produced `"[{...}]"`, so Dynatrace
received a quoted string where it expected an array. The block hid this in
the UI path by pre-parsing, but the parse lived in `tools.config.params` and
*threw* on malformed input, and it never covered the direct tool-call path at
all. Both tools now normalize through the shared `parseJsonParam`, so the
tool is correct regardless of who calls it, and the block just forwards the
raw value. `ingest_event.properties` had the identical bug.
Path identifiers were not trimmed. A problem or entity ID pasted with a
trailing newline became `%0A` in the URL and 404'd with nothing to suggest
whitespace was the cause.
Errors dropped the part that matters. Dynatrace's ErrorEnvelope carries
`constraintViolations[]`, which names the offending selector or parameter;
the generic `nested-error-object` extractor returns only `error.message`
("Constraints violated."), and which extractor won was left to fallback
order. Adds a `dynatrace-errors` extractor that folds the violations into the
message and pins it on all 22 tools. It sits after `nested-error-object` in
the chain, which already matches this shape, so no other service's error
handling changes.
Adds 21 tests covering URL construction for SaaS/Managed/ActiveGate, cursor
pagination dropping sibling filters, identifier trimming, metric-key colon
preservation, both JSON-param paths, the `eventTimeout` -> `timeout` mapping,
EntityStub flattening, the audit log's dotted `dt.settings.*` keys, and the
204/200 split on log ingestion.
* docs(dynatrace): add the page intro, and pin every response key in tests
Adds a MANUAL-CONTENT:intro block to the generated integration page covering
what the block reaches, how to get an environment URL and a scoped token for
SaaS vs Managed, how selectors work, and how cursor pagination behaves.
Verified it survives `generate-docs.ts` byte-identically.
Also closes the last silent-failure gap the validation pass left open. A
wrong top-level response key does not throw — it maps to an empty array and
reads as "no results", which is indistinguishable from a genuinely empty
environment. Dynatrace is unusually easy to get wrong here: the SLO list
returns `slo` (singular) and the metric query returns `result` (singular).
Adds a table-driven test asserting the documented key for all ten list
endpoints plus the scalar keys of the ingest and single-entity responses.
Confirmed it bites by flipping `data.slo` to `data.slos` and watching only
that row fail.
* chore(dynatrace): type the shared param map as unknown
Review follow-up. `Record<string, any>` in the block's params builder dropped
compile-time checking from every operation's shared params; `unknown` is
enough here since the values flow straight into the tool param maps. Matches
.claude/rules/sim-typescript.md, which sibling blocks (Datadog, Grafana)
still violate.
* fix(dynatrace): stop three silent failures found in a final read-through
All three turn a failed call into something that looks like a successful
empty one, which is the worst shape for an observability integration — you
cannot tell "nothing is wrong" from "the call did not work".
`readJsonBody` swallowed any unparseable body and returned `{}`. A gateway
HTML page, a captive-portal interstitial, or a truncated payload therefore
mapped every field to null and read as "no problems found". Only genuinely
empty bodies are tolerated now (201 from add-comment, 204 from log ingest);
anything else that will not parse raises with a truncated preview.
`ingest_logs` sent `[]` when the payload was missing or empty. Dynatrace
answers 204 to that, so the tool reported `accepted: true` for a call that
shipped no logs. It now fails loudly instead.
`encodeDynatracePathSegment` percent-encoded the whole metric key and then
regex-unescaped `%3A` back to `:`. Same output, but it undoes the encoder's
work and hides the intent. Colons are structural in a metric key, so it now
splits on them, encodes each part, and rejoins — which says that directly.
Each fix has a test, and each test was confirmed to fail in isolation with
only its own fix reverted.
This commit is contained in:
@@ -6048,6 +6048,37 @@ export function RDSIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DynatraceIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 89 90' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fill='#1496FF'
|
||||
d='M29.9672771,7.52412666 C28.8124385,13.6191079 27.4009692,22.6653432 26.6310768,31.8398939 C25.2837652,48.0076335 26.1178152,58.8502844 26.1178152,58.8502844 L3.34183275,80.4714283 C3.34183275,80.4714283 1.60957493,68.3456235 0.711367168,54.680034 C0.198105592,46.211218 0.00563250016,38.7689252 0.00563250016,34.2778864 C0.00563250016,34.0212556 0.133947894,33.7646248 0.133947894,33.507994 C0.133947894,33.1872055 0.518894077,30.1717937 3.47014814,27.3488551 C6.678033,24.2692856 30.3522232,5.72771114 29.9672771,7.52412666 Z'
|
||||
/>
|
||||
<path
|
||||
fill='#1284EA'
|
||||
d='M29.9672771,7.52412666 C28.8124385,13.6191079 27.4009692,22.6653432 26.6310768,31.8398939 C26.6310768,31.8398939 1.41710184,28.8244821 0.00563250016,34.9194633 C0.00563250016,34.5986748 0.45473638,30.8775284 3.40599045,28.0545897 C6.6138753,24.9750203 30.3522232,5.72771114 29.9672771,7.52412666 L29.9672771,7.52412666 Z'
|
||||
/>
|
||||
<path
|
||||
fill='#B4DC00'
|
||||
d='M0.00563250016,33.4438363 L0.00563250016,34.8553056 C0.262263289,33.7646248 0.711367168,32.9947324 1.60957493,31.7757362 C3.47014814,29.4019014 6.48555991,28.7603244 7.70455615,28.632009 C13.8636951,27.797959 22.9740881,26.8355935 32.1486388,26.5789627 C48.3805361,26.0657011 59.0948715,27.4130128 59.0948715,27.4130128 L81.870854,5.79186884 C81.870854,5.79186884 69.9375224,3.54634944 56.3360906,1.94240701 C47.4181707,0.851726163 39.5909316,0.274306889 35.1640505,0.0176761002 C34.843262,0.0176761002 31.6995349,-0.367270083 28.7482808,2.45566859 C25.540396,5.53523805 9.24434089,20.9972431 2.70025578,27.2205397 C-0.250998288,30.0434784 0.00563250016,33.1872055 0.00563250016,33.4438363 Z'
|
||||
/>
|
||||
<path
|
||||
fill='#6F2DA8'
|
||||
d='M81.036804,61.0958038 C74.877665,61.9298538 65.767272,62.956377 56.5927214,63.2771655 C40.360824,63.790427 29.5823309,62.4431154 29.5823309,62.4431154 L6.8063484,84.128417 C6.8063484,84.128417 18.8679955,86.5022518 32.4694272,88.0420366 C40.8099279,89.004402 48.188063,89.5176636 52.6791018,89.7742944 C52.9998903,89.7742944 53.5131519,89.5176636 53.8339404,89.5176636 C54.1547289,89.5176636 57.298456,88.9402443 60.2497101,86.1173056 C63.457595,83.0377362 82.8332195,60.9033307 81.036804,61.0958038 L81.036804,61.0958038 Z'
|
||||
/>
|
||||
<path
|
||||
fill='#591F91'
|
||||
d='M81.036804,61.0958038 C74.877665,61.9298538 65.767272,62.956377 56.5927214,63.2771655 C56.5927214,63.2771655 58.3249792,88.6194558 52.229998,89.7101367 C52.5507864,89.7101367 56.7210368,89.5176636 59.6722908,86.6947249 C62.8801757,83.6151554 82.8332195,60.9033307 81.036804,61.0958038 L81.036804,61.0958038 Z'
|
||||
/>
|
||||
<path
|
||||
fill='#73BE28'
|
||||
d='M53.5773096,89.8384521 C53.1282057,89.8384521 52.6791018,89.7742944 52.1658403,89.7742944 C53.3206788,89.5818213 54.0905712,89.1968751 55.3095674,88.2986673 C57.7475599,86.5664095 58.5174523,83.5509978 58.7740831,82.3320015 C59.8647639,76.2370203 61.3403909,67.190785 62.0461256,58.0162343 C63.3292796,41.8484946 62.5593872,31.0700015 62.5593872,31.0700015 L85.3353697,9.38469988 C85.3353697,9.38469988 87.0034698,21.4463469 87.9658353,35.1119364 C88.5432545,44.0298563 88.7357276,51.9212531 88.7998853,56.2839765 C88.7998853,56.604765 89.0565161,59.7484921 86.105262,62.5714308 C82.8973772,65.6510003 66.6013221,81.177163 60.1213947,87.4004596 C57.0418252,90.2233983 53.8980981,89.8384521 53.5773096,89.8384521 L53.5773096,89.8384521 Z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DynamoDBIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
DubIcon,
|
||||
DuckDuckGoIcon,
|
||||
DynamoDBIcon,
|
||||
DynatraceIcon,
|
||||
ElasticsearchIcon,
|
||||
ElevenLabsIcon,
|
||||
EmailBisonIcon,
|
||||
@@ -316,6 +317,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
dub: DubIcon,
|
||||
duckduckgo: DuckDuckGoIcon,
|
||||
dynamodb: DynamoDBIcon,
|
||||
dynatrace: DynatraceIcon,
|
||||
elasticsearch: ElasticsearchIcon,
|
||||
elevenlabs: ElevenLabsIcon,
|
||||
emailbison: EmailBisonIcon,
|
||||
|
||||
@@ -0,0 +1,601 @@
|
||||
---
|
||||
title: Dynatrace
|
||||
description: Query Dynatrace problems, metrics, entities, logs, SLOs, and vulnerabilities
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="dynatrace"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Dynatrace](https://www.dynatrace.com/) is an observability platform that monitors applications, infrastructure, and user experience from a single agent. Its Davis AI correlates signals across the stack into **problems** — a single incident with a root cause, an impact assessment, and the affected entities attached — instead of a stream of disconnected alerts.
|
||||
|
||||
**What you can reach from Sim**
|
||||
|
||||
- **Problems** — list and inspect Davis problems, read their root cause and affected entities, close them, and add comments.
|
||||
- **Metrics** — query time series with a metric selector, discover which metrics exist, read a metric's descriptor, and push your own data points.
|
||||
- **Entities** — list and inspect monitored hosts, services, applications, and Kubernetes workloads, and enumerate the entity types available for building selectors.
|
||||
- **Events** — read deployments, availability changes, and annotations, and ingest your own.
|
||||
- **Logs** — search log records and ingest new ones.
|
||||
- **SLOs** — read service-level objectives with their attainment, error budget, and burn rate.
|
||||
- **Application Security** — list and inspect vulnerabilities with risk assessment and remediation guidance.
|
||||
- **Audit log** — read who changed which configuration, and when.
|
||||
|
||||
**Setup**
|
||||
|
||||
You need two values: your **environment URL** and an **access token**.
|
||||
|
||||
The environment URL is the base address of your Dynatrace environment, without the API path:
|
||||
|
||||
| Deployment | Environment URL |
|
||||
| --- | --- |
|
||||
| SaaS | `https://abc12345.live.dynatrace.com` |
|
||||
| Managed / environment ActiveGate | `https://your-activegate:9999/e/abc12345` |
|
||||
|
||||
Create the token under **Access tokens** in Dynatrace, and grant only the scopes for the operations you plan to call. Each action's `apiToken` description names the scope it needs — `problems.read`, `metrics.read`, `entities.read`, `events.read`, `logs.read`, `slo.read`, `securityProblems.read`, `auditLogs.read` for reads, and `problems.write`, `metrics.ingest`, `events.ingest`, `logs.ingest` for writes.
|
||||
|
||||
**Selectors**
|
||||
|
||||
Most read operations are scoped by a selector rather than by fixed filter fields. Criteria are comma-separated, and every criterion matched must hold:
|
||||
|
||||
```
|
||||
entitySelector: type("HOST"),tag("env:prod")
|
||||
problemSelector: status("open"),severityLevel("AVAILABILITY")
|
||||
metricSelector: builtin:host.cpu.usage:splitBy("dt.entity.host"):avg:names
|
||||
securityProblemSelector: status("OPEN"),riskLevel("CRITICAL")
|
||||
```
|
||||
|
||||
Every selector field in the block has a wand — describe what you want in plain language and Sim writes the selector for you.
|
||||
|
||||
**Pagination**
|
||||
|
||||
List operations return a `nextPageKey` (a `nextSliceKey` for log search). Feed it back into the next call to read the following page. Dynatrace encodes the original filters into the cursor, so Sim sends the cursor alone and ignores the other filters on that call — which is what the API requires.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Dynatrace into workflows. Investigate and close Davis problems, query metrics and monitored entities, search and ingest logs, push deployment events, track SLO burn rates, review Application Security vulnerabilities, and read the audit log.
|
||||
|
||||
|
||||
|
||||
## Actions
|
||||
|
||||
### Dynatrace List Problems
|
||||
|
||||
List Davis-detected problems in a Dynatrace environment, filtered by timeframe, problem selector, or entity selector.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.read scope |
|
||||
| `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-1d. Defaults to now-2h |
|
||||
| `to` | string | No | End of the timeframe in the same formats as From. Defaults to now |
|
||||
| `problemSelector` | string | No | Problem selector, e.g. status\("open"\),severityLevel\("AVAILABILITY"\),impactLevel\("SERVICES"\) |
|
||||
| `entitySelector` | string | No | Entity selector scoping the result, e.g. type\("HOST"\),tag\("env:prod"\) |
|
||||
| `sort` | string | No | Sort order: status, startTime, or relevance, each optionally prefixed with + or - \(e.g. -startTime\) |
|
||||
| `fields` | string | No | Comma-separated optional properties to include: evidenceDetails, impactAnalysis, recentComments |
|
||||
| `pageSize` | number | No | Problems per page \(max 500, default 50\) |
|
||||
| `nextPageKey` | string | No | Cursor for the next page. All other filters are ignored when it is set |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `problems` | array | Matching problems |
|
||||
|
||||
### Dynatrace Get Problem
|
||||
|
||||
Get the full details of a single Dynatrace problem, including root cause, affected entities, and optionally its evidence and impact analysis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.read scope |
|
||||
| `problemId` | string | Yes | ID of the problem \(e.g., -1234567890123456789_1700000000000V2\) |
|
||||
| `fields` | string | No | Comma-separated optional properties to include: evidenceDetails, impactAnalysis, recentComments |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `problem` | object | The requested problem |
|
||||
|
||||
### Dynatrace Close Problem
|
||||
|
||||
Close a Dynatrace problem and record the closing comment.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.write scope |
|
||||
| `problemId` | string | Yes | ID of the problem to close |
|
||||
| `message` | string | Yes | Text of the closing comment |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `problemId` | string | ID of the closed problem |
|
||||
| `closeTimestamp` | number | Timestamp when closing was triggered, in UTC milliseconds |
|
||||
| `closing` | boolean | Whether the problem is in the process of being closed |
|
||||
| `comment` | object | The closing comment that was recorded |
|
||||
|
||||
### Dynatrace List Problem Comments
|
||||
|
||||
List the comments recorded on a Dynatrace problem.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.read scope |
|
||||
| `problemId` | string | Yes | ID of the problem whose comments should be listed |
|
||||
| `pageSize` | number | No | Comments per page \(max 500, default 10\) |
|
||||
| `nextPageKey` | string | No | Cursor for the next page. Page size is ignored when it is set |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `comments` | array | Comments on the problem |
|
||||
|
||||
### Dynatrace Add Problem Comment
|
||||
|
||||
Add a comment to a Dynatrace problem.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the problems.write scope |
|
||||
| `problemId` | string | Yes | ID of the problem to comment on |
|
||||
| `message` | string | Yes | Text of the comment |
|
||||
| `context` | string | No | Context of the comment, shown alongside the author \(e.g., the source system\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `problemId` | string | ID of the problem the comment was added to |
|
||||
| `message` | string | Text of the comment that was added |
|
||||
| `context` | string | Context of the comment |
|
||||
|
||||
### Dynatrace Query Metrics
|
||||
|
||||
Read metric data points from Dynatrace using a metric selector, with optional entity and management-zone scoping.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the metrics.read scope |
|
||||
| `metricSelector` | string | Yes | Metric selector, up to 10 metrics comma-separated, with optional transformations after a colon \(e.g. builtin:host.cpu.usage:avg:names\) |
|
||||
| `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-2h. Defaults to now-2h |
|
||||
| `to` | string | No | End of the timeframe in the same formats as From. Defaults to now |
|
||||
| `resolution` | string | No | Number of data points \(default 120\), a timespan such as 10m or 3w, or Inf for a single aggregated value |
|
||||
| `entitySelector` | string | No | Entity selector scoping the query, e.g. type\("HOST"\),tag\("env:prod"\) |
|
||||
| `mzSelector` | string | No | Management zone selector, e.g. mzName\("Production"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `result` | array | One entry per queried metric |
|
||||
| ↳ `metricId` | string | Metric key including transformations |
|
||||
| ↳ `dataPointCountRatio` | number | Queried data points relative to the query limit |
|
||||
| ↳ `dimensionCountRatio` | number | Queried dimension tuples relative to the query limit |
|
||||
| ↳ `appliedOptionalFilters` | array | Optional filters Dynatrace applied to the query |
|
||||
| ↳ `dql` | json | DQL translation of the query, when available |
|
||||
| ↳ `warnings` | array | Warnings for this metric |
|
||||
| ↳ `data` | array | Series of the metric, one per dimension tuple |
|
||||
| ↳ `dimensions` | array | Dimension values of the series |
|
||||
| ↳ `dimensionMap` | json | Dimension values keyed by dimension key |
|
||||
| ↳ `timestamps` | array | Timestamps in UTC milliseconds, one per value |
|
||||
| ↳ `values` | array | Metric values. Null where no data exists |
|
||||
| `resolution` | string | Resolution Dynatrace actually used |
|
||||
|
||||
### Dynatrace List Metrics
|
||||
|
||||
Discover the metrics available in a Dynatrace environment, filtered by metric selector, free text, or metadata.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the metrics.read scope |
|
||||
| `metricSelector` | string | No | Metric selector, supporting wildcards \(e.g. builtin:host.cpu.*\) |
|
||||
| `text` | string | No | Free-text search across metric display names and descriptions |
|
||||
| `fields` | string | No | Comma-separated descriptor properties. Prefix with + to add a non-default property and - to drop a default one; metricId is always returned \(e.g. +aggregationTypes,-description\) |
|
||||
| `writtenSince` | string | No | Only metrics written since this point, as UTC milliseconds, ISO 8601, or a relative expression such as now-7d |
|
||||
| `writtenSinceMode` | string | No | INCLUDE \(default\) keeps metrics written since Written Since; EXCLUDE keeps the ones not written since then |
|
||||
| `metadataSelector` | string | No | Metadata selector, e.g. unit\("Percent"\),tags\("dashboard"\) |
|
||||
| `pageSize` | number | No | Metrics per page \(max 500, default 100\) |
|
||||
| `nextPageKey` | string | No | Cursor for the next page. All other filters are ignored when it is set |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `metrics` | array | Matching metric descriptors |
|
||||
|
||||
### Dynatrace Get Metric Descriptor
|
||||
|
||||
Get the descriptor of a single Dynatrace metric — its unit, dimensions, supported aggregations, and transformations.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the metrics.read scope |
|
||||
| `metricKey` | string | Yes | Metric key, optionally followed by transformation operators separated by a colon \(e.g. builtin:host.cpu.usage\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `metric` | object | The requested metric descriptor |
|
||||
|
||||
### Dynatrace Ingest Metrics
|
||||
|
||||
Push custom metric data points into Dynatrace using the metric line protocol, one data point per line.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the metrics.ingest scope |
|
||||
| `payload` | string | Yes | Metric line protocol payload, one data point per line, max 1 MB \(e.g. cpu.temperature,dt.entity.host=HOST-06F288EE2A930951,cpu=1 55\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `linesOk` | number | Number of accepted data points |
|
||||
| `linesInvalid` | number | Number of rejected data points |
|
||||
| `ingestError` | json | Details of the invalid lines, with the line number and reason for each |
|
||||
| `warnings` | json | Warnings raised during ingestion, such as changed metric keys |
|
||||
|
||||
### Dynatrace List Entities
|
||||
|
||||
List monitored entities — hosts, services, applications, Kubernetes workloads — matching an entity selector.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the entities.read scope |
|
||||
| `entitySelector` | string | Yes | Entity selector defining the scope, e.g. type\("HOST"\),tag\("env:prod"\) or entityId\("HOST-06F288EE2A930951"\) |
|
||||
| `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-3d. Defaults to now-3d |
|
||||
| `to` | string | No | End of the timeframe in the same formats as From. Defaults to now |
|
||||
| `fields` | string | No | Comma-separated additional entity properties to include \(e.g. +lastSeenTms,+properties.BITNESS,+tags\) |
|
||||
| `sort` | string | No | Sort by display name: +displayName ascending or -displayName descending |
|
||||
| `pageSize` | number | No | Entities per page \(default 50\) |
|
||||
| `nextPageKey` | string | No | Cursor for the next page. All other filters are ignored when it is set |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `entities` | array | Matching monitored entities |
|
||||
|
||||
### Dynatrace Get Entity
|
||||
|
||||
Get the properties, tags, management zones, and relationships of a single monitored entity.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the entities.read scope |
|
||||
| `entityId` | string | Yes | ID of the entity \(e.g., HOST-06F288EE2A930951\) |
|
||||
| `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-3d. Defaults to now-3d |
|
||||
| `to` | string | No | End of the timeframe in the same formats as From. Defaults to now |
|
||||
| `fields` | string | No | Comma-separated additional entity properties to include \(e.g. +lastSeenTms,+properties.BITNESS\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `entity` | object | The requested monitored entity |
|
||||
|
||||
### Dynatrace List Entity Types
|
||||
|
||||
List the monitored entity types available in the environment, with the properties and relationships each supports. Use it to build valid entity selectors.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the entities.read scope |
|
||||
| `pageSize` | number | No | Entity types per page \(max 500, default 50\) |
|
||||
| `nextPageKey` | string | No | Cursor for the next page. Page size is ignored when it is set |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `types` | array | Available entity types |
|
||||
| ↳ `type` | string | Entity type \(e.g., HOST, SERVICE\) |
|
||||
| ↳ `displayName` | string | Display name of the type |
|
||||
| ↳ `dimensionKey` | string | Metric dimension key of the type |
|
||||
| ↳ `entityLimitExceeded` | boolean | Whether the environment exceeded the entity limit for this type |
|
||||
| ↳ `fromRelationships` | array | Relationships originating at this type |
|
||||
| ↳ `id` | string | Relationship ID |
|
||||
| ↳ `toTypes` | array | Entity types the relationship points to |
|
||||
| ↳ `toRelationships` | array | Relationships pointing at this type |
|
||||
| ↳ `id` | string | Relationship ID |
|
||||
| ↳ `fromTypes` | array | Entity types the relationship originates from |
|
||||
|
||||
### Dynatrace List Events
|
||||
|
||||
List events — deployments, availability changes, alerts, custom annotations — in a Dynatrace environment.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the events.read scope |
|
||||
| `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-1d. Defaults to now-2h |
|
||||
| `to` | string | No | End of the timeframe in the same formats as From. Defaults to now |
|
||||
| `eventSelector` | string | No | Event selector, e.g. eventType\("CUSTOM_DEPLOYMENT"\),status\("OPEN"\),correlationId\("build-42"\) |
|
||||
| `entitySelector` | string | No | Entity selector scoping the result, e.g. type\("SERVICE"\),tag\("env:prod"\) |
|
||||
| `pageSize` | number | No | Events per page \(max 1000, default 100\) |
|
||||
| `nextPageKey` | string | No | Cursor for the next page. All other filters are ignored when it is set |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `events` | array | Matching events |
|
||||
|
||||
### Dynatrace Get Event
|
||||
|
||||
Get the full details of a single Dynatrace event.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the events.read scope |
|
||||
| `eventId` | string | Yes | ID of the event |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `event` | object | The requested event |
|
||||
|
||||
### Dynatrace Ingest Event
|
||||
|
||||
Push an event into Dynatrace — a deployment marker, custom annotation, or custom alert — attached to the entities matched by an entity selector.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the events.ingest scope |
|
||||
| `eventType` | string | Yes | One of AVAILABILITY_EVENT, CUSTOM_ALERT, CUSTOM_ANNOTATION, CUSTOM_CONFIGURATION, CUSTOM_DEPLOYMENT, CUSTOM_INFO, ERROR_EVENT, MARKED_FOR_TERMINATION, PERFORMANCE_EVENT, RESOURCE_CONTENTION_EVENT, WARNING |
|
||||
| `title` | string | Yes | Title of the event |
|
||||
| `entitySelector` | string | No | Entity selector for the entities to attach the event to. Defaults to the environment entity |
|
||||
| `startTime` | number | No | Event start in UTC milliseconds. Defaults to now |
|
||||
| `endTime` | number | No | Event end in UTC milliseconds. Defaults to the start time plus the timeout |
|
||||
| `eventTimeout` | number | No | Minutes the event stays open when no end time is given. Defaults to 15, capped at 360 |
|
||||
| `properties` | json | No | Event properties as a key-value object. Max 100 entries, keys up to 100 and values up to 4096 characters |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `reportCount` | number | Number of events Dynatrace reported |
|
||||
| `eventIngestResults` | array | One result per ingested event |
|
||||
| ↳ `correlationId` | string | Correlation ID of the ingested event |
|
||||
| ↳ `status` | string | OK, INVALID_ENTITY_TYPE, INVALID_METADATA, or INVALID_TIMESTAMPS |
|
||||
|
||||
### Dynatrace Search Logs
|
||||
|
||||
Search log records in Dynatrace by query and timeframe, for troubleshooting and incident analysis.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the logs.read scope, or storage:logs:read and storage:buckets:read on Grail |
|
||||
| `query` | string | No | Log search query, e.g. status="ERROR" AND dt.entity.host="HOST-06F288EE2A930951" |
|
||||
| `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-1h. Defaults to now-2w |
|
||||
| `to` | string | No | End of the timeframe in the same formats as From. Defaults to now |
|
||||
| `sort` | string | No | Sort by field with a + or - prefix, e.g. -timestamp for newest first |
|
||||
| `limit` | number | No | Number of log records to return \(max 1000, default 1000\) |
|
||||
| `nextSliceKey` | string | No | Cursor for the next slice. All other filters are ignored when it is set |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | array | Matching log records |
|
||||
| ↳ `timestamp` | number | Log timestamp in UTC milliseconds |
|
||||
| ↳ `status` | string | Log level: ERROR, WARN, INFO, NONE, or NOT_APPLICABLE |
|
||||
| ↳ `content` | string | Log message content |
|
||||
| ↳ `eventType` | string | Event type of the record |
|
||||
| ↳ `additionalColumns` | json | Additional log attributes keyed by column name |
|
||||
| `sliceSize` | number | Number of records in this slice |
|
||||
| `nextSliceKey` | string | Cursor for the next slice. Null when the result is complete |
|
||||
| `warnings` | string | Warning raised while searching |
|
||||
|
||||
### Dynatrace Ingest Logs
|
||||
|
||||
Push log events into Dynatrace. Accepts a single log event object or an array of them.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the logs.ingest scope |
|
||||
| `logs` | json | Yes | A log event object, or an array of them. Recognized keys are content, timestamp, and severity; any other key becomes a custom attribute \(e.g. \[\{"content":"Deploy finished","severity":"info","service":"checkout"\}\]\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `accepted` | boolean | True when Dynatrace accepted every log event \(HTTP 204\) |
|
||||
| `statusCode` | number | HTTP status Dynatrace returned. 204 is full success, 200 is partial success |
|
||||
| `details` | json | Partial-success details, present only when some events were rejected |
|
||||
|
||||
### Dynatrace List SLOs
|
||||
|
||||
List service-level objectives with their current attainment, error budget, and burn rate.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the slo.read scope |
|
||||
| `sloSelector` | string | No | SLO selector, e.g. healthState\("WARNING"\),text\("checkout"\),problems\("true"\). Combine criteria with commas |
|
||||
| `from` | string | No | Start of the evaluation timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-7d |
|
||||
| `to` | string | No | End of the evaluation timeframe in the same formats as From. Defaults to now |
|
||||
| `timeFrame` | string | No | CURRENT evaluates each SLO over its own timeframe; GTF evaluates over the From/To range |
|
||||
| `sort` | string | No | Sort by name: name ascending or -name descending |
|
||||
| `enabledSlos` | string | No | Filter by enabled state: true, false, or all |
|
||||
| `evaluate` | boolean | No | Evaluate each SLO and include its calculated values |
|
||||
| `showGlobalSlos` | boolean | No | Include SLOs that are not scoped to a management zone |
|
||||
| `pageSize` | number | No | SLOs per page \(max 10000, default 10\) |
|
||||
| `nextPageKey` | string | No | Cursor for the next page. All other filters are ignored when it is set |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `slos` | array | Matching service-level objectives |
|
||||
|
||||
### Dynatrace Get SLO
|
||||
|
||||
Get a single service-level objective with its evaluated attainment, error budget, and burn rate.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the slo.read scope |
|
||||
| `sloId` | string | Yes | ID of the SLO |
|
||||
| `from` | string | No | Start of the evaluation timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-7d. Defaults to now-2w |
|
||||
| `to` | string | No | End of the evaluation timeframe in the same formats as From. Defaults to now |
|
||||
| `timeFrame` | string | No | CURRENT evaluates the SLO over its own timeframe; GTF evaluates over the From/To range |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `slo` | object | The requested service-level objective |
|
||||
|
||||
### Dynatrace List Security Problems
|
||||
|
||||
List vulnerabilities detected by Dynatrace Application Security, filtered by risk level, status, CVE, or technology.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the securityProblems.read scope |
|
||||
| `securityProblemSelector` | string | No | Security problem selector, e.g. status\("OPEN"\),riskLevel\("CRITICAL"\),technology\("JAVA"\) |
|
||||
| `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-7d. Defaults to now-30d |
|
||||
| `to` | string | No | End of the timeframe in the same formats as From. Defaults to now |
|
||||
| `fields` | string | No | Comma-separated optional properties to include: +riskAssessment, +managementZones, +codeLevelVulnerabilityDetails, +globalCounts |
|
||||
| `sort` | string | No | Sort by a field with a + or - prefix, e.g. -riskAssessment.riskScore |
|
||||
| `pageSize` | number | No | Security problems per page \(max 500, default 100\) |
|
||||
| `nextPageKey` | string | No | Cursor for the next page. All other filters are ignored when it is set |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `securityProblems` | array | Matching security problems |
|
||||
|
||||
### Dynatrace Get Security Problem
|
||||
|
||||
Get a single vulnerability with its description, remediation guidance, affected entities, and risk assessment.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the securityProblems.read scope |
|
||||
| `securityProblemId` | string | Yes | ID of the security problem |
|
||||
| `fields` | string | No | Comma-separated optional properties to include: +riskAssessment, +managementZones, +codeLevelVulnerabilityDetails, +globalCounts |
|
||||
| `managementZoneFilter` | string | No | Restrict the counts to management zones, e.g. names\("Production"\) |
|
||||
| `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-24h. Defaults to the last 24 hours |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `securityProblem` | object | The requested security problem |
|
||||
|
||||
### Dynatrace Get Audit Logs
|
||||
|
||||
Read the Dynatrace audit log — who changed which configuration, when, and whether it succeeded.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dynatrace environment URL \(e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed\) |
|
||||
| `apiToken` | string | Yes | Dynatrace access token \(dt0c01...\) with the auditLogs.read scope |
|
||||
| `filter` | string | No | Audit log filter, e.g. eventType\("UPDATE"\),user\("someone@example.com"\),category\("CONFIG"\) |
|
||||
| `from` | string | No | Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-1d. Defaults to now-2w |
|
||||
| `to` | string | No | End of the timeframe in the same formats as From. Defaults to now |
|
||||
| `sort` | string | No | timestamp for oldest first, or -timestamp for newest first \(default\) |
|
||||
| `pageSize` | number | No | Entries per page \(max 5000, default 1000\) |
|
||||
| `nextPageKey` | string | No | Cursor for the next page. All other filters are ignored when it is set |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `auditLogs` | array | Matching audit log entries |
|
||||
| ↳ `logId` | string | Audit log entry ID |
|
||||
| ↳ `eventType` | string | Type of the audited change |
|
||||
| ↳ `category` | string | Category of the audited change |
|
||||
| ↳ `entityId` | string | ID of the changed entity |
|
||||
| ↳ `environmentId` | string | Environment the change happened in |
|
||||
| ↳ `user` | string | User or token that made the change |
|
||||
| ↳ `userType` | string | Type of the acting user |
|
||||
| ↳ `userOrigin` | string | Origin of the request |
|
||||
| ↳ `timestamp` | number | Change timestamp in UTC milliseconds |
|
||||
| ↳ `success` | boolean | Whether the change succeeded |
|
||||
| ↳ `message` | string | Description of the change |
|
||||
| ↳ `patch` | json | JSON patch of the change |
|
||||
| ↳ `settingsSchemaId` | string | Settings schema ID \(dt.settings.schema_id\) |
|
||||
| ↳ `settingsScopeId` | string | Settings scope ID \(dt.settings.scope_id\) |
|
||||
| ↳ `settingsKey` | string | Settings key \(dt.settings.key\) |
|
||||
| ↳ `settingsObjectId` | string | Settings object ID \(dt.settings.object_id\) |
|
||||
| ↳ `settingsObjectSummary` | string | Settings object summary \(dt.settings.object_summary\) |
|
||||
| ↳ `settingsScopeName` | string | Settings scope name \(dt.settings.scope_name\) |
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"dub",
|
||||
"duckduckgo",
|
||||
"dynamodb",
|
||||
"dynatrace",
|
||||
"elasticsearch",
|
||||
"elevenlabs",
|
||||
"emailbison",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -60,6 +60,7 @@ import { DSPyBlock, DSPyBlockMeta } from '@/blocks/blocks/dspy'
|
||||
import { DubBlock, DubBlockMeta } from '@/blocks/blocks/dub'
|
||||
import { DuckDuckGoBlock, DuckDuckGoBlockMeta } from '@/blocks/blocks/duckduckgo'
|
||||
import { DynamoDBBlock, DynamoDBBlockMeta } from '@/blocks/blocks/dynamodb'
|
||||
import { DynatraceBlock, DynatraceBlockMeta } from '@/blocks/blocks/dynatrace'
|
||||
import { ElasticsearchBlock, ElasticsearchBlockMeta } from '@/blocks/blocks/elasticsearch'
|
||||
import { ElevenLabsBlock, ElevenLabsBlockMeta } from '@/blocks/blocks/elevenlabs'
|
||||
import { EmailBisonBlock, EmailBisonBlockMeta } from '@/blocks/blocks/emailbison'
|
||||
@@ -411,6 +412,7 @@ export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
|
||||
dub: DubBlock,
|
||||
duckduckgo: DuckDuckGoBlock,
|
||||
dynamodb: DynamoDBBlock,
|
||||
dynatrace: DynatraceBlock,
|
||||
elasticsearch: ElasticsearchBlock,
|
||||
elevenlabs: ElevenLabsBlock,
|
||||
emailbison: EmailBisonBlock,
|
||||
@@ -735,6 +737,7 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
|
||||
dub: DubBlockMeta,
|
||||
duckduckgo: DuckDuckGoBlockMeta,
|
||||
dynamodb: DynamoDBBlockMeta,
|
||||
dynatrace: DynatraceBlockMeta,
|
||||
elasticsearch: ElasticsearchBlockMeta,
|
||||
elevenlabs: ElevenLabsBlockMeta,
|
||||
emailbison: EmailBisonBlockMeta,
|
||||
|
||||
@@ -6048,6 +6048,37 @@ export function RDSIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DynatraceIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 89 90' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
fill='#1496FF'
|
||||
d='M29.9672771,7.52412666 C28.8124385,13.6191079 27.4009692,22.6653432 26.6310768,31.8398939 C25.2837652,48.0076335 26.1178152,58.8502844 26.1178152,58.8502844 L3.34183275,80.4714283 C3.34183275,80.4714283 1.60957493,68.3456235 0.711367168,54.680034 C0.198105592,46.211218 0.00563250016,38.7689252 0.00563250016,34.2778864 C0.00563250016,34.0212556 0.133947894,33.7646248 0.133947894,33.507994 C0.133947894,33.1872055 0.518894077,30.1717937 3.47014814,27.3488551 C6.678033,24.2692856 30.3522232,5.72771114 29.9672771,7.52412666 Z'
|
||||
/>
|
||||
<path
|
||||
fill='#1284EA'
|
||||
d='M29.9672771,7.52412666 C28.8124385,13.6191079 27.4009692,22.6653432 26.6310768,31.8398939 C26.6310768,31.8398939 1.41710184,28.8244821 0.00563250016,34.9194633 C0.00563250016,34.5986748 0.45473638,30.8775284 3.40599045,28.0545897 C6.6138753,24.9750203 30.3522232,5.72771114 29.9672771,7.52412666 L29.9672771,7.52412666 Z'
|
||||
/>
|
||||
<path
|
||||
fill='#B4DC00'
|
||||
d='M0.00563250016,33.4438363 L0.00563250016,34.8553056 C0.262263289,33.7646248 0.711367168,32.9947324 1.60957493,31.7757362 C3.47014814,29.4019014 6.48555991,28.7603244 7.70455615,28.632009 C13.8636951,27.797959 22.9740881,26.8355935 32.1486388,26.5789627 C48.3805361,26.0657011 59.0948715,27.4130128 59.0948715,27.4130128 L81.870854,5.79186884 C81.870854,5.79186884 69.9375224,3.54634944 56.3360906,1.94240701 C47.4181707,0.851726163 39.5909316,0.274306889 35.1640505,0.0176761002 C34.843262,0.0176761002 31.6995349,-0.367270083 28.7482808,2.45566859 C25.540396,5.53523805 9.24434089,20.9972431 2.70025578,27.2205397 C-0.250998288,30.0434784 0.00563250016,33.1872055 0.00563250016,33.4438363 Z'
|
||||
/>
|
||||
<path
|
||||
fill='#6F2DA8'
|
||||
d='M81.036804,61.0958038 C74.877665,61.9298538 65.767272,62.956377 56.5927214,63.2771655 C40.360824,63.790427 29.5823309,62.4431154 29.5823309,62.4431154 L6.8063484,84.128417 C6.8063484,84.128417 18.8679955,86.5022518 32.4694272,88.0420366 C40.8099279,89.004402 48.188063,89.5176636 52.6791018,89.7742944 C52.9998903,89.7742944 53.5131519,89.5176636 53.8339404,89.5176636 C54.1547289,89.5176636 57.298456,88.9402443 60.2497101,86.1173056 C63.457595,83.0377362 82.8332195,60.9033307 81.036804,61.0958038 L81.036804,61.0958038 Z'
|
||||
/>
|
||||
<path
|
||||
fill='#591F91'
|
||||
d='M81.036804,61.0958038 C74.877665,61.9298538 65.767272,62.956377 56.5927214,63.2771655 C56.5927214,63.2771655 58.3249792,88.6194558 52.229998,89.7101367 C52.5507864,89.7101367 56.7210368,89.5176636 59.6722908,86.6947249 C62.8801757,83.6151554 82.8332195,60.9033307 81.036804,61.0958038 L81.036804,61.0958038 Z'
|
||||
/>
|
||||
<path
|
||||
fill='#73BE28'
|
||||
d='M53.5773096,89.8384521 C53.1282057,89.8384521 52.6791018,89.7742944 52.1658403,89.7742944 C53.3206788,89.5818213 54.0905712,89.1968751 55.3095674,88.2986673 C57.7475599,86.5664095 58.5174523,83.5509978 58.7740831,82.3320015 C59.8647639,76.2370203 61.3403909,67.190785 62.0461256,58.0162343 C63.3292796,41.8484946 62.5593872,31.0700015 62.5593872,31.0700015 L85.3353697,9.38469988 C85.3353697,9.38469988 87.0034698,21.4463469 87.9658353,35.1119364 C88.5432545,44.0298563 88.7357276,51.9212531 88.7998853,56.2839765 C88.7998853,56.604765 89.0565161,59.7484921 86.105262,62.5714308 C82.8973772,65.6510003 66.6013221,81.177163 60.1213947,87.4004596 C57.0418252,90.2233983 53.8980981,89.8384521 53.5773096,89.8384521 L53.5773096,89.8384521 Z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DynamoDBIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
DubIcon,
|
||||
DuckDuckGoIcon,
|
||||
DynamoDBIcon,
|
||||
DynatraceIcon,
|
||||
ElasticsearchIcon,
|
||||
ElevenLabsIcon,
|
||||
EmailBisonIcon,
|
||||
@@ -312,6 +313,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
dub: DubIcon,
|
||||
duckduckgo: DuckDuckGoIcon,
|
||||
dynamodb: DynamoDBIcon,
|
||||
dynatrace: DynatraceIcon,
|
||||
elasticsearch: ElasticsearchIcon,
|
||||
elevenlabs: ElevenLabsIcon,
|
||||
emailbison: EmailBisonIcon,
|
||||
|
||||
@@ -5427,6 +5427,113 @@
|
||||
"integrationType": "search",
|
||||
"tags": ["web-scraping"]
|
||||
},
|
||||
{
|
||||
"type": "dynatrace",
|
||||
"slug": "dynatrace",
|
||||
"name": "Dynatrace",
|
||||
"description": "Query Dynatrace problems, metrics, entities, logs, SLOs, and vulnerabilities",
|
||||
"longDescription": "Integrate Dynatrace into workflows. Investigate and close Davis problems, query metrics and monitored entities, search and ingest logs, push deployment events, track SLO burn rates, review Application Security vulnerabilities, and read the audit log.",
|
||||
"bgColor": "#FFFFFF",
|
||||
"iconName": "DynatraceIcon",
|
||||
"docsUrl": "https://docs.sim.ai/integrations/dynatrace",
|
||||
"operations": [
|
||||
{
|
||||
"name": "List Problems",
|
||||
"description": "List Davis-detected problems in a Dynatrace environment, filtered by timeframe, problem selector, or entity selector."
|
||||
},
|
||||
{
|
||||
"name": "Get Problem",
|
||||
"description": "Get the full details of a single Dynatrace problem, including root cause, affected entities, and optionally its evidence and impact analysis."
|
||||
},
|
||||
{
|
||||
"name": "Close Problem",
|
||||
"description": "Close a Dynatrace problem and record the closing comment."
|
||||
},
|
||||
{
|
||||
"name": "List Problem Comments",
|
||||
"description": "List the comments recorded on a Dynatrace problem."
|
||||
},
|
||||
{
|
||||
"name": "Add Problem Comment",
|
||||
"description": "Add a comment to a Dynatrace problem."
|
||||
},
|
||||
{
|
||||
"name": "Query Metrics",
|
||||
"description": "Read metric data points from Dynatrace using a metric selector, with optional entity and management-zone scoping."
|
||||
},
|
||||
{
|
||||
"name": "List Metrics",
|
||||
"description": "Discover the metrics available in a Dynatrace environment, filtered by metric selector, free text, or metadata."
|
||||
},
|
||||
{
|
||||
"name": "Get Metric Descriptor",
|
||||
"description": "Get the descriptor of a single Dynatrace metric — its unit, dimensions, supported aggregations, and transformations."
|
||||
},
|
||||
{
|
||||
"name": "Ingest Metrics",
|
||||
"description": "Push custom metric data points into Dynatrace using the metric line protocol, one data point per line."
|
||||
},
|
||||
{
|
||||
"name": "List Entities",
|
||||
"description": "List monitored entities — hosts, services, applications, Kubernetes workloads — matching an entity selector."
|
||||
},
|
||||
{
|
||||
"name": "Get Entity",
|
||||
"description": "Get the properties, tags, management zones, and relationships of a single monitored entity."
|
||||
},
|
||||
{
|
||||
"name": "List Entity Types",
|
||||
"description": "List the monitored entity types available in the environment, with the properties and relationships each supports. Use it to build valid entity selectors."
|
||||
},
|
||||
{
|
||||
"name": "List Events",
|
||||
"description": "List events — deployments, availability changes, alerts, custom annotations — in a Dynatrace environment."
|
||||
},
|
||||
{
|
||||
"name": "Get Event",
|
||||
"description": "Get the full details of a single Dynatrace event."
|
||||
},
|
||||
{
|
||||
"name": "Ingest Event",
|
||||
"description": "Push an event into Dynatrace — a deployment marker, custom annotation, or custom alert — attached to the entities matched by an entity selector."
|
||||
},
|
||||
{
|
||||
"name": "Search Logs",
|
||||
"description": "Search log records in Dynatrace by query and timeframe, for troubleshooting and incident analysis."
|
||||
},
|
||||
{
|
||||
"name": "Ingest Logs",
|
||||
"description": "Push log events into Dynatrace. Accepts a single log event object or an array of them."
|
||||
},
|
||||
{
|
||||
"name": "List SLOs",
|
||||
"description": "List service-level objectives with their current attainment, error budget, and burn rate."
|
||||
},
|
||||
{
|
||||
"name": "Get SLO",
|
||||
"description": "Get a single service-level objective with its evaluated attainment, error budget, and burn rate."
|
||||
},
|
||||
{
|
||||
"name": "List Security Problems",
|
||||
"description": "List vulnerabilities detected by Dynatrace Application Security, filtered by risk level, status, CVE, or technology."
|
||||
},
|
||||
{
|
||||
"name": "Get Security Problem",
|
||||
"description": "Get a single vulnerability with its description, remediation guidance, affected entities, and risk assessment."
|
||||
},
|
||||
{
|
||||
"name": "Get Audit Logs",
|
||||
"description": "Read the Dynatrace audit log — who changed which configuration, when, and whether it succeeded."
|
||||
}
|
||||
],
|
||||
"operationCount": 22,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "api-key",
|
||||
"category": "tools",
|
||||
"integrationType": "observability",
|
||||
"tags": ["monitoring", "incident-management", "error-tracking"]
|
||||
},
|
||||
{
|
||||
"type": "elasticsearch",
|
||||
"slug": "elasticsearch",
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type {
|
||||
DynatraceAddProblemCommentParams,
|
||||
DynatraceAddProblemCommentResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import { buildDynatraceUrl, dynatraceHeaders, encodeDynatraceId } from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const addProblemCommentTool: ToolConfig<
|
||||
DynatraceAddProblemCommentParams,
|
||||
DynatraceAddProblemCommentResponse
|
||||
> = {
|
||||
id: 'dynatrace_add_problem_comment',
|
||||
name: 'Dynatrace Add Problem Comment',
|
||||
description: 'Add a comment to a Dynatrace problem.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the problems.write scope',
|
||||
},
|
||||
problemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the problem to comment on',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Text of the comment',
|
||||
},
|
||||
context: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Context of the comment, shown alongside the author (e.g., the source system)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildDynatraceUrl(
|
||||
params.environmentUrl,
|
||||
`/problems/${encodeDynatraceId(params.problemId)}/comments`
|
||||
),
|
||||
method: 'POST',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
body: (params) => {
|
||||
const body: Record<string, string> = { message: params.message }
|
||||
if (params.context) body.context = params.context
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
/** The endpoint answers 201 with no body, so the created comment is echoed back. */
|
||||
transformResponse: async (_response: Response, params?: DynatraceAddProblemCommentParams) => ({
|
||||
success: true,
|
||||
output: {
|
||||
problemId: params?.problemId ?? '',
|
||||
message: params?.message ?? '',
|
||||
context: params?.context ?? null,
|
||||
},
|
||||
}),
|
||||
|
||||
outputs: {
|
||||
problemId: { type: 'string', description: 'ID of the problem the comment was added to' },
|
||||
message: { type: 'string', description: 'Text of the comment that was added' },
|
||||
context: { type: 'string', description: 'Context of the comment', nullable: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { commentProperties } from '@/tools/dynatrace/outputs'
|
||||
import type {
|
||||
DynatraceCloseProblemParams,
|
||||
DynatraceCloseProblemResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
encodeDynatraceId,
|
||||
mapComment,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const closeProblemTool: ToolConfig<
|
||||
DynatraceCloseProblemParams,
|
||||
DynatraceCloseProblemResponse
|
||||
> = {
|
||||
id: 'dynatrace_close_problem',
|
||||
name: 'Dynatrace Close Problem',
|
||||
description: 'Close a Dynatrace problem and record the closing comment.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the problems.write scope',
|
||||
},
|
||||
problemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the problem to close',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Text of the closing comment',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildDynatraceUrl(
|
||||
params.environmentUrl,
|
||||
`/problems/${encodeDynatraceId(params.problemId)}/close`
|
||||
),
|
||||
method: 'POST',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
body: (params) => ({ message: params.message }),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
problemId: (data.problemId as string) ?? null,
|
||||
closeTimestamp: (data.closeTimestamp as number) ?? null,
|
||||
closing: (data.closing as boolean) ?? null,
|
||||
comment: mapComment(data.comment),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
problemId: { type: 'string', description: 'ID of the closed problem', nullable: true },
|
||||
closeTimestamp: {
|
||||
type: 'number',
|
||||
description: 'Timestamp when closing was triggered, in UTC milliseconds',
|
||||
nullable: true,
|
||||
},
|
||||
closing: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the problem is in the process of being closed',
|
||||
nullable: true,
|
||||
},
|
||||
comment: {
|
||||
type: 'object',
|
||||
description: 'The closing comment that was recorded',
|
||||
nullable: true,
|
||||
properties: commentProperties,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { closeProblemTool } from '@/tools/dynatrace/close_problem'
|
||||
import { getAuditLogsTool } from '@/tools/dynatrace/get_audit_logs'
|
||||
import { getEntityTool } from '@/tools/dynatrace/get_entity'
|
||||
import { getMetricTool } from '@/tools/dynatrace/get_metric'
|
||||
import { getProblemTool } from '@/tools/dynatrace/get_problem'
|
||||
import { getSloTool } from '@/tools/dynatrace/get_slo'
|
||||
import { ingestEventTool } from '@/tools/dynatrace/ingest_event'
|
||||
import { ingestLogsTool } from '@/tools/dynatrace/ingest_logs'
|
||||
import { ingestMetricsTool } from '@/tools/dynatrace/ingest_metrics'
|
||||
import { listEntitiesTool } from '@/tools/dynatrace/list_entities'
|
||||
import { listEntityTypesTool } from '@/tools/dynatrace/list_entity_types'
|
||||
import { listEventsTool } from '@/tools/dynatrace/list_events'
|
||||
import { listMetricsTool } from '@/tools/dynatrace/list_metrics'
|
||||
import { listProblemCommentsTool } from '@/tools/dynatrace/list_problem_comments'
|
||||
import { listProblemsTool } from '@/tools/dynatrace/list_problems'
|
||||
import { listSecurityProblemsTool } from '@/tools/dynatrace/list_security_problems'
|
||||
import { listSlosTool } from '@/tools/dynatrace/list_slos'
|
||||
import { queryMetricsTool } from '@/tools/dynatrace/query_metrics'
|
||||
import { searchLogsTool } from '@/tools/dynatrace/search_logs'
|
||||
import { buildDynatraceUrl, dynatraceHeaders } from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId, extractErrorMessageWithId } from '@/tools/error-extractors'
|
||||
|
||||
const ENV = 'https://abc12345.live.dynatrace.com'
|
||||
const TOKEN = 'dt0c01.TOKEN'
|
||||
|
||||
function url(tool: { request: { url: string | ((p: never) => string) } }, params: object) {
|
||||
const build = tool.request.url
|
||||
return typeof build === 'function' ? build(params as never) : build
|
||||
}
|
||||
|
||||
function body(tool: { request: { body?: (p: never) => unknown } }, params: object) {
|
||||
return tool.request.body?.(params as never)
|
||||
}
|
||||
|
||||
describe('buildDynatraceUrl', () => {
|
||||
it('appends the v2 API path to a SaaS environment URL', () => {
|
||||
expect(buildDynatraceUrl(ENV, '/problems')).toBe(`${ENV}/api/v2/problems`)
|
||||
})
|
||||
|
||||
it('tolerates a trailing slash and a trailing /api/v2 segment', () => {
|
||||
expect(buildDynatraceUrl(`${ENV}/`, '/problems')).toBe(`${ENV}/api/v2/problems`)
|
||||
expect(buildDynatraceUrl(`${ENV}/api/v2`, '/problems')).toBe(`${ENV}/api/v2/problems`)
|
||||
expect(buildDynatraceUrl(` ${ENV}/api/v2/ `, '/problems')).toBe(`${ENV}/api/v2/problems`)
|
||||
})
|
||||
|
||||
it('keeps the environment path of a Managed / ActiveGate URL', () => {
|
||||
expect(buildDynatraceUrl('https://ag.example.com:9999/e/abc12345', '/problems')).toBe(
|
||||
'https://ag.example.com:9999/e/abc12345/api/v2/problems'
|
||||
)
|
||||
})
|
||||
|
||||
it('omits unset and empty query params but keeps false', () => {
|
||||
expect(
|
||||
buildDynatraceUrl(ENV, '/slo', {
|
||||
sloSelector: undefined,
|
||||
sort: '',
|
||||
from: null,
|
||||
evaluate: false,
|
||||
pageSize: 10,
|
||||
})
|
||||
).toBe(`${ENV}/api/v2/slo?evaluate=false&pageSize=10`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('auth header', () => {
|
||||
it('uses the Api-Token scheme and trims the token', () => {
|
||||
expect(dynatraceHeaders(` ${TOKEN} `).Authorization).toBe(`Api-Token ${TOKEN}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('path identifiers', () => {
|
||||
const base = { environmentUrl: ENV, apiToken: TOKEN }
|
||||
|
||||
it('trims whitespace pasted around an identifier', () => {
|
||||
expect(url(getProblemTool, { ...base, problemId: ' P-123_456V2 ' })).toBe(
|
||||
`${ENV}/api/v2/problems/P-123_456V2`
|
||||
)
|
||||
expect(url(getEntityTool, { ...base, entityId: ' HOST-06F288EE2A930951\n' })).toBe(
|
||||
`${ENV}/api/v2/entities/HOST-06F288EE2A930951`
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves the colon separators of a metric key unencoded', () => {
|
||||
expect(url(getMetricTool, { ...base, metricKey: ' builtin:host.cpu.usage:avg ' })).toBe(
|
||||
`${ENV}/api/v2/metrics/builtin:host.cpu.usage:avg`
|
||||
)
|
||||
})
|
||||
|
||||
it('drops every other filter once a page cursor is supplied', () => {
|
||||
expect(
|
||||
url(listProblemsTool, { ...base, nextPageKey: 'CURSOR', from: 'now-7d', pageSize: 500 })
|
||||
).toBe(`${ENV}/api/v2/problems?nextPageKey=CURSOR`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('json request params', () => {
|
||||
const base = { environmentUrl: ENV, apiToken: TOKEN }
|
||||
|
||||
it('sends a JSON-string log payload as JSON, not as a quoted string', () => {
|
||||
const sent = body(ingestLogsTool, {
|
||||
...base,
|
||||
logs: '[{"content":"Deploy finished","severity":"info"}]',
|
||||
})
|
||||
expect(sent).toBe('[{"content":"Deploy finished","severity":"info"}]')
|
||||
expect(JSON.parse(sent as string)).toEqual([{ content: 'Deploy finished', severity: 'info' }])
|
||||
})
|
||||
|
||||
it('sends an already-parsed log payload unchanged', () => {
|
||||
const sent = body(ingestLogsTool, { ...base, logs: [{ content: 'hi' }] })
|
||||
expect(JSON.parse(sent as string)).toEqual([{ content: 'hi' }])
|
||||
})
|
||||
|
||||
it('parses a JSON-string properties object on event ingest', () => {
|
||||
const sent = body(ingestEventTool, {
|
||||
...base,
|
||||
eventType: 'CUSTOM_DEPLOYMENT',
|
||||
title: 'Deploy 4.12.2',
|
||||
properties: '{"version":"4.12.2"}',
|
||||
}) as Record<string, unknown>
|
||||
expect(sent.properties).toEqual({ version: '4.12.2' })
|
||||
})
|
||||
|
||||
it('maps the event timeout onto Dynatrace’s timeout field', () => {
|
||||
const sent = body(ingestEventTool, {
|
||||
...base,
|
||||
eventType: 'CUSTOM_INFO',
|
||||
title: 'x',
|
||||
eventTimeout: 30,
|
||||
}) as Record<string, unknown>
|
||||
expect(sent.timeout).toBe(30)
|
||||
expect(sent.eventTimeout).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects an empty log payload instead of sending a no-op that reports success', () => {
|
||||
// Dynatrace answers 204 to `[]`, which would otherwise surface as accepted:true.
|
||||
expect(() => body(ingestLogsTool, { ...base, logs: [] })).toThrow(/at least one log event/)
|
||||
expect(() => body(ingestLogsTool, { ...base, logs: '' })).toThrow(/at least one log event/)
|
||||
expect(() => body(ingestLogsTool, { ...base, logs: undefined })).toThrow(
|
||||
/at least one log event/
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a malformed JSON string rather than silently dropping the payload', () => {
|
||||
expect(() => body(ingestLogsTool, { ...base, logs: '{not json' })).toThrow(/valid JSON/)
|
||||
})
|
||||
|
||||
it('omits optional event fields that were not provided', () => {
|
||||
const sent = body(ingestEventTool, {
|
||||
...base,
|
||||
eventType: 'CUSTOM_INFO',
|
||||
title: 'x',
|
||||
}) as Record<string, unknown>
|
||||
expect(Object.keys(sent).sort()).toEqual(['eventType', 'title'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('error extraction', () => {
|
||||
const extract = (data: unknown) =>
|
||||
extractErrorMessageWithId({ status: 400, data } as never, ErrorExtractorId.DYNATRACE_ERRORS)
|
||||
|
||||
it('names the offending parameter from constraintViolations', () => {
|
||||
expect(
|
||||
extract({
|
||||
error: {
|
||||
code: 400,
|
||||
message: 'Constraints violated.',
|
||||
constraintViolations: [
|
||||
{ path: 'metricSelector', message: "Unknown metric key 'builtin:bogus'." },
|
||||
],
|
||||
},
|
||||
})
|
||||
).toBe("Constraints violated. (metricSelector: Unknown metric key 'builtin:bogus'.)")
|
||||
})
|
||||
|
||||
it('joins several violations', () => {
|
||||
expect(
|
||||
extract({
|
||||
error: {
|
||||
message: 'Constraints violated.',
|
||||
constraintViolations: [
|
||||
{ path: 'from', message: 'Invalid timeframe.' },
|
||||
{ path: 'pageSize', message: 'Must be at most 500.' },
|
||||
],
|
||||
},
|
||||
})
|
||||
).toBe('Constraints violated. (from: Invalid timeframe.; pageSize: Must be at most 500.)')
|
||||
})
|
||||
|
||||
it('falls back to the bare message when there are no violations', () => {
|
||||
expect(extract({ error: { code: 404, message: 'Problem not found.' } })).toBe(
|
||||
'Problem not found.'
|
||||
)
|
||||
})
|
||||
|
||||
it('every Dynatrace tool pins the extractor so selection is deterministic', () => {
|
||||
for (const tool of [
|
||||
listProblemsTool,
|
||||
getProblemTool,
|
||||
getEntityTool,
|
||||
getMetricTool,
|
||||
getAuditLogsTool,
|
||||
ingestEventTool,
|
||||
ingestLogsTool,
|
||||
]) {
|
||||
expect(tool.errorExtractor).toBe(ErrorExtractorId.DYNATRACE_ERRORS)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('response mapping', () => {
|
||||
it('flattens nested EntityStub ids and normalizes absent optional blocks', async () => {
|
||||
const response = new Response(
|
||||
JSON.stringify({
|
||||
totalCount: 1,
|
||||
pageSize: 50,
|
||||
nextPageKey: null,
|
||||
problems: [
|
||||
{
|
||||
problemId: 'P-1',
|
||||
title: 'CPU saturation',
|
||||
status: 'OPEN',
|
||||
endTime: -1,
|
||||
rootCauseEntity: { entityId: { id: 'HOST-1', type: 'HOST' }, name: 'web-01' },
|
||||
affectedEntities: [{ entityId: { id: 'SERVICE-1', type: 'SERVICE' }, name: 'api' }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ status: 200 }
|
||||
)
|
||||
|
||||
const result = await listProblemsTool.transformResponse!(response)
|
||||
const problem = result.output.problems[0]
|
||||
|
||||
expect(problem.rootCauseEntity).toEqual({ id: 'HOST-1', type: 'HOST', name: 'web-01' })
|
||||
expect(problem.affectedEntities).toEqual([{ id: 'SERVICE-1', type: 'SERVICE', name: 'api' }])
|
||||
expect(problem.endTime).toBe(-1)
|
||||
expect(problem.impactedEntities).toEqual([])
|
||||
expect(problem.evidenceDetails).toBeNull()
|
||||
expect(result.output.nextPageKey).toBeNull()
|
||||
expect(result.output.warnings).toEqual([])
|
||||
})
|
||||
|
||||
it('lifts the dotted dt.settings keys of an audit entry into camelCase', async () => {
|
||||
const response = new Response(
|
||||
JSON.stringify({
|
||||
auditLogs: [
|
||||
{
|
||||
logId: 'L-1',
|
||||
user: 'someone@example.com',
|
||||
success: true,
|
||||
'dt.settings.schema_id': 'builtin:alerting.profile',
|
||||
'dt.settings.object_id': 'OBJ-1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ status: 200 }
|
||||
)
|
||||
|
||||
const result = await getAuditLogsTool.transformResponse!(response)
|
||||
expect(result.output.auditLogs[0].settingsSchemaId).toBe('builtin:alerting.profile')
|
||||
expect(result.output.auditLogs[0].settingsObjectId).toBe('OBJ-1')
|
||||
expect(result.output.auditLogs[0].message).toBeNull()
|
||||
})
|
||||
|
||||
it('reads a 204 log ingestion as fully accepted despite the empty body', async () => {
|
||||
const result = await ingestLogsTool.transformResponse!(new Response(null, { status: 204 }))
|
||||
expect(result.output).toEqual({ accepted: true, statusCode: 204, details: null })
|
||||
})
|
||||
|
||||
/**
|
||||
* A wrong top-level key does not throw — it yields an empty list and looks like
|
||||
* "no results". Each payload below is shaped exactly like the documented schema,
|
||||
* so an incorrect key fails loudly here instead of silently in production.
|
||||
*/
|
||||
it('reads the documented top-level key of every list response', async () => {
|
||||
const cases: Array<{
|
||||
name: string
|
||||
tool: { transformResponse?: (r: Response) => Promise<{ output: Record<string, never> }> }
|
||||
payload: Record<string, unknown>
|
||||
read: (out: Record<string, never>) => unknown
|
||||
}> = [
|
||||
{
|
||||
name: 'GET /slo -> slo',
|
||||
tool: listSlosTool,
|
||||
payload: { totalCount: 1, slo: [{ id: 'SLO-1', name: 'Checkout', status: 'WARNING' }] },
|
||||
read: (o) => o.slos,
|
||||
},
|
||||
{
|
||||
name: 'GET /metrics/query -> result',
|
||||
tool: queryMetricsTool,
|
||||
payload: {
|
||||
resolution: '1h',
|
||||
result: [
|
||||
{
|
||||
metricId: 'builtin:host.cpu.usage',
|
||||
data: [{ dimensions: ['HOST-1'], timestamps: [1], values: [42.5] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
read: (o) => o.result,
|
||||
},
|
||||
{
|
||||
name: 'GET /metrics -> metrics',
|
||||
tool: listMetricsTool,
|
||||
payload: { totalCount: 1, metrics: [{ metricId: 'builtin:host.cpu.usage' }] },
|
||||
read: (o) => o.metrics,
|
||||
},
|
||||
{
|
||||
name: 'GET /entities -> entities',
|
||||
tool: listEntitiesTool,
|
||||
payload: { totalCount: 1, entities: [{ entityId: 'HOST-1', type: 'HOST' }] },
|
||||
read: (o) => o.entities,
|
||||
},
|
||||
{
|
||||
name: 'GET /entityTypes -> types',
|
||||
tool: listEntityTypesTool,
|
||||
payload: { totalCount: 1, types: [{ type: 'HOST', displayName: 'Host' }] },
|
||||
read: (o) => o.types,
|
||||
},
|
||||
{
|
||||
name: 'GET /events -> events',
|
||||
tool: listEventsTool,
|
||||
payload: { totalCount: 1, events: [{ eventId: 'E-1', eventType: 'CUSTOM_DEPLOYMENT' }] },
|
||||
read: (o) => o.events,
|
||||
},
|
||||
{
|
||||
name: 'GET /securityProblems -> securityProblems',
|
||||
tool: listSecurityProblemsTool,
|
||||
payload: {
|
||||
totalCount: 1,
|
||||
securityProblems: [{ securityProblemId: 'S-1', status: 'OPEN' }],
|
||||
},
|
||||
read: (o) => o.securityProblems,
|
||||
},
|
||||
{
|
||||
name: 'GET /logs/search -> results',
|
||||
tool: searchLogsTool,
|
||||
payload: { sliceSize: 1, results: [{ timestamp: 1, status: 'ERROR', content: 'boom' }] },
|
||||
read: (o) => o.results,
|
||||
},
|
||||
{
|
||||
name: 'GET /problems/{id}/comments -> comments',
|
||||
tool: listProblemCommentsTool,
|
||||
payload: { totalCount: 1, comments: [{ id: 'C-1', content: 'looking into it' }] },
|
||||
read: (o) => o.comments,
|
||||
},
|
||||
{
|
||||
name: 'GET /auditlogs -> auditLogs',
|
||||
tool: getAuditLogsTool,
|
||||
payload: { totalCount: 1, auditLogs: [{ logId: 'L-1' }] },
|
||||
read: (o) => o.auditLogs,
|
||||
},
|
||||
]
|
||||
|
||||
for (const { name, tool, payload, read } of cases) {
|
||||
const out = (
|
||||
await tool.transformResponse!(new Response(JSON.stringify(payload), { status: 200 }))
|
||||
).output
|
||||
expect(read(out), `${name} produced an empty list`).toHaveLength(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('reads the documented scalar keys of the ingest and single-entity responses', async () => {
|
||||
const metrics = (
|
||||
await ingestMetricsTool.transformResponse!(
|
||||
new Response(JSON.stringify({ linesOk: 7, linesInvalid: 1, error: { code: 400 } }), {
|
||||
status: 202,
|
||||
})
|
||||
)
|
||||
).output
|
||||
expect(metrics.linesOk).toBe(7)
|
||||
expect(metrics.linesInvalid).toBe(1)
|
||||
expect(metrics.ingestError).toEqual({ code: 400 })
|
||||
|
||||
const event = (
|
||||
await ingestEventTool.transformResponse!(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
reportCount: 1,
|
||||
eventIngestResults: [{ correlationId: 'c-1', status: 'OK' }],
|
||||
}),
|
||||
{ status: 201 }
|
||||
)
|
||||
)
|
||||
).output
|
||||
expect(event.reportCount).toBe(1)
|
||||
expect(event.eventIngestResults).toEqual([{ correlationId: 'c-1', status: 'OK' }])
|
||||
|
||||
// Single-entity endpoints return the object at the document root, not nested.
|
||||
const slo = (
|
||||
await getSloTool.transformResponse!(
|
||||
new Response(JSON.stringify({ id: 'SLO-1', name: 'Checkout', evaluatedPercentage: 99.5 }), {
|
||||
status: 200,
|
||||
})
|
||||
)
|
||||
).output
|
||||
expect(slo.slo.id).toBe('SLO-1')
|
||||
expect(slo.slo.evaluatedPercentage).toBe(99.5)
|
||||
|
||||
const entity = (
|
||||
await getEntityTool.transformResponse!(
|
||||
new Response(JSON.stringify({ entityId: 'HOST-1', displayName: 'web-01' }), { status: 200 })
|
||||
)
|
||||
).output
|
||||
expect(entity.entity.entityId).toBe('HOST-1')
|
||||
|
||||
const closed = (
|
||||
await closeProblemTool.transformResponse!(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
problemId: 'P-1',
|
||||
closeTimestamp: 123,
|
||||
closing: true,
|
||||
comment: { id: 'C-1', content: 'fixed' },
|
||||
}),
|
||||
{ status: 200 }
|
||||
)
|
||||
)
|
||||
).output
|
||||
expect(closed.problemId).toBe('P-1')
|
||||
expect(closed.comment?.content).toBe('fixed')
|
||||
})
|
||||
|
||||
it('raises on a non-JSON body instead of reporting an empty result', async () => {
|
||||
// A gateway HTML page or a truncated payload must not read as "no results".
|
||||
const html = new Response('<html><body>502 Bad Gateway</body></html>', {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'text/html' },
|
||||
})
|
||||
await expect(listProblemsTool.transformResponse!(html)).rejects.toThrow(/non-JSON body/)
|
||||
})
|
||||
|
||||
it('still tolerates the genuinely empty bodies of 201 and 204', async () => {
|
||||
const created = await listProblemCommentsTool.transformResponse!(
|
||||
new Response(null, { status: 204 })
|
||||
)
|
||||
expect(created.output.comments).toEqual([])
|
||||
})
|
||||
|
||||
it('surfaces a 200 partial-success log ingestion body', async () => {
|
||||
const response = new Response(JSON.stringify({ error: { message: 'some invalid' } }), {
|
||||
status: 200,
|
||||
})
|
||||
const result = await ingestLogsTool.transformResponse!(response)
|
||||
expect(result.output.accepted).toBe(false)
|
||||
expect(result.output.details).toEqual({ error: { message: 'some invalid' } })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,170 @@
|
||||
import { nextPageKeyOutput, pageSizeOutput, totalCountOutput } from '@/tools/dynatrace/outputs'
|
||||
import type {
|
||||
DynatraceGetAuditLogsParams,
|
||||
DynatraceGetAuditLogsResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
mapAuditLog,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getAuditLogsTool: ToolConfig<
|
||||
DynatraceGetAuditLogsParams,
|
||||
DynatraceGetAuditLogsResponse
|
||||
> = {
|
||||
id: 'dynatrace_get_audit_logs',
|
||||
name: 'Dynatrace Get Audit Logs',
|
||||
description:
|
||||
'Read the Dynatrace audit log — who changed which configuration, when, and whether it succeeded.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the auditLogs.read scope',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Audit log filter, e.g. eventType("UPDATE"),user("someone@example.com"),category("CONFIG")',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-1d. Defaults to now-2w',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End of the timeframe in the same formats as From. Defaults to now',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'timestamp for oldest first, or -timestamp for newest first (default)',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Entries per page (max 5000, default 1000)',
|
||||
},
|
||||
nextPageKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Cursor for the next page. All other filters are ignored when it is set',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
params.nextPageKey
|
||||
? buildDynatraceUrl(params.environmentUrl, '/auditlogs', {
|
||||
nextPageKey: params.nextPageKey,
|
||||
})
|
||||
: buildDynatraceUrl(params.environmentUrl, '/auditlogs', {
|
||||
filter: params.filter,
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
sort: params.sort,
|
||||
pageSize: params.pageSize,
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
const auditLogs = Array.isArray(data.auditLogs)
|
||||
? (data.auditLogs as Array<Record<string, unknown>>)
|
||||
: []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
auditLogs: auditLogs.map(mapAuditLog),
|
||||
totalCount: (data.totalCount as number) ?? null,
|
||||
pageSize: (data.pageSize as number) ?? null,
|
||||
nextPageKey: (data.nextPageKey as string) ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
auditLogs: {
|
||||
type: 'array',
|
||||
description: 'Matching audit log entries',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
logId: { type: 'string', description: 'Audit log entry ID' },
|
||||
eventType: { type: 'string', description: 'Type of the audited change' },
|
||||
category: { type: 'string', description: 'Category of the audited change' },
|
||||
entityId: { type: 'string', description: 'ID of the changed entity', nullable: true },
|
||||
environmentId: { type: 'string', description: 'Environment the change happened in' },
|
||||
user: { type: 'string', description: 'User or token that made the change' },
|
||||
userType: { type: 'string', description: 'Type of the acting user' },
|
||||
userOrigin: { type: 'string', description: 'Origin of the request', nullable: true },
|
||||
timestamp: { type: 'number', description: 'Change timestamp in UTC milliseconds' },
|
||||
success: { type: 'boolean', description: 'Whether the change succeeded' },
|
||||
message: { type: 'string', description: 'Description of the change', nullable: true },
|
||||
patch: { type: 'json', description: 'JSON patch of the change', nullable: true },
|
||||
settingsSchemaId: {
|
||||
type: 'string',
|
||||
description: 'Settings schema ID (dt.settings.schema_id)',
|
||||
nullable: true,
|
||||
},
|
||||
settingsScopeId: {
|
||||
type: 'string',
|
||||
description: 'Settings scope ID (dt.settings.scope_id)',
|
||||
nullable: true,
|
||||
},
|
||||
settingsKey: {
|
||||
type: 'string',
|
||||
description: 'Settings key (dt.settings.key)',
|
||||
nullable: true,
|
||||
},
|
||||
settingsObjectId: {
|
||||
type: 'string',
|
||||
description: 'Settings object ID (dt.settings.object_id)',
|
||||
nullable: true,
|
||||
},
|
||||
settingsObjectSummary: {
|
||||
type: 'string',
|
||||
description: 'Settings object summary (dt.settings.object_summary)',
|
||||
nullable: true,
|
||||
},
|
||||
settingsScopeName: {
|
||||
type: 'string',
|
||||
description: 'Settings scope name (dt.settings.scope_name)',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalCount: totalCountOutput,
|
||||
pageSize: pageSizeOutput,
|
||||
nextPageKey: nextPageKeyOutput,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { entityProperties } from '@/tools/dynatrace/outputs'
|
||||
import type { DynatraceGetEntityParams, DynatraceGetEntityResponse } from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
encodeDynatraceId,
|
||||
mapEntity,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getEntityTool: ToolConfig<DynatraceGetEntityParams, DynatraceGetEntityResponse> = {
|
||||
id: 'dynatrace_get_entity',
|
||||
name: 'Dynatrace Get Entity',
|
||||
description:
|
||||
'Get the properties, tags, management zones, and relationships of a single monitored entity.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the entities.read scope',
|
||||
},
|
||||
entityId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the entity (e.g., HOST-06F288EE2A930951)',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-3d. Defaults to now-3d',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End of the timeframe in the same formats as From. Defaults to now',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated additional entity properties to include (e.g. +lastSeenTms,+properties.BITNESS)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildDynatraceUrl(params.environmentUrl, `/entities/${encodeDynatraceId(params.entityId)}`, {
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
fields: params.fields,
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
entity: mapEntity(data),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
entity: {
|
||||
type: 'object',
|
||||
description: 'The requested monitored entity',
|
||||
properties: entityProperties,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { eventProperties } from '@/tools/dynatrace/outputs'
|
||||
import type { DynatraceGetEventParams, DynatraceGetEventResponse } from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
encodeDynatraceId,
|
||||
mapEvent,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getEventTool: ToolConfig<DynatraceGetEventParams, DynatraceGetEventResponse> = {
|
||||
id: 'dynatrace_get_event',
|
||||
name: 'Dynatrace Get Event',
|
||||
description: 'Get the full details of a single Dynatrace event.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the events.read scope',
|
||||
},
|
||||
eventId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the event',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildDynatraceUrl(params.environmentUrl, `/events/${encodeDynatraceId(params.eventId)}`),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
event: mapEvent(data),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
event: {
|
||||
type: 'object',
|
||||
description: 'The requested event',
|
||||
properties: eventProperties,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { metricDescriptorProperties } from '@/tools/dynatrace/outputs'
|
||||
import type { DynatraceGetMetricParams, DynatraceGetMetricResponse } from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
encodeDynatracePathSegment,
|
||||
mapMetricDescriptor,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getMetricTool: ToolConfig<DynatraceGetMetricParams, DynatraceGetMetricResponse> = {
|
||||
id: 'dynatrace_get_metric',
|
||||
name: 'Dynatrace Get Metric Descriptor',
|
||||
description:
|
||||
'Get the descriptor of a single Dynatrace metric — its unit, dimensions, supported aggregations, and transformations.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the metrics.read scope',
|
||||
},
|
||||
metricKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Metric key, optionally followed by transformation operators separated by a colon (e.g. builtin:host.cpu.usage)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildDynatraceUrl(
|
||||
params.environmentUrl,
|
||||
`/metrics/${encodeDynatracePathSegment(params.metricKey)}`
|
||||
),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
metric: mapMetricDescriptor(data),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
metric: {
|
||||
type: 'object',
|
||||
description: 'The requested metric descriptor',
|
||||
properties: metricDescriptorProperties,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { problemProperties } from '@/tools/dynatrace/outputs'
|
||||
import type {
|
||||
DynatraceGetProblemParams,
|
||||
DynatraceGetProblemResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
encodeDynatraceId,
|
||||
mapProblem,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getProblemTool: ToolConfig<DynatraceGetProblemParams, DynatraceGetProblemResponse> = {
|
||||
id: 'dynatrace_get_problem',
|
||||
name: 'Dynatrace Get Problem',
|
||||
description:
|
||||
'Get the full details of a single Dynatrace problem, including root cause, affected entities, and optionally its evidence and impact analysis.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the problems.read scope',
|
||||
},
|
||||
problemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the problem (e.g., -1234567890123456789_1700000000000V2)',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated optional properties to include: evidenceDetails, impactAnalysis, recentComments',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildDynatraceUrl(params.environmentUrl, `/problems/${encodeDynatraceId(params.problemId)}`, {
|
||||
fields: params.fields,
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
problem: mapProblem(data),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
problem: {
|
||||
type: 'object',
|
||||
description: 'The requested problem',
|
||||
properties: problemProperties,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { securityProblemDetailsProperties } from '@/tools/dynatrace/outputs'
|
||||
import type {
|
||||
DynatraceGetSecurityProblemParams,
|
||||
DynatraceGetSecurityProblemResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
encodeDynatraceId,
|
||||
mapSecurityProblemDetails,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getSecurityProblemTool: ToolConfig<
|
||||
DynatraceGetSecurityProblemParams,
|
||||
DynatraceGetSecurityProblemResponse
|
||||
> = {
|
||||
id: 'dynatrace_get_security_problem',
|
||||
name: 'Dynatrace Get Security Problem',
|
||||
description:
|
||||
'Get a single vulnerability with its description, remediation guidance, affected entities, and risk assessment.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the securityProblems.read scope',
|
||||
},
|
||||
securityProblemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the security problem',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated optional properties to include: +riskAssessment, +managementZones, +codeLevelVulnerabilityDetails, +globalCounts',
|
||||
},
|
||||
managementZoneFilter: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Restrict the counts to management zones, e.g. names("Production")',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-24h. Defaults to the last 24 hours',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildDynatraceUrl(
|
||||
params.environmentUrl,
|
||||
`/securityProblems/${encodeDynatraceId(params.securityProblemId)}`,
|
||||
{
|
||||
fields: params.fields,
|
||||
managementZoneFilter: params.managementZoneFilter,
|
||||
from: params.from,
|
||||
}
|
||||
),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
securityProblem: mapSecurityProblemDetails(data),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
securityProblem: {
|
||||
type: 'object',
|
||||
description: 'The requested security problem',
|
||||
properties: securityProblemDetailsProperties,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { sloProperties } from '@/tools/dynatrace/outputs'
|
||||
import type { DynatraceGetSloParams, DynatraceGetSloResponse } from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
encodeDynatraceId,
|
||||
mapSlo,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getSloTool: ToolConfig<DynatraceGetSloParams, DynatraceGetSloResponse> = {
|
||||
id: 'dynatrace_get_slo',
|
||||
name: 'Dynatrace Get SLO',
|
||||
description:
|
||||
'Get a single service-level objective with its evaluated attainment, error budget, and burn rate.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the slo.read scope',
|
||||
},
|
||||
sloId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the SLO',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start of the evaluation timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-7d. Defaults to now-2w',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End of the evaluation timeframe in the same formats as From. Defaults to now',
|
||||
},
|
||||
timeFrame: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'CURRENT evaluates the SLO over its own timeframe; GTF evaluates over the From/To range',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildDynatraceUrl(params.environmentUrl, `/slo/${encodeDynatraceId(params.sloId)}`, {
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
timeFrame: params.timeFrame,
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
slo: mapSlo(data),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
slo: {
|
||||
type: 'object',
|
||||
description: 'The requested service-level objective',
|
||||
properties: sloProperties,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { addProblemCommentTool } from '@/tools/dynatrace/add_problem_comment'
|
||||
import { closeProblemTool } from '@/tools/dynatrace/close_problem'
|
||||
import { getAuditLogsTool } from '@/tools/dynatrace/get_audit_logs'
|
||||
import { getEntityTool } from '@/tools/dynatrace/get_entity'
|
||||
import { getEventTool } from '@/tools/dynatrace/get_event'
|
||||
import { getMetricTool } from '@/tools/dynatrace/get_metric'
|
||||
import { getProblemTool } from '@/tools/dynatrace/get_problem'
|
||||
import { getSecurityProblemTool } from '@/tools/dynatrace/get_security_problem'
|
||||
import { getSloTool } from '@/tools/dynatrace/get_slo'
|
||||
import { ingestEventTool } from '@/tools/dynatrace/ingest_event'
|
||||
import { ingestLogsTool } from '@/tools/dynatrace/ingest_logs'
|
||||
import { ingestMetricsTool } from '@/tools/dynatrace/ingest_metrics'
|
||||
import { listEntitiesTool } from '@/tools/dynatrace/list_entities'
|
||||
import { listEntityTypesTool } from '@/tools/dynatrace/list_entity_types'
|
||||
import { listEventsTool } from '@/tools/dynatrace/list_events'
|
||||
import { listMetricsTool } from '@/tools/dynatrace/list_metrics'
|
||||
import { listProblemCommentsTool } from '@/tools/dynatrace/list_problem_comments'
|
||||
import { listProblemsTool } from '@/tools/dynatrace/list_problems'
|
||||
import { listSecurityProblemsTool } from '@/tools/dynatrace/list_security_problems'
|
||||
import { listSlosTool } from '@/tools/dynatrace/list_slos'
|
||||
import { queryMetricsTool } from '@/tools/dynatrace/query_metrics'
|
||||
import { searchLogsTool } from '@/tools/dynatrace/search_logs'
|
||||
|
||||
export const dynatraceAddProblemCommentTool = addProblemCommentTool
|
||||
export const dynatraceCloseProblemTool = closeProblemTool
|
||||
export const dynatraceGetAuditLogsTool = getAuditLogsTool
|
||||
export const dynatraceGetEntityTool = getEntityTool
|
||||
export const dynatraceGetEventTool = getEventTool
|
||||
export const dynatraceGetMetricTool = getMetricTool
|
||||
export const dynatraceGetProblemTool = getProblemTool
|
||||
export const dynatraceGetSecurityProblemTool = getSecurityProblemTool
|
||||
export const dynatraceGetSloTool = getSloTool
|
||||
export const dynatraceIngestEventTool = ingestEventTool
|
||||
export const dynatraceIngestLogsTool = ingestLogsTool
|
||||
export const dynatraceIngestMetricsTool = ingestMetricsTool
|
||||
export const dynatraceListEntitiesTool = listEntitiesTool
|
||||
export const dynatraceListEntityTypesTool = listEntityTypesTool
|
||||
export const dynatraceListEventsTool = listEventsTool
|
||||
export const dynatraceListMetricsTool = listMetricsTool
|
||||
export const dynatraceListProblemCommentsTool = listProblemCommentsTool
|
||||
export const dynatraceListProblemsTool = listProblemsTool
|
||||
export const dynatraceListSecurityProblemsTool = listSecurityProblemsTool
|
||||
export const dynatraceListSlosTool = listSlosTool
|
||||
export const dynatraceQueryMetricsTool = queryMetricsTool
|
||||
export const dynatraceSearchLogsTool = searchLogsTool
|
||||
@@ -0,0 +1,147 @@
|
||||
import type {
|
||||
DynatraceIngestEventParams,
|
||||
DynatraceIngestEventResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
parseJsonParam,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const ingestEventTool: ToolConfig<DynatraceIngestEventParams, DynatraceIngestEventResponse> =
|
||||
{
|
||||
id: 'dynatrace_ingest_event',
|
||||
name: 'Dynatrace Ingest Event',
|
||||
description:
|
||||
'Push an event into Dynatrace — a deployment marker, custom annotation, or custom alert — attached to the entities matched by an entity selector.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the events.ingest scope',
|
||||
},
|
||||
eventType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'One of AVAILABILITY_EVENT, CUSTOM_ALERT, CUSTOM_ANNOTATION, CUSTOM_CONFIGURATION, CUSTOM_DEPLOYMENT, CUSTOM_INFO, ERROR_EVENT, MARKED_FOR_TERMINATION, PERFORMANCE_EVENT, RESOURCE_CONTENTION_EVENT, WARNING',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Title of the event',
|
||||
},
|
||||
entitySelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Entity selector for the entities to attach the event to. Defaults to the environment entity',
|
||||
},
|
||||
startTime: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Event start in UTC milliseconds. Defaults to now',
|
||||
},
|
||||
endTime: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Event end in UTC milliseconds. Defaults to the start time plus the timeout',
|
||||
},
|
||||
eventTimeout: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Minutes the event stays open when no end time is given. Defaults to 15, capped at 360',
|
||||
},
|
||||
properties: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Event properties as a key-value object. Max 100 entries, keys up to 100 and values up to 4096 characters',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => buildDynatraceUrl(params.environmentUrl, '/events/ingest'),
|
||||
method: 'POST',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
eventType: params.eventType,
|
||||
title: params.title,
|
||||
}
|
||||
if (params.entitySelector) body.entitySelector = params.entitySelector
|
||||
if (params.startTime !== undefined) body.startTime = params.startTime
|
||||
if (params.endTime !== undefined) body.endTime = params.endTime
|
||||
if (params.eventTimeout !== undefined) body.timeout = params.eventTimeout
|
||||
const properties = parseJsonParam(params.properties)
|
||||
if (properties) body.properties = properties
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
const results = Array.isArray(data.eventIngestResults)
|
||||
? (data.eventIngestResults as Array<Record<string, unknown>>)
|
||||
: []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
reportCount: (data.reportCount as number) ?? null,
|
||||
eventIngestResults: results.map((result) => ({
|
||||
correlationId: (result.correlationId as string) ?? null,
|
||||
status: (result.status as string) ?? null,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
reportCount: {
|
||||
type: 'number',
|
||||
description: 'Number of events Dynatrace reported',
|
||||
nullable: true,
|
||||
},
|
||||
eventIngestResults: {
|
||||
type: 'array',
|
||||
description: 'One result per ingested event',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
correlationId: {
|
||||
type: 'string',
|
||||
description: 'Correlation ID of the ingested event',
|
||||
nullable: true,
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'OK, INVALID_ENTITY_TYPE, INVALID_METADATA, or INVALID_TIMESTAMPS',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import type {
|
||||
DynatraceIngestLogsParams,
|
||||
DynatraceIngestLogsResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
parseJsonParam,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const ingestLogsTool: ToolConfig<DynatraceIngestLogsParams, DynatraceIngestLogsResponse> = {
|
||||
id: 'dynatrace_ingest_logs',
|
||||
name: 'Dynatrace Ingest Logs',
|
||||
description:
|
||||
'Push log events into Dynatrace. Accepts a single log event object or an array of them.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the logs.ingest scope',
|
||||
},
|
||||
logs: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'A log event object, or an array of them. Recognized keys are content, timestamp, and severity; any other key becomes a custom attribute (e.g. [{"content":"Deploy finished","severity":"info","service":"checkout"}])',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => buildDynatraceUrl(params.environmentUrl, '/logs/ingest'),
|
||||
method: 'POST',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
body: (params) => {
|
||||
const logs = parseJsonParam(params.logs)
|
||||
// Dynatrace answers 204 for an empty array, which would report a send that
|
||||
// never happened as a success. Fail loudly instead.
|
||||
if (logs === undefined || (Array.isArray(logs) && logs.length === 0)) {
|
||||
throw new Error('logs must contain at least one log event')
|
||||
}
|
||||
return JSON.stringify(logs)
|
||||
},
|
||||
},
|
||||
|
||||
/** Ingestion answers 204 with no body, or 200 with a partial-success body. */
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
const hasDetails = Object.keys(data).length > 0
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
accepted: response.status === 204,
|
||||
statusCode: response.status,
|
||||
details: hasDetails ? data : null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
accepted: {
|
||||
type: 'boolean',
|
||||
description: 'True when Dynatrace accepted every log event (HTTP 204)',
|
||||
},
|
||||
statusCode: {
|
||||
type: 'number',
|
||||
description: 'HTTP status Dynatrace returned. 204 is full success, 200 is partial success',
|
||||
},
|
||||
details: {
|
||||
type: 'json',
|
||||
description: 'Partial-success details, present only when some events were rejected',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import type {
|
||||
DynatraceIngestMetricsParams,
|
||||
DynatraceIngestMetricsResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import { buildDynatraceUrl, dynatraceHeaders, readJsonBody } from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const ingestMetricsTool: ToolConfig<
|
||||
DynatraceIngestMetricsParams,
|
||||
DynatraceIngestMetricsResponse
|
||||
> = {
|
||||
id: 'dynatrace_ingest_metrics',
|
||||
name: 'Dynatrace Ingest Metrics',
|
||||
description:
|
||||
'Push custom metric data points into Dynatrace using the metric line protocol, one data point per line.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the metrics.ingest scope',
|
||||
},
|
||||
payload: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Metric line protocol payload, one data point per line, max 1 MB (e.g. cpu.temperature,dt.entity.host=HOST-06F288EE2A930951,cpu=1 55)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => buildDynatraceUrl(params.environmentUrl, '/metrics/ingest'),
|
||||
method: 'POST',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken, 'text/plain'),
|
||||
body: (params) => params.payload,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
linesOk: (data.linesOk as number) ?? null,
|
||||
linesInvalid: (data.linesInvalid as number) ?? null,
|
||||
ingestError: (data.error as Record<string, unknown>) ?? null,
|
||||
warnings: (data.warnings as Record<string, unknown>) ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
linesOk: {
|
||||
type: 'number',
|
||||
description: 'Number of accepted data points',
|
||||
nullable: true,
|
||||
},
|
||||
linesInvalid: {
|
||||
type: 'number',
|
||||
description: 'Number of rejected data points',
|
||||
nullable: true,
|
||||
},
|
||||
ingestError: {
|
||||
type: 'json',
|
||||
description: 'Details of the invalid lines, with the line number and reason for each',
|
||||
nullable: true,
|
||||
},
|
||||
warnings: {
|
||||
type: 'json',
|
||||
description: 'Warnings raised during ingestion, such as changed metric keys',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import {
|
||||
entityProperties,
|
||||
nextPageKeyOutput,
|
||||
pageSizeOutput,
|
||||
totalCountOutput,
|
||||
} from '@/tools/dynatrace/outputs'
|
||||
import type {
|
||||
DynatraceListEntitiesParams,
|
||||
DynatraceListEntitiesResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
mapEntity,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listEntitiesTool: ToolConfig<
|
||||
DynatraceListEntitiesParams,
|
||||
DynatraceListEntitiesResponse
|
||||
> = {
|
||||
id: 'dynatrace_list_entities',
|
||||
name: 'Dynatrace List Entities',
|
||||
description:
|
||||
'List monitored entities — hosts, services, applications, Kubernetes workloads — matching an entity selector.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the entities.read scope',
|
||||
},
|
||||
entitySelector: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Entity selector defining the scope, e.g. type("HOST"),tag("env:prod") or entityId("HOST-06F288EE2A930951")',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-3d. Defaults to now-3d',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End of the timeframe in the same formats as From. Defaults to now',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated additional entity properties to include (e.g. +lastSeenTms,+properties.BITNESS,+tags)',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Sort by display name: +displayName ascending or -displayName descending',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Entities per page (default 50)',
|
||||
},
|
||||
nextPageKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Cursor for the next page. All other filters are ignored when it is set',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
params.nextPageKey
|
||||
? buildDynatraceUrl(params.environmentUrl, '/entities', {
|
||||
nextPageKey: params.nextPageKey,
|
||||
})
|
||||
: buildDynatraceUrl(params.environmentUrl, '/entities', {
|
||||
entitySelector: params.entitySelector,
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
fields: params.fields,
|
||||
sort: params.sort,
|
||||
pageSize: params.pageSize,
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
const entities = Array.isArray(data.entities)
|
||||
? (data.entities as Array<Record<string, unknown>>)
|
||||
: []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
entities: entities.map(mapEntity),
|
||||
totalCount: (data.totalCount as number) ?? null,
|
||||
pageSize: (data.pageSize as number) ?? null,
|
||||
nextPageKey: (data.nextPageKey as string) ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
entities: {
|
||||
type: 'array',
|
||||
description: 'Matching monitored entities',
|
||||
items: { type: 'object', properties: entityProperties },
|
||||
},
|
||||
totalCount: totalCountOutput,
|
||||
pageSize: pageSizeOutput,
|
||||
nextPageKey: nextPageKeyOutput,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { nextPageKeyOutput, pageSizeOutput, totalCountOutput } from '@/tools/dynatrace/outputs'
|
||||
import type {
|
||||
DynatraceListEntityTypesParams,
|
||||
DynatraceListEntityTypesResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
mapEntityType,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listEntityTypesTool: ToolConfig<
|
||||
DynatraceListEntityTypesParams,
|
||||
DynatraceListEntityTypesResponse
|
||||
> = {
|
||||
id: 'dynatrace_list_entity_types',
|
||||
name: 'Dynatrace List Entity Types',
|
||||
description:
|
||||
'List the monitored entity types available in the environment, with the properties and relationships each supports. Use it to build valid entity selectors.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the entities.read scope',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Entity types per page (max 500, default 50)',
|
||||
},
|
||||
nextPageKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Cursor for the next page. Page size is ignored when it is set',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildDynatraceUrl(
|
||||
params.environmentUrl,
|
||||
'/entityTypes',
|
||||
params.nextPageKey ? { nextPageKey: params.nextPageKey } : { pageSize: params.pageSize }
|
||||
),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
const types = Array.isArray(data.types) ? (data.types as Array<Record<string, unknown>>) : []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
types: types.map(mapEntityType),
|
||||
totalCount: (data.totalCount as number) ?? null,
|
||||
pageSize: (data.pageSize as number) ?? null,
|
||||
nextPageKey: (data.nextPageKey as string) ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
types: {
|
||||
type: 'array',
|
||||
description: 'Available entity types',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: 'string', description: 'Entity type (e.g., HOST, SERVICE)' },
|
||||
displayName: { type: 'string', description: 'Display name of the type', nullable: true },
|
||||
dimensionKey: {
|
||||
type: 'string',
|
||||
description: 'Metric dimension key of the type',
|
||||
nullable: true,
|
||||
},
|
||||
entityLimitExceeded: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the environment exceeded the entity limit for this type',
|
||||
nullable: true,
|
||||
},
|
||||
properties: {
|
||||
type: 'array',
|
||||
description: 'Properties available on the type',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Property ID' },
|
||||
displayName: { type: 'string', description: 'Property display name' },
|
||||
type: { type: 'string', description: 'Property value type' },
|
||||
},
|
||||
},
|
||||
},
|
||||
fromRelationships: {
|
||||
type: 'array',
|
||||
description: 'Relationships originating at this type',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Relationship ID' },
|
||||
toTypes: {
|
||||
type: 'array',
|
||||
description: 'Entity types the relationship points to',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
toRelationships: {
|
||||
type: 'array',
|
||||
description: 'Relationships pointing at this type',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Relationship ID' },
|
||||
fromTypes: {
|
||||
type: 'array',
|
||||
description: 'Entity types the relationship originates from',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalCount: totalCountOutput,
|
||||
pageSize: pageSizeOutput,
|
||||
nextPageKey: nextPageKeyOutput,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
eventProperties,
|
||||
nextPageKeyOutput,
|
||||
pageSizeOutput,
|
||||
totalCountOutput,
|
||||
warningsOutput,
|
||||
} from '@/tools/dynatrace/outputs'
|
||||
import type {
|
||||
DynatraceListEventsParams,
|
||||
DynatraceListEventsResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
mapEvent,
|
||||
mapWarnings,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listEventsTool: ToolConfig<DynatraceListEventsParams, DynatraceListEventsResponse> = {
|
||||
id: 'dynatrace_list_events',
|
||||
name: 'Dynatrace List Events',
|
||||
description:
|
||||
'List events — deployments, availability changes, alerts, custom annotations — in a Dynatrace environment.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the events.read scope',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-1d. Defaults to now-2h',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End of the timeframe in the same formats as From. Defaults to now',
|
||||
},
|
||||
eventSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Event selector, e.g. eventType("CUSTOM_DEPLOYMENT"),status("OPEN"),correlationId("build-42")',
|
||||
},
|
||||
entitySelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity selector scoping the result, e.g. type("SERVICE"),tag("env:prod")',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Events per page (max 1000, default 100)',
|
||||
},
|
||||
nextPageKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Cursor for the next page. All other filters are ignored when it is set',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
params.nextPageKey
|
||||
? buildDynatraceUrl(params.environmentUrl, '/events', {
|
||||
nextPageKey: params.nextPageKey,
|
||||
})
|
||||
: buildDynatraceUrl(params.environmentUrl, '/events', {
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
eventSelector: params.eventSelector,
|
||||
entitySelector: params.entitySelector,
|
||||
pageSize: params.pageSize,
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
const events = Array.isArray(data.events) ? (data.events as Array<Record<string, unknown>>) : []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
events: events.map(mapEvent),
|
||||
totalCount: (data.totalCount as number) ?? null,
|
||||
pageSize: (data.pageSize as number) ?? null,
|
||||
nextPageKey: (data.nextPageKey as string) ?? null,
|
||||
warnings: mapWarnings(data.warnings),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
events: {
|
||||
type: 'array',
|
||||
description: 'Matching events',
|
||||
items: { type: 'object', properties: eventProperties },
|
||||
},
|
||||
totalCount: totalCountOutput,
|
||||
pageSize: pageSizeOutput,
|
||||
nextPageKey: nextPageKeyOutput,
|
||||
warnings: warningsOutput,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
metricDescriptorProperties,
|
||||
nextPageKeyOutput,
|
||||
totalCountOutput,
|
||||
warningsOutput,
|
||||
} from '@/tools/dynatrace/outputs'
|
||||
import type {
|
||||
DynatraceListMetricsParams,
|
||||
DynatraceListMetricsResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
mapMetricDescriptor,
|
||||
mapWarnings,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listMetricsTool: ToolConfig<DynatraceListMetricsParams, DynatraceListMetricsResponse> =
|
||||
{
|
||||
id: 'dynatrace_list_metrics',
|
||||
name: 'Dynatrace List Metrics',
|
||||
description:
|
||||
'Discover the metrics available in a Dynatrace environment, filtered by metric selector, free text, or metadata.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the metrics.read scope',
|
||||
},
|
||||
metricSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Metric selector, supporting wildcards (e.g. builtin:host.cpu.*)',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Free-text search across metric display names and descriptions',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated descriptor properties. Prefix with + to add a non-default property and - to drop a default one; metricId is always returned (e.g. +aggregationTypes,-description)',
|
||||
},
|
||||
writtenSince: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Only metrics written since this point, as UTC milliseconds, ISO 8601, or a relative expression such as now-7d',
|
||||
},
|
||||
writtenSinceMode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'INCLUDE (default) keeps metrics written since Written Since; EXCLUDE keeps the ones not written since then',
|
||||
},
|
||||
metadataSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Metadata selector, e.g. unit("Percent"),tags("dashboard")',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Metrics per page (max 500, default 100)',
|
||||
},
|
||||
nextPageKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Cursor for the next page. All other filters are ignored when it is set',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
params.nextPageKey
|
||||
? buildDynatraceUrl(params.environmentUrl, '/metrics', {
|
||||
nextPageKey: params.nextPageKey,
|
||||
})
|
||||
: buildDynatraceUrl(params.environmentUrl, '/metrics', {
|
||||
metricSelector: params.metricSelector,
|
||||
text: params.text,
|
||||
fields: params.fields,
|
||||
writtenSince: params.writtenSince,
|
||||
writtenSinceMode: params.writtenSinceMode,
|
||||
metadataSelector: params.metadataSelector,
|
||||
pageSize: params.pageSize,
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
const metrics = Array.isArray(data.metrics)
|
||||
? (data.metrics as Array<Record<string, unknown>>)
|
||||
: []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
metrics: metrics.map(mapMetricDescriptor),
|
||||
totalCount: (data.totalCount as number) ?? null,
|
||||
nextPageKey: (data.nextPageKey as string) ?? null,
|
||||
warnings: mapWarnings(data.warnings),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
metrics: {
|
||||
type: 'array',
|
||||
description: 'Matching metric descriptors',
|
||||
items: { type: 'object', properties: metricDescriptorProperties },
|
||||
},
|
||||
totalCount: totalCountOutput,
|
||||
nextPageKey: nextPageKeyOutput,
|
||||
warnings: warningsOutput,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import {
|
||||
commentProperties,
|
||||
nextPageKeyOutput,
|
||||
pageSizeOutput,
|
||||
totalCountOutput,
|
||||
} from '@/tools/dynatrace/outputs'
|
||||
import type {
|
||||
DynatraceComment,
|
||||
DynatraceListProblemCommentsParams,
|
||||
DynatraceListProblemCommentsResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
encodeDynatraceId,
|
||||
mapComment,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listProblemCommentsTool: ToolConfig<
|
||||
DynatraceListProblemCommentsParams,
|
||||
DynatraceListProblemCommentsResponse
|
||||
> = {
|
||||
id: 'dynatrace_list_problem_comments',
|
||||
name: 'Dynatrace List Problem Comments',
|
||||
description: 'List the comments recorded on a Dynatrace problem.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the problems.read scope',
|
||||
},
|
||||
problemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the problem whose comments should be listed',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Comments per page (max 500, default 10)',
|
||||
},
|
||||
nextPageKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Cursor for the next page. Page size is ignored when it is set',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildDynatraceUrl(
|
||||
params.environmentUrl,
|
||||
`/problems/${encodeDynatraceId(params.problemId)}/comments`,
|
||||
params.nextPageKey ? { nextPageKey: params.nextPageKey } : { pageSize: params.pageSize }
|
||||
),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
const comments = Array.isArray(data.comments)
|
||||
? (data.comments as Array<Record<string, unknown>>)
|
||||
: []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
comments: comments
|
||||
.map(mapComment)
|
||||
.filter((comment): comment is DynatraceComment => comment !== null),
|
||||
totalCount: (data.totalCount as number) ?? null,
|
||||
pageSize: (data.pageSize as number) ?? null,
|
||||
nextPageKey: (data.nextPageKey as string) ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
comments: {
|
||||
type: 'array',
|
||||
description: 'Comments on the problem',
|
||||
items: { type: 'object', properties: commentProperties },
|
||||
},
|
||||
totalCount: totalCountOutput,
|
||||
pageSize: pageSizeOutput,
|
||||
nextPageKey: nextPageKeyOutput,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
nextPageKeyOutput,
|
||||
pageSizeOutput,
|
||||
problemProperties,
|
||||
totalCountOutput,
|
||||
warningsOutput,
|
||||
} from '@/tools/dynatrace/outputs'
|
||||
import type {
|
||||
DynatraceListProblemsParams,
|
||||
DynatraceListProblemsResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
mapProblem,
|
||||
mapWarnings,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listProblemsTool: ToolConfig<
|
||||
DynatraceListProblemsParams,
|
||||
DynatraceListProblemsResponse
|
||||
> = {
|
||||
id: 'dynatrace_list_problems',
|
||||
name: 'Dynatrace List Problems',
|
||||
description:
|
||||
'List Davis-detected problems in a Dynatrace environment, filtered by timeframe, problem selector, or entity selector.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the problems.read scope',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-1d. Defaults to now-2h',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End of the timeframe in the same formats as From. Defaults to now',
|
||||
},
|
||||
problemSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Problem selector, e.g. status("open"),severityLevel("AVAILABILITY"),impactLevel("SERVICES")',
|
||||
},
|
||||
entitySelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity selector scoping the result, e.g. type("HOST"),tag("env:prod")',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Sort order: status, startTime, or relevance, each optionally prefixed with + or - (e.g. -startTime)',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated optional properties to include: evidenceDetails, impactAnalysis, recentComments',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Problems per page (max 500, default 50)',
|
||||
},
|
||||
nextPageKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Cursor for the next page. All other filters are ignored when it is set',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
params.nextPageKey
|
||||
? buildDynatraceUrl(params.environmentUrl, '/problems', {
|
||||
nextPageKey: params.nextPageKey,
|
||||
})
|
||||
: buildDynatraceUrl(params.environmentUrl, '/problems', {
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
problemSelector: params.problemSelector,
|
||||
entitySelector: params.entitySelector,
|
||||
sort: params.sort,
|
||||
fields: params.fields,
|
||||
pageSize: params.pageSize,
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
const problems = Array.isArray(data.problems)
|
||||
? (data.problems as Array<Record<string, unknown>>)
|
||||
: []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
problems: problems.map(mapProblem),
|
||||
totalCount: (data.totalCount as number) ?? null,
|
||||
pageSize: (data.pageSize as number) ?? null,
|
||||
nextPageKey: (data.nextPageKey as string) ?? null,
|
||||
warnings: mapWarnings(data.warnings),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
problems: {
|
||||
type: 'array',
|
||||
description: 'Matching problems',
|
||||
items: { type: 'object', properties: problemProperties },
|
||||
},
|
||||
totalCount: totalCountOutput,
|
||||
pageSize: pageSizeOutput,
|
||||
nextPageKey: nextPageKeyOutput,
|
||||
warnings: warningsOutput,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import {
|
||||
nextPageKeyOutput,
|
||||
pageSizeOutput,
|
||||
securityProblemProperties,
|
||||
totalCountOutput,
|
||||
} from '@/tools/dynatrace/outputs'
|
||||
import type {
|
||||
DynatraceListSecurityProblemsParams,
|
||||
DynatraceListSecurityProblemsResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
mapSecurityProblem,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listSecurityProblemsTool: ToolConfig<
|
||||
DynatraceListSecurityProblemsParams,
|
||||
DynatraceListSecurityProblemsResponse
|
||||
> = {
|
||||
id: 'dynatrace_list_security_problems',
|
||||
name: 'Dynatrace List Security Problems',
|
||||
description:
|
||||
'List vulnerabilities detected by Dynatrace Application Security, filtered by risk level, status, CVE, or technology.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the securityProblems.read scope',
|
||||
},
|
||||
securityProblemSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Security problem selector, e.g. status("OPEN"),riskLevel("CRITICAL"),technology("JAVA")',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-7d. Defaults to now-30d',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End of the timeframe in the same formats as From. Defaults to now',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated optional properties to include: +riskAssessment, +managementZones, +codeLevelVulnerabilityDetails, +globalCounts',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Sort by a field with a + or - prefix, e.g. -riskAssessment.riskScore',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Security problems per page (max 500, default 100)',
|
||||
},
|
||||
nextPageKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Cursor for the next page. All other filters are ignored when it is set',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
params.nextPageKey
|
||||
? buildDynatraceUrl(params.environmentUrl, '/securityProblems', {
|
||||
nextPageKey: params.nextPageKey,
|
||||
})
|
||||
: buildDynatraceUrl(params.environmentUrl, '/securityProblems', {
|
||||
securityProblemSelector: params.securityProblemSelector,
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
fields: params.fields,
|
||||
sort: params.sort,
|
||||
pageSize: params.pageSize,
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
const securityProblems = Array.isArray(data.securityProblems)
|
||||
? (data.securityProblems as Array<Record<string, unknown>>)
|
||||
: []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
securityProblems: securityProblems.map(mapSecurityProblem),
|
||||
totalCount: (data.totalCount as number) ?? null,
|
||||
pageSize: (data.pageSize as number) ?? null,
|
||||
nextPageKey: (data.nextPageKey as string) ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
securityProblems: {
|
||||
type: 'array',
|
||||
description: 'Matching security problems',
|
||||
items: { type: 'object', properties: securityProblemProperties },
|
||||
},
|
||||
totalCount: totalCountOutput,
|
||||
pageSize: pageSizeOutput,
|
||||
nextPageKey: nextPageKeyOutput,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
nextPageKeyOutput,
|
||||
pageSizeOutput,
|
||||
sloProperties,
|
||||
totalCountOutput,
|
||||
} from '@/tools/dynatrace/outputs'
|
||||
import type { DynatraceListSlosParams, DynatraceListSlosResponse } from '@/tools/dynatrace/types'
|
||||
import { buildDynatraceUrl, dynatraceHeaders, mapSlo, readJsonBody } from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listSlosTool: ToolConfig<DynatraceListSlosParams, DynatraceListSlosResponse> = {
|
||||
id: 'dynatrace_list_slos',
|
||||
name: 'Dynatrace List SLOs',
|
||||
description:
|
||||
'List service-level objectives with their current attainment, error budget, and burn rate.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the slo.read scope',
|
||||
},
|
||||
sloSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'SLO selector, e.g. healthState("WARNING"),text("checkout"),problems("true"). Combine criteria with commas',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start of the evaluation timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-7d',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End of the evaluation timeframe in the same formats as From. Defaults to now',
|
||||
},
|
||||
timeFrame: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'CURRENT evaluates each SLO over its own timeframe; GTF evaluates over the From/To range',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Sort by name: name ascending or -name descending',
|
||||
},
|
||||
enabledSlos: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Filter by enabled state: true, false, or all',
|
||||
},
|
||||
evaluate: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Evaluate each SLO and include its calculated values',
|
||||
},
|
||||
showGlobalSlos: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Include SLOs that are not scoped to a management zone',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'SLOs per page (max 10000, default 10)',
|
||||
},
|
||||
nextPageKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Cursor for the next page. All other filters are ignored when it is set',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
params.nextPageKey
|
||||
? buildDynatraceUrl(params.environmentUrl, '/slo', { nextPageKey: params.nextPageKey })
|
||||
: buildDynatraceUrl(params.environmentUrl, '/slo', {
|
||||
sloSelector: params.sloSelector,
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
timeFrame: params.timeFrame,
|
||||
sort: params.sort,
|
||||
enabledSlos: params.enabledSlos,
|
||||
evaluate: params.evaluate,
|
||||
showGlobalSlos: params.showGlobalSlos,
|
||||
pageSize: params.pageSize,
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
const slos = Array.isArray(data.slo) ? (data.slo as Array<Record<string, unknown>>) : []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
slos: slos.map(mapSlo),
|
||||
totalCount: (data.totalCount as number) ?? null,
|
||||
pageSize: (data.pageSize as number) ?? null,
|
||||
nextPageKey: (data.nextPageKey as string) ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
slos: {
|
||||
type: 'array',
|
||||
description: 'Matching service-level objectives',
|
||||
items: { type: 'object', properties: sloProperties },
|
||||
},
|
||||
totalCount: totalCountOutput,
|
||||
pageSize: pageSizeOutput,
|
||||
nextPageKey: nextPageKeyOutput,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
import type { OutputProperty } from '@/tools/types'
|
||||
|
||||
/** Flattened `EntityStub` shape produced by `mapEntityStub`. */
|
||||
export const entityStubProperties: Record<string, OutputProperty> = {
|
||||
id: { type: 'string', description: 'Entity ID', nullable: true },
|
||||
type: { type: 'string', description: 'Entity type', nullable: true },
|
||||
name: { type: 'string', description: 'Entity display name', nullable: true },
|
||||
}
|
||||
|
||||
/** `METag` shape. */
|
||||
export const tagProperties: Record<string, OutputProperty> = {
|
||||
context: { type: 'string', description: 'Tag origin (e.g., AWS, KUBERNETES, CONTEXTLESS)' },
|
||||
key: { type: 'string', description: 'Tag key' },
|
||||
value: { type: 'string', description: 'Tag value', nullable: true },
|
||||
stringRepresentation: { type: 'string', description: 'Tag rendered as a string' },
|
||||
}
|
||||
|
||||
/** Short management zone shape. */
|
||||
export const managementZoneProperties: Record<string, OutputProperty> = {
|
||||
id: { type: 'string', description: 'Management zone ID' },
|
||||
name: { type: 'string', description: 'Management zone name' },
|
||||
}
|
||||
|
||||
/** Problem comment shape. */
|
||||
export const commentProperties: Record<string, OutputProperty> = {
|
||||
id: { type: 'string', description: 'Comment ID' },
|
||||
authorName: { type: 'string', description: 'Name of the comment author' },
|
||||
content: { type: 'string', description: 'Comment text' },
|
||||
context: { type: 'string', description: 'Context of the comment', nullable: true },
|
||||
createdAtTimestamp: {
|
||||
type: 'number',
|
||||
description: 'Creation timestamp in UTC milliseconds',
|
||||
},
|
||||
}
|
||||
|
||||
/** Problem shape produced by `mapProblem`. */
|
||||
export const problemProperties: Record<string, OutputProperty> = {
|
||||
problemId: { type: 'string', description: 'Problem ID' },
|
||||
displayId: { type: 'string', description: 'Human-readable problem ID (e.g., P-2401234)' },
|
||||
title: { type: 'string', description: 'Problem title' },
|
||||
status: { type: 'string', description: 'Problem status: OPEN or CLOSED' },
|
||||
severityLevel: {
|
||||
type: 'string',
|
||||
description:
|
||||
'AVAILABILITY, CUSTOM_ALERT, ERROR, INFO, MONITORING_UNAVAILABLE, PERFORMANCE, or RESOURCE_CONTENTION',
|
||||
},
|
||||
impactLevel: {
|
||||
type: 'string',
|
||||
description: 'APPLICATION, ENVIRONMENT, INFRASTRUCTURE, or SERVICES',
|
||||
},
|
||||
startTime: { type: 'number', description: 'Problem start in UTC milliseconds' },
|
||||
endTime: {
|
||||
type: 'number',
|
||||
description: 'Problem end in UTC milliseconds, or -1 while the problem is open',
|
||||
},
|
||||
rootCauseEntity: {
|
||||
type: 'object',
|
||||
description: 'Entity Dynatrace determined to be the root cause',
|
||||
nullable: true,
|
||||
properties: entityStubProperties,
|
||||
},
|
||||
affectedEntities: {
|
||||
type: 'array',
|
||||
description: 'Entities affected by the problem',
|
||||
items: { type: 'object', properties: entityStubProperties },
|
||||
},
|
||||
impactedEntities: {
|
||||
type: 'array',
|
||||
description: 'Entities impacted by the problem',
|
||||
items: { type: 'object', properties: entityStubProperties },
|
||||
},
|
||||
managementZones: {
|
||||
type: 'array',
|
||||
description: 'Management zones the problem belongs to',
|
||||
items: { type: 'object', properties: managementZoneProperties },
|
||||
},
|
||||
entityTags: {
|
||||
type: 'array',
|
||||
description: 'Tags of the affected entities',
|
||||
items: { type: 'object', properties: tagProperties },
|
||||
},
|
||||
problemFilters: {
|
||||
type: 'array',
|
||||
description: 'Alerting profiles that matched the problem',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Alerting profile ID' },
|
||||
name: { type: 'string', description: 'Alerting profile name' },
|
||||
},
|
||||
},
|
||||
},
|
||||
linkedProblemInfo: {
|
||||
type: 'object',
|
||||
description: 'The problem this one is linked to',
|
||||
nullable: true,
|
||||
properties: {
|
||||
problemId: { type: 'string', description: 'Linked problem ID' },
|
||||
displayId: { type: 'string', description: 'Linked problem display ID' },
|
||||
},
|
||||
},
|
||||
evidenceDetails: {
|
||||
type: 'json',
|
||||
description: 'Evidence behind the problem. Only present when requested via Fields',
|
||||
nullable: true,
|
||||
},
|
||||
impactAnalysis: {
|
||||
type: 'json',
|
||||
description: 'Estimated user impact. Only present when requested via Fields',
|
||||
nullable: true,
|
||||
},
|
||||
recentComments: {
|
||||
type: 'json',
|
||||
description: 'Most recent comments. Only present when requested via Fields',
|
||||
nullable: true,
|
||||
},
|
||||
}
|
||||
|
||||
/** Monitored entity shape produced by `mapEntity`. */
|
||||
export const entityProperties: Record<string, OutputProperty> = {
|
||||
entityId: { type: 'string', description: 'Entity ID (e.g., HOST-06F288EE2A930951)' },
|
||||
type: { type: 'string', description: 'Entity type (e.g., HOST, SERVICE)' },
|
||||
displayName: { type: 'string', description: 'Entity display name' },
|
||||
firstSeenTms: { type: 'number', description: 'First seen timestamp in UTC milliseconds' },
|
||||
lastSeenTms: { type: 'number', description: 'Last seen timestamp in UTC milliseconds' },
|
||||
properties: { type: 'json', description: 'Type-specific entity properties' },
|
||||
tags: {
|
||||
type: 'array',
|
||||
description: 'Tags of the entity',
|
||||
items: { type: 'object', properties: tagProperties },
|
||||
},
|
||||
managementZones: {
|
||||
type: 'array',
|
||||
description: 'Management zones of the entity',
|
||||
items: { type: 'object', properties: managementZoneProperties },
|
||||
},
|
||||
icon: { type: 'json', description: 'Icon of the entity', nullable: true },
|
||||
fromRelationships: { type: 'json', description: 'Relationships originating at this entity' },
|
||||
toRelationships: { type: 'json', description: 'Relationships pointing at this entity' },
|
||||
}
|
||||
|
||||
/** Event shape produced by `mapEvent`. */
|
||||
export const eventProperties: Record<string, OutputProperty> = {
|
||||
eventId: { type: 'string', description: 'Event ID' },
|
||||
eventType: { type: 'string', description: 'Event type' },
|
||||
title: { type: 'string', description: 'Event title' },
|
||||
startTime: { type: 'number', description: 'Event start in UTC milliseconds' },
|
||||
endTime: { type: 'number', description: 'Event end in UTC milliseconds', nullable: true },
|
||||
status: { type: 'string', description: 'Event status: OPEN or CLOSED' },
|
||||
correlationId: { type: 'string', description: 'Correlation ID of the event', nullable: true },
|
||||
frequentEvent: { type: 'boolean', description: 'Whether the event is a frequent event' },
|
||||
underMaintenance: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the event occurred during a maintenance window',
|
||||
},
|
||||
suppressAlert: { type: 'boolean', description: 'Whether alerting is suppressed' },
|
||||
suppressProblem: { type: 'boolean', description: 'Whether problem creation is suppressed' },
|
||||
entityId: {
|
||||
type: 'object',
|
||||
description: 'Entity the event belongs to',
|
||||
nullable: true,
|
||||
properties: entityStubProperties,
|
||||
},
|
||||
properties: {
|
||||
type: 'array',
|
||||
description: 'Event properties',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string', description: 'Property key' },
|
||||
value: { type: 'string', description: 'Property value' },
|
||||
},
|
||||
},
|
||||
},
|
||||
managementZones: {
|
||||
type: 'array',
|
||||
description: 'Management zones of the event',
|
||||
items: { type: 'object', properties: managementZoneProperties },
|
||||
},
|
||||
entityTags: {
|
||||
type: 'array',
|
||||
description: 'Tags of the related entity',
|
||||
items: { type: 'object', properties: tagProperties },
|
||||
},
|
||||
}
|
||||
|
||||
/** Metric descriptor shape produced by `mapMetricDescriptor`. */
|
||||
export const metricDescriptorProperties: Record<string, OutputProperty> = {
|
||||
metricId: { type: 'string', description: 'Metric key, including any transformations' },
|
||||
displayName: { type: 'string', description: 'Metric display name', nullable: true },
|
||||
description: { type: 'string', description: 'Metric description', nullable: true },
|
||||
unit: { type: 'string', description: 'Metric unit', nullable: true },
|
||||
unitDisplayFormat: { type: 'string', description: 'Preferred unit format', nullable: true },
|
||||
tags: { type: 'array', description: 'Metric tags', items: { type: 'string' } },
|
||||
billable: { type: 'boolean', description: 'Whether the metric is billable', nullable: true },
|
||||
dduBillable: { type: 'boolean', description: 'Whether the metric consumes DDUs', nullable: true },
|
||||
created: { type: 'number', description: 'Creation timestamp in UTC ms', nullable: true },
|
||||
lastWritten: { type: 'number', description: 'Last write timestamp in UTC ms', nullable: true },
|
||||
aggregationTypes: {
|
||||
type: 'array',
|
||||
description: 'Supported aggregations',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
defaultAggregation: { type: 'json', description: 'Default aggregation', nullable: true },
|
||||
dimensionDefinitions: { type: 'json', description: 'Dimension definitions of the metric' },
|
||||
dimensionCardinalities: { type: 'json', description: 'Estimated dimension cardinalities' },
|
||||
transformations: {
|
||||
type: 'array',
|
||||
description: 'Supported transformations',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
entityType: {
|
||||
type: 'array',
|
||||
description: 'Entity types the metric can be split by',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
minimumValue: { type: 'number', description: 'Smallest allowed value', nullable: true },
|
||||
maximumValue: { type: 'number', description: 'Largest allowed value', nullable: true },
|
||||
rootCauseRelevant: { type: 'boolean', description: 'Root-cause relevant', nullable: true },
|
||||
impactRelevant: { type: 'boolean', description: 'Impact relevant', nullable: true },
|
||||
metricValueType: { type: 'json', description: 'Value type of the metric', nullable: true },
|
||||
latency: { type: 'number', description: 'Expected write latency in minutes', nullable: true },
|
||||
metricSelector: {
|
||||
type: 'string',
|
||||
description: 'Selector the descriptor was resolved from',
|
||||
nullable: true,
|
||||
},
|
||||
scalar: { type: 'boolean', description: 'Whether the result is a single value', nullable: true },
|
||||
resolutionInfSupported: {
|
||||
type: 'boolean',
|
||||
description: 'Whether resolution=Inf is supported',
|
||||
nullable: true,
|
||||
},
|
||||
warnings: { type: 'array', description: 'Warnings for this metric', items: { type: 'string' } },
|
||||
}
|
||||
|
||||
/** SLO shape produced by `mapSlo`. */
|
||||
export const sloProperties: Record<string, OutputProperty> = {
|
||||
id: { type: 'string', description: 'SLO ID' },
|
||||
name: { type: 'string', description: 'SLO name' },
|
||||
description: { type: 'string', description: 'SLO description', nullable: true },
|
||||
enabled: { type: 'boolean', description: 'Whether the SLO is enabled' },
|
||||
target: { type: 'number', description: 'Target success rate' },
|
||||
warning: { type: 'number', description: 'Warning threshold' },
|
||||
timeframe: { type: 'string', description: 'Evaluation timeframe of the SLO' },
|
||||
filter: { type: 'string', description: 'Entity filter of the SLO', nullable: true },
|
||||
evaluationType: { type: 'string', description: 'Evaluation type of the SLO' },
|
||||
evaluatedPercentage: { type: 'number', description: 'Calculated SLO value', nullable: true },
|
||||
status: { type: 'string', description: 'SLO status: SUCCESS, WARNING, or FAILURE' },
|
||||
error: { type: 'string', description: 'Error that prevented evaluation', nullable: true },
|
||||
errorBudget: { type: 'number', description: 'Remaining error budget', nullable: true },
|
||||
errorBudgetBurnRate: { type: 'json', description: 'Error budget burn rate', nullable: true },
|
||||
metricKey: { type: 'string', description: 'Metric key of the SLO', nullable: true },
|
||||
metricName: { type: 'string', description: 'Metric name of the SLO', nullable: true },
|
||||
metricExpression: { type: 'string', description: 'Metric expression', nullable: true },
|
||||
relatedOpenProblems: { type: 'number', description: 'Open related problems', nullable: true },
|
||||
relatedTotalProblems: { type: 'number', description: 'Total related problems', nullable: true },
|
||||
}
|
||||
|
||||
/** Security problem shape produced by `mapSecurityProblem`. */
|
||||
export const securityProblemProperties: Record<string, OutputProperty> = {
|
||||
securityProblemId: { type: 'string', description: 'Security problem ID' },
|
||||
displayId: { type: 'string', description: 'Human-readable security problem ID' },
|
||||
status: { type: 'string', description: 'Status: OPEN or RESOLVED' },
|
||||
muted: { type: 'boolean', description: 'Whether the security problem is muted' },
|
||||
title: { type: 'string', description: 'Security problem title' },
|
||||
technology: {
|
||||
type: 'string',
|
||||
description: 'DOTNET, GO, JAVA, KUBERNETES, NODE_JS, PHP, or PYTHON',
|
||||
},
|
||||
vulnerabilityType: { type: 'string', description: 'CODE_LEVEL, RUNTIME, or THIRD_PARTY' },
|
||||
packageName: { type: 'string', description: 'Affected package name', nullable: true },
|
||||
externalVulnerabilityId: {
|
||||
type: 'string',
|
||||
description: 'External vulnerability ID',
|
||||
nullable: true,
|
||||
},
|
||||
cveIds: { type: 'array', description: 'Related CVE IDs', items: { type: 'string' } },
|
||||
url: { type: 'string', description: 'Link to the security problem in Dynatrace', nullable: true },
|
||||
firstSeenTimestamp: { type: 'number', description: 'First seen in UTC milliseconds' },
|
||||
lastUpdatedTimestamp: { type: 'number', description: 'Last update in UTC milliseconds' },
|
||||
lastOpenedTimestamp: {
|
||||
type: 'number',
|
||||
description: 'Last opened in UTC milliseconds',
|
||||
nullable: true,
|
||||
},
|
||||
lastResolvedTimestamp: {
|
||||
type: 'number',
|
||||
description: 'Last resolved in UTC milliseconds',
|
||||
nullable: true,
|
||||
},
|
||||
riskAssessment: {
|
||||
type: 'json',
|
||||
description: 'Davis risk assessment. Only present when requested via Fields',
|
||||
nullable: true,
|
||||
},
|
||||
managementZones: {
|
||||
type: 'array',
|
||||
description: 'Management zones. Only present when requested via Fields',
|
||||
items: { type: 'object', properties: managementZoneProperties },
|
||||
},
|
||||
globalCounts: {
|
||||
type: 'json',
|
||||
description: 'Global affected-entity counts. Only present when requested via Fields',
|
||||
nullable: true,
|
||||
},
|
||||
codeLevelVulnerabilityDetails: {
|
||||
type: 'json',
|
||||
description: 'Code-level vulnerability details. Only present when requested via Fields',
|
||||
nullable: true,
|
||||
},
|
||||
}
|
||||
|
||||
/** Additional fields the single security problem endpoint returns. */
|
||||
export const securityProblemDetailsProperties: Record<string, OutputProperty> = {
|
||||
...securityProblemProperties,
|
||||
description: { type: 'string', description: 'Vulnerability description', nullable: true },
|
||||
remediationDescription: {
|
||||
type: 'string',
|
||||
description: 'How to remediate the vulnerability',
|
||||
nullable: true,
|
||||
},
|
||||
muteStateChangeInProgress: {
|
||||
type: 'boolean',
|
||||
description: 'Whether a mute state change is in progress',
|
||||
nullable: true,
|
||||
},
|
||||
affectedEntities: {
|
||||
type: 'array',
|
||||
description: 'IDs of affected process group instances',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
exposedEntities: {
|
||||
type: 'array',
|
||||
description: 'IDs of publicly exposed entities',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
reachableDataAssets: {
|
||||
type: 'array',
|
||||
description: 'IDs of entities with reachable data assets',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
vulnerableComponents: { type: 'json', description: 'Vulnerable components' },
|
||||
filteredCounts: {
|
||||
type: 'json',
|
||||
description: 'Counts within the management zone filter',
|
||||
nullable: true,
|
||||
},
|
||||
events: { type: 'json', description: 'Lifecycle events of the security problem' },
|
||||
entryPoints: { type: 'json', description: 'Entry points into the vulnerability', nullable: true },
|
||||
relatedEntities: { type: 'json', description: 'Related entities', nullable: true },
|
||||
relatedAttacks: { type: 'json', description: 'Related attacks', nullable: true },
|
||||
relatedContainerImages: { type: 'json', description: 'Related container images', nullable: true },
|
||||
}
|
||||
|
||||
/** Shared pagination outputs of the Environment API v2 list endpoints. */
|
||||
export const totalCountOutput: OutputProperty = {
|
||||
type: 'number',
|
||||
description: 'Total number of matching entries',
|
||||
nullable: true,
|
||||
}
|
||||
|
||||
export const pageSizeOutput: OutputProperty = {
|
||||
type: 'number',
|
||||
description: 'Number of entries in this page',
|
||||
nullable: true,
|
||||
}
|
||||
|
||||
export const nextPageKeyOutput: OutputProperty = {
|
||||
type: 'string',
|
||||
description: 'Cursor for the next page. Null on the last page',
|
||||
nullable: true,
|
||||
}
|
||||
|
||||
export const warningsOutput: OutputProperty = {
|
||||
type: 'array',
|
||||
description: 'Warnings returned alongside the result',
|
||||
items: { type: 'string' },
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { nextPageKeyOutput, totalCountOutput, warningsOutput } from '@/tools/dynatrace/outputs'
|
||||
import type {
|
||||
DynatraceQueryMetricsParams,
|
||||
DynatraceQueryMetricsResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
mapMetricResult,
|
||||
mapWarnings,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const queryMetricsTool: ToolConfig<
|
||||
DynatraceQueryMetricsParams,
|
||||
DynatraceQueryMetricsResponse
|
||||
> = {
|
||||
id: 'dynatrace_query_metrics',
|
||||
name: 'Dynatrace Query Metrics',
|
||||
description:
|
||||
'Read metric data points from Dynatrace using a metric selector, with optional entity and management-zone scoping.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dynatrace access token (dt0c01...) with the metrics.read scope',
|
||||
},
|
||||
metricSelector: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Metric selector, up to 10 metrics comma-separated, with optional transformations after a colon (e.g. builtin:host.cpu.usage:avg:names)',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-2h. Defaults to now-2h',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End of the timeframe in the same formats as From. Defaults to now',
|
||||
},
|
||||
resolution: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Number of data points (default 120), a timespan such as 10m or 3w, or Inf for a single aggregated value',
|
||||
},
|
||||
entitySelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity selector scoping the query, e.g. type("HOST"),tag("env:prod")',
|
||||
},
|
||||
mzSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Management zone selector, e.g. mzName("Production")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildDynatraceUrl(params.environmentUrl, '/metrics/query', {
|
||||
metricSelector: params.metricSelector,
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
resolution: params.resolution,
|
||||
entitySelector: params.entitySelector,
|
||||
mzSelector: params.mzSelector,
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
const result = Array.isArray(data.result) ? (data.result as Array<Record<string, unknown>>) : []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
result: result.map(mapMetricResult),
|
||||
resolution: (data.resolution as string) ?? null,
|
||||
totalCount: (data.totalCount as number) ?? null,
|
||||
nextPageKey: (data.nextPageKey as string) ?? null,
|
||||
warnings: mapWarnings(data.warnings),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'array',
|
||||
description: 'One entry per queried metric',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
metricId: { type: 'string', description: 'Metric key including transformations' },
|
||||
dataPointCountRatio: {
|
||||
type: 'number',
|
||||
description: 'Queried data points relative to the query limit',
|
||||
nullable: true,
|
||||
},
|
||||
dimensionCountRatio: {
|
||||
type: 'number',
|
||||
description: 'Queried dimension tuples relative to the query limit',
|
||||
nullable: true,
|
||||
},
|
||||
appliedOptionalFilters: {
|
||||
type: 'array',
|
||||
description: 'Optional filters Dynatrace applied to the query',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
dql: {
|
||||
type: 'json',
|
||||
description: 'DQL translation of the query, when available',
|
||||
nullable: true,
|
||||
},
|
||||
warnings: {
|
||||
type: 'array',
|
||||
description: 'Warnings for this metric',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
data: {
|
||||
type: 'array',
|
||||
description: 'Series of the metric, one per dimension tuple',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
dimensions: {
|
||||
type: 'array',
|
||||
description: 'Dimension values of the series',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
dimensionMap: {
|
||||
type: 'json',
|
||||
description: 'Dimension values keyed by dimension key',
|
||||
},
|
||||
timestamps: {
|
||||
type: 'array',
|
||||
description: 'Timestamps in UTC milliseconds, one per value',
|
||||
items: { type: 'number' },
|
||||
},
|
||||
values: {
|
||||
type: 'array',
|
||||
description: 'Metric values. Null where no data exists',
|
||||
items: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolution: {
|
||||
type: 'string',
|
||||
description: 'Resolution Dynatrace actually used',
|
||||
nullable: true,
|
||||
},
|
||||
totalCount: totalCountOutput,
|
||||
nextPageKey: nextPageKeyOutput,
|
||||
warnings: warningsOutput,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import type {
|
||||
DynatraceSearchLogsParams,
|
||||
DynatraceSearchLogsResponse,
|
||||
} from '@/tools/dynatrace/types'
|
||||
import {
|
||||
buildDynatraceUrl,
|
||||
dynatraceHeaders,
|
||||
mapLogRecord,
|
||||
readJsonBody,
|
||||
} from '@/tools/dynatrace/utils'
|
||||
import { ErrorExtractorId } from '@/tools/error-extractors'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const searchLogsTool: ToolConfig<DynatraceSearchLogsParams, DynatraceSearchLogsResponse> = {
|
||||
id: 'dynatrace_search_logs',
|
||||
name: 'Dynatrace Search Logs',
|
||||
description:
|
||||
'Search log records in Dynatrace by query and timeframe, for troubleshooting and incident analysis.',
|
||||
version: '1.0.0',
|
||||
errorExtractor: ErrorExtractorId.DYNATRACE_ERRORS,
|
||||
|
||||
params: {
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace environment URL (e.g., https://abc12345.live.dynatrace.com, or https://your-activegate:9999/e/abc12345 for Managed)',
|
||||
},
|
||||
apiToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Dynatrace access token (dt0c01...) with the logs.read scope, or storage:logs:read and storage:buckets:read on Grail',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Log search query, e.g. status="ERROR" AND dt.entity.host="HOST-06F288EE2A930951"',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start of the timeframe as UTC milliseconds, ISO 8601, or a relative expression such as now-1h. Defaults to now-2w',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End of the timeframe in the same formats as From. Defaults to now',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort by field with a + or - prefix, e.g. -timestamp for newest first',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of log records to return (max 1000, default 1000)',
|
||||
},
|
||||
nextSliceKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Cursor for the next slice. All other filters are ignored when it is set',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
params.nextSliceKey
|
||||
? buildDynatraceUrl(params.environmentUrl, '/logs/search', {
|
||||
nextSliceKey: params.nextSliceKey,
|
||||
})
|
||||
: buildDynatraceUrl(params.environmentUrl, '/logs/search', {
|
||||
query: params.query,
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
sort: params.sort,
|
||||
limit: params.limit,
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: (params) => dynatraceHeaders(params.apiToken),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await readJsonBody(response)
|
||||
const results = Array.isArray(data.results)
|
||||
? (data.results as Array<Record<string, unknown>>)
|
||||
: []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results: results.map(mapLogRecord),
|
||||
sliceSize: (data.sliceSize as number) ?? null,
|
||||
nextSliceKey: (data.nextSliceKey as string) ?? null,
|
||||
warnings: (data.warnings as string) ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Matching log records',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
timestamp: { type: 'number', description: 'Log timestamp in UTC milliseconds' },
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Log level: ERROR, WARN, INFO, NONE, or NOT_APPLICABLE',
|
||||
},
|
||||
content: { type: 'string', description: 'Log message content' },
|
||||
eventType: { type: 'string', description: 'Event type of the record', nullable: true },
|
||||
additionalColumns: {
|
||||
type: 'json',
|
||||
description: 'Additional log attributes keyed by column name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sliceSize: {
|
||||
type: 'number',
|
||||
description: 'Number of records in this slice',
|
||||
nullable: true,
|
||||
},
|
||||
nextSliceKey: {
|
||||
type: 'string',
|
||||
description: 'Cursor for the next slice. Null when the result is complete',
|
||||
nullable: true,
|
||||
},
|
||||
warnings: {
|
||||
type: 'string',
|
||||
description: 'Warning raised while searching',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
/** Credentials every Dynatrace Environment API v2 call needs. */
|
||||
export interface DynatraceBaseParams {
|
||||
environmentUrl: string
|
||||
apiToken: string
|
||||
}
|
||||
|
||||
/** Short representation of a monitored entity (`EntityStub`), flattened. */
|
||||
export interface DynatraceEntityStub {
|
||||
id: string | null
|
||||
type: string | null
|
||||
name: string | null
|
||||
}
|
||||
|
||||
/** Tag of a monitored entity (`METag`). */
|
||||
export interface DynatraceTag {
|
||||
context: string | null
|
||||
key: string | null
|
||||
value: string | null
|
||||
stringRepresentation: string | null
|
||||
}
|
||||
|
||||
/** Short representation of a management zone. */
|
||||
export interface DynatraceManagementZone {
|
||||
id: string | null
|
||||
name: string | null
|
||||
}
|
||||
|
||||
/** Short representation of an alerting profile (`AlertingProfileStub`). */
|
||||
export interface DynatraceProblemFilter {
|
||||
id: string | null
|
||||
name: string | null
|
||||
}
|
||||
|
||||
/** A comment on a problem. */
|
||||
export interface DynatraceComment {
|
||||
id: string | null
|
||||
authorName: string | null
|
||||
content: string | null
|
||||
context: string | null
|
||||
createdAtTimestamp: number | null
|
||||
}
|
||||
|
||||
/** A problem as returned by the Problems API v2. */
|
||||
export interface DynatraceProblem {
|
||||
problemId: string | null
|
||||
displayId: string | null
|
||||
title: string | null
|
||||
status: string | null
|
||||
severityLevel: string | null
|
||||
impactLevel: string | null
|
||||
startTime: number | null
|
||||
endTime: number | null
|
||||
rootCauseEntity: DynatraceEntityStub | null
|
||||
affectedEntities: DynatraceEntityStub[]
|
||||
impactedEntities: DynatraceEntityStub[]
|
||||
managementZones: DynatraceManagementZone[]
|
||||
entityTags: DynatraceTag[]
|
||||
problemFilters: DynatraceProblemFilter[]
|
||||
linkedProblemInfo: { problemId: string | null; displayId: string | null } | null
|
||||
evidenceDetails: Record<string, unknown> | null
|
||||
impactAnalysis: Record<string, unknown> | null
|
||||
recentComments: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
/** A monitored entity as returned by the Monitored entities API v2. */
|
||||
export interface DynatraceEntity {
|
||||
entityId: string | null
|
||||
type: string | null
|
||||
displayName: string | null
|
||||
firstSeenTms: number | null
|
||||
lastSeenTms: number | null
|
||||
properties: Record<string, unknown>
|
||||
tags: DynatraceTag[]
|
||||
managementZones: DynatraceManagementZone[]
|
||||
icon: Record<string, unknown> | null
|
||||
fromRelationships: Record<string, unknown>
|
||||
toRelationships: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** An entity type descriptor. */
|
||||
export interface DynatraceEntityType {
|
||||
type: string | null
|
||||
displayName: string | null
|
||||
dimensionKey: string | null
|
||||
entityLimitExceeded: boolean | null
|
||||
properties: Array<{ id: string | null; displayName: string | null; type: string | null }>
|
||||
fromRelationships: Array<{ id: string | null; toTypes: string[] }>
|
||||
toRelationships: Array<{ id: string | null; fromTypes: string[] }>
|
||||
}
|
||||
|
||||
/** An event as returned by the Events API v2. */
|
||||
export interface DynatraceEvent {
|
||||
eventId: string | null
|
||||
eventType: string | null
|
||||
title: string | null
|
||||
startTime: number | null
|
||||
endTime: number | null
|
||||
status: string | null
|
||||
correlationId: string | null
|
||||
frequentEvent: boolean | null
|
||||
underMaintenance: boolean | null
|
||||
suppressAlert: boolean | null
|
||||
suppressProblem: boolean | null
|
||||
entityId: DynatraceEntityStub | null
|
||||
properties: Array<{ key: string | null; value: string | null }>
|
||||
managementZones: DynatraceManagementZone[]
|
||||
entityTags: DynatraceTag[]
|
||||
}
|
||||
|
||||
/** One series of a metric query result. */
|
||||
export interface DynatraceMetricSeries {
|
||||
dimensions: string[]
|
||||
dimensionMap: Record<string, string>
|
||||
timestamps: number[]
|
||||
values: Array<number | null>
|
||||
}
|
||||
|
||||
/** One metric of a metric query result. */
|
||||
export interface DynatraceMetricResult {
|
||||
metricId: string | null
|
||||
dataPointCountRatio: number | null
|
||||
dimensionCountRatio: number | null
|
||||
appliedOptionalFilters: unknown[]
|
||||
dql: Record<string, unknown> | null
|
||||
warnings: string[]
|
||||
data: DynatraceMetricSeries[]
|
||||
}
|
||||
|
||||
/** A metric descriptor. */
|
||||
export interface DynatraceMetricDescriptor {
|
||||
metricId: string | null
|
||||
displayName: string | null
|
||||
description: string | null
|
||||
unit: string | null
|
||||
unitDisplayFormat: string | null
|
||||
tags: string[]
|
||||
billable: boolean | null
|
||||
dduBillable: boolean | null
|
||||
created: number | null
|
||||
lastWritten: number | null
|
||||
aggregationTypes: string[]
|
||||
defaultAggregation: Record<string, unknown> | null
|
||||
dimensionDefinitions: Array<Record<string, unknown>>
|
||||
dimensionCardinalities: Array<Record<string, unknown>>
|
||||
transformations: string[]
|
||||
entityType: string[]
|
||||
minimumValue: number | null
|
||||
maximumValue: number | null
|
||||
rootCauseRelevant: boolean | null
|
||||
impactRelevant: boolean | null
|
||||
metricValueType: Record<string, unknown> | null
|
||||
latency: number | null
|
||||
metricSelector: string | null
|
||||
scalar: boolean | null
|
||||
resolutionInfSupported: boolean | null
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
/** A service-level objective. */
|
||||
export interface DynatraceSlo {
|
||||
id: string | null
|
||||
name: string | null
|
||||
description: string | null
|
||||
enabled: boolean | null
|
||||
target: number | null
|
||||
warning: number | null
|
||||
timeframe: string | null
|
||||
filter: string | null
|
||||
evaluationType: string | null
|
||||
evaluatedPercentage: number | null
|
||||
status: string | null
|
||||
error: string | null
|
||||
errorBudget: number | null
|
||||
errorBudgetBurnRate: Record<string, unknown> | null
|
||||
metricKey: string | null
|
||||
metricName: string | null
|
||||
metricExpression: string | null
|
||||
relatedOpenProblems: number | null
|
||||
relatedTotalProblems: number | null
|
||||
}
|
||||
|
||||
/** A security problem (vulnerability). */
|
||||
export interface DynatraceSecurityProblem {
|
||||
securityProblemId: string | null
|
||||
displayId: string | null
|
||||
status: string | null
|
||||
muted: boolean | null
|
||||
title: string | null
|
||||
technology: string | null
|
||||
vulnerabilityType: string | null
|
||||
packageName: string | null
|
||||
externalVulnerabilityId: string | null
|
||||
cveIds: string[]
|
||||
url: string | null
|
||||
firstSeenTimestamp: number | null
|
||||
lastUpdatedTimestamp: number | null
|
||||
lastOpenedTimestamp: number | null
|
||||
lastResolvedTimestamp: number | null
|
||||
riskAssessment: Record<string, unknown> | null
|
||||
managementZones: DynatraceManagementZone[]
|
||||
globalCounts: Record<string, unknown> | null
|
||||
codeLevelVulnerabilityDetails: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
/** A security problem with the detail-only fields the single-problem endpoint adds. */
|
||||
export interface DynatraceSecurityProblemDetails extends DynatraceSecurityProblem {
|
||||
description: string | null
|
||||
remediationDescription: string | null
|
||||
muteStateChangeInProgress: boolean | null
|
||||
affectedEntities: string[]
|
||||
exposedEntities: string[]
|
||||
reachableDataAssets: string[]
|
||||
vulnerableComponents: Array<Record<string, unknown>>
|
||||
filteredCounts: Record<string, unknown> | null
|
||||
events: Array<Record<string, unknown>>
|
||||
entryPoints: Record<string, unknown> | null
|
||||
relatedEntities: Record<string, unknown> | null
|
||||
relatedAttacks: Record<string, unknown> | null
|
||||
relatedContainerImages: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
/** An audit log entry. */
|
||||
export interface DynatraceAuditLog {
|
||||
logId: string | null
|
||||
eventType: string | null
|
||||
category: string | null
|
||||
entityId: string | null
|
||||
environmentId: string | null
|
||||
user: string | null
|
||||
userType: string | null
|
||||
userOrigin: string | null
|
||||
timestamp: number | null
|
||||
success: boolean | null
|
||||
message: string | null
|
||||
patch: Record<string, unknown> | null
|
||||
settingsSchemaId: string | null
|
||||
settingsScopeId: string | null
|
||||
settingsKey: string | null
|
||||
settingsObjectId: string | null
|
||||
settingsObjectSummary: string | null
|
||||
settingsScopeName: string | null
|
||||
}
|
||||
|
||||
/** A log record returned by the Log Monitoring API v2 search endpoint. */
|
||||
export interface DynatraceLogRecord {
|
||||
timestamp: number | null
|
||||
status: string | null
|
||||
content: string | null
|
||||
eventType: string | null
|
||||
additionalColumns: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface DynatraceListProblemsParams extends DynatraceBaseParams {
|
||||
from?: string
|
||||
to?: string
|
||||
problemSelector?: string
|
||||
entitySelector?: string
|
||||
sort?: string
|
||||
fields?: string
|
||||
pageSize?: number
|
||||
nextPageKey?: string
|
||||
}
|
||||
|
||||
export interface DynatraceListProblemsResponse extends ToolResponse {
|
||||
output: {
|
||||
problems: DynatraceProblem[]
|
||||
totalCount: number | null
|
||||
pageSize: number | null
|
||||
nextPageKey: string | null
|
||||
warnings: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceGetProblemParams extends DynatraceBaseParams {
|
||||
problemId: string
|
||||
fields?: string
|
||||
}
|
||||
|
||||
export interface DynatraceGetProblemResponse extends ToolResponse {
|
||||
output: {
|
||||
problem: DynatraceProblem
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceCloseProblemParams extends DynatraceBaseParams {
|
||||
problemId: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface DynatraceCloseProblemResponse extends ToolResponse {
|
||||
output: {
|
||||
problemId: string | null
|
||||
closeTimestamp: number | null
|
||||
closing: boolean | null
|
||||
comment: DynatraceComment | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceListProblemCommentsParams extends DynatraceBaseParams {
|
||||
problemId: string
|
||||
pageSize?: number
|
||||
nextPageKey?: string
|
||||
}
|
||||
|
||||
export interface DynatraceListProblemCommentsResponse extends ToolResponse {
|
||||
output: {
|
||||
comments: DynatraceComment[]
|
||||
totalCount: number | null
|
||||
pageSize: number | null
|
||||
nextPageKey: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceAddProblemCommentParams extends DynatraceBaseParams {
|
||||
problemId: string
|
||||
message: string
|
||||
context?: string
|
||||
}
|
||||
|
||||
export interface DynatraceAddProblemCommentResponse extends ToolResponse {
|
||||
output: {
|
||||
problemId: string
|
||||
message: string
|
||||
context: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceQueryMetricsParams extends DynatraceBaseParams {
|
||||
metricSelector: string
|
||||
from?: string
|
||||
to?: string
|
||||
resolution?: string
|
||||
entitySelector?: string
|
||||
mzSelector?: string
|
||||
}
|
||||
|
||||
export interface DynatraceQueryMetricsResponse extends ToolResponse {
|
||||
output: {
|
||||
result: DynatraceMetricResult[]
|
||||
resolution: string | null
|
||||
totalCount: number | null
|
||||
nextPageKey: string | null
|
||||
warnings: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceListMetricsParams extends DynatraceBaseParams {
|
||||
metricSelector?: string
|
||||
text?: string
|
||||
fields?: string
|
||||
writtenSince?: string
|
||||
writtenSinceMode?: string
|
||||
metadataSelector?: string
|
||||
pageSize?: number
|
||||
nextPageKey?: string
|
||||
}
|
||||
|
||||
export interface DynatraceListMetricsResponse extends ToolResponse {
|
||||
output: {
|
||||
metrics: DynatraceMetricDescriptor[]
|
||||
totalCount: number | null
|
||||
nextPageKey: string | null
|
||||
warnings: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceGetMetricParams extends DynatraceBaseParams {
|
||||
metricKey: string
|
||||
}
|
||||
|
||||
export interface DynatraceGetMetricResponse extends ToolResponse {
|
||||
output: {
|
||||
metric: DynatraceMetricDescriptor
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceIngestMetricsParams extends DynatraceBaseParams {
|
||||
payload: string
|
||||
}
|
||||
|
||||
export interface DynatraceIngestMetricsResponse extends ToolResponse {
|
||||
output: {
|
||||
linesOk: number | null
|
||||
linesInvalid: number | null
|
||||
ingestError: Record<string, unknown> | null
|
||||
warnings: Record<string, unknown> | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceListEntitiesParams extends DynatraceBaseParams {
|
||||
entitySelector: string
|
||||
from?: string
|
||||
to?: string
|
||||
fields?: string
|
||||
sort?: string
|
||||
pageSize?: number
|
||||
nextPageKey?: string
|
||||
}
|
||||
|
||||
export interface DynatraceListEntitiesResponse extends ToolResponse {
|
||||
output: {
|
||||
entities: DynatraceEntity[]
|
||||
totalCount: number | null
|
||||
pageSize: number | null
|
||||
nextPageKey: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceGetEntityParams extends DynatraceBaseParams {
|
||||
entityId: string
|
||||
from?: string
|
||||
to?: string
|
||||
fields?: string
|
||||
}
|
||||
|
||||
export interface DynatraceGetEntityResponse extends ToolResponse {
|
||||
output: {
|
||||
entity: DynatraceEntity
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceListEntityTypesParams extends DynatraceBaseParams {
|
||||
pageSize?: number
|
||||
nextPageKey?: string
|
||||
}
|
||||
|
||||
export interface DynatraceListEntityTypesResponse extends ToolResponse {
|
||||
output: {
|
||||
types: DynatraceEntityType[]
|
||||
totalCount: number | null
|
||||
pageSize: number | null
|
||||
nextPageKey: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceListEventsParams extends DynatraceBaseParams {
|
||||
from?: string
|
||||
to?: string
|
||||
eventSelector?: string
|
||||
entitySelector?: string
|
||||
pageSize?: number
|
||||
nextPageKey?: string
|
||||
}
|
||||
|
||||
export interface DynatraceListEventsResponse extends ToolResponse {
|
||||
output: {
|
||||
events: DynatraceEvent[]
|
||||
totalCount: number | null
|
||||
pageSize: number | null
|
||||
nextPageKey: string | null
|
||||
warnings: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceGetEventParams extends DynatraceBaseParams {
|
||||
eventId: string
|
||||
}
|
||||
|
||||
export interface DynatraceGetEventResponse extends ToolResponse {
|
||||
output: {
|
||||
event: DynatraceEvent
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceIngestEventParams extends DynatraceBaseParams {
|
||||
eventType: string
|
||||
title: string
|
||||
entitySelector?: string
|
||||
startTime?: number
|
||||
endTime?: number
|
||||
/**
|
||||
* Dynatrace's event timeout in minutes. Deliberately not named `timeout` —
|
||||
* the tool transport reserves that param name for the HTTP request timeout.
|
||||
*/
|
||||
eventTimeout?: number
|
||||
/** Accepts a parsed object or a JSON string; normalized by `parseJsonParam`. */
|
||||
properties?: Record<string, string> | string
|
||||
}
|
||||
|
||||
export interface DynatraceIngestEventResponse extends ToolResponse {
|
||||
output: {
|
||||
reportCount: number | null
|
||||
eventIngestResults: Array<{ correlationId: string | null; status: string | null }>
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceSearchLogsParams extends DynatraceBaseParams {
|
||||
query?: string
|
||||
from?: string
|
||||
to?: string
|
||||
sort?: string
|
||||
limit?: number
|
||||
nextSliceKey?: string
|
||||
}
|
||||
|
||||
export interface DynatraceSearchLogsResponse extends ToolResponse {
|
||||
output: {
|
||||
results: DynatraceLogRecord[]
|
||||
sliceSize: number | null
|
||||
nextSliceKey: string | null
|
||||
warnings: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceIngestLogsParams extends DynatraceBaseParams {
|
||||
/** Accepts a parsed object/array or a JSON string; normalized by `parseJsonParam`. */
|
||||
logs: Record<string, unknown> | Array<Record<string, unknown>> | string
|
||||
}
|
||||
|
||||
export interface DynatraceIngestLogsResponse extends ToolResponse {
|
||||
output: {
|
||||
accepted: boolean
|
||||
statusCode: number
|
||||
details: Record<string, unknown> | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceListSlosParams extends DynatraceBaseParams {
|
||||
sloSelector?: string
|
||||
from?: string
|
||||
to?: string
|
||||
timeFrame?: string
|
||||
sort?: string
|
||||
enabledSlos?: string
|
||||
evaluate?: boolean
|
||||
showGlobalSlos?: boolean
|
||||
pageSize?: number
|
||||
nextPageKey?: string
|
||||
}
|
||||
|
||||
export interface DynatraceListSlosResponse extends ToolResponse {
|
||||
output: {
|
||||
slos: DynatraceSlo[]
|
||||
totalCount: number | null
|
||||
pageSize: number | null
|
||||
nextPageKey: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceGetSloParams extends DynatraceBaseParams {
|
||||
sloId: string
|
||||
from?: string
|
||||
to?: string
|
||||
timeFrame?: string
|
||||
}
|
||||
|
||||
export interface DynatraceGetSloResponse extends ToolResponse {
|
||||
output: {
|
||||
slo: DynatraceSlo
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceListSecurityProblemsParams extends DynatraceBaseParams {
|
||||
securityProblemSelector?: string
|
||||
from?: string
|
||||
to?: string
|
||||
fields?: string
|
||||
sort?: string
|
||||
pageSize?: number
|
||||
nextPageKey?: string
|
||||
}
|
||||
|
||||
export interface DynatraceListSecurityProblemsResponse extends ToolResponse {
|
||||
output: {
|
||||
securityProblems: DynatraceSecurityProblem[]
|
||||
totalCount: number | null
|
||||
pageSize: number | null
|
||||
nextPageKey: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceGetSecurityProblemParams extends DynatraceBaseParams {
|
||||
securityProblemId: string
|
||||
fields?: string
|
||||
managementZoneFilter?: string
|
||||
from?: string
|
||||
}
|
||||
|
||||
export interface DynatraceGetSecurityProblemResponse extends ToolResponse {
|
||||
output: {
|
||||
securityProblem: DynatraceSecurityProblemDetails
|
||||
}
|
||||
}
|
||||
|
||||
export interface DynatraceGetAuditLogsParams extends DynatraceBaseParams {
|
||||
filter?: string
|
||||
from?: string
|
||||
to?: string
|
||||
sort?: string
|
||||
pageSize?: number
|
||||
nextPageKey?: string
|
||||
}
|
||||
|
||||
export interface DynatraceGetAuditLogsResponse extends ToolResponse {
|
||||
output: {
|
||||
auditLogs: DynatraceAuditLog[]
|
||||
totalCount: number | null
|
||||
pageSize: number | null
|
||||
nextPageKey: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/** Union of every Dynatrace tool response, used as the block's response type. */
|
||||
export type DynatraceResponse =
|
||||
| DynatraceListProblemsResponse
|
||||
| DynatraceGetProblemResponse
|
||||
| DynatraceCloseProblemResponse
|
||||
| DynatraceListProblemCommentsResponse
|
||||
| DynatraceAddProblemCommentResponse
|
||||
| DynatraceQueryMetricsResponse
|
||||
| DynatraceListMetricsResponse
|
||||
| DynatraceGetMetricResponse
|
||||
| DynatraceIngestMetricsResponse
|
||||
| DynatraceListEntitiesResponse
|
||||
| DynatraceGetEntityResponse
|
||||
| DynatraceListEntityTypesResponse
|
||||
| DynatraceListEventsResponse
|
||||
| DynatraceGetEventResponse
|
||||
| DynatraceIngestEventResponse
|
||||
| DynatraceSearchLogsResponse
|
||||
| DynatraceIngestLogsResponse
|
||||
| DynatraceListSlosResponse
|
||||
| DynatraceGetSloResponse
|
||||
| DynatraceListSecurityProblemsResponse
|
||||
| DynatraceGetSecurityProblemResponse
|
||||
| DynatraceGetAuditLogsResponse
|
||||
@@ -0,0 +1,453 @@
|
||||
import { truncate } from '@sim/utils/string'
|
||||
import type {
|
||||
DynatraceAuditLog,
|
||||
DynatraceComment,
|
||||
DynatraceEntity,
|
||||
DynatraceEntityStub,
|
||||
DynatraceEntityType,
|
||||
DynatraceEvent,
|
||||
DynatraceLogRecord,
|
||||
DynatraceManagementZone,
|
||||
DynatraceMetricDescriptor,
|
||||
DynatraceMetricResult,
|
||||
DynatraceProblem,
|
||||
DynatraceProblemFilter,
|
||||
DynatraceSecurityProblem,
|
||||
DynatraceSecurityProblemDetails,
|
||||
DynatraceSlo,
|
||||
DynatraceTag,
|
||||
} from '@/tools/dynatrace/types'
|
||||
|
||||
type QueryValue = string | number | boolean | undefined | null
|
||||
|
||||
/**
|
||||
* Builds an absolute Environment API v2 URL. Tolerates a trailing slash and a
|
||||
* trailing `/api/v2` segment on the user-supplied environment URL, and omits
|
||||
* query parameters that are unset or empty.
|
||||
*/
|
||||
export function buildDynatraceUrl(
|
||||
environmentUrl: string,
|
||||
path: string,
|
||||
query?: Record<string, QueryValue>
|
||||
): string {
|
||||
const base = environmentUrl
|
||||
.trim()
|
||||
.replace(/\/+$/, '')
|
||||
.replace(/\/api\/v2$/, '')
|
||||
const search = new URLSearchParams()
|
||||
|
||||
for (const [key, value] of Object.entries(query ?? {})) {
|
||||
if (value === undefined || value === null || value === '') continue
|
||||
search.set(key, String(value))
|
||||
}
|
||||
|
||||
const queryString = search.toString()
|
||||
return `${base}/api/v2${path}${queryString ? `?${queryString}` : ''}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a metric key for a URL path. In Dynatrace a `:` is structural — it
|
||||
* separates the metric key from its transformation operators
|
||||
* (`builtin:host.cpu.usage:avg`) — so each colon-delimited part is encoded and
|
||||
* the separators are rejoined verbatim, matching the unencoded form the API
|
||||
* reference uses in its own examples.
|
||||
*/
|
||||
export function encodeDynatracePathSegment(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.split(':')
|
||||
.map((part) => encodeURIComponent(part))
|
||||
.join(':')
|
||||
}
|
||||
|
||||
/** Encodes an identifier for use in a URL path, tolerating copy-pasted whitespace. */
|
||||
export function encodeDynatraceId(value: string): string {
|
||||
return encodeURIComponent(value.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a `json` param that may arrive already parsed (from a block input or
|
||||
* an upstream block reference) or as a JSON string (from an LLM tool call or a
|
||||
* long-input field). Returns `undefined` when there is nothing to send, so callers
|
||||
* can omit the field rather than transmit `"undefined"`.
|
||||
*/
|
||||
export function parseJsonParam(value: unknown): unknown {
|
||||
if (value === undefined || value === null) return undefined
|
||||
if (typeof value !== 'string') return value
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return undefined
|
||||
try {
|
||||
return JSON.parse(trimmed)
|
||||
} catch {
|
||||
throw new Error(`Expected valid JSON but received: ${truncate(trimmed, 100)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds the Dynatrace authorization headers. */
|
||||
export function dynatraceHeaders(
|
||||
apiToken: string,
|
||||
contentType = 'application/json'
|
||||
): Record<string, string> {
|
||||
return {
|
||||
Authorization: `Api-Token ${apiToken.trim()}`,
|
||||
'Content-Type': contentType,
|
||||
Accept: 'application/json',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a JSON response body, tolerating the empty bodies Dynatrace returns for
|
||||
* 201 (comment created) and 204 (logs accepted).
|
||||
*
|
||||
* A non-empty body that will not parse is an error, not an empty result — a
|
||||
* gateway HTML page or a truncated payload would otherwise map to `{}` and
|
||||
* surface as "no results", which is indistinguishable from a genuinely empty
|
||||
* environment.
|
||||
*/
|
||||
export async function readJsonBody(response: Response): Promise<Record<string, unknown>> {
|
||||
const text = await response.text()
|
||||
if (!text.trim()) return {}
|
||||
try {
|
||||
return JSON.parse(text) as Record<string, unknown>
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Dynatrace returned a non-JSON body (HTTP ${response.status}): ${truncate(text.trim(), 200)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function toRecordArray(value: unknown): Array<Record<string, unknown>> {
|
||||
return Array.isArray(value) ? (value as Array<Record<string, unknown>>) : []
|
||||
}
|
||||
|
||||
function toStringArray(value: unknown): string[] {
|
||||
return Array.isArray(value) ? (value as string[]) : []
|
||||
}
|
||||
|
||||
function toRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {}
|
||||
}
|
||||
|
||||
function toRecordOrNull(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null
|
||||
}
|
||||
|
||||
/** Flattens an `EntityStub` (`{ entityId: { id, type }, name }`) into a single object. */
|
||||
export function mapEntityStub(stub: unknown): DynatraceEntityStub | null {
|
||||
const record = toRecordOrNull(stub)
|
||||
if (!record) return null
|
||||
const entityId = toRecord(record.entityId)
|
||||
return {
|
||||
id: (entityId.id as string) ?? null,
|
||||
type: (entityId.type as string) ?? null,
|
||||
name: (record.name as string) ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
function mapEntityStubs(value: unknown): DynatraceEntityStub[] {
|
||||
return toRecordArray(value)
|
||||
.map(mapEntityStub)
|
||||
.filter((stub): stub is DynatraceEntityStub => stub !== null)
|
||||
}
|
||||
|
||||
/** Maps a `METag` list. */
|
||||
export function mapTags(value: unknown): DynatraceTag[] {
|
||||
return toRecordArray(value).map((tag) => ({
|
||||
context: (tag.context as string) ?? null,
|
||||
key: (tag.key as string) ?? null,
|
||||
value: (tag.value as string) ?? null,
|
||||
stringRepresentation: (tag.stringRepresentation as string) ?? null,
|
||||
}))
|
||||
}
|
||||
|
||||
/** Maps a management zone list. */
|
||||
export function mapManagementZones(value: unknown): DynatraceManagementZone[] {
|
||||
return toRecordArray(value).map((zone) => ({
|
||||
id: (zone.id as string) ?? null,
|
||||
name: (zone.name as string) ?? null,
|
||||
}))
|
||||
}
|
||||
|
||||
function mapProblemFilters(value: unknown): DynatraceProblemFilter[] {
|
||||
return toRecordArray(value).map((filter) => ({
|
||||
id: (filter.id as string) ?? null,
|
||||
name: (filter.name as string) ?? null,
|
||||
}))
|
||||
}
|
||||
|
||||
/** Maps a single problem comment. */
|
||||
export function mapComment(value: unknown): DynatraceComment | null {
|
||||
const comment = toRecordOrNull(value)
|
||||
if (!comment) return null
|
||||
return {
|
||||
id: (comment.id as string) ?? null,
|
||||
authorName: (comment.authorName as string) ?? null,
|
||||
content: (comment.content as string) ?? null,
|
||||
context: (comment.context as string) ?? null,
|
||||
createdAtTimestamp: (comment.createdAtTimestamp as number) ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps a problem from the Problems API v2. */
|
||||
export function mapProblem(value: Record<string, unknown>): DynatraceProblem {
|
||||
const linkedProblem = toRecordOrNull(value.linkedProblemInfo)
|
||||
return {
|
||||
problemId: (value.problemId as string) ?? null,
|
||||
displayId: (value.displayId as string) ?? null,
|
||||
title: (value.title as string) ?? null,
|
||||
status: (value.status as string) ?? null,
|
||||
severityLevel: (value.severityLevel as string) ?? null,
|
||||
impactLevel: (value.impactLevel as string) ?? null,
|
||||
startTime: (value.startTime as number) ?? null,
|
||||
endTime: (value.endTime as number) ?? null,
|
||||
rootCauseEntity: mapEntityStub(value.rootCauseEntity),
|
||||
affectedEntities: mapEntityStubs(value.affectedEntities),
|
||||
impactedEntities: mapEntityStubs(value.impactedEntities),
|
||||
managementZones: mapManagementZones(value.managementZones),
|
||||
entityTags: mapTags(value.entityTags),
|
||||
problemFilters: mapProblemFilters(value.problemFilters),
|
||||
linkedProblemInfo: linkedProblem
|
||||
? {
|
||||
problemId: (linkedProblem.problemId as string) ?? null,
|
||||
displayId: (linkedProblem.displayId as string) ?? null,
|
||||
}
|
||||
: null,
|
||||
evidenceDetails: toRecordOrNull(value.evidenceDetails),
|
||||
impactAnalysis: toRecordOrNull(value.impactAnalysis),
|
||||
recentComments: toRecordOrNull(value.recentComments),
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps a monitored entity. */
|
||||
export function mapEntity(value: Record<string, unknown>): DynatraceEntity {
|
||||
return {
|
||||
entityId: (value.entityId as string) ?? null,
|
||||
type: (value.type as string) ?? null,
|
||||
displayName: (value.displayName as string) ?? null,
|
||||
firstSeenTms: (value.firstSeenTms as number) ?? null,
|
||||
lastSeenTms: (value.lastSeenTms as number) ?? null,
|
||||
properties: toRecord(value.properties),
|
||||
tags: mapTags(value.tags),
|
||||
managementZones: mapManagementZones(value.managementZones),
|
||||
icon: toRecordOrNull(value.icon),
|
||||
fromRelationships: toRecord(value.fromRelationships),
|
||||
toRelationships: toRecord(value.toRelationships),
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps an entity type descriptor. */
|
||||
export function mapEntityType(value: Record<string, unknown>): DynatraceEntityType {
|
||||
return {
|
||||
type: (value.type as string) ?? null,
|
||||
displayName: (value.displayName as string) ?? null,
|
||||
dimensionKey: (value.dimensionKey as string) ?? null,
|
||||
entityLimitExceeded: (value.entityLimitExceeded as boolean) ?? null,
|
||||
properties: toRecordArray(value.properties).map((property) => ({
|
||||
id: (property.id as string) ?? null,
|
||||
displayName: (property.displayName as string) ?? null,
|
||||
type: (property.type as string) ?? null,
|
||||
})),
|
||||
fromRelationships: toRecordArray(value.fromRelationships).map((relationship) => ({
|
||||
id: (relationship.id as string) ?? null,
|
||||
toTypes: toStringArray(relationship.toTypes),
|
||||
})),
|
||||
toRelationships: toRecordArray(value.toRelationships).map((relationship) => ({
|
||||
id: (relationship.id as string) ?? null,
|
||||
fromTypes: toStringArray(relationship.fromTypes),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps an event from the Events API v2. */
|
||||
export function mapEvent(value: Record<string, unknown>): DynatraceEvent {
|
||||
return {
|
||||
eventId: (value.eventId as string) ?? null,
|
||||
eventType: (value.eventType as string) ?? null,
|
||||
title: (value.title as string) ?? null,
|
||||
startTime: (value.startTime as number) ?? null,
|
||||
endTime: (value.endTime as number) ?? null,
|
||||
status: (value.status as string) ?? null,
|
||||
correlationId: (value.correlationId as string) ?? null,
|
||||
frequentEvent: (value.frequentEvent as boolean) ?? null,
|
||||
underMaintenance: (value.underMaintenance as boolean) ?? null,
|
||||
suppressAlert: (value.suppressAlert as boolean) ?? null,
|
||||
suppressProblem: (value.suppressProblem as boolean) ?? null,
|
||||
entityId: mapEntityStub(value.entityId),
|
||||
properties: toRecordArray(value.properties).map((property) => ({
|
||||
key: (property.key as string) ?? null,
|
||||
value: (property.value as string) ?? null,
|
||||
})),
|
||||
managementZones: mapManagementZones(value.managementZones),
|
||||
entityTags: mapTags(value.entityTags),
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps one metric of a `/metrics/query` result. */
|
||||
export function mapMetricResult(value: Record<string, unknown>): DynatraceMetricResult {
|
||||
return {
|
||||
metricId: (value.metricId as string) ?? null,
|
||||
dataPointCountRatio: (value.dataPointCountRatio as number) ?? null,
|
||||
dimensionCountRatio: (value.dimensionCountRatio as number) ?? null,
|
||||
appliedOptionalFilters: Array.isArray(value.appliedOptionalFilters)
|
||||
? value.appliedOptionalFilters
|
||||
: [],
|
||||
dql: toRecordOrNull(value.dql),
|
||||
warnings: toStringArray(value.warnings),
|
||||
data: toRecordArray(value.data).map((series) => ({
|
||||
dimensions: toStringArray(series.dimensions),
|
||||
dimensionMap: toRecord(series.dimensionMap) as Record<string, string>,
|
||||
timestamps: Array.isArray(series.timestamps) ? (series.timestamps as number[]) : [],
|
||||
values: Array.isArray(series.values) ? (series.values as Array<number | null>) : [],
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps a metric descriptor. */
|
||||
export function mapMetricDescriptor(value: Record<string, unknown>): DynatraceMetricDescriptor {
|
||||
return {
|
||||
metricId: (value.metricId as string) ?? null,
|
||||
displayName: (value.displayName as string) ?? null,
|
||||
description: (value.description as string) ?? null,
|
||||
unit: (value.unit as string) ?? null,
|
||||
unitDisplayFormat: (value.unitDisplayFormat as string) ?? null,
|
||||
tags: toStringArray(value.tags),
|
||||
billable: (value.billable as boolean) ?? null,
|
||||
dduBillable: (value.dduBillable as boolean) ?? null,
|
||||
created: (value.created as number) ?? null,
|
||||
lastWritten: (value.lastWritten as number) ?? null,
|
||||
aggregationTypes: toStringArray(value.aggregationTypes),
|
||||
defaultAggregation: toRecordOrNull(value.defaultAggregation),
|
||||
dimensionDefinitions: toRecordArray(value.dimensionDefinitions),
|
||||
dimensionCardinalities: toRecordArray(value.dimensionCardinalities),
|
||||
transformations: toStringArray(value.transformations),
|
||||
entityType: toStringArray(value.entityType),
|
||||
minimumValue: (value.minimumValue as number) ?? null,
|
||||
maximumValue: (value.maximumValue as number) ?? null,
|
||||
rootCauseRelevant: (value.rootCauseRelevant as boolean) ?? null,
|
||||
impactRelevant: (value.impactRelevant as boolean) ?? null,
|
||||
metricValueType: toRecordOrNull(value.metricValueType),
|
||||
latency: (value.latency as number) ?? null,
|
||||
metricSelector: (value.metricSelector as string) ?? null,
|
||||
scalar: (value.scalar as boolean) ?? null,
|
||||
resolutionInfSupported: (value.resolutionInfSupported as boolean) ?? null,
|
||||
warnings: toStringArray(value.warnings),
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps a service-level objective. */
|
||||
export function mapSlo(value: Record<string, unknown>): DynatraceSlo {
|
||||
return {
|
||||
id: (value.id as string) ?? null,
|
||||
name: (value.name as string) ?? null,
|
||||
description: (value.description as string) ?? null,
|
||||
enabled: (value.enabled as boolean) ?? null,
|
||||
target: (value.target as number) ?? null,
|
||||
warning: (value.warning as number) ?? null,
|
||||
timeframe: (value.timeframe as string) ?? null,
|
||||
filter: (value.filter as string) ?? null,
|
||||
evaluationType: (value.evaluationType as string) ?? null,
|
||||
evaluatedPercentage: (value.evaluatedPercentage as number) ?? null,
|
||||
status: (value.status as string) ?? null,
|
||||
error: (value.error as string) ?? null,
|
||||
errorBudget: (value.errorBudget as number) ?? null,
|
||||
errorBudgetBurnRate: toRecordOrNull(value.errorBudgetBurnRate),
|
||||
metricKey: (value.metricKey as string) ?? null,
|
||||
metricName: (value.metricName as string) ?? null,
|
||||
metricExpression: (value.metricExpression as string) ?? null,
|
||||
relatedOpenProblems: (value.relatedOpenProblems as number) ?? null,
|
||||
relatedTotalProblems: (value.relatedTotalProblems as number) ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps a security problem (vulnerability). */
|
||||
export function mapSecurityProblem(value: Record<string, unknown>): DynatraceSecurityProblem {
|
||||
return {
|
||||
securityProblemId: (value.securityProblemId as string) ?? null,
|
||||
displayId: (value.displayId as string) ?? null,
|
||||
status: (value.status as string) ?? null,
|
||||
muted: (value.muted as boolean) ?? null,
|
||||
title: (value.title as string) ?? null,
|
||||
technology: (value.technology as string) ?? null,
|
||||
vulnerabilityType: (value.vulnerabilityType as string) ?? null,
|
||||
packageName: (value.packageName as string) ?? null,
|
||||
externalVulnerabilityId: (value.externalVulnerabilityId as string) ?? null,
|
||||
cveIds: toStringArray(value.cveIds),
|
||||
url: (value.url as string) ?? null,
|
||||
firstSeenTimestamp: (value.firstSeenTimestamp as number) ?? null,
|
||||
lastUpdatedTimestamp: (value.lastUpdatedTimestamp as number) ?? null,
|
||||
lastOpenedTimestamp: (value.lastOpenedTimestamp as number) ?? null,
|
||||
lastResolvedTimestamp: (value.lastResolvedTimestamp as number) ?? null,
|
||||
riskAssessment: toRecordOrNull(value.riskAssessment),
|
||||
managementZones: mapManagementZones(value.managementZones),
|
||||
globalCounts: toRecordOrNull(value.globalCounts),
|
||||
codeLevelVulnerabilityDetails: toRecordOrNull(value.codeLevelVulnerabilityDetails),
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps a security problem including the detail-only fields. */
|
||||
export function mapSecurityProblemDetails(
|
||||
value: Record<string, unknown>
|
||||
): DynatraceSecurityProblemDetails {
|
||||
return {
|
||||
...mapSecurityProblem(value),
|
||||
description: (value.description as string) ?? null,
|
||||
remediationDescription: (value.remediationDescription as string) ?? null,
|
||||
muteStateChangeInProgress: (value.muteStateChangeInProgress as boolean) ?? null,
|
||||
affectedEntities: toStringArray(value.affectedEntities),
|
||||
exposedEntities: toStringArray(value.exposedEntities),
|
||||
reachableDataAssets: toStringArray(value.reachableDataAssets),
|
||||
vulnerableComponents: toRecordArray(value.vulnerableComponents),
|
||||
filteredCounts: toRecordOrNull(value.filteredCounts),
|
||||
events: toRecordArray(value.events),
|
||||
entryPoints: toRecordOrNull(value.entryPoints),
|
||||
relatedEntities: toRecordOrNull(value.relatedEntities),
|
||||
relatedAttacks: toRecordOrNull(value.relatedAttacks),
|
||||
relatedContainerImages: toRecordOrNull(value.relatedContainerImages),
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps an audit log entry, lifting the dotted `dt.settings.*` keys into camelCase. */
|
||||
export function mapAuditLog(value: Record<string, unknown>): DynatraceAuditLog {
|
||||
return {
|
||||
logId: (value.logId as string) ?? null,
|
||||
eventType: (value.eventType as string) ?? null,
|
||||
category: (value.category as string) ?? null,
|
||||
entityId: (value.entityId as string) ?? null,
|
||||
environmentId: (value.environmentId as string) ?? null,
|
||||
user: (value.user as string) ?? null,
|
||||
userType: (value.userType as string) ?? null,
|
||||
userOrigin: (value.userOrigin as string) ?? null,
|
||||
timestamp: (value.timestamp as number) ?? null,
|
||||
success: (value.success as boolean) ?? null,
|
||||
message: (value.message as string) ?? null,
|
||||
patch: toRecordOrNull(value.patch),
|
||||
settingsSchemaId: (value['dt.settings.schema_id'] as string) ?? null,
|
||||
settingsScopeId: (value['dt.settings.scope_id'] as string) ?? null,
|
||||
settingsKey: (value['dt.settings.key'] as string) ?? null,
|
||||
settingsObjectId: (value['dt.settings.object_id'] as string) ?? null,
|
||||
settingsObjectSummary: (value['dt.settings.object_summary'] as string) ?? null,
|
||||
settingsScopeName: (value['dt.settings.scope_name'] as string) ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps a log record from the Log Monitoring API v2 search endpoint. */
|
||||
export function mapLogRecord(value: Record<string, unknown>): DynatraceLogRecord {
|
||||
return {
|
||||
timestamp: (value.timestamp as number) ?? null,
|
||||
status: (value.status as string) ?? null,
|
||||
content: (value.content as string) ?? null,
|
||||
eventType: (value.eventType as string) ?? null,
|
||||
additionalColumns: toRecord(value.additionalColumns),
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalizes the `warnings` array Dynatrace list endpoints return. */
|
||||
export function mapWarnings(value: unknown): string[] {
|
||||
if (Array.isArray(value)) return value.map((warning) => String(warning))
|
||||
if (typeof value === 'string') return [value]
|
||||
return []
|
||||
}
|
||||
@@ -243,6 +243,32 @@ const ERROR_EXTRACTORS: ErrorExtractorConfig[] = [
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'dynatrace-errors',
|
||||
description:
|
||||
'Dynatrace ErrorEnvelope: {error: {code, message, constraintViolations[]}}. The violations name the offending selector or parameter, which the bare message does not',
|
||||
examples: ['Dynatrace Environment API v2'],
|
||||
extract: (errorInfo) => {
|
||||
const error = errorInfo?.data?.error
|
||||
if (!error || typeof error !== 'object') return undefined
|
||||
|
||||
const message = typeof error.message === 'string' ? error.message.trim() : ''
|
||||
const violations = Array.isArray(error.constraintViolations)
|
||||
? error.constraintViolations
|
||||
.map((violation: { path?: unknown; message?: unknown }) => {
|
||||
const detail = typeof violation?.message === 'string' ? violation.message.trim() : ''
|
||||
if (!detail) return ''
|
||||
const path = typeof violation?.path === 'string' ? violation.path.trim() : ''
|
||||
return path ? `${path}: ${detail}` : detail
|
||||
})
|
||||
.filter(Boolean)
|
||||
: []
|
||||
|
||||
if (!message && violations.length === 0) return undefined
|
||||
if (violations.length === 0) return message
|
||||
return message ? `${message} (${violations.join('; ')})` : violations.join('; ')
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'smartlead-errors',
|
||||
description:
|
||||
@@ -349,6 +375,7 @@ export const ErrorExtractorId = {
|
||||
SOAP_FAULT: 'soap-fault',
|
||||
OAUTH_ERROR_DESCRIPTION: 'oauth-error-description',
|
||||
NESTED_ERROR_OBJECT: 'nested-error-object',
|
||||
DYNATRACE_ERRORS: 'dynatrace-errors',
|
||||
SMARTLEAD_ERRORS: 'smartlead-errors',
|
||||
POSTHOG_ERRORS: 'posthog-errors',
|
||||
PLAIN_TEXT_DATA: 'plain-text-data',
|
||||
|
||||
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
@@ -915,6 +915,30 @@ import {
|
||||
dynamodbScanTool,
|
||||
dynamodbUpdateTool,
|
||||
} from '@/tools/dynamodb'
|
||||
import {
|
||||
dynatraceAddProblemCommentTool,
|
||||
dynatraceCloseProblemTool,
|
||||
dynatraceGetAuditLogsTool,
|
||||
dynatraceGetEntityTool,
|
||||
dynatraceGetEventTool,
|
||||
dynatraceGetMetricTool,
|
||||
dynatraceGetProblemTool,
|
||||
dynatraceGetSecurityProblemTool,
|
||||
dynatraceGetSloTool,
|
||||
dynatraceIngestEventTool,
|
||||
dynatraceIngestLogsTool,
|
||||
dynatraceIngestMetricsTool,
|
||||
dynatraceListEntitiesTool,
|
||||
dynatraceListEntityTypesTool,
|
||||
dynatraceListEventsTool,
|
||||
dynatraceListMetricsTool,
|
||||
dynatraceListProblemCommentsTool,
|
||||
dynatraceListProblemsTool,
|
||||
dynatraceListSecurityProblemsTool,
|
||||
dynatraceListSlosTool,
|
||||
dynatraceQueryMetricsTool,
|
||||
dynatraceSearchLogsTool,
|
||||
} from '@/tools/dynatrace'
|
||||
import {
|
||||
elasticsearchBulkTool,
|
||||
elasticsearchClusterHealthTool,
|
||||
@@ -6222,6 +6246,28 @@ export const tools: Record<string, ToolConfig> = {
|
||||
dynamodb_update: dynamodbUpdateTool,
|
||||
dynamodb_delete: dynamodbDeleteTool,
|
||||
dynamodb_introspect: dynamodbIntrospectTool,
|
||||
dynatrace_list_problems: dynatraceListProblemsTool,
|
||||
dynatrace_get_problem: dynatraceGetProblemTool,
|
||||
dynatrace_close_problem: dynatraceCloseProblemTool,
|
||||
dynatrace_list_problem_comments: dynatraceListProblemCommentsTool,
|
||||
dynatrace_add_problem_comment: dynatraceAddProblemCommentTool,
|
||||
dynatrace_query_metrics: dynatraceQueryMetricsTool,
|
||||
dynatrace_list_metrics: dynatraceListMetricsTool,
|
||||
dynatrace_get_metric: dynatraceGetMetricTool,
|
||||
dynatrace_ingest_metrics: dynatraceIngestMetricsTool,
|
||||
dynatrace_list_entities: dynatraceListEntitiesTool,
|
||||
dynatrace_get_entity: dynatraceGetEntityTool,
|
||||
dynatrace_list_entity_types: dynatraceListEntityTypesTool,
|
||||
dynatrace_list_events: dynatraceListEventsTool,
|
||||
dynatrace_get_event: dynatraceGetEventTool,
|
||||
dynatrace_ingest_event: dynatraceIngestEventTool,
|
||||
dynatrace_search_logs: dynatraceSearchLogsTool,
|
||||
dynatrace_ingest_logs: dynatraceIngestLogsTool,
|
||||
dynatrace_list_slos: dynatraceListSlosTool,
|
||||
dynatrace_get_slo: dynatraceGetSloTool,
|
||||
dynatrace_list_security_problems: dynatraceListSecurityProblemsTool,
|
||||
dynatrace_get_security_problem: dynatraceGetSecurityProblemTool,
|
||||
dynatrace_get_audit_logs: dynatraceGetAuditLogsTool,
|
||||
dropbox_upload: dropboxUploadTool,
|
||||
dropbox_download: dropboxDownloadTool,
|
||||
dropbox_list_folder: dropboxListFolderTool,
|
||||
|
||||
Reference in New Issue
Block a user