chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
import type {
|
||||
NewRelicChangeTrackingEvent,
|
||||
NewRelicCreateDeploymentEventParams,
|
||||
NewRelicCreateDeploymentEventResponse,
|
||||
NewRelicDeploymentType,
|
||||
} from '@/tools/new_relic/types'
|
||||
import {
|
||||
cleanOptionalString,
|
||||
getNerdGraphEndpoint,
|
||||
gqlString,
|
||||
newRelicHeaders,
|
||||
parseNerdGraphResponse,
|
||||
} from '@/tools/new_relic/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface CreateDeploymentEventData {
|
||||
changeTrackingCreateEvent?: {
|
||||
changeTrackingEvent?: NewRelicChangeTrackingEvent | null
|
||||
messages?: string[]
|
||||
} | null
|
||||
}
|
||||
|
||||
const DEPLOYMENT_TYPES: NewRelicDeploymentType[] = [
|
||||
'basic',
|
||||
'blue green',
|
||||
'canary',
|
||||
'rolling',
|
||||
'shadow',
|
||||
]
|
||||
|
||||
const CUSTOM_ATTRIBUTE_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
|
||||
const RESTRICTED_CUSTOM_ATTRIBUTE_NAMES = new Set([
|
||||
'accountId',
|
||||
'ago',
|
||||
'and',
|
||||
'appID',
|
||||
'as',
|
||||
'auto',
|
||||
'begin',
|
||||
'begintime',
|
||||
'category',
|
||||
'categoryType',
|
||||
'changeTrackingId',
|
||||
'compare',
|
||||
'customAttributes',
|
||||
'customType',
|
||||
'day',
|
||||
'days',
|
||||
'description',
|
||||
'end',
|
||||
'endtime',
|
||||
'explain',
|
||||
'entityGuid',
|
||||
'entityName',
|
||||
'eventType',
|
||||
'facet',
|
||||
'from',
|
||||
'groupId',
|
||||
'hostname',
|
||||
'hour',
|
||||
'hours',
|
||||
'in',
|
||||
'is',
|
||||
'like',
|
||||
'limit',
|
||||
'log',
|
||||
'minute',
|
||||
'minutes',
|
||||
'month',
|
||||
'months',
|
||||
'not',
|
||||
'null',
|
||||
'offset',
|
||||
'or',
|
||||
'raw',
|
||||
'second',
|
||||
'seconds',
|
||||
'select',
|
||||
'since',
|
||||
'timeseries',
|
||||
'timestamp',
|
||||
'type',
|
||||
'until',
|
||||
'user',
|
||||
'week',
|
||||
'weeks',
|
||||
'where',
|
||||
'with',
|
||||
])
|
||||
|
||||
const getDeploymentType = (value?: string): NewRelicDeploymentType => {
|
||||
const normalized = value?.toLowerCase().replace(/[_-]/g, ' ')
|
||||
return DEPLOYMENT_TYPES.includes(normalized as NewRelicDeploymentType)
|
||||
? (normalized as NewRelicDeploymentType)
|
||||
: 'basic'
|
||||
}
|
||||
|
||||
const graphqlLiteral = (value: string | number | boolean): string => {
|
||||
if (typeof value === 'string') return gqlString(value)
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const buildCustomAttributes = (
|
||||
customAttributes?: NewRelicCreateDeploymentEventParams['customAttributes']
|
||||
): string | undefined => {
|
||||
if (!customAttributes) return undefined
|
||||
|
||||
const entries = Object.entries(customAttributes)
|
||||
if (!entries.length) return undefined
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
if (!CUSTOM_ATTRIBUTE_NAME_PATTERN.test(key)) {
|
||||
throw new Error(
|
||||
`Invalid New Relic custom attribute name "${key}". Use letters, numbers, and underscores, and do not start with a number.`
|
||||
)
|
||||
}
|
||||
if (RESTRICTED_CUSTOM_ATTRIBUTE_NAMES.has(key) || key.includes('.')) {
|
||||
throw new Error(`New Relic custom attribute name "${key}" is restricted`)
|
||||
}
|
||||
if (!['string', 'number', 'boolean'].includes(typeof value)) {
|
||||
throw new Error(
|
||||
`Invalid value for New Relic custom attribute "${key}". Use a string, number, or boolean.`
|
||||
)
|
||||
}
|
||||
if (typeof value === 'number' && !Number.isFinite(value)) {
|
||||
throw new Error(`Invalid numeric value for New Relic custom attribute "${key}"`)
|
||||
}
|
||||
}
|
||||
|
||||
const fields = entries.map(([key, value]) => `${key}: ${graphqlLiteral(value)}`).join(', ')
|
||||
return `customAttributes: { ${fields} }`
|
||||
}
|
||||
|
||||
const buildDeploymentMutation = (params: NewRelicCreateDeploymentEventParams): string => {
|
||||
const deploymentType = getDeploymentType(params.deploymentType)
|
||||
const entityGuid = params.entityGuid.trim()
|
||||
if (!entityGuid || entityGuid.includes("'")) {
|
||||
throw new Error('Invalid entity GUID: value must not be empty or contain single quotes')
|
||||
}
|
||||
const version = params.version.trim()
|
||||
const shortDescription = cleanOptionalString(params.shortDescription)
|
||||
const description = cleanOptionalString(params.description)
|
||||
const changelog = cleanOptionalString(params.changelog)
|
||||
const commit = cleanOptionalString(params.commit)
|
||||
const deepLink = cleanOptionalString(params.deepLink)
|
||||
const user = cleanOptionalString(params.user)
|
||||
const groupId = cleanOptionalString(params.groupId)
|
||||
const customAttributes = buildCustomAttributes(params.customAttributes)
|
||||
const deploymentFields = [
|
||||
`version: ${gqlString(version)}`,
|
||||
changelog ? `changelog: ${gqlString(changelog)}` : undefined,
|
||||
commit ? `commit: ${gqlString(commit)}` : undefined,
|
||||
deepLink ? `deepLink: ${gqlString(deepLink)}` : undefined,
|
||||
]
|
||||
.filter((field): field is string => Boolean(field))
|
||||
.join(', ')
|
||||
const optionalFields = [
|
||||
shortDescription ? `shortDescription: ${gqlString(shortDescription)}` : undefined,
|
||||
description ? `description: ${gqlString(description)}` : undefined,
|
||||
user ? `user: ${gqlString(user)}` : undefined,
|
||||
groupId ? `groupId: ${gqlString(groupId)}` : undefined,
|
||||
customAttributes,
|
||||
params.timestamp ? `timestamp: ${Math.trunc(Number(params.timestamp))}` : undefined,
|
||||
]
|
||||
.filter((field): field is string => Boolean(field))
|
||||
.join('\n ')
|
||||
|
||||
return `mutation {
|
||||
changeTrackingCreateEvent(
|
||||
changeTrackingEvent: {
|
||||
categoryAndTypeData: {
|
||||
categoryFields: { deployment: { ${deploymentFields} } }
|
||||
kind: { category: "deployment", type: ${gqlString(deploymentType)} }
|
||||
}
|
||||
entitySearch: { query: ${gqlString(`id = '${entityGuid}'`)} }
|
||||
${optionalFields}
|
||||
}
|
||||
) {
|
||||
changeTrackingEvent {
|
||||
category
|
||||
categoryAndType
|
||||
changeTrackingId
|
||||
customAttributes
|
||||
description
|
||||
entity {
|
||||
guid
|
||||
name
|
||||
}
|
||||
groupId
|
||||
shortDescription
|
||||
timestamp
|
||||
type
|
||||
user
|
||||
}
|
||||
messages
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
export const newRelicCreateDeploymentEventTool: ToolConfig<
|
||||
NewRelicCreateDeploymentEventParams,
|
||||
NewRelicCreateDeploymentEventResponse
|
||||
> = {
|
||||
id: 'new_relic_create_deployment_event',
|
||||
name: 'New Relic Create Deployment Event',
|
||||
description: 'Record a deployment change event in New Relic change tracking.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'New Relic user API key for NerdGraph',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'New Relic data center region: us or eu',
|
||||
},
|
||||
entityGuid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'GUID of the entity associated with the deployment',
|
||||
},
|
||||
version: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Deployment version, release name, or commit SHA',
|
||||
},
|
||||
shortDescription: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Short description of the deployment',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Longer deployment description',
|
||||
},
|
||||
changelog: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Deployment changelog text or URL',
|
||||
},
|
||||
commit: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Commit SHA or identifier associated with the deployment',
|
||||
},
|
||||
deepLink: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'URL to the deployment, build, or release details',
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'User or automation that performed the deployment',
|
||||
},
|
||||
groupId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional group ID to correlate related changes',
|
||||
},
|
||||
customAttributes: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Custom change event metadata as key-value pairs with string, number, or boolean values',
|
||||
},
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Deployment type: basic, blue green, canary, rolling, or shadow',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Event timestamp in milliseconds since Unix epoch',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => getNerdGraphEndpoint(params.region),
|
||||
method: 'POST',
|
||||
headers: (params) => newRelicHeaders(params.apiKey),
|
||||
body: (params) => ({
|
||||
query: buildDeploymentMutation(params),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const payload = await parseNerdGraphResponse<CreateDeploymentEventData>(response)
|
||||
const result = payload.data?.changeTrackingCreateEvent
|
||||
if (!result) {
|
||||
throw new Error('New Relic did not return a deployment change tracking result')
|
||||
}
|
||||
if (!result.changeTrackingEvent) {
|
||||
const message = result.messages?.length
|
||||
? result.messages.join('; ')
|
||||
: 'New Relic did not create a deployment change tracking event'
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
event: result.changeTrackingEvent,
|
||||
messages: result?.messages ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
event: {
|
||||
type: 'object',
|
||||
description: 'Created New Relic change tracking event',
|
||||
properties: {
|
||||
changeTrackingId: {
|
||||
type: 'string',
|
||||
description: 'New Relic change tracking ID',
|
||||
nullable: true,
|
||||
},
|
||||
customAttributes: {
|
||||
type: 'json',
|
||||
description: 'Custom attributes on the change tracking event',
|
||||
optional: true,
|
||||
nullable: true,
|
||||
},
|
||||
category: { type: 'string', description: 'Change category', nullable: true },
|
||||
categoryAndType: {
|
||||
type: 'string',
|
||||
description: 'Combined category and type',
|
||||
nullable: true,
|
||||
},
|
||||
type: { type: 'string', description: 'Change type', nullable: true },
|
||||
shortDescription: {
|
||||
type: 'string',
|
||||
description: 'Short change description',
|
||||
nullable: true,
|
||||
},
|
||||
description: { type: 'string', description: 'Change description', nullable: true },
|
||||
timestamp: {
|
||||
type: 'number',
|
||||
description: 'Change timestamp in milliseconds',
|
||||
nullable: true,
|
||||
},
|
||||
user: { type: 'string', description: 'User associated with the change', nullable: true },
|
||||
groupId: { type: 'string', description: 'Change group ID', nullable: true },
|
||||
entity: {
|
||||
type: 'object',
|
||||
description: 'Entity associated with the change',
|
||||
nullable: true,
|
||||
properties: {
|
||||
guid: { type: 'string', description: 'Entity GUID', nullable: true },
|
||||
name: { type: 'string', description: 'Entity name', nullable: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
type: 'array',
|
||||
description: 'Messages returned by New Relic for the created change event',
|
||||
items: {
|
||||
type: 'string',
|
||||
description: 'New Relic message',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { NewRelicGetEntityParams, NewRelicGetEntityResponse } from '@/tools/new_relic/types'
|
||||
import {
|
||||
getNerdGraphEndpoint,
|
||||
gqlString,
|
||||
type NewRelicRawEntity,
|
||||
newRelicHeaders,
|
||||
normalizeNewRelicEntity,
|
||||
parseNerdGraphResponse,
|
||||
} from '@/tools/new_relic/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface GetEntityData {
|
||||
actor?: {
|
||||
entity?: NewRelicRawEntity | null
|
||||
} | null
|
||||
}
|
||||
|
||||
export const newRelicGetEntityTool: ToolConfig<NewRelicGetEntityParams, NewRelicGetEntityResponse> =
|
||||
{
|
||||
id: 'new_relic_get_entity',
|
||||
name: 'New Relic Get Entity',
|
||||
description: 'Fetch a New Relic entity by GUID.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'New Relic user API key for NerdGraph',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'New Relic data center region: us or eu',
|
||||
},
|
||||
guid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity GUID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => getNerdGraphEndpoint(params.region),
|
||||
method: 'POST',
|
||||
headers: (params) => newRelicHeaders(params.apiKey),
|
||||
body: (params) => ({
|
||||
query: `{
|
||||
actor {
|
||||
entity(guid: ${gqlString(params.guid.trim())}) {
|
||||
name
|
||||
entityType
|
||||
domain
|
||||
reporting
|
||||
... on AlertableEntity {
|
||||
alertSeverity
|
||||
}
|
||||
tags {
|
||||
key
|
||||
values
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params) => {
|
||||
const payload = await parseNerdGraphResponse<GetEntityData>(response)
|
||||
const entity = payload.data?.actor?.entity
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
entity: entity
|
||||
? { ...normalizeNewRelicEntity(entity), guid: params?.guid ?? null }
|
||||
: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
entity: {
|
||||
type: 'object',
|
||||
description: 'New Relic entity details',
|
||||
optional: true,
|
||||
properties: {
|
||||
guid: { type: 'string', description: 'Entity GUID', nullable: true },
|
||||
name: { type: 'string', description: 'Entity name', nullable: true },
|
||||
entityType: { type: 'string', description: 'Entity type', nullable: true },
|
||||
domain: { type: 'string', description: 'Entity domain, e.g. APM, INFRA', nullable: true },
|
||||
reporting: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the entity is currently reporting data',
|
||||
nullable: true,
|
||||
},
|
||||
alertSeverity: {
|
||||
type: 'string',
|
||||
description: 'Current alert severity for the entity',
|
||||
nullable: true,
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
description: 'Entity tags',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string', description: 'Tag key', nullable: true },
|
||||
values: { type: 'array', description: 'Tag values', items: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { newRelicCreateDeploymentEventTool } from '@/tools/new_relic/create_deployment_event'
|
||||
export { newRelicGetEntityTool } from '@/tools/new_relic/get_entity'
|
||||
export { newRelicNrqlQueryTool } from '@/tools/new_relic/nrql_query'
|
||||
export { newRelicSearchEntitiesTool } from '@/tools/new_relic/search_entities'
|
||||
export type * from '@/tools/new_relic/types'
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { NewRelicNrqlQueryParams, NewRelicNrqlQueryResponse } from '@/tools/new_relic/types'
|
||||
import {
|
||||
getNerdGraphEndpoint,
|
||||
gqlString,
|
||||
newRelicHeaders,
|
||||
parseNerdGraphResponse,
|
||||
} from '@/tools/new_relic/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface NrqlQueryData {
|
||||
actor?: {
|
||||
account?: {
|
||||
nrql?: {
|
||||
results?: Record<string, unknown>[]
|
||||
} | null
|
||||
} | null
|
||||
} | null
|
||||
}
|
||||
|
||||
export const newRelicNrqlQueryTool: ToolConfig<NewRelicNrqlQueryParams, NewRelicNrqlQueryResponse> =
|
||||
{
|
||||
id: 'new_relic_nrql_query',
|
||||
name: 'New Relic NRQL Query',
|
||||
description: 'Run a NRQL query against a New Relic account using NerdGraph.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'New Relic user API key for NerdGraph',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'New Relic data center region: us or eu',
|
||||
},
|
||||
accountId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New Relic account ID to query',
|
||||
},
|
||||
nrql: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'NRQL query to execute',
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional query timeout in seconds',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => getNerdGraphEndpoint(params.region),
|
||||
method: 'POST',
|
||||
headers: (params) => newRelicHeaders(params.apiKey),
|
||||
body: (params) => {
|
||||
const timeout = params.timeout ? `, timeout: ${Math.trunc(Number(params.timeout))}` : ''
|
||||
return {
|
||||
query: `{
|
||||
actor {
|
||||
account(id: ${Math.trunc(Number(params.accountId))}) {
|
||||
nrql(query: ${gqlString(params.nrql)}${timeout}) {
|
||||
results
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const payload = await parseNerdGraphResponse<NrqlQueryData>(response)
|
||||
const nrql = payload.data?.actor?.account?.nrql
|
||||
if (!nrql) {
|
||||
throw new Error('New Relic did not return NRQL data for the requested account')
|
||||
}
|
||||
const results = nrql.results ?? []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results,
|
||||
resultCount: results.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'NRQL result rows. Row fields depend on the query projection.',
|
||||
items: {
|
||||
type: 'object',
|
||||
description: 'A NRQL result row',
|
||||
},
|
||||
},
|
||||
resultCount: {
|
||||
type: 'number',
|
||||
description: 'Number of NRQL result rows returned',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import type {
|
||||
NewRelicSearchEntitiesParams,
|
||||
NewRelicSearchEntitiesResponse,
|
||||
} from '@/tools/new_relic/types'
|
||||
import {
|
||||
getNerdGraphEndpoint,
|
||||
type NewRelicRawEntity,
|
||||
newRelicHeaders,
|
||||
normalizeNewRelicEntity,
|
||||
parseNerdGraphResponse,
|
||||
} from '@/tools/new_relic/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface SearchEntitiesData {
|
||||
actor?: {
|
||||
entitySearch?: {
|
||||
count?: number
|
||||
query?: string
|
||||
results?: {
|
||||
nextCursor?: string | null
|
||||
entities?: NewRelicRawEntity[]
|
||||
} | null
|
||||
} | null
|
||||
} | null
|
||||
}
|
||||
|
||||
export const newRelicSearchEntitiesTool: ToolConfig<
|
||||
NewRelicSearchEntitiesParams,
|
||||
NewRelicSearchEntitiesResponse
|
||||
> = {
|
||||
id: 'new_relic_search_entities',
|
||||
name: 'New Relic Search Entities',
|
||||
description: 'Search New Relic entities by name, GUID, domain type, tags, or reporting state.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'New Relic user API key for NerdGraph',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'New Relic data center region: us or eu',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Entity search query, for example: name like "api" or domainType = "APM-APPLICATION"',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor from a previous entity search',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => getNerdGraphEndpoint(params.region),
|
||||
method: 'POST',
|
||||
headers: (params) => newRelicHeaders(params.apiKey),
|
||||
body: (params) => ({
|
||||
query: `query($query: String!, $cursor: String) {
|
||||
actor {
|
||||
entitySearch(query: $query) {
|
||||
count
|
||||
query
|
||||
results(cursor: $cursor) {
|
||||
nextCursor
|
||||
entities {
|
||||
guid
|
||||
name
|
||||
entityType
|
||||
domain
|
||||
reporting
|
||||
... on AlertableEntityOutline {
|
||||
alertSeverity
|
||||
}
|
||||
tags {
|
||||
key
|
||||
values
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
query: params.query,
|
||||
cursor: params.cursor || null,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const payload = await parseNerdGraphResponse<SearchEntitiesData>(response)
|
||||
const entitySearch = payload.data?.actor?.entitySearch
|
||||
if (!entitySearch) {
|
||||
throw new Error('New Relic did not return entity search data')
|
||||
}
|
||||
const entities = (entitySearch?.results?.entities ?? []).map(normalizeNewRelicEntity)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
count: entitySearch?.count ?? entities.length,
|
||||
query: entitySearch?.query ?? '',
|
||||
entities,
|
||||
nextCursor: entitySearch?.results?.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
count: { type: 'number', description: 'Total number of entities matching the query' },
|
||||
query: { type: 'string', description: 'Entity search query New Relic executed' },
|
||||
entities: {
|
||||
type: 'array',
|
||||
description: 'Matching New Relic entities',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
guid: { type: 'string', description: 'Entity GUID', nullable: true },
|
||||
name: { type: 'string', description: 'Entity name', nullable: true },
|
||||
entityType: { type: 'string', description: 'Entity type', nullable: true },
|
||||
domain: { type: 'string', description: 'Entity domain, e.g. APM, INFRA', nullable: true },
|
||||
reporting: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the entity is currently reporting data',
|
||||
nullable: true,
|
||||
},
|
||||
alertSeverity: {
|
||||
type: 'string',
|
||||
description: 'Current alert severity for the entity',
|
||||
nullable: true,
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
description: 'Entity tags',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string', description: 'Tag key', nullable: true },
|
||||
values: { type: 'array', description: 'Tag values', items: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export type NewRelicRegion = 'us' | 'eu'
|
||||
|
||||
export interface NewRelicBaseParams {
|
||||
apiKey: string
|
||||
region?: NewRelicRegion
|
||||
}
|
||||
|
||||
export interface NewRelicNrqlQueryParams extends NewRelicBaseParams {
|
||||
accountId: number
|
||||
nrql: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export interface NewRelicNrqlQueryResponse extends ToolResponse {
|
||||
output: {
|
||||
results: Record<string, unknown>[]
|
||||
resultCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface NewRelicSearchEntitiesParams extends NewRelicBaseParams {
|
||||
query: string
|
||||
cursor?: string
|
||||
}
|
||||
|
||||
export interface NewRelicEntityTag {
|
||||
key: string | null
|
||||
values: string[]
|
||||
}
|
||||
|
||||
export interface NewRelicEntity {
|
||||
guid: string | null
|
||||
name: string | null
|
||||
entityType: string | null
|
||||
domain: string | null
|
||||
reporting: boolean | null
|
||||
alertSeverity: string | null
|
||||
tags: NewRelicEntityTag[]
|
||||
}
|
||||
|
||||
export interface NewRelicSearchEntitiesResponse extends ToolResponse {
|
||||
output: {
|
||||
count: number
|
||||
query: string
|
||||
entities: NewRelicEntity[]
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface NewRelicGetEntityParams extends NewRelicBaseParams {
|
||||
guid: string
|
||||
}
|
||||
|
||||
export interface NewRelicGetEntityResponse extends ToolResponse {
|
||||
output: {
|
||||
entity: NewRelicEntity | null
|
||||
}
|
||||
}
|
||||
|
||||
export type NewRelicDeploymentType = 'basic' | 'blue green' | 'canary' | 'rolling' | 'shadow'
|
||||
|
||||
export type NewRelicCustomAttributes = Record<string, string | number | boolean>
|
||||
|
||||
export interface NewRelicCreateDeploymentEventParams extends NewRelicBaseParams {
|
||||
entityGuid: string
|
||||
version: string
|
||||
shortDescription?: string
|
||||
description?: string
|
||||
changelog?: string
|
||||
commit?: string
|
||||
deepLink?: string
|
||||
user?: string
|
||||
groupId?: string
|
||||
customAttributes?: NewRelicCustomAttributes
|
||||
deploymentType?: NewRelicDeploymentType
|
||||
timestamp?: number
|
||||
}
|
||||
|
||||
export interface NewRelicChangeTrackingEvent {
|
||||
category: string | null
|
||||
categoryAndType: string | null
|
||||
changeTrackingId: string | null
|
||||
customAttributes?: Record<string, unknown> | null
|
||||
description: string | null
|
||||
groupId: string | null
|
||||
shortDescription: string | null
|
||||
timestamp: number | null
|
||||
type: string | null
|
||||
user: string | null
|
||||
entity: {
|
||||
guid: string | null
|
||||
name: string | null
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface NewRelicCreateDeploymentEventResponse extends ToolResponse {
|
||||
output: {
|
||||
event: NewRelicChangeTrackingEvent | null
|
||||
messages: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export type NewRelicResponse =
|
||||
| NewRelicNrqlQueryResponse
|
||||
| NewRelicSearchEntitiesResponse
|
||||
| NewRelicGetEntityResponse
|
||||
| NewRelicCreateDeploymentEventResponse
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { NewRelicEntity, NewRelicRegion } from '@/tools/new_relic/types'
|
||||
|
||||
interface GraphQLError {
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface NewRelicRawEntity {
|
||||
guid?: string | null
|
||||
name?: string | null
|
||||
entityType?: string | null
|
||||
domain?: string | null
|
||||
reporting?: boolean | null
|
||||
alertSeverity?: string | null
|
||||
tags?: ({ key?: string | null; values?: string[] | null } | null)[] | null
|
||||
}
|
||||
|
||||
interface GraphQLResponse<TData> {
|
||||
data?: TData
|
||||
errors?: GraphQLError[]
|
||||
}
|
||||
|
||||
export const getNerdGraphEndpoint = (region?: NewRelicRegion): string =>
|
||||
region === 'eu' ? 'https://api.eu.newrelic.com/graphql' : 'https://api.newrelic.com/graphql'
|
||||
|
||||
export const newRelicHeaders = (apiKey: string): Record<string, string> => ({
|
||||
'API-Key': apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
})
|
||||
|
||||
export const gqlString = (value: string): string => JSON.stringify(value)
|
||||
|
||||
export async function parseNerdGraphResponse<TData>(
|
||||
response: Response
|
||||
): Promise<GraphQLResponse<TData>> {
|
||||
const payload = (await response.json().catch(() => ({}))) as GraphQLResponse<TData>
|
||||
|
||||
if (!response.ok || payload.errors?.length) {
|
||||
const message =
|
||||
payload.errors
|
||||
?.map((error) => error.message)
|
||||
.filter((errorMessage): errorMessage is string => Boolean(errorMessage))
|
||||
.join('; ') || `HTTP ${response.status}: ${response.statusText}`
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
export const cleanOptionalString = (value?: string): string | undefined => {
|
||||
const trimmed = value?.trim()
|
||||
return trimmed ? trimmed : undefined
|
||||
}
|
||||
|
||||
export const normalizeNewRelicEntity = (entity: NewRelicRawEntity): NewRelicEntity => ({
|
||||
guid: entity.guid ?? null,
|
||||
name: entity.name ?? null,
|
||||
entityType: entity.entityType ?? null,
|
||||
domain: entity.domain ?? null,
|
||||
reporting: entity.reporting ?? null,
|
||||
alertSeverity: entity.alertSeverity ?? null,
|
||||
tags:
|
||||
entity.tags?.map((tag) => ({
|
||||
key: tag?.key ?? null,
|
||||
values: tag?.values ?? [],
|
||||
})) ?? [],
|
||||
})
|
||||
Reference in New Issue
Block a user