feat(observability): Replace Prometheus package with OpenTelemetry

This commit is contained in:
Julian Tölle 2022-02-27 17:57:33 +01:00
parent f67383b761
commit 6b1640b753
22 changed files with 2391 additions and 568 deletions

View file

@ -50,16 +50,20 @@ You can use Sentry to automatically detect and report any exceptions thrown.
- `SENTRY_ENABLED`: **false**, Set to `true` to enable Sentry.
- `SENTRY_DSN`: _Required_, but only if `SENTRY_ENABLED` is `true`. The [DSN](https://docs.sentry.io/product/sentry-basics/dsn-explainer/) for your Sentry project.
#### Prometheus
#### OpenTelemetry
You can use Prometheus to track various metrics about your Listory deployment.
We use OpenTelemetry to provide observability into Listory API.
The metrics will be exposed on the `/api/metrics` endpoint. Make sure that this endpoint is not publicly available in your deployment.
The metrics will be exposed on a seperate port at `:9464/metrics`. Make sure that this endpoint is not publicly available in your deployment.
- `PROMETHEUS_ENABLED`: **false**, Set to `true` to enable Prometheus Metrics.
- `PROMETHEUS_BASIC_AUTH`: **false**, Set to `true` to require basic auth to access the metrics endpoint.
- `PROMETHEUS_BASIC_AUTH_USERNAME`: _Required_, if `PROMETHEUS_BASIC_AUTH` is `true`.
- `PROMETHEUS_BASIC_AUTH_PASSWORD`: _Required_, if `PROMETHEUS_BASIC_AUTH` is `true`.
Traces will be sent to the specified endpoint.
To use observability tools locally, check out `docker-compose` setup in `observability/`.
- `OTEL_METRICS_ENABLED`: **false**, Set to `true` to activate metrics.
- `OTEL_TRACES_ENABLED`: **false**, Set to `true` to activate traces.
- `OTEL_EXPORTER_OTLP_ENDPOINT`: _Required_, but only if `OTEL_TRACES_ENABLED` is `true`. The endpoint that traces are sent to, see [OpenTelemetry docs](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-trace-otlp-http#configuration-options-as-environment-variables)
- `OTEL_EXPORTER_PROMETHEUS_PORT`: **9464**, Set to configure non-standard port for Prometheus metrics
## Development

View file

@ -63,32 +63,30 @@ spec:
value: {{ .Values.sentry.dsn }}
{{- end }}
{{- if .Values.prometheus.enabled }}
- name: PROMETHEUS_ENABLED
{{- if .Values.opentelemetry.metrics.enabled }}
- name: OTEL_METRICS_ENABLED
value: "true"
{{- if .Values.prometheus.basicAuth.enabled }}
- name: PROMETHEUS_BASIC_AUTH
value: "true"
- name: PROMETHEUS_BASIC_AUTH_USERNAME
valueFrom:
secretKeyRef:
name: {{ include "listory.fullname" . }}
key: prometheus-basic-auth-username
- name: PROMETHEUS_BASIC_AUTH_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "listory.fullname" . }}
key: prometheus-basic-auth-password
{{- end }}
- name: OTEL_EXPORTER_PROMETHEUS_PORT
value: "{{ .Values.opentelemetry.metrics.port }}"
{{- end }}
{{- if .Values.opentelemetry.traces.enabled }}
- name: OTEL_TRACES_ENABLED
value: "true"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "{{ .Values.opentelemetry.traces.otlpEndpoint }}"
{{- end }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
ports:
- name: http
containerPort: 3000
protocol: TCP
{{- if .Values.opentelemetry.metrics.enabled }}
- name: metrics
containerPort: {{ .Values.opentelemetry.metrics.port }}
protocol: TCP
{{- end }}
readinessProbe:
httpGet:
path: /api/v1/health

View file

@ -11,5 +11,11 @@ spec:
targetPort: http
protocol: TCP
name: http
{{- if .Values.opentelemetry.metrics.enabled }}
- port: {{ .Values.opentelemetry.metrics.port }}
targetPort: metrics
protocol: TCP
name: metrics
{{- end }}
selector:
{{- include "listory.selectorLabels" . | nindent 4 }}

View file

@ -90,10 +90,10 @@ sentry:
enabled: false
dsn: ""
prometheus:
enabled: false
basicAuth:
opentelemetry:
metrics:
enabled: false
username: ""
password: ""
port: 9464
traces:
enabled: false
otlpEndpoint: ""

View file

@ -22,11 +22,15 @@ services:
DB_DATABASE: listory
JWT_SECRET: listory
APP_URL: "http://localhost:3000"
NODE_ENV: local # pretty logs
OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:55681/v1/traces
env_file: .env
volumes:
- ./src:/app/src
ports:
- 3000
- 3000 # API
- "9464:9464" # Metrics
labels:
- "traefik.enable=true" # Enable reverse-proxy for this service
- "traefik.http.routers.api.rule=PathPrefix(`/api`)"

2473
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -25,31 +25,50 @@
"test:e2e": "jest --config ./apps/listory/test/jest-e2e.json"
},
"dependencies": {
"@digikare/nestjs-prom": "1.0.0",
"@hapi/joi": "17.1.1",
"@nestjs/axios": "0.0.4",
"@nestjs/common": "8.1.2",
"@nestjs/config": "1.1.5",
"@nestjs/core": "8.1.2",
"@nestjs/jwt": "8.0.0",
"@nestjs/passport": "8.0.1",
"@nestjs/passport": "8.2.2",
"@nestjs/platform-express": "8.1.2",
"@nestjs/schedule": "1.0.2",
"@nestjs/serve-static": "2.2.2",
"@nestjs/swagger": "5.1.5",
"@nestjs/swagger": "5.2.1",
"@nestjs/terminus": "8.0.3",
"@nestjs/typeorm": "8.0.2",
"@opentelemetry/api": "1.0.4",
"@opentelemetry/context-async-hooks": "1.0.1",
"@opentelemetry/exporter-prometheus": "0.27.0",
"@opentelemetry/exporter-trace-otlp-http": "0.27.0",
"@opentelemetry/instrumentation": "0.27.0",
"@opentelemetry/instrumentation-dns": "0.27.1",
"@opentelemetry/instrumentation-express": "0.27.0",
"@opentelemetry/instrumentation-http": "0.27.0",
"@opentelemetry/instrumentation-nestjs-core": "0.28.3",
"@opentelemetry/instrumentation-pg": "0.27.0",
"@opentelemetry/instrumentation-pino": "0.28.1",
"@opentelemetry/resources": "1.0.1",
"@opentelemetry/sdk-metrics-base": "0.27.0",
"@opentelemetry/sdk-node": "0.27.0",
"@opentelemetry/sdk-trace-base": "1.0.1",
"@opentelemetry/semantic-conventions": "1.0.1",
"@sentry/node": "6.19.7",
"class-transformer": "0.5.1",
"class-validator": "0.13.2",
"cookie-parser": "1.4.6",
"date-fns": "2.27.0",
"nest-raven": "8.1.0",
"nestjs-otel": "3.0.4",
"nestjs-pino": "2.6.0",
"nestjs-typeorm-paginate": "2.6.3",
"passport": "0.5.0",
"passport-jwt": "4.0.0",
"passport-spotify": "2.0.0",
"pg": "8.7.1",
"pino": "7.11.0",
"pino-http": "6.6.0",
"reflect-metadata": "0.1.13",
"rimraf": "3.0.2",
"rxjs": "7.4.0",
@ -64,6 +83,7 @@
"@types/express": "4.17.13",
"@types/hapi__joi": "17.1.7",
"@types/jest": "27.0.3",
"@types/long": "^4.0.2",
"@types/node": "15.6.0",
"@types/passport-jwt": "3.0.6",
"@types/supertest": "2.0.11",
@ -80,6 +100,7 @@
"eslint-plugin-react": "7.30.0",
"eslint-plugin-react-hooks": "4.3.0",
"jest": "27.4.3",
"pino-pretty": "8.0.0",
"prettier": "2.5.1",
"supertest": "6.1.6",
"ts-jest": "27.1.1",

View file

@ -9,11 +9,11 @@ import { DatabaseModule } from "./database/database.module";
import { HealthCheckModule } from "./health-check/health-check.module";
import { ListensModule } from "./listens/listens.module";
import { LoggerModule } from "./logger/logger.module";
import { MetricsModule } from "./metrics/metrics.module";
import { MusicLibraryModule } from "./music-library/music-library.module";
import { ReportsModule } from "./reports/reports.module";
import { SourcesModule } from "./sources/sources.module";
import { UsersModule } from "./users/users.module";
import { OpenTelemetryModule } from "./open-telemetry/open-telemetry.module";
@Module({
imports: [
@ -26,7 +26,7 @@ import { UsersModule } from "./users/users.module";
exclude: ["/api*"],
}),
RavenModule,
MetricsModule.forRoot(),
OpenTelemetryModule,
AuthModule,
UsersModule,
SourcesModule,

View file

@ -1,6 +1,5 @@
import { Test, TestingModule } from "@nestjs/testing";
import type { Response } from "express";
import { Logger } from "../logger/logger.service";
import { User } from "../users/user.entity";
import { AuthSession } from "./auth-session.entity";
import { AuthController } from "./auth.controller";
@ -14,10 +13,7 @@ describe("AuthController", () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [
{ provide: AuthService, useFactory: () => ({}) },
{ provide: Logger, useClass: Logger },
],
providers: [{ provide: AuthService, useFactory: () => ({}) }],
}).compile();
controller = module.get<AuthController>(AuthController);

View file

@ -3,15 +3,13 @@ import {
Catch,
ExceptionFilter,
ForbiddenException,
Logger,
} from "@nestjs/common";
import type { Response } from "express";
import { Logger } from "../logger/logger.service";
@Catch()
export class SpotifyAuthFilter implements ExceptionFilter {
constructor(private readonly logger: Logger) {
this.logger.setContext(this.constructor.name);
}
private readonly logger = new Logger(this.constructor.name);
catch(exception: Error, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse<Response>();

View file

@ -1,9 +1,18 @@
import { Module, Global } from "@nestjs/common";
import { Logger } from "./logger.service";
import { Module, RequestMethod } from "@nestjs/common";
import { LoggerModule as PinoLoggerModule } from "nestjs-pino";
import { logger } from "./logger";
@Global()
@Module({
providers: [Logger],
exports: [Logger],
imports: [
PinoLoggerModule.forRoot({
pinoHttp: {
logger: logger,
autoLogging: true,
quietReqLogger: true,
redact: ["req.headers", "res.headers"],
},
exclude: [{ method: RequestMethod.ALL, path: "/" }],
}),
],
})
export class LoggerModule {}

View file

@ -1,4 +0,0 @@
import { Injectable, Scope, ConsoleLogger } from "@nestjs/common";
@Injectable({ scope: Scope.TRANSIENT })
export class Logger extends ConsoleLogger {}

32
src/logger/logger.ts Normal file
View file

@ -0,0 +1,32 @@
import { context, trace } from "@opentelemetry/api";
import Pino, { Logger, LoggerOptions } from "pino";
export const loggerOptions: LoggerOptions = {
level: "debug",
formatters: {
level(label) {
return { level: label };
},
log(object) {
const span = trace.getSpan(context.active());
if (!span) return { ...object };
const { spanId, traceId } = trace
.getSpan(context.active())
?.spanContext();
return { ...object, spanId, traceId };
},
},
transport:
process.env.NODE_ENV === "local"
? {
target: "pino-pretty",
options: {
colorize: true,
levelFirst: true,
translateTime: true,
},
}
: null,
};
export const logger: Logger = Pino(loggerOptions);

View file

@ -1,11 +1,31 @@
import { otelSDK } from "./open-telemetry/sdk"; // needs to be loaded first - always -
import { ValidationPipe } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { NestFactory } from "@nestjs/core";
import { NestExpressApplication } from "@nestjs/platform-express";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { context, trace } from "@opentelemetry/api";
import * as Sentry from "@sentry/node";
import { RavenInterceptor } from "nest-raven";
import Pino from "pino";
import { AppModule } from "./app.module";
import { Logger } from "nestjs-pino";
const logger = Pino({
formatters: {
log(object) {
const span = trace.getSpan(context.active());
if (!span) return { ...object };
const { spanId, traceId } = trace
.getSpan(context.active())
?.spanContext();
return { ...object, spanId, traceId };
},
},
});
// @ts-expect-error
logger.log = logger.info;
function setupSentry(
app: NestExpressApplication,
@ -23,7 +43,12 @@ function setupSentry(
}
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
await otelSDK.start();
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
bufferLogs: true,
});
app.useLogger(app.get(Logger));
app.useGlobalPipes(
new ValidationPipe({
transform: true,

View file

@ -1,74 +0,0 @@
import { UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Test, TestingModule } from "@nestjs/testing";
import { IncomingMessage } from "http";
import { MetricsAuthMiddleware } from "./metrics-auth.middleware";
describe("MetricsAuthMiddleware", () => {
let middleware: MetricsAuthMiddleware;
let config: ConfigService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
MetricsAuthMiddleware,
{
provide: ConfigService,
useFactory: () => ({
get: jest
.fn()
.mockReturnValueOnce("foo") // Username
.mockReturnValueOnce("bar"), // Password
}),
},
],
}).compile();
middleware = module.get<MetricsAuthMiddleware>(MetricsAuthMiddleware);
config = module.get<ConfigService>(ConfigService);
});
it("should be defined", () => {
expect(middleware).toBeDefined();
expect(config).toBeDefined();
});
describe("use", () => {
let req: IncomingMessage;
let res: any;
let next: () => void;
beforeEach(() => {
req = {
headers: { authorization: `Basic Zm9vOmJhcg==` },
} as IncomingMessage; // Buffer.from("foo:bar").toString("base64")
res = {};
next = jest.fn();
});
it("calls next", async () => {
middleware.use(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
expect(next).toHaveBeenCalledWith();
});
it("throws UnauthorizedException if header is not set", async () => {
delete req.headers.authorization;
expect(() => middleware.use(req, res, next)).toThrow(
UnauthorizedException
);
});
it("throws UnauthorizedException if header does not match", async () => {
req.headers.authorization = `Basic doesnotmatch`;
expect(() => middleware.use(req, res, next)).toThrow(
UnauthorizedException
);
});
});
});

View file

@ -1,36 +0,0 @@
import {
Injectable,
NestMiddleware,
UnauthorizedException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { IncomingMessage } from "http";
@Injectable()
export class MetricsAuthMiddleware implements NestMiddleware {
private readonly expectedHeaderValue: string;
constructor(config: ConfigService) {
const username = config.get<string>("PROMETHEUS_BASIC_AUTH_USERNAME");
const password = config.get<string>("PROMETHEUS_BASIC_AUTH_PASSWORD");
this.expectedHeaderValue = MetricsAuthMiddleware.buildHeaderValue(
username,
password
);
}
private static buildHeaderValue(username: string, password: string): string {
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
}
use(req: IncomingMessage, res: any, next: () => void) {
const header = req.headers?.authorization;
if (header !== this.expectedHeaderValue) {
throw new UnauthorizedException("MetricsBasicAuthNotMatching");
}
next();
}
}

View file

@ -1,61 +0,0 @@
import { InboundMiddleware, PromModule } from "@digikare/nestjs-prom";
import { DEFAULT_PROM_OPTIONS } from "@digikare/nestjs-prom/dist/prom.constants";
import {
DynamicModule,
MiddlewareConsumer,
Module,
NestModule,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { MetricsAuthMiddleware } from "./metrics-auth.middleware";
// Dirty hack because we can not conditionally import modules based on
// injected services and upstream module does not support dynamic configuration
//
// https://github.com/digikare/nestjs-prom/issues/27
const promEnabled = process.env.PROMETHEUS_ENABLED === "true";
const METRIC_PATH = "/api/metrics";
@Module({})
export class MetricsModule implements NestModule {
constructor(private readonly config: ConfigService) {}
static forRoot(): DynamicModule {
const module = {
imports: [],
providers: [],
};
if (promEnabled) {
const promOptions = {
metricPath: METRIC_PATH,
withDefaultsMetrics: true,
withDefaultController: true,
};
module.imports.push(PromModule.forRoot(promOptions));
module.providers.push({
provide: DEFAULT_PROM_OPTIONS,
useValue: promOptions,
});
}
return {
module: MetricsModule,
...module,
};
}
configure(consumer: MiddlewareConsumer) {
if (this.config.get<boolean>("PROMETHEUS_ENABLED")) {
// We register the Middleware ourselves to avoid tracking
// latency for static files served for the frontend.
consumer.apply(InboundMiddleware).exclude(METRIC_PATH).forRoutes("/api");
if (this.config.get<boolean>("PROMETHEUS_BASIC_AUTH")) {
consumer.apply(MetricsAuthMiddleware).forRoutes(METRIC_PATH);
}
}
}
}

View file

@ -0,0 +1,25 @@
import { Module, OnApplicationShutdown } from "@nestjs/common";
import { OpenTelemetryModule as UpstreamModule } from "nestjs-otel";
import { otelSDK } from "./sdk";
@Module({
imports: [
UpstreamModule.forRoot({
metrics: {
hostMetrics: true, // Includes Host Metrics
defaultMetrics: true, // Includes Default Metrics
apiMetrics: {
enable: true, // Includes api metrics
timeBuckets: [], // You can change the default time buckets
ignoreUndefinedRoutes: false, //Records metrics for all URLs, even undefined ones
},
},
}),
],
exports: [UpstreamModule],
})
export class OpenTelemetryModule implements OnApplicationShutdown {
async onApplicationShutdown(): Promise<void> {
await otelSDK.shutdown();
}
}

48
src/open-telemetry/sdk.ts Normal file
View file

@ -0,0 +1,48 @@
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { DnsInstrumentation } from "@opentelemetry/instrumentation-dns";
import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import { NestInstrumentation } from "@opentelemetry/instrumentation-nestjs-core";
import { PgInstrumentation } from "@opentelemetry/instrumentation-pg";
import { PinoInstrumentation } from "@opentelemetry/instrumentation-pino";
import { Resource } from "@opentelemetry/resources";
import { NodeSDK, NodeSDKConfiguration } from "@opentelemetry/sdk-node";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
import { hostname } from "os";
const metricsEnabled = process.env.OTEL_METRICS_ENABLED === "true";
const tracesEnabled = process.env.OTEL_TRACES_ENABLED === "true";
const anyEnabled = metricsEnabled || tracesEnabled;
// We can not use ConfigService because the SDK needs to be initialized before
// Nest is allowed to start.
let sdkOptions: Partial<NodeSDKConfiguration> = {};
if (metricsEnabled) {
sdkOptions.metricExporter = new PrometheusExporter();
}
if (tracesEnabled) {
sdkOptions.traceExporter = new OTLPTraceExporter({});
sdkOptions.contextManager = new AsyncLocalStorageContextManager();
sdkOptions.resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAMESPACE]: "listory",
[SemanticResourceAttributes.SERVICE_NAME]: "api",
[SemanticResourceAttributes.SERVICE_INSTANCE_ID]: hostname(),
});
}
if (anyEnabled) {
sdkOptions.instrumentations = [
new DnsInstrumentation(),
new HttpInstrumentation(),
new ExpressInstrumentation(),
new NestInstrumentation(),
new PgInstrumentation(),
new PinoInstrumentation(),
];
}
export const otelSDK = new NodeSDK(sdkOptions);

View file

@ -1,20 +1,18 @@
import { Injectable, OnApplicationBootstrap } from "@nestjs/common";
import { Injectable, Logger, OnApplicationBootstrap } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { SchedulerRegistry } from "@nestjs/schedule";
import { captureException } from "@sentry/node";
import { Logger } from "../logger/logger.service";
import { SpotifyService } from "./spotify/spotify.service";
@Injectable()
export class SchedulerService implements OnApplicationBootstrap {
private readonly logger = new Logger(this.constructor.name);
constructor(
private readonly config: ConfigService,
private readonly registry: SchedulerRegistry,
private readonly spotifyService: SpotifyService,
private readonly logger: Logger
) {
this.logger.setContext(this.constructor.name);
}
private readonly spotifyService: SpotifyService
) {}
onApplicationBootstrap() {
this.setupSpotifyCrawler();

View file

@ -1,6 +1,5 @@
import { Test, TestingModule } from "@nestjs/testing";
import { ListensService } from "../../listens/listens.service";
import { Logger } from "../../logger/logger.service";
import { MusicLibraryService } from "../../music-library/music-library.service";
import { UsersService } from "../../users/users.service";
import { SpotifyApiService } from "./spotify-api/spotify-api.service";
@ -14,7 +13,6 @@ describe("SpotifyService", () => {
let musicLibraryService: MusicLibraryService;
let spotifyApi: SpotifyApiService;
let spotifyAuth: SpotifyAuthService;
let logger: Logger;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -25,7 +23,6 @@ describe("SpotifyService", () => {
{ provide: MusicLibraryService, useFactory: () => ({}) },
{ provide: SpotifyApiService, useFactory: () => ({}) },
{ provide: SpotifyAuthService, useFactory: () => ({}) },
{ provide: Logger, useValue: new Logger() },
],
}).compile();
@ -35,7 +32,6 @@ describe("SpotifyService", () => {
musicLibraryService = module.get<MusicLibraryService>(MusicLibraryService);
spotifyApi = module.get<SpotifyApiService>(SpotifyApiService);
spotifyAuth = module.get<SpotifyAuthService>(SpotifyAuthService);
logger = module.get<Logger>(Logger);
});
it("should be defined", () => {
@ -45,6 +41,5 @@ describe("SpotifyService", () => {
expect(musicLibraryService).toBeDefined();
expect(spotifyApi).toBeDefined();
expect(spotifyAuth).toBeDefined();
expect(logger).toBeDefined();
});
});

View file

@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common";
import { Injectable, Logger } from "@nestjs/common";
import { Span } from "nestjs-otel";
import { ListensService } from "../../listens/listens.service";
import { Logger } from "../../logger/logger.service";
import { Album } from "../../music-library/album.entity";
import { Artist } from "../../music-library/artist.entity";
import { Genre } from "../../music-library/genre.entity";
@ -17,6 +17,8 @@ import { SpotifyAuthService } from "./spotify-auth/spotify-auth.service";
@Injectable()
export class SpotifyService {
private readonly logger = new Logger(this.constructor.name);
private appAccessToken: string | null;
private appAccessTokenInProgress: Promise<void> | null;
@ -25,12 +27,10 @@ export class SpotifyService {
private readonly listensService: ListensService,
private readonly musicLibraryService: MusicLibraryService,
private readonly spotifyApi: SpotifyApiService,
private readonly spotifyAuth: SpotifyAuthService,
private readonly logger: Logger
) {
this.logger.setContext(this.constructor.name);
}
private readonly spotifyAuth: SpotifyAuthService
) {}
@Span()
async runCrawlerForAllUsers(): Promise<void> {
this.logger.debug("Starting Spotify crawler loop");
const users = await this.usersService.findAll();
@ -42,11 +42,12 @@ export class SpotifyService {
}
}
@Span()
async crawlListensForUser(
user: User,
retryOnExpiredToken: boolean = true
): Promise<void> {
this.logger.debug(`Crawling recently played tracks for user "${user.id}"`);
this.logger.debug({ userId: user.id }, `Crawling recently played tracks`);
let playHistory: PlayHistoryObject[];
try {
@ -64,6 +65,7 @@ export class SpotifyService {
await this.crawlListensForUser(user, false);
} catch (errFromAuth) {
this.logger.error(
{ userId: user.id },
`Refreshing access token failed for user "${user.id}": ${errFromAuth}`
);
}
@ -95,6 +97,7 @@ export class SpotifyService {
if (!isDuplicate) {
this.logger.debug(
{ userId: user.id },
`New listen found! ${user.id} listened to "${
track.name
}" by ${track.artists
@ -126,6 +129,7 @@ export class SpotifyService {
});
}
@Span()
async runUpdaterForAllEntities(): Promise<void> {
this.logger.debug("Starting Spotify updater loop");
@ -137,6 +141,7 @@ export class SpotifyService {
}
}
@Span()
async importTrack(
spotifyID: string,
retryOnExpiredToken: boolean = true
@ -187,6 +192,7 @@ export class SpotifyService {
});
}
@Span()
async importAlbum(
spotifyID: string,
retryOnExpiredToken: boolean = true
@ -233,6 +239,7 @@ export class SpotifyService {
});
}
@Span()
async importArtist(
spotifyID: string,
retryOnExpiredToken: boolean = true
@ -277,6 +284,7 @@ export class SpotifyService {
});
}
@Span()
async updateArtist(
spotifyID: string,
retryOnExpiredToken: boolean = true
@ -315,6 +323,7 @@ export class SpotifyService {
return artist;
}
@Span()
async importGenre(name: string): Promise<Genre> {
const genre = await this.musicLibraryService.findGenre({
name,
@ -328,6 +337,7 @@ export class SpotifyService {
});
}
@Span()
private async refreshAppAccessToken(): Promise<void> {
if (!this.appAccessTokenInProgress) {
this.logger.debug("refreshing spotify app access token");