Add event caching, filtering, and metrics
This commit is contained in:
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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 { }
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user