Add cron jobs to make timed metrics

/metrics
/metrics/monthly
/metrics/weekly
/metrics/daily
This commit is contained in:
Pas 2025-06-26 15:51:15 -06:00
parent 863748730d
commit b7f8d8899c
12 changed files with 1935 additions and 3786 deletions

3
.gitignore vendored
View file

@ -8,3 +8,6 @@ dist
.vscode
.metrics.json
pnpm-lock.yaml
.metrics_weekly.json
.metrics_monthly.json
.metrics_daily.json

View file

@ -7,6 +7,15 @@ export default defineNitroConfig({
compatibilityDate: '2025-03-05',
experimental: {
asyncContext: true,
tasks: true,
},
scheduledTasks: {
// Daily cron jobs (midnight)
'0 0 * * *': ['jobs:clear-metrics:daily'],
// Weekly cron jobs (Sunday midnight)
'0 0 * * 0': ['jobs:clear-metrics:weekly'],
// Monthly cron jobs (1st of month at midnight)
'0 0 1 * *': ['jobs:clear-metrics:monthly']
},
runtimeConfig: {
public: {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,33 @@
import { defineEventHandler, getQuery, readBody, createError } from 'h3';
import { runTask } from '#imports';
export default defineEventHandler(async (event) => {
// Get job name from query parameters
const query = getQuery(event);
const jobName = query.job as string;
if (!jobName) {
throw createError({
statusCode: 400,
statusMessage: "Missing job parameter"
});
}
try {
// Run the specified task
const result = await runTask(jobName, {
payload: await readBody(event).catch(() => ({}))
});
return {
success: true,
job: jobName,
result
};
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: `Failed to run job: ${error.message || 'Unknown error'}`
});
}
});

View file

@ -0,0 +1,24 @@
import { getRegistry } from '../../utils/metrics';
import { scopedLogger } from '../../utils/logger';
const log = scopedLogger('metrics-daily-endpoint');
export default defineEventHandler(async event => {
try {
// Get the daily registry
const dailyRegistry = getRegistry('daily');
const metrics = await dailyRegistry.metrics();
event.node.res.setHeader('Content-Type', dailyRegistry.contentType);
return metrics;
} catch (error) {
log.error('Error in daily metrics endpoint:', {
evt: 'metrics_error',
error: error instanceof Error ? error.message : String(error),
});
throw createError({
statusCode: 500,
message: error instanceof Error ? error.message : 'Failed to collect daily metrics',
});
}
});

View file

@ -1,5 +1,5 @@
import { register } from 'prom-client';
import { setupMetrics } from '../../utils/metrics';
import { setupMetrics, initializeAllMetrics } from '../../utils/metrics';
import { scopedLogger } from '../../utils/logger';
const log = scopedLogger('metrics-endpoint');
@ -9,7 +9,7 @@ let isInitialized = false;
async function ensureMetricsInitialized() {
if (!isInitialized) {
log.info('Initializing metrics from endpoint...', { evt: 'init_start' });
await setupMetrics();
await initializeAllMetrics();
isInitialized = true;
log.info('Metrics initialized from endpoint', { evt: 'init_complete' });
}
@ -18,6 +18,7 @@ async function ensureMetricsInitialized() {
export default defineEventHandler(async event => {
try {
await ensureMetricsInitialized();
// Use the default registry (all-time metrics)
const metrics = await register.metrics();
event.node.res.setHeader('Content-Type', register.contentType);
return metrics;

View file

@ -0,0 +1,24 @@
import { getRegistry } from '../../utils/metrics';
import { scopedLogger } from '../../utils/logger';
const log = scopedLogger('metrics-monthly-endpoint');
export default defineEventHandler(async event => {
try {
// Get the monthly registry
const monthlyRegistry = getRegistry('monthly');
const metrics = await monthlyRegistry.metrics();
event.node.res.setHeader('Content-Type', monthlyRegistry.contentType);
return metrics;
} catch (error) {
log.error('Error in monthly metrics endpoint:', {
evt: 'metrics_error',
error: error instanceof Error ? error.message : String(error),
});
throw createError({
statusCode: 500,
message: error instanceof Error ? error.message : 'Failed to collect monthly metrics',
});
}
});

View file

@ -0,0 +1,24 @@
import { getRegistry } from '../../utils/metrics';
import { scopedLogger } from '../../utils/logger';
const log = scopedLogger('metrics-weekly-endpoint');
export default defineEventHandler(async event => {
try {
// Get the weekly registry
const weeklyRegistry = getRegistry('weekly');
const metrics = await weeklyRegistry.metrics();
event.node.res.setHeader('Content-Type', weeklyRegistry.contentType);
return metrics;
} catch (error) {
log.error('Error in weekly metrics endpoint:', {
evt: 'metrics_error',
error: error instanceof Error ? error.message : String(error),
});
throw createError({
statusCode: 500,
message: error instanceof Error ? error.message : 'Failed to collect weekly metrics',
});
}
});

View file

@ -0,0 +1,48 @@
import { defineTask } from '#imports';
import { scopedLogger } from '../../../utils/logger';
import { setupMetrics } from '../../../utils/metrics';
const logger = scopedLogger('tasks:clear-metrics:daily');
export default defineTask({
meta: {
name: "jobs:clear-metrics:daily",
description: "Clear daily metrics at midnight",
},
async run() {
logger.info("Clearing daily metrics");
const startTime = Date.now();
try {
// Clear and reinitialize daily metrics
if (global.metrics_daily) {
global.metrics_daily.clear();
await setupMetrics('daily');
} else {
await setupMetrics('daily');
}
const executionTime = Date.now() - startTime;
logger.info(`Daily metrics cleared in ${executionTime}ms`);
return {
result: {
status: "success",
message: "Successfully cleared daily metrics",
executionTimeMs: executionTime,
timestamp: new Date().toISOString()
}
};
} catch (error) {
logger.error("Error clearing daily metrics:", { error: error.message });
return {
result: {
status: "error",
message: error.message || "An error occurred clearing daily metrics",
executionTimeMs: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
}
},
});

View file

@ -0,0 +1,48 @@
import { defineTask } from '#imports';
import { scopedLogger } from '../../../utils/logger';
import { setupMetrics } from '../../../utils/metrics';
const logger = scopedLogger('tasks:clear-metrics:monthly');
export default defineTask({
meta: {
name: "jobs:clear-metrics:monthly",
description: "Clear monthly metrics on the 1st of each month at midnight",
},
async run() {
logger.info("Clearing monthly metrics");
const startTime = Date.now();
try {
// Clear and reinitialize monthly metrics
if (global.metrics_monthly) {
global.metrics_monthly.clear();
await setupMetrics('monthly');
} else {
await setupMetrics('monthly');
}
const executionTime = Date.now() - startTime;
logger.info(`Monthly metrics cleared in ${executionTime}ms`);
return {
result: {
status: "success",
message: "Successfully cleared monthly metrics",
executionTimeMs: executionTime,
timestamp: new Date().toISOString()
}
};
} catch (error) {
logger.error("Error clearing monthly metrics:", { error: error.message });
return {
result: {
status: "error",
message: error.message || "An error occurred clearing monthly metrics",
executionTimeMs: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
}
},
});

View file

@ -0,0 +1,48 @@
import { defineTask } from '#imports';
import { scopedLogger } from '../../../utils/logger';
import { setupMetrics } from '../../../utils/metrics';
const logger = scopedLogger('tasks:clear-metrics:weekly');
export default defineTask({
meta: {
name: "jobs:clear-metrics:weekly",
description: "Clear weekly metrics every Sunday at midnight",
},
async run() {
logger.info("Clearing weekly metrics");
const startTime = Date.now();
try {
// Clear and reinitialize weekly metrics
if (global.metrics_weekly) {
global.metrics_weekly.clear();
await setupMetrics('weekly');
} else {
await setupMetrics('weekly');
}
const executionTime = Date.now() - startTime;
logger.info(`Weekly metrics cleared in ${executionTime}ms`);
return {
result: {
status: "success",
message: "Successfully cleared weekly metrics",
executionTimeMs: executionTime,
timestamp: new Date().toISOString()
}
};
} catch (error) {
logger.error("Error clearing weekly metrics:", { error: error.message });
return {
result: {
status: "error",
message: error.message || "An error occurred clearing weekly metrics",
executionTimeMs: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
}
},
});

View file

@ -1,4 +1,4 @@
import { Counter, register, collectDefaultMetrics, Histogram, Summary } from 'prom-client';
import { Counter, register, collectDefaultMetrics, Histogram, Summary, Registry } from 'prom-client';
import { prisma } from './prisma';
import { scopedLogger } from '~/utils/logger';
import fs from 'fs';
@ -6,6 +6,24 @@ import path from 'path';
const log = scopedLogger('metrics');
const METRICS_FILE = '.metrics.json';
const METRICS_DAILY_FILE = '.metrics_daily.json';
const METRICS_WEEKLY_FILE = '.metrics_weekly.json';
const METRICS_MONTHLY_FILE = '.metrics_monthly.json';
// Global registries
const registries = {
default: register, // All-time metrics (never cleared)
daily: new Registry(),
weekly: new Registry(),
monthly: new Registry()
};
// Expose the registries on global for tasks to access
if (typeof global !== 'undefined') {
global.metrics_daily = registries.daily;
global.metrics_weekly = registries.weekly;
global.metrics_monthly = registries.monthly;
}
export type Metrics = {
user: Counter<'namespace'>;
@ -18,185 +36,230 @@ export type Metrics = {
httpRequestSummary: Summary<'method' | 'route' | 'status_code'>;
};
let metrics: null | Metrics = null;
// Store metrics for each time period
const metricsStore: Record<string, Metrics | null> = {
default: null,
daily: null,
weekly: null,
monthly: null
};
export function getMetrics() {
if (!metrics) throw new Error('metrics not initialized');
return metrics;
export function getMetrics(interval: 'default' | 'daily' | 'weekly' | 'monthly' = 'default') {
if (!metricsStore[interval]) throw new Error(`metrics for ${interval} not initialized`);
return metricsStore[interval];
}
async function createMetrics(): Promise<Metrics> {
export function getRegistry(interval: 'default' | 'daily' | 'weekly' | 'monthly' = 'default') {
return registries[interval];
}
async function createMetrics(registry: Registry, interval: string): Promise<Metrics> {
const newMetrics = {
user: new Counter({
name: 'mw_user_count',
help: 'Number of users by namespace',
name: `mw_user_count_${interval !== 'default' ? interval : ''}`,
help: `Number of users by namespace (${interval})`,
labelNames: ['namespace'],
registers: [registry]
}),
captchaSolves: new Counter({
name: 'mw_captcha_solves',
help: 'Number of captcha solves by success status',
name: `mw_captcha_solves_${interval !== 'default' ? interval : ''}`,
help: `Number of captcha solves by success status (${interval})`,
labelNames: ['success'],
registers: [registry]
}),
providerHostnames: new Counter({
name: 'mw_provider_hostname_count',
help: 'Number of requests by provider hostname',
name: `mw_provider_hostname_count_${interval !== 'default' ? interval : ''}`,
help: `Number of requests by provider hostname (${interval})`,
labelNames: ['hostname'],
registers: [registry]
}),
providerStatuses: new Counter({
name: 'mw_provider_status_count',
help: 'Number of provider requests by status',
name: `mw_provider_status_count_${interval !== 'default' ? interval : ''}`,
help: `Number of provider requests by status (${interval})`,
labelNames: ['provider_id', 'status'],
registers: [registry]
}),
watchMetrics: new Counter({
name: 'mw_media_watch_count',
help: 'Number of media watch events',
name: `mw_media_watch_count_${interval !== 'default' ? interval : ''}`,
help: `Number of media watch events (${interval})`,
labelNames: ['title', 'tmdb_full_id', 'provider_id', 'success'],
registers: [registry]
}),
toolMetrics: new Counter({
name: 'mw_provider_tool_count',
help: 'Number of provider tool usages',
name: `mw_provider_tool_count_${interval !== 'default' ? interval : ''}`,
help: `Number of provider tool usages (${interval})`,
labelNames: ['tool'],
registers: [registry]
}),
httpRequestDuration: new Histogram({
name: 'http_request_duration_seconds',
help: 'request duration in seconds',
name: `http_request_duration_seconds_${interval !== 'default' ? interval : ''}`,
help: `request duration in seconds (${interval})`,
labelNames: ['method', 'route', 'status_code'],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
registers: [registry]
}),
httpRequestSummary: new Summary({
name: 'http_request_summary_seconds',
help: 'request duration in seconds summary',
name: `http_request_summary_seconds_${interval !== 'default' ? interval : ''}`,
help: `request duration in seconds summary (${interval})`,
labelNames: ['method', 'route', 'status_code'],
percentiles: [0.01, 0.05, 0.5, 0.9, 0.95, 0.99, 0.999],
registers: [registry]
}),
};
// Register all metrics with the Prometheus registry
register.registerMetric(newMetrics.user);
register.registerMetric(newMetrics.captchaSolves);
register.registerMetric(newMetrics.providerHostnames);
register.registerMetric(newMetrics.providerStatuses);
register.registerMetric(newMetrics.watchMetrics);
register.registerMetric(newMetrics.toolMetrics);
register.registerMetric(newMetrics.httpRequestDuration);
register.registerMetric(newMetrics.httpRequestSummary);
return newMetrics;
}
async function saveMetricsToFile() {
async function saveMetricsToFile(interval: string = 'default') {
try {
if (!metrics) return;
const registry = registries[interval];
if (!registry) return;
const metricsData = await register.getMetricsAsJSON();
const fileName = interval === 'default'
? METRICS_FILE
: interval === 'daily'
? METRICS_DAILY_FILE
: interval === 'weekly'
? METRICS_WEEKLY_FILE
: METRICS_MONTHLY_FILE;
const metricsData = await registry.getMetricsAsJSON();
const relevantMetrics = metricsData.filter(
metric => metric.name.startsWith('mw_') || metric.name === 'http_request_duration_seconds'
metric => metric.name.startsWith('mw_') || metric.name.startsWith('http_request')
);
fs.writeFileSync(METRICS_FILE, JSON.stringify(relevantMetrics, null, 2));
fs.writeFileSync(fileName, JSON.stringify(relevantMetrics, null, 2));
log.info('Metrics saved to file', { evt: 'metrics_saved' });
log.info(`${interval} metrics saved to file`, { evt: 'metrics_saved', interval });
} catch (error) {
log.error('Failed to save metrics', {
log.error(`Failed to save ${interval} metrics`, {
evt: 'save_metrics_error',
interval,
error: error instanceof Error ? error.message : String(error),
});
}
}
async function loadMetricsFromFile(): Promise<any[]> {
async function loadMetricsFromFile(interval: string = 'default'): Promise<any[]> {
try {
if (!fs.existsSync(METRICS_FILE)) {
log.info('No saved metrics found', { evt: 'no_saved_metrics' });
const fileName = interval === 'default'
? METRICS_FILE
: interval === 'daily'
? METRICS_DAILY_FILE
: interval === 'weekly'
? METRICS_WEEKLY_FILE
: METRICS_MONTHLY_FILE;
if (!fs.existsSync(fileName)) {
log.info(`No saved ${interval} metrics found`, { evt: 'no_saved_metrics', interval });
return [];
}
const data = fs.readFileSync(METRICS_FILE, 'utf8');
const data = fs.readFileSync(fileName, 'utf8');
const savedMetrics = JSON.parse(data);
log.info('Loaded saved metrics', {
log.info(`Loaded saved ${interval} metrics`, {
evt: 'metrics_loaded',
interval,
count: savedMetrics.length,
});
return savedMetrics;
} catch (error) {
log.error('Failed to load metrics', {
log.error(`Failed to load ${interval} metrics`, {
evt: 'load_metrics_error',
interval,
error: error instanceof Error ? error.message : String(error),
});
return [];
}
}
// Periodically save metrics
// Periodically save all metrics
const SAVE_INTERVAL = 60000; // Save every minute
setInterval(saveMetricsToFile, SAVE_INTERVAL);
setInterval(() => {
Object.keys(registries).forEach(interval => {
saveMetricsToFile(interval);
});
}, SAVE_INTERVAL);
// Save metrics on process exit
process.on('SIGTERM', async () => {
log.info('Saving metrics before exit...', { evt: 'exit_save' });
await saveMetricsToFile();
log.info('Saving all metrics before exit...', { evt: 'exit_save' });
for (const interval of Object.keys(registries)) {
await saveMetricsToFile(interval);
}
process.exit(0);
});
process.on('SIGINT', async () => {
log.info('Saving metrics before exit...', { evt: 'exit_save' });
await saveMetricsToFile();
log.info('Saving all metrics before exit...', { evt: 'exit_save' });
for (const interval of Object.keys(registries)) {
await saveMetricsToFile(interval);
}
process.exit(0);
});
export async function setupMetrics() {
export async function setupMetrics(interval: 'default' | 'daily' | 'weekly' | 'monthly' = 'default') {
try {
log.info('Setting up metrics...', { evt: 'start' });
log.info(`Setting up ${interval} metrics...`, { evt: 'start', interval });
// Clear all existing metrics
log.info('Clearing metrics registry...', { evt: 'clear' });
register.clear();
const registry = registries[interval];
// Enable default Node.js metrics collection with appropriate settings
collectDefaultMetrics({
register,
prefix: '', // No prefix to match the example output
eventLoopMonitoringPrecision: 1, // Ensure eventloop metrics are collected
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // Match the example buckets
});
// Clear registry if it already exists
registry.clear();
// Enable default Node.js metrics collection for default registry only
if (interval === 'default') {
collectDefaultMetrics({
register: registry,
prefix: '', // No prefix to match the example output
eventLoopMonitoringPrecision: 1, // Ensure eventloop metrics are collected
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // Match the example buckets
});
}
// Create new metrics instance
metrics = await createMetrics();
log.info('Created new metrics...', { evt: 'created' });
metricsStore[interval] = await createMetrics(registry, interval);
log.info(`Created new ${interval} metrics...`, { evt: 'created', interval });
// Load saved metrics
const savedMetrics = await loadMetricsFromFile();
const savedMetrics = await loadMetricsFromFile(interval);
if (savedMetrics.length > 0) {
log.info('Restoring saved metrics...', { evt: 'restore_metrics' });
log.info(`Restoring saved ${interval} metrics...`, { evt: 'restore_metrics', interval });
savedMetrics.forEach(metric => {
if (metric.values) {
metric.values.forEach(value => {
switch (metric.name) {
const metrics = metricsStore[interval];
if (!metrics) return;
// Extract the base metric name without the interval suffix
const baseName = metric.name.replace(/_daily$|_weekly$|_monthly$/, '');
switch (baseName) {
case 'mw_user_count':
metrics?.user.inc(value.labels, value.value);
metrics.user.inc(value.labels, value.value);
break;
case 'mw_captcha_solves':
metrics?.captchaSolves.inc(value.labels, value.value);
metrics.captchaSolves.inc(value.labels, value.value);
break;
case 'mw_provider_hostname_count':
metrics?.providerHostnames.inc(value.labels, value.value);
metrics.providerHostnames.inc(value.labels, value.value);
break;
case 'mw_provider_status_count':
metrics?.providerStatuses.inc(value.labels, value.value);
metrics.providerStatuses.inc(value.labels, value.value);
break;
case 'mw_media_watch_count':
metrics?.watchMetrics.inc(value.labels, value.value);
metrics.watchMetrics.inc(value.labels, value.value);
break;
case 'mw_provider_tool_count':
metrics?.toolMetrics.inc(value.labels, value.value);
metrics.toolMetrics.inc(value.labels, value.value);
break;
case 'http_request_duration_seconds':
// For histograms, special handling for sum and count
if (
value.metricName === 'http_request_duration_seconds_sum' ||
value.metricName === 'http_request_duration_seconds_count'
value.metricName === `http_request_duration_seconds_${interval !== 'default' ? interval + '_' : ''}sum` ||
value.metricName === `http_request_duration_seconds_${interval !== 'default' ? interval + '_' : ''}count`
) {
metrics?.httpRequestDuration.observe(value.labels, value.value);
metrics.httpRequestDuration.observe(value.labels, value.value);
}
break;
}
@ -206,112 +269,132 @@ export async function setupMetrics() {
}
// Initialize metrics with current data
log.info('Syncing up metrics...', { evt: 'sync' });
await updateMetrics();
log.info('Metrics initialized!', { evt: 'end' });
log.info(`Syncing up ${interval} metrics...`, { evt: 'sync', interval });
await updateMetrics(interval);
log.info(`${interval} metrics initialized!`, { evt: 'end', interval });
// Save initial state
await saveMetricsToFile();
await saveMetricsToFile(interval);
} catch (error) {
log.error('Failed to setup metrics', {
log.error(`Failed to setup ${interval} metrics`, {
evt: 'setup_error',
interval,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
async function updateMetrics() {
async function updateMetrics(interval: 'default' | 'daily' | 'weekly' | 'monthly' = 'default') {
try {
log.info('Fetching users from database...', { evt: 'update_metrics_start' });
log.info(`Fetching users from database for ${interval} metrics...`, { evt: 'update_metrics_start', interval });
const users = await prisma.users.groupBy({
by: ['namespace'],
_count: true,
});
log.info('Found users', { evt: 'users_found', count: users.length });
log.info('Found users', { evt: 'users_found', count: users.length, interval });
metrics?.user.reset();
log.info('Reset user metrics counter', { evt: 'metrics_reset' });
const metrics = metricsStore[interval];
if (!metrics) return;
metrics.user.reset();
log.info(`Reset user metrics counter for ${interval}`, { evt: 'metrics_reset', interval });
users.forEach(v => {
log.info('Incrementing user metric', {
log.info(`Incrementing user metric for ${interval}`, {
evt: 'increment_metric',
interval,
namespace: v.namespace,
count: v._count,
});
metrics?.user.inc({ namespace: v.namespace }, v._count);
metrics.user.inc({ namespace: v.namespace }, v._count);
});
log.info('Successfully updated metrics', { evt: 'update_metrics_complete' });
log.info(`Successfully updated ${interval} metrics`, { evt: 'update_metrics_complete', interval });
} catch (error) {
log.error('Failed to update metrics', {
log.error(`Failed to update ${interval} metrics`, {
evt: 'update_metrics_error',
interval,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
// Export function to record HTTP request duration
// Export function to record HTTP request duration for all registries
export function recordHttpRequest(
method: string,
route: string,
statusCode: number,
duration: number
) {
if (!metrics) return;
const labels = {
method,
route,
status_code: statusCode.toString(),
};
// Record in both histogram and summary
metrics.httpRequestDuration.observe(labels, duration);
metrics.httpRequestSummary.observe(labels, duration);
// Record in all active registries
Object.entries(metricsStore).forEach(([interval, metrics]) => {
if (!metrics) return;
// Record in both histogram and summary
metrics.httpRequestDuration.observe(labels, duration);
metrics.httpRequestSummary.observe(labels, duration);
});
}
// Functions to match previous backend API
// Functions to match previous backend API - record in all registries
export function recordProviderMetrics(items: any[], hostname: string, tool?: string) {
if (!metrics) return;
Object.values(metricsStore).forEach(metrics => {
if (!metrics) return;
// Record hostname once per request
metrics.providerHostnames.inc({ hostname });
// Record hostname once per request
metrics.providerHostnames.inc({ hostname });
// Record status metrics for each item
items.forEach(item => {
// Record provider status
metrics.providerStatuses.inc({
provider_id: item.embedId ?? item.providerId,
status: item.status,
// Record status metrics for each item
items.forEach(item => {
// Record provider status
metrics.providerStatuses.inc({
provider_id: item.embedId ?? item.providerId,
status: item.status,
});
});
// Reverse items to get the last one, and find the last successful item
const itemList = [...items];
itemList.reverse();
const lastSuccessfulItem = items.find(v => v.status === 'success');
const lastItem = itemList[0];
// Record watch metrics only for the last item
if (lastItem) {
metrics.watchMetrics.inc({
tmdb_full_id: lastItem.type + '-' + lastItem.tmdbId,
provider_id: lastSuccessfulItem?.providerId ?? lastItem.providerId,
title: lastItem.title,
success: (!!lastSuccessfulItem).toString(),
});
}
// Record tool metrics
if (tool) {
metrics.toolMetrics.inc({ tool });
}
});
// Reverse items to get the last one, and find the last successful item
const itemList = [...items];
itemList.reverse();
const lastSuccessfulItem = items.find(v => v.status === 'success');
const lastItem = itemList[0];
// Record watch metrics only for the last item
if (lastItem) {
metrics.watchMetrics.inc({
tmdb_full_id: lastItem.type + '-' + lastItem.tmdbId,
provider_id: lastSuccessfulItem?.providerId ?? lastItem.providerId,
title: lastItem.title,
success: (!!lastSuccessfulItem).toString(),
});
}
// Record tool metrics
if (tool) {
metrics.toolMetrics.inc({ tool });
}
}
export function recordCaptchaMetrics(success: boolean) {
metrics?.captchaSolves.inc({ success: success.toString() });
Object.values(metricsStore).forEach(metrics => {
metrics?.captchaSolves.inc({ success: success.toString() });
});
}
// Initialize all metrics registries on startup
export async function initializeAllMetrics() {
for (const interval of Object.keys(registries) as Array<'default' | 'daily' | 'weekly' | 'monthly'>) {
await setupMetrics(interval);
}
}