implement a helper to fetch metrics
All checks were successful
Build and push image for doorman-homeassistant / docker (push) Successful in 30s
Build and push Doorman UI / API / docker (push) Successful in 1m26s
Build and push image for doorman-homeassistant / deploy-gitainer (push) Successful in 4s

This commit is contained in:
Martin Dimitrov 2024-12-21 14:12:09 -08:00
parent a57f54bd58
commit c3c12e1591
2 changed files with 59 additions and 13 deletions

View File

@ -1,6 +1,7 @@
import { ServerlessCallback, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types"; import { ServerlessCallback, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
import { PrometheusContentType, Registry, Pushgateway, Summary } from "prom-client"; import { PrometheusContentType, Registry, Pushgateway, Summary, Counter } from "prom-client";
import { DoormanLambdaContext } from "./DoormanHandlerContext"; import { DoormanLambdaContext } from "./DoormanHandlerContext";
import { shouldBlockRequest } from "../utils/blockUserAgent";
export type BaseEvent = { request: { cookies: {}; headers: {}; }; } export type BaseEvent = { request: { cookies: {}; headers: {}; }; }
@ -13,6 +14,12 @@ export type DoormanLambda<T extends DoormanLambdaContext, U extends BaseEvent> =
export enum CommonMetrics { export enum CommonMetrics {
RUNTIME = "FunctionRuntime", RUNTIME = "FunctionRuntime",
BLOCKED_REQUEST = "BlockedRequest",
HTTP_CLIENT_ERROR = "HttpClientError",
};
export function getMetricFromRegistry<T>(metricsRegistry: Registry, metric: string): T {
return metricsRegistry.getSingleMetric(metric) as T;
}; };
/** /**
@ -34,7 +41,18 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
help: "Runtime of the function", help: "Runtime of the function",
})); }));
const summaryTimer = (metricsRegistry.getSingleMetric(CommonMetrics.RUNTIME) as Summary).startTimer(); metricsRegistry.registerMetric(new Counter({
name: CommonMetrics.BLOCKED_REQUEST,
help: "Blocked requests",
}));
metricsRegistry.registerMetric(new Counter({
name: CommonMetrics.HTTP_CLIENT_ERROR,
help: "Client side HTTP error",
labelNames: [ "ErrorCode" ],
}));
const summaryTimer = getMetricFromRegistry<Summary>(metricsRegistry, CommonMetrics.RUNTIME).startTimer();
const startTime = Date.now(); const startTime = Date.now();
console.log(`[CommonHandler] started handler at ${startTime}`); console.log(`[CommonHandler] started handler at ${startTime}`);
@ -46,11 +64,28 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
callbackResult = [err, payload]; callbackResult = [err, payload];
} }
// block requests before we even call the handler
if (shouldBlockRequest(event)) {
getMetricFromRegistry<Counter>(metricsRegistry, CommonMetrics.BLOCKED_REQUEST).inc(1);
const response = new Twilio.Response();
response.setStatusCode(200);
callbackResult = [null, response];
} else {
await handler(context, event, tempCallback, metricsRegistry); await handler(context, event, tempCallback, metricsRegistry);
}
if (!callbackResult) { if (!callbackResult) {
reject("No callback was given"); reject("No callback was given");
} }
let statusCode: number | undefined = (callbackResult?.[1] as any)?.statusCode;
if (statusCode && statusCode >= 400) {
getMetricFromRegistry<Counter>(metricsRegistry, CommonMetrics.HTTP_CLIENT_ERROR).inc({
ErrorCode: statusCode,
}, 1);
}
resolve(callbackResult as Parameters<ServerlessCallback>); resolve(callbackResult as Parameters<ServerlessCallback>);
}); });
@ -66,7 +101,7 @@ 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"); console.error("[CommonHandler] cutting it too close, abandoning metrics");
callback(...result); callback(...result);
}, remainingTime - 250); }, remainingTime - 250);
@ -74,6 +109,7 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
console.log("[CommonHandler] attempting to push metrics..."); console.log("[CommonHandler] attempting to push metrics...");
try { try {
console.log(await getMetricFromRegistry<Summary>(metricsRegistry, CommonMetrics.RUNTIME).get());
await pushGateway.push({ await pushGateway.push({
jobName: functionName, jobName: functionName,
groupings: { groupings: {
@ -82,7 +118,7 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
}); });
console.log("[CommonHandler] pushed metrics successfully"); console.log("[CommonHandler] pushed metrics successfully");
} catch (e: any) { } catch (e: any) {
console.log("[CommonHandler] failed to push metrics, quietly discarding them", e); console.error("[CommonHandler] failed to push metrics, quietly discarding them", e);
} }
clearTimeout(metricsTimeout); clearTimeout(metricsTimeout);

View File

@ -15,7 +15,7 @@ import VoiceResponse from 'twilio/lib/twiml/VoiceResponse';
import { DoorStatus } from '../../../doorman-api/src/types/DoorStatus'; import { DoorStatus } from '../../../doorman-api/src/types/DoorStatus';
import { StatusResponse } from '../../../doorman-api/src/functions/api/door/status'; import { StatusResponse } from '../../../doorman-api/src/functions/api/door/status';
import { InfoResponseClient } from '../../../doorman-api/src/functions/api/door/info'; import { InfoResponseClient } from '../../../doorman-api/src/functions/api/door/info';
import { withMetrics } from '../../../doorman-api/src/common/DoormanHandler'; import { getMetricFromRegistry, withMetrics } from '../../../doorman-api/src/common/DoormanHandler';
import { Counter, Summary } from 'prom-client'; import { Counter, Summary } from 'prom-client';
import { BuzzerActivatedMetrics, registerMetrics } from '../metrics/BuzzerActivatedMetrics'; import { BuzzerActivatedMetrics, registerMetrics } from '../metrics/BuzzerActivatedMetrics';
@ -43,7 +43,9 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
if (!config || !config.door) { if (!config || !config.door) {
let twiml = new Twilio.twiml.VoiceResponse(); let twiml = new Twilio.twiml.VoiceResponse();
twiml.reject(); twiml.reject();
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.CALL_REJECTED) as Counter).inc({ From: event.From }, 1);
getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.CALL_REJECTED)
.inc({ From: event.From }, 1);
callback(null, twiml); callback(null, twiml);
return; return;
} }
@ -62,8 +64,11 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
const unlockPromise = new Promise<VoiceResponse>((resolve, reject) => { const unlockPromise = new Promise<VoiceResponse>((resolve, reject) => {
intervals.push(setInterval(() => { intervals.push(setInterval(() => {
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.POLL_ATTEMPTS) as Counter).inc({ door: config.door }, 1); getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.POLL_ATTEMPTS)
const recordPollLatency = (metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.POLL_LATENCY) as Summary).startTimer({ door: config.door }); .inc({ door: config.door }, 1);
const recordPollLatency = getMetricFromRegistry<Summary>(metricsRegistry, BuzzerActivatedMetrics.POLL_LATENCY)
.startTimer({ door: config.door });
fetch(context.DOORMAN_URL + `/api/door/status?door=${config.door}`) fetch(context.DOORMAN_URL + `/api/door/status?door=${config.door}`)
.then(res => res.json()) .then(res => res.json())
@ -79,7 +84,8 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
console.log( console.log(
invokeId, "UnlockPromise: I was the fastest, so I will attempt to notify discord users before resolving with unlock" invokeId, "UnlockPromise: I was the fastest, so I will attempt to notify discord users before resolving with unlock"
); );
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.API_UNLOCK) as Counter).inc({ door: config.door }, 1); getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.API_UNLOCK)
.inc({ door: config.door }, 1);
await notifyAllDiscord(context, config, `🔓 Doorman buzzed someone up @ Door "${config.door}"`, metricsRegistry, JSON.stringify(body.fingerprint)); await notifyAllDiscord(context, config, `🔓 Doorman buzzed someone up @ Door "${config.door}"`, metricsRegistry, JSON.stringify(body.fingerprint));
resolve(twiml); resolve(twiml);
@ -99,7 +105,8 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
if (!discordLock) { if (!discordLock) {
discordLock = true; discordLock = true;
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.DIAL_THROUGH) as Counter).inc({ door: config.door }, 1); getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.DIAL_THROUGH)
.inc({ door: config.door }, 1);
console.log( console.log(
invokeId, "GracefulFallbackPromise: I was the fastest, so I will attempt to notify discord users before resolving with a call" invokeId, "GracefulFallbackPromise: I was the fastest, so I will attempt to notify discord users before resolving with a call"
@ -121,8 +128,11 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
const ungracefulFallbackPromise = new Promise<VoiceResponse>((resolve, reject) => { const ungracefulFallbackPromise = new Promise<VoiceResponse>((resolve, reject) => {
timeouts.push(setTimeout(async () => { timeouts.push(setTimeout(async () => {
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.RESULT_NOTIFICATION_FATE_UNKNOWN) as Counter).inc({ door: config.door }, 1); getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.RESULT_NOTIFICATION_FATE_UNKNOWN)
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.DIAL_THROUGH) as Counter).inc({ door: config.door }, 1); .inc({ door: config.door }, 1);
getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.DIAL_THROUGH)
.inc({ door: config.door }, 1);
const twiml = dialFallbackTwiml(config); const twiml = dialFallbackTwiml(config);
console.error( console.error(