Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions integrations/datadog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Datadog Integration for CORE

Connects your Datadog account to CORE, syncing monitor alerts and infrastructure events as activities.

## Auth

API Key + Application Key. Both are required.

You will need:
- **DD-API-KEY** — found in Datadog → Organization Settings → API Keys
- **DD-APPLICATION-KEY** — found in Datadog → Organization Settings → Application Keys
- **Region** — one of `US1` (default), `US3`, `US5`, `EU`, `AP1`

Region → base URL mapping:

| Region | Base URL |
|--------|----------|
| US1 | `https://api.datadoghq.com` |
| US3 | `https://us3.datadoghq.com` |
| US5 | `https://us5.datadoghq.com` |
| EU | `https://api.datadoghq.eu` |
| AP1 | `https://ap1.datadoghq.com` |

## Sync

Polls every 15 minutes using incremental timestamp-based sync.

- **Monitors**: fetches all monitors; surfaces those in ALERT or WARN state as activities.
- **Events**: fetches events since the last sync timestamp using cursor-based pagination (100 events per page).

Activities tracked:
- Monitor state changes (ALERT, WARN, NO DATA)
- Infrastructure events (deployments, restarts, custom events)

## Build

```bash
pnpm install
pnpm build
```

## Register

```bash
DATABASE_URL=<your-db-url> npx ts-node scripts/register.ts
```
50 changes: 50 additions & 0 deletions integrations/datadog/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@core/datadog",
"version": "0.1.0",
"description": "Datadog extension for CORE",
"main": "./bin/index.js",
"module": "./bin/index.mjs",
"type": "module",
"files": [
"datadog",
"bin"
],
"bin": {
"datadog": "./bin/index.js"
},
"scripts": {
"build": "rimraf dist && bun build src/index.ts --outfile dist/index.js --target node --minify",
"lint": "eslint --ext js,ts,tsx src/ --fix",
"prettier": "prettier --config .prettierrc --write .",
"test": "bun test"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/preset-typescript": "^7.26.0",
"@types/node": "^18.0.20",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.2",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-unused-imports": "^2.0.0",
"prettier": "^3.4.2",
"rimraf": "^3.0.2",
"tslib": "^2.8.1",
"typescript": "^4.7.2"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"axios": "^1.7.9",
"commander": "^12.0.0",
"@redplanethq/sdk": "0.1.9",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.1"
}
}
85 changes: 85 additions & 0 deletions integrations/datadog/scripts/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import pg from 'pg';

const { Client } = pg;

async function main() {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
console.error('DATABASE_URL environment variable is required');
process.exit(1);
}

const client = new Client({ connectionString });

const spec = {
name: 'Datadog',
key: 'datadog',
description:
'Connect Datadog to CORE to surface monitor alerts and infrastructure events as activities.',
icon: 'datadog',
schedule: {
frequency: '*/15 * * * *',
},
auth: {
api_key: {
fields: [
{
name: 'api_key',
label: 'API Key (DD-API-KEY)',
placeholder: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
description:
'Your Datadog API key. Found in Datadog → Organization Settings → API Keys.',
},
{
name: 'app_key',
label: 'Application Key (DD-APPLICATION-KEY)',
placeholder: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
description:
'Your Datadog Application key. Found in Datadog → Organization Settings → Application Keys.',
},
{
name: 'region',
label: 'Region',
placeholder: 'US1',
description:
'Your Datadog region. One of: US1, US3, US5, EU, AP1. Defaults to US1.',
},
],
},
},
};

try {
await client.connect();

await client.query(
`
INSERT INTO core."IntegrationDefinitionV2" ("id", "name", "slug", "description", "icon", "spec", "config", "version", "url", "updatedAt", "createdAt")
VALUES (gen_random_uuid(), 'Datadog', 'datadog', 'Connect Datadog to CORE to surface monitor alerts and infrastructure events as activities.', 'datadog', $1, $2, '0.1.0', $3, NOW(), NOW())
ON CONFLICT (name) DO UPDATE SET
"slug" = EXCLUDED."slug",
"description" = EXCLUDED."description",
"icon" = EXCLUDED."icon",
"spec" = EXCLUDED."spec",
"config" = EXCLUDED."config",
"version" = EXCLUDED."version",
"url" = EXCLUDED."url",
"updatedAt" = NOW()
RETURNING *;
`,
[
JSON.stringify(spec),
JSON.stringify({}),
'../../integrations/datadog/dist/index.js',
],
);

console.log('Datadog integration registered successfully in the database.');
} catch (error) {
console.error('Error registering Datadog integration:', error);
} finally {
await client.end();
}
}

main().catch(console.error);
51 changes: 51 additions & 0 deletions integrations/datadog/src/account-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { createDatadogClient, getBaseUrl } from './utils';

export async function integrationCreate(data: Record<string, string>) {
const { api_key, app_key, region = 'US1' } = data;

if (!api_key || !app_key) {
throw new Error('DD-API-KEY and DD-APPLICATION-KEY are required');
}

const client = createDatadogClient(api_key, app_key, region);

// Validate credentials via /api/v1/validate
const validateRes = await client.get('/api/v1/validate');
if (!validateRes.data?.valid) {
throw new Error('Invalid Datadog API key — /api/v1/validate returned invalid');
}

// Fetch org info for display name and account ID
let orgName = 'Datadog';
let orgPublicId = 'unknown';
try {
const orgRes = await client.get('/api/v1/org');
const org = orgRes.data?.org ?? orgRes.data?.orgs?.[0];
orgName = org?.name ?? orgName;
orgPublicId = org?.public_id ?? orgPublicId;
} catch {
// non-fatal: org info is cosmetic
}

const baseUrl = getBaseUrl(region);

return [
{
type: 'account',
data: {
settings: {
orgName,
orgPublicId,
region,
baseUrl,
},
accountId: orgPublicId,
config: {
api_key,
app_key,
region,
},
},
},
];
}
65 changes: 65 additions & 0 deletions integrations/datadog/src/create-activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { DatadogEvent, DatadogMonitor } from './utils';

function monitorStateLabel(state: string): string {
switch (state.toLowerCase()) {
case 'alert':
return 'ALERT';
case 'warn':
return 'WARN';
case 'no data':
case 'no_data':
return 'NO DATA';
case 'ok':
return 'OK';
case 'ignored':
return 'IGNORED';
case 'skipped':
return 'SKIPPED';
default:
return state.toUpperCase();
}
}

export function createActivityFromMonitor(
monitor: DatadogMonitor,
baseUrl: string,
): { type: string; data: { text: string; sourceURL: string } } | null {
// Only surface non-OK monitors as activities
const state = (monitor.overall_state || monitor.status || '').toLowerCase();
if (state === 'ok' || state === 'ignored' || state === 'skipped') {
return null;
}

const label = monitorStateLabel(monitor.overall_state || monitor.status);
const monitorUrl = `${baseUrl}/monitors/${monitor.id}`;
const tagsStr = monitor.tags?.length ? ` [${monitor.tags.join(', ')}]` : '';

return {
type: 'activity',
data: {
text: `[${label}] Monitor "${monitor.name}"${tagsStr}`,
sourceURL: monitorUrl,
},
};
}

export function createActivityFromEvent(
event: DatadogEvent,
): { type: string; data: { text: string; sourceURL: string } } | null {
if (!event.title && !event.text) {
return null;
}

const alertType = event.alert_type ? ` [${event.alert_type.toUpperCase()}]` : '';
const host = event.host ? ` on ${event.host}` : '';
const title = event.title || event.text.slice(0, 120);
const text = `${alertType} ${title}${host}`.trim();

return {
type: 'activity',
data: {
text,
sourceURL: event.url || '',
},
};
}
83 changes: 83 additions & 0 deletions integrations/datadog/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { fileURLToPath } from 'url';

import {
IntegrationCLI,
IntegrationEventPayload,
IntegrationEventType,
Spec,
} from '@redplanethq/sdk';

import { integrationCreate } from './account-create';
import { handleSchedule } from './schedule';

export async function run(eventPayload: IntegrationEventPayload) {
switch (eventPayload.event) {
case IntegrationEventType.SETUP:
return await integrationCreate(eventPayload.eventBody);

case IntegrationEventType.SYNC:
return await handleSchedule(eventPayload.config as Record<string, unknown>, eventPayload.state);

default:
return { message: `The event payload type is ${eventPayload.event}` };
}
}

class DatadogCLI extends IntegrationCLI {
constructor() {
super('datadog', '1.0.0');
}

protected async handleEvent(eventPayload: IntegrationEventPayload): Promise<any> {
return await run(eventPayload);
}

protected async getSpec(): Promise<Spec> {
return {
name: 'Datadog',
key: 'datadog',
description:
'Connect Datadog to CORE to surface monitor alerts and infrastructure events as activities.',
icon: 'datadog',
schedule: {
frequency: '*/15 * * * *',
},
auth: {
api_key: {
fields: [
{
name: 'api_key',
label: 'API Key (DD-API-KEY)',
placeholder: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
description:
'Your Datadog API key. Found in Datadog → Organization Settings → API Keys.',
},
{
name: 'app_key',
label: 'Application Key (DD-APPLICATION-KEY)',
placeholder: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
description:
'Your Datadog Application key. Found in Datadog → Organization Settings → Application Keys.',
},
{
name: 'region',
label: 'Region',
placeholder: 'US1',
description:
'Your Datadog region. One of: US1, US3, US5, EU, AP1. Defaults to US1.',
},
],
},
},
};
}
}

function main() {
const datadogCLI = new DatadogCLI();
datadogCLI.parse();
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
main();
}
Loading