- Create new @link-stack/logger package wrapping Pino for structured logging - Replace all console.log/error/warn statements across the monorepo - Configure environment-aware logging (pretty-print in dev, JSON in prod) - Add automatic redaction of sensitive fields (passwords, tokens, etc.) - Remove dead commented-out logger file from bridge-worker - Follow Pino's standard argument order (context object first, message second) - Support log levels via LOG_LEVEL environment variable - Export TypeScript types for better IDE support This provides consistent, structured logging across all applications and packages, making debugging easier and production logs more parseable.
162 lines
4.3 KiB
TypeScript
162 lines
4.3 KiB
TypeScript
/* eslint-disable camelcase */
|
|
/*
|
|
import { URLSearchParams } from "url";
|
|
import { withDb, AppDatabase } from "../../lib/db.js";
|
|
import { createLogger } from "@link-stack/logger";
|
|
|
|
const logger = createLogger('bridge-worker-import-leafcutter');
|
|
// import { loadConfig } from "@digiresilience/bridge-config";
|
|
|
|
const config: any = {};
|
|
|
|
type LabelStudioTicket = {
|
|
id: string;
|
|
is_labeled: boolean;
|
|
annotations: Record<string, unknown>[];
|
|
data: Record<string, unknown>;
|
|
updated_at: string;
|
|
};
|
|
|
|
type LeafcutterTicket = {
|
|
id: string;
|
|
incident: string[];
|
|
technology: string[];
|
|
targeted_group: string[];
|
|
country: string[];
|
|
region: string[];
|
|
continent: string[];
|
|
date: Date;
|
|
origin: string;
|
|
origin_id: string;
|
|
source_created_at: string;
|
|
source_updated_at: string;
|
|
};
|
|
|
|
const getLabelStudioTickets = async (
|
|
page: number,
|
|
): Promise<LabelStudioTicket[]> => {
|
|
const {
|
|
leafcutter: { labelStudioApiUrl, labelStudioApiKey },
|
|
} = config;
|
|
const headers = {
|
|
Authorization: `Token ${labelStudioApiKey}`,
|
|
Accept: "application/json",
|
|
};
|
|
const ticketsQuery = new URLSearchParams({
|
|
page_size: "50",
|
|
page: `${page}`,
|
|
});
|
|
const res = await fetch(
|
|
`${labelStudioApiUrl}/projects/1/tasks?${ticketsQuery}`,
|
|
{ headers },
|
|
);
|
|
const tasksResult: any = await res.json();
|
|
|
|
return tasksResult;
|
|
};
|
|
|
|
const fetchFromLabelStudio = async (
|
|
minUpdatedTimestamp: Date,
|
|
): Promise<LabelStudioTicket[]> => {
|
|
const pages = [...Array.from({ length: 10000 }).keys()];
|
|
const allDocs: LabelStudioTicket[] = [];
|
|
|
|
for await (const page of pages) {
|
|
const docs = await getLabelStudioTickets(page + 1);
|
|
|
|
if (docs && docs.length > 0) {
|
|
for (const doc of docs) {
|
|
const updatedAt = new Date(doc.updated_at);
|
|
if (updatedAt > minUpdatedTimestamp) {
|
|
allDocs.push(doc);
|
|
}
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return allDocs;
|
|
};
|
|
|
|
const sendToLeafcutter = async (tickets: LabelStudioTicket[]) => {
|
|
const {
|
|
leafcutter: {
|
|
contributorId,
|
|
opensearchApiUrl,
|
|
opensearchUsername,
|
|
opensearchPassword,
|
|
},
|
|
} = config;
|
|
|
|
const filteredTickets = tickets.filter((ticket) => ticket.is_labeled);
|
|
const finalTickets: LeafcutterTicket[] = filteredTickets.map((ticket) => {
|
|
const {
|
|
id,
|
|
annotations,
|
|
data: { source_id, source_created_at, source_updated_at },
|
|
} = ticket;
|
|
|
|
const getTags = (tags: Record<string, any>[], name: string) =>
|
|
tags
|
|
.filter((tag) => tag.from_name === name)
|
|
.map((tag) => tag.value.choices)
|
|
.flat();
|
|
|
|
const allTags = annotations.map(({ result }) => result).flat();
|
|
const incident = getTags(allTags, "incidentType tag");
|
|
const technology = getTags(allTags, "platform tag");
|
|
const country = getTags(allTags, "country tag");
|
|
const targetedGroup = getTags(allTags, "targetedGroup tag");
|
|
|
|
return {
|
|
id,
|
|
incident,
|
|
technology,
|
|
targeted_group: targetedGroup,
|
|
country,
|
|
region: [],
|
|
continent: [],
|
|
date: new Date(source_created_at as string),
|
|
origin: contributorId,
|
|
origin_id: source_id as string,
|
|
source_created_at: source_created_at as string,
|
|
source_updated_at: source_updated_at as string,
|
|
};
|
|
});
|
|
|
|
logger.info("Sending to Leafcutter");
|
|
|
|
const result = await fetch(opensearchApiUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Basic ${Buffer.from(`${opensearchUsername}:${opensearchPassword}`).toString("base64")}`,
|
|
},
|
|
body: JSON.stringify({ tickets: finalTickets }),
|
|
});
|
|
|
|
};
|
|
*/
|
|
const importLeafcutterTask = async (): Promise<void> => {
|
|
/*
|
|
withDb(async (db: AppDatabase) => {
|
|
const {
|
|
leafcutter: { contributorName },
|
|
} = config;
|
|
const settingName = `${contributorName}ImportLeafcutterTask`;
|
|
const res: any = await db.settings.findByName(settingName);
|
|
const startTimestamp = res?.value?.minUpdatedTimestamp
|
|
? new Date(res.value.minUpdatedTimestamp as string)
|
|
: new Date("2023-03-01");
|
|
const newLastTimestamp = new Date();
|
|
const tickets = await fetchFromLabelStudio(startTimestamp);
|
|
await sendToLeafcutter(tickets);
|
|
await db.settings.upsert(settingName, {
|
|
minUpdatedTimestamp: newLastTimestamp,
|
|
});
|
|
});
|
|
*/
|
|
};
|
|
|
|
export default importLeafcutterTask;
|