150 lines
3.9 KiB
TypeScript
150 lines
3.9 KiB
TypeScript
/**
|
|
* Idempotently provisions the Axiom `leaf` dataset used for Leaf runtime logs
|
|
* and MCP usage analytics, and configures its map fields.
|
|
*
|
|
* Map fields ("vacuum" the unpredictable nested payloads into a single column):
|
|
* Tool payloads, req/res bodies, and per-log details have open-ended shape.
|
|
* Every distinct top-level key would otherwise become its own mapped field and
|
|
* quickly blow Axiom's per-dataset field limit. These map fields keep nested
|
|
* keys inside one field each while staying queryable.
|
|
*
|
|
* Run via the Axiom CLI (resolves AXIOM_ADMIN_TOKEN from infisical):
|
|
* bun axiom create-leaf # dev
|
|
* bun axiom:prod create-leaf # prod
|
|
*
|
|
* Notes:
|
|
* - AXIOM_ADMIN_TOKEN must be a personal API token with dataset create/update
|
|
* scope, NOT the `xaat-` ingest token used at runtime.
|
|
* - Safe to re-run: dataset creation tolerates "already exists", and existing
|
|
* map fields are read before missing fields are created.
|
|
*/
|
|
|
|
const AXIOM_BASE = "https://api.axiom.co/v2";
|
|
const DATASET = "leaf";
|
|
const DATASET_DESCRIPTION = "Leaf runtime logs and MCP usage analytics";
|
|
|
|
// Nested, open-ended payloads stored as map fields to stay under the field
|
|
// limit. Keep this list minimal — only genuinely high-cardinality objects.
|
|
const MAP_FIELDS = [
|
|
"context",
|
|
"data",
|
|
"extras",
|
|
"input",
|
|
"output",
|
|
"req",
|
|
"res",
|
|
];
|
|
|
|
const authHeaders = (token: string) => ({
|
|
Authorization: `Bearer ${token}`,
|
|
"Content-Type": "application/json",
|
|
});
|
|
|
|
const createDataset = async (token: string) => {
|
|
const res = await fetch(`${AXIOM_BASE}/datasets`, {
|
|
method: "POST",
|
|
headers: authHeaders(token),
|
|
body: JSON.stringify({
|
|
name: DATASET,
|
|
description: DATASET_DESCRIPTION,
|
|
}),
|
|
});
|
|
|
|
if (res.ok) {
|
|
console.log(` + created dataset \`${DATASET}\``);
|
|
return;
|
|
}
|
|
|
|
// 409 (or a 400 mentioning existence) means it's already there — fine.
|
|
const text = await res.text();
|
|
if (res.status === 409 || /exist/i.test(text)) {
|
|
console.log(` = dataset \`${DATASET}\` already exists`);
|
|
return;
|
|
}
|
|
|
|
throw new Error(`Failed to create dataset: ${res.status} ${text}`);
|
|
};
|
|
|
|
const getMapFields = async (token: string) => {
|
|
const res = await fetch(
|
|
`${AXIOM_BASE}/datasets/${encodeURIComponent(DATASET)}/mapfields`,
|
|
{
|
|
method: "GET",
|
|
headers: authHeaders(token),
|
|
},
|
|
);
|
|
|
|
const text = await res.text();
|
|
if (!res.ok) {
|
|
throw new Error(`Failed to list map fields: ${res.status} ${text}`);
|
|
}
|
|
|
|
const parsed = JSON.parse(text) as unknown;
|
|
if (!Array.isArray(parsed) || parsed.some((name) => typeof name !== "string")) {
|
|
throw new Error(`Unexpected map fields response: ${text}`);
|
|
}
|
|
|
|
return new Set(parsed);
|
|
};
|
|
|
|
const setMapField = async ({
|
|
existing,
|
|
name,
|
|
token,
|
|
}: {
|
|
existing: Set<string>;
|
|
name: string;
|
|
token: string;
|
|
}) => {
|
|
if (existing.has(name)) {
|
|
console.log(` = map field: ${name} (already set)`);
|
|
return;
|
|
}
|
|
|
|
const res = await fetch(
|
|
`${AXIOM_BASE}/datasets/${encodeURIComponent(DATASET)}/mapfields`,
|
|
{
|
|
method: "POST",
|
|
headers: authHeaders(token),
|
|
body: JSON.stringify({ name }),
|
|
},
|
|
);
|
|
|
|
const text = await res.text();
|
|
if (res.ok) {
|
|
existing.add(name);
|
|
console.log(` + map field: ${name}`);
|
|
return;
|
|
}
|
|
|
|
if (/exist/i.test(text)) {
|
|
existing.add(name);
|
|
console.log(` = map field: ${name} (already set)`);
|
|
return;
|
|
}
|
|
|
|
throw new Error(`Failed to set map field "${name}": ${res.status} ${text}`);
|
|
};
|
|
|
|
const setMapFields = async (token: string) => {
|
|
const existing = await getMapFields(token);
|
|
for (const name of MAP_FIELDS) {
|
|
await setMapField({ existing, name, token });
|
|
}
|
|
};
|
|
|
|
/** Provisions the `leaf` dataset and its map fields. */
|
|
export const createLeafDataset = async () => {
|
|
const token = process.env.AXIOM_ADMIN_TOKEN;
|
|
if (!token) {
|
|
throw new Error(
|
|
"AXIOM_ADMIN_TOKEN env var is required (personal API token, not xaat-* ingest token)",
|
|
);
|
|
}
|
|
|
|
console.log(`Provisioning Axiom dataset \`${DATASET}\`...`);
|
|
await createDataset(token);
|
|
await setMapFields(token);
|
|
console.log("\nDone.");
|
|
};
|