mirror of
https://github.com/apricote/Listory.git
synced 2026-01-13 21:21:02 +00:00
feat(api): metrics for Spotify API http requests
This will help evaluate how much we are being rate limited, and on what routes it happens most.
This commit is contained in:
parent
85c31705ef
commit
41dfae3c50
7 changed files with 229 additions and 3 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { Module, OnApplicationShutdown } from "@nestjs/common";
|
||||
import { Global, Module, OnApplicationShutdown } from "@nestjs/common";
|
||||
import { OpenTelemetryModule as UpstreamModule } from "nestjs-otel";
|
||||
import { otelSDK } from "./sdk";
|
||||
import { UrlValueParserService } from "./url-value-parser.service";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -16,8 +17,10 @@ import { otelSDK } from "./sdk";
|
|||
},
|
||||
}),
|
||||
],
|
||||
exports: [UpstreamModule],
|
||||
providers: [UrlValueParserService],
|
||||
exports: [UpstreamModule, UrlValueParserService],
|
||||
})
|
||||
@Global()
|
||||
export class OpenTelemetryModule implements OnApplicationShutdown {
|
||||
async onApplicationShutdown(): Promise<void> {
|
||||
await otelSDK.shutdown();
|
||||
|
|
|
|||
49
src/open-telemetry/url-value-parser.service.spec.ts
Normal file
49
src/open-telemetry/url-value-parser.service.spec.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { UrlValueParserService } from "./url-value-parser.service";
|
||||
|
||||
describe("UrlValueParserService", () => {
|
||||
let service: UrlValueParserService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UrlValueParserService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UrlValueParserService>(UrlValueParserService);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe("replacePathValues", () => {
|
||||
it("works with default replacement", () => {
|
||||
const replaced = service.replacePathValues(
|
||||
"/in/world/14/userId/abca12d231"
|
||||
);
|
||||
expect(replaced).toBe("/in/world/#val/userId/#val");
|
||||
});
|
||||
|
||||
it("works with custom replacement", () => {
|
||||
const replaced = service.replacePathValues(
|
||||
"/in/world/14/userId/abca12d231",
|
||||
"<id>"
|
||||
);
|
||||
expect(replaced).toBe("/in/world/<id>/userId/<id>");
|
||||
});
|
||||
|
||||
it("works with negative decimal numbers", () => {
|
||||
const replaced = service.replacePathValues(
|
||||
"/some/path/-154/userId/-ABC363AFE2"
|
||||
);
|
||||
expect(replaced).toBe("/some/path/#val/userId/-ABC363AFE2");
|
||||
});
|
||||
|
||||
it("works with spotify ids", () => {
|
||||
const replaced = service.replacePathValues(
|
||||
"/v1/albums/2PzfMWIpq6JKucGhkS1X5M"
|
||||
);
|
||||
expect(replaced).toBe("/v1/albums/#val");
|
||||
});
|
||||
});
|
||||
});
|
||||
67
src/open-telemetry/url-value-parser.service.ts
Normal file
67
src/open-telemetry/url-value-parser.service.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
// Service adapted from https://github.com/disjunction/url-value-parser
|
||||
|
||||
const REPLACE_MASKS: RegExp[] = [
|
||||
/^\-?\d+$/,
|
||||
|
||||
/^(\d{2}|\d{4})\-\d\d\-\d\d$/, // date
|
||||
|
||||
/^[\da-f]{8}\-[\da-f]{4}\-[\da-f]{4}\-[\da-f]{4}\-[\da-f]{12}$/, // UUID
|
||||
/^[\dA-F]{8}\-[\dA-F]{4}\-[\dA-F]{4}\-[\dA-F]{4}\-[\dA-F]{12}$/, // UUID uppercased
|
||||
|
||||
// hex code sould have a consistent case
|
||||
/^[\da-f]{7,}$/,
|
||||
/^[\dA-F]{7,}$/,
|
||||
|
||||
// base64 encoded with URL safe Base64
|
||||
/^[a-zA-Z0-9\-_]{22,}$/,
|
||||
|
||||
// classic Base64
|
||||
/^(?:[A-Za-z0-9+/]{4}){16,}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?/,
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class UrlValueParserService {
|
||||
/**
|
||||
* replacePathValues replaces IDs and other identifiers from URL paths.
|
||||
*/
|
||||
public replacePathValues(path: string, replacement: string = "#val"): string {
|
||||
const parseResult = this.parsePathValues(path);
|
||||
console.log({ parseResult, path, replacement });
|
||||
return (
|
||||
"/" +
|
||||
parseResult.chunks
|
||||
.map((chunk, i) =>
|
||||
parseResult.valueIndexes.includes(i) ? replacement : chunk
|
||||
)
|
||||
.join("/")
|
||||
);
|
||||
}
|
||||
|
||||
private getPathChunks(path: string): string[] {
|
||||
return path.split("/").filter((chunk) => chunk !== "");
|
||||
}
|
||||
|
||||
private parsePathValues(path: string): {
|
||||
chunks: string[];
|
||||
valueIndexes: number[];
|
||||
} {
|
||||
const chunks = this.getPathChunks(path);
|
||||
const valueIndexes = chunks
|
||||
.map((chunk, index) => (this.isValue(chunk) ? index : null))
|
||||
.filter((index) => index !== null);
|
||||
|
||||
return { chunks, valueIndexes };
|
||||
}
|
||||
|
||||
private isValue(str: string): boolean {
|
||||
for (let mask of REPLACE_MASKS) {
|
||||
if (str.match(mask)) {
|
||||
console.log("isValue", { str, mask });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue