log to loki
Some checks are pending
Build and push image for doorman-homeassistant / docker (push) Successful in 31s
Build and push image for doorman-homeassistant / deploy-gitainer (push) Successful in 4s
Build and push Doorman UI / API / docker (push) Waiting to run

This commit is contained in:
Martin Dimitrov 2025-02-09 19:55:17 -08:00
parent cc408ab0fe
commit 80f45cce0e
7 changed files with 117 additions and 16 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -14,3 +14,8 @@ PUSHGATEWAY_URL=https://metrics.chromart.cc
STAGE=prod STAGE=prod
PUSHGATEWAY_USER=doorman PUSHGATEWAY_USER=doorman
PUSHGATEWAY_PW=doormanmetrics PUSHGATEWAY_PW=doormanmetrics
# logs
LOKI_URL=https://logs.chromart.cc
LOKI_USER=doorman
LOKI_PW=doormanlogs

View File

@ -12,11 +12,13 @@
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-dynamodb": "^3.609.0", "@aws-sdk/client-dynamodb": "^3.609.0",
"@twilio-labs/serverless-runtime-types": "^3.0.0",
"@twilio/runtime-handler": "1.3.0", "@twilio/runtime-handler": "1.3.0",
"discord.js": "^14.16.3", "discord.js": "^14.16.3",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"twilio": "^3.56", "twilio": "^3.56",
"@twilio-labs/serverless-runtime-types": "^3.0.0" "winston": "^3.17.0",
"winston-loki": "^6.1.3"
}, },
"devDependencies": { "devDependencies": {
"twilio-run": "^3.5.4", "twilio-run": "^3.5.4",

View File

@ -5,6 +5,9 @@ import { shouldBlockRequest } from "../utils/blockUserAgent";
import { RequestOptions } from "https"; import { RequestOptions } from "https";
import { FastHttpPushgateway } from "../metrics/FastHttpPromGateway"; import { FastHttpPushgateway } from "../metrics/FastHttpPromGateway";
import { createLogger, format, Logger, transports } from "winston";
import LokiTransport from "winston-loki";
export type BaseEvent = { request: { cookies: {}; headers: {}; }; } export type BaseEvent = { request: { cookies: {}; headers: {}; }; }
export type DoormanLambda<T extends DoormanLambdaContext, U extends BaseEvent> = ( export type DoormanLambda<T extends DoormanLambdaContext, U extends BaseEvent> = (
@ -12,12 +15,15 @@ export type DoormanLambda<T extends DoormanLambdaContext, U extends BaseEvent> =
event: Parameters<ServerlessFunctionSignature<T, U>>[1], event: Parameters<ServerlessFunctionSignature<T, U>>[1],
callback: Parameters<ServerlessFunctionSignature<T, U>>[2], callback: Parameters<ServerlessFunctionSignature<T, U>>[2],
metricsRegistry: Registry<PrometheusContentType>, metricsRegistry: Registry<PrometheusContentType>,
logger: Logger,
) => void; ) => void;
export enum CommonMetrics { export enum CommonMetrics {
RUNTIME = "FunctionRuntime", RUNTIME = "FunctionRuntime",
BLOCKED_REQUEST = "BlockedRequest", BLOCKED_REQUEST = "BlockedRequest",
HTTP_CLIENT_ERROR = "HttpClientError", HTTP_CLIENT_ERROR = "HttpClientError",
LOKI_ERROR = "LokiError",
LOKI_LOG_AFTER_CLOSE = "LokiLogAfterClose",
}; };
export function getMetricFromRegistry<T>(metricsRegistry: Registry, metric: string): T { export function getMetricFromRegistry<T>(metricsRegistry: Registry, metric: string): T {
@ -34,6 +40,7 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
handler: DoormanLambda<T, U> handler: DoormanLambda<T, U>
): ServerlessFunctionSignature<T, U> { ): ServerlessFunctionSignature<T, U> {
return async (context, event, callback) => { return async (context, event, callback) => {
console.log("[CommonHandler] created loki logger");
console.log("[CommonHandler] clearing old metrics"); console.log("[CommonHandler] clearing old metrics");
register.clear(); register.clear();
@ -63,6 +70,80 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
labelNames: [ "ErrorCode" ], labelNames: [ "ErrorCode" ],
})); }));
metricsRegistry.registerMetric(new Counter({
name: CommonMetrics.LOKI_ERROR,
help: "Error connecting to loki for logs",
}));
metricsRegistry.registerMetric(new Counter({
name: CommonMetrics.LOKI_LOG_AFTER_CLOSE,
help: "Tried to write logs after log stream closed",
}));
// Create a Winston logger
const logger = createLogger({
level: 'info',
format: format.json(),
transports: [
new LokiTransport({
host: context.LOKI_URL,
labels: {
service: "doorman",
stage: context.STAGE,
handler: functionName,
},
basicAuth: `${context.LOKI_USER}:${context.LOKI_PW}`,
interval: 500,
gracefulShutdown: true,
clearOnError: true,
batching: true,
onConnectionError: (error) => {
console.log("in error block");
console.error(error);
getMetricFromRegistry<Counter>(metricsRegistry, CommonMetrics.LOKI_ERROR).inc(1);
},
}),
new transports.Console(), // Add other transports if needed
],
});
const orignalLog = {
log: console.log,
error: console.error,
warn: console.warn,
info: console.info,
}
// Override the base console log with winston
console.log = function (...args) {
if (logger.writable) {
return logger.info.apply(logger, [...args]);
}
getMetricFromRegistry<Counter>(metricsRegistry, CommonMetrics.LOKI_LOG_AFTER_CLOSE).inc(1);
return orignalLog.log(...args);
};
console.error = function (...args) {
if (logger.writable) {
return logger.error.apply(logger, [...args]);
}
getMetricFromRegistry<Counter>(metricsRegistry, CommonMetrics.LOKI_LOG_AFTER_CLOSE).inc(1);
return orignalLog.error(...args);
};
console.warn = function (...args) {
if (logger.writable) {
return logger.warn.apply(logger, [...args]);
}
getMetricFromRegistry<Counter>(metricsRegistry, CommonMetrics.LOKI_LOG_AFTER_CLOSE).inc(1);
return orignalLog.warn(...args);
};
console.info = function (...args) {
if (logger.writable) {
return logger.info.apply(logger, [...args]);
}
getMetricFromRegistry<Counter>(metricsRegistry, CommonMetrics.LOKI_LOG_AFTER_CLOSE).inc(1);
return orignalLog.info(...args);
};
const summaryTimer = getMetricFromRegistry<Summary>(metricsRegistry, CommonMetrics.RUNTIME).startTimer(); const summaryTimer = getMetricFromRegistry<Summary>(metricsRegistry, CommonMetrics.RUNTIME).startTimer();
const startTime = Date.now(); const startTime = Date.now();
@ -82,7 +163,7 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
response.setStatusCode(200); response.setStatusCode(200);
callbackResult = [null, response]; callbackResult = [null, response];
} else { } else {
await handler(context, event, tempCallback, metricsRegistry); await handler(context, event, tempCallback, metricsRegistry, logger);
} }
if (!callbackResult) { if (!callbackResult) {
@ -112,14 +193,14 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
console.log(`[CommonHandler] there is ${remainingTime} ms left to send metrics`); console.log(`[CommonHandler] there is ${remainingTime} ms left to send metrics`);
let metricsTimeout = setTimeout(() => { let metricsTimeout = setTimeout(() => {
console.log("[CommonHandler] cutting it too close, abandoning metrics");
callback(...result); callback(...result);
}, remainingTime - 250); }, remainingTime - 250);
summaryTimer(); summaryTimer();
console.log("[CommonHandler] attempting to push metrics..."); console.log("[CommonHandler] attempting to push metrics...");
try { try {
logger.end(async () => {
await pushGateway.push({ await pushGateway.push({
jobName: functionName, jobName: functionName,
groupings: { groupings: {
@ -127,11 +208,14 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
}, },
}); });
console.log("[CommonHandler] pushed metrics successfully"); console.log("[CommonHandler] pushed metrics successfully");
} catch (e: any) {
console.log("[CommonHandler] failed to push metrics, quietly discarding them", e);
}
clearTimeout(metricsTimeout); clearTimeout(metricsTimeout);
callback(...result); callback(...result);
});
} catch (e: any) {
console.log("[CommonHandler] failed to push metrics, quietly discarding them", e);
clearTimeout(metricsTimeout);
callback(...result);
}
}; };
}; };

View File

@ -10,4 +10,7 @@ export interface DoormanLambdaContext extends EnvironmentVariables {
PUSHGATEWAY_USER: string; PUSHGATEWAY_USER: string;
PUSHGATEWAY_PW: string; PUSHGATEWAY_PW: string;
STAGE: string; STAGE: string;
LOKI_URL: string;
LOKI_USER: string;
LOKI_PW: string;
}; };

View File

@ -9,3 +9,8 @@ PUSHGATEWAY_URL=https://metrics.chromart.cc
STAGE=prod STAGE=prod
PUSHGATEWAY_USER=doorman PUSHGATEWAY_USER=doorman
PUSHGATEWAY_PW=doormanmetrics PUSHGATEWAY_PW=doormanmetrics
# logs
LOKI_URL=https://logs.chromart.cc
LOKI_USER=doorman
LOKI_PW=doormanlogs

View File

@ -11,12 +11,14 @@
"deploy": "twilio-run deploy --load-system-env --env .env.example --service-name buzzer --environment=prod --override-existing-project" "deploy": "twilio-run deploy --load-system-env --env .env.example --service-name buzzer --environment=prod --override-existing-project"
}, },
"dependencies": { "dependencies": {
"@twilio-labs/serverless-runtime-types": "^3.0.0",
"@twilio/runtime-handler": "1.3.0", "@twilio/runtime-handler": "1.3.0",
"node-fetch": "^2.7.0", "node-fetch": "^2.7.0",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"prometheus-remote-write": "^0.5.0", "prometheus-remote-write": "^0.5.0",
"twilio": "^3.84.1", "twilio": "^3.84.1",
"@twilio-labs/serverless-runtime-types": "^3.0.0" "winston": "^3.17.0",
"winston-loki": "^6.1.3"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",