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 { PrometheusContentType, Registry, Pushgateway, Summary } from "prom-client";
import { PrometheusContentType, Registry, Pushgateway, Summary, Counter } from "prom-client";
import { DoormanLambdaContext } from "./DoormanHandlerContext";
import { shouldBlockRequest } from "../utils/blockUserAgent";
export type BaseEvent = { request: { cookies: {}; headers: {}; }; }
@ -13,6 +14,12 @@ export type DoormanLambda<T extends DoormanLambdaContext, U extends BaseEvent> =
export enum CommonMetrics {
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",
}));
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();
console.log(`[CommonHandler] started handler at ${startTime}`);
@ -46,11 +64,28 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
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);
}
if (!callbackResult) {
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>);
});
@ -66,7 +101,7 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
console.log(`[CommonHandler] there is ${remainingTime} ms left to send metrics`);
let metricsTimeout = setTimeout(() => {
console.log("[CommonHandler] cutting it too close, abandoning metrics");
console.error("[CommonHandler] cutting it too close, abandoning metrics");
callback(...result);
}, remainingTime - 250);
@ -74,6 +109,7 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
console.log("[CommonHandler] attempting to push metrics...");
try {
console.log(await getMetricFromRegistry<Summary>(metricsRegistry, CommonMetrics.RUNTIME).get());
await pushGateway.push({
jobName: functionName,
groupings: {
@ -82,7 +118,7 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
});
console.log("[CommonHandler] pushed metrics successfully");
} 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);

View File

@ -15,7 +15,7 @@ import VoiceResponse from 'twilio/lib/twiml/VoiceResponse';
import { DoorStatus } from '../../../doorman-api/src/types/DoorStatus';
import { StatusResponse } from '../../../doorman-api/src/functions/api/door/status';
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 { BuzzerActivatedMetrics, registerMetrics } from '../metrics/BuzzerActivatedMetrics';
@ -43,7 +43,9 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
if (!config || !config.door) {
let twiml = new Twilio.twiml.VoiceResponse();
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);
return;
}
@ -62,8 +64,11 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
const unlockPromise = new Promise<VoiceResponse>((resolve, reject) => {
intervals.push(setInterval(() => {
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.POLL_ATTEMPTS) as Counter).inc({ door: config.door }, 1);
const recordPollLatency = (metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.POLL_LATENCY) as Summary).startTimer({ door: config.door });
getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.POLL_ATTEMPTS)
.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}`)
.then(res => res.json())
@ -79,7 +84,8 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
console.log(
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));
resolve(twiml);
@ -99,7 +105,8 @@ export const handler: ServerlessFunctionSignature<TwilioContext, BuzzerDialEvent
if (!discordLock) {
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(
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) => {
timeouts.push(setTimeout(async () => {
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.RESULT_NOTIFICATION_FATE_UNKNOWN) as Counter).inc({ door: config.door }, 1);
(metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.DIAL_THROUGH) as Counter).inc({ door: config.door }, 1);
getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.RESULT_NOTIFICATION_FATE_UNKNOWN)
.inc({ door: config.door }, 1);
getMetricFromRegistry<Counter>(metricsRegistry, BuzzerActivatedMetrics.DIAL_THROUGH)
.inc({ door: config.door }, 1);
const twiml = dialFallbackTwiml(config);
console.error(