Add event caching, filtering, and metrics

This commit is contained in:
2024-04-02 15:56:17 -06:00
parent 20caec034f
commit 27954f6a8c
3 changed files with 91 additions and 10 deletions

View File

@@ -1,16 +1,26 @@
import { Controller, Get } from '@nestjs/common'; import { Controller, Get, Logger, Query } from '@nestjs/common';
import { FocoLiveService } from './foco-live.service'; import { FocoLiveService } from './foco-live.service';
import { ApiTags } from '@nestjs/swagger'; import { ApiQuery, ApiTags } from '@nestjs/swagger';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import { Gauge } from 'prom-client';
@ApiTags('foco-live') @ApiTags('foco-live')
@Controller('foco-live') @Controller('foco-live')
export class FocoLiveController { export class FocoLiveController {
private readonly logger = new Logger(FocoLiveController.name);
constructor( constructor(
private readonly focoLiveService: FocoLiveService, private readonly focoLiveService: FocoLiveService,
@InjectMetric('query_count') public queryCount: Gauge<string>,
) { } ) { }
@Get('events') @Get('events')
async getEvents() { @ApiQuery({ name: 'venue', required: false })
return this.focoLiveService.getEvents(); @ApiQuery({ name: 'before', required: false, description: "Filter events that are before the provided date, any date format" })
@ApiQuery({ name: 'after', required: false, description: "Filter events that are after the provided date, any date format" })
async getEvents(@Query('venue') venue?: string, @Query('before') before?: string, @Query('after') after?: string) {
this.queryCount.inc();
this.logger.verbose(`GET /foco-live/events?venue=${venue}&before=${before}&after=${after}`);
return this.focoLiveService.getEvents({ venue, before: before ? new Date(before) : undefined, after: after ? new Date(after) : undefined });
} }
} }

View File

@@ -1,9 +1,33 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { FocoLiveController } from './foco-live.controller'; import { FocoLiveController } from './foco-live.controller';
import { FocoLiveService } from './foco-live.service'; import { FocoLiveService } from './foco-live.service';
import { PrometheusModule, makeGaugeProvider } from '@willsoto/nestjs-prometheus';
import { CacheModule } from '@nestjs/cache-manager';
@Module({ @Module({
imports: [
CacheModule.register(),
PrometheusModule.register({
customMetricPrefix: 'foco_live',
defaultMetrics: {
enabled: false,
},
})
],
controllers: [FocoLiveController], controllers: [FocoLiveController],
providers: [FocoLiveService] providers: [FocoLiveService,
makeGaugeProvider({
name: 'event_count',
help: 'The current number of events in the Foco Live database at last query time',
}),
makeGaugeProvider({
name: 'event_cache_misses',
help: 'The total number of cache misses for the Foco Live events',
}),
makeGaugeProvider({
name: 'query_count',
help: 'The total number of queries to the Foco Live API',
}),
]
}) })
export class FocoLiveModule {} export class FocoLiveModule { }

View File

@@ -1,7 +1,12 @@
import { Injectable } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import * as Airtable from 'airtable'; import * as Airtable from 'airtable';
import { AirtableBase } from 'airtable/lib/airtable_base'; import { AirtableBase } from 'airtable/lib/airtable_base';
import { Cache } from 'cache-manager';
import { Gauge } from 'prom-client';
import { filter, pipe } from 'ramda';
const tables = { const tables = {
venues: 'tblRi4wDorKqNJJbs', venues: 'tblRi4wDorKqNJJbs',
@@ -10,24 +15,66 @@ const tables = {
const compareDates = (a: any, b: any) => new Date(a.Date).getTime() - new Date(b.Date).getTime(); const compareDates = (a: any, b: any) => new Date(a.Date).getTime() - new Date(b.Date).getTime();
const beforeFilter = (before?: Date) => (a: Event) => before ? new Date(a.Date) <= before : true;
const afterFilter = (after?: Date) => (a: Event) => after ? new Date(a.Date) >= after : true;
export interface Event {
"Date": string,
"Music Start Time": string,
"Bar or Venue Name": string,
"Band/DJ/Musician Name": string,
Cost: string,
"Date Select": string
}
const cacheKeys = {
allEvents: 'foco_live_events',
}
@Injectable() @Injectable()
export class FocoLiveService { export class FocoLiveService {
private readonly airtableBase: AirtableBase; private readonly airtableBase: AirtableBase;
private readonly logger = new Logger(FocoLiveService.name);
constructor( constructor(
private readonly config: ConfigService, private readonly config: ConfigService,
@InjectMetric('event_count') public eventCount: Gauge<string>,
@InjectMetric('event_cache_misses') public cacheMisses: Gauge<string>,
@Inject(CACHE_MANAGER) private cacheManager: Cache
) { ) {
this.airtableBase = new Airtable({ this.airtableBase = new Airtable({
apiKey: config.get('focoLive.airtable.apiKey'), apiKey: config.get('focoLive.airtable.apiKey'),
}).base('app1SjPrn5qrhr59J'); }).base('app1SjPrn5qrhr59J');
} }
async getEvents() { async getAllEvents(): Promise<Event[]> {
return (await this.airtableBase('Events').select({ return ((await this.airtableBase('Events').select({
view: "Grid view", view: "Grid view",
}).all()) }).all())
.map(record => record.fields) .map(record => record.fields) as any as Event[])
.sort(compareDates) .sort(compareDates)
.reverse(); .reverse();
} }
async getAllEventsCached(): Promise<Event[]> {
let events: Event[] | null | undefined = await this.cacheManager.get(cacheKeys.allEvents);
if (!events) {
events = await this.getAllEvents();
this.cacheMisses.inc();
this.cacheManager.set(cacheKeys.allEvents, events, 5 * 60 * 1000);
}
this.eventCount.set(events.length);
return events;
}
async getEvents(options: { venue?: string, before?: Date, after?: Date } = {}) {
const events = await this.getAllEventsCached();
const results = pipe(
filter((a: Event) => a["Bar or Venue Name"] === (options.venue ?? a["Bar or Venue Name"])),
filter(beforeFilter(options.before)),
filter(afterFilter(options.after)),
)(events);
this.logger.verbose(`Returning ${results.length} events, ${events.length} total events in database.`);
return results
}
} }