feat(server): OpenTelemetry integration (#7356)
* wip * span class decorator fix typing * improvements * noisy postgres logs formatting * add source * strict string comparison Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> * remove debug code * execution time histogram * remove prometheus stuff remove prometheus data * disable by default disable nestjs-otel stuff by default update imports * re-add postgres instrumentation formatting formatting * refactor: execution time histogram * decorator alias * formatting * keep original method order in filesystem repo * linting * enable otel sdk in e2e * actually enable otel sdk in e2e * share exclude paths * formatting * fix rebase * more buckets * add example setup * add envs fix actual fix * linting * update comments * update docker env * use more specific env --------- Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
} from '../entities';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { ChunkedSet } from '../infra.utils';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
type IActivityAccess = IAccessRepository['activity'];
|
||||
type IAlbumAccess = IAccessRepository['album'];
|
||||
@@ -24,6 +25,7 @@ type ITimelineAccess = IAccessRepository['timeline'];
|
||||
type IPersonAccess = IAccessRepository['person'];
|
||||
type IPartnerAccess = IAccessRepository['partner'];
|
||||
|
||||
@Instrumentation()
|
||||
class ActivityAccess implements IActivityAccess {
|
||||
constructor(
|
||||
private activityRepository: Repository<ActivityEntity>,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { ActivityEntity } from '../entities/activity.entity';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
export interface ActivitySearch {
|
||||
albumId?: string;
|
||||
@@ -12,6 +13,7 @@ export interface ActivitySearch {
|
||||
isLiked?: boolean;
|
||||
}
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class ActivityRepository implements IActivityRepository {
|
||||
constructor(@InjectRepository(ActivityEntity) private repository: Repository<ActivityEntity>) {}
|
||||
|
||||
@@ -8,7 +8,9 @@ import { dataSource } from '../database.config';
|
||||
import { AlbumEntity, AssetEntity } from '../entities';
|
||||
import { DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from '../infra.util';
|
||||
import { Chunked, ChunkedArray } from '../infra.utils';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class AlbumRepository implements IAlbumRepository {
|
||||
constructor(
|
||||
|
||||
@@ -4,7 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { APIKeyEntity } from '../entities';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class ApiKeyRepository implements IKeyRepository {
|
||||
constructor(@InjectRepository(APIKeyEntity) private repository: Repository<APIKeyEntity>) {}
|
||||
|
||||
@@ -3,7 +3,9 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AssetStackEntity } from '../entities';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class AssetStackRepository implements IAssetStackRepository {
|
||||
constructor(@InjectRepository(AssetStackEntity) private repository: Repository<AssetStackEntity>) {}
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity, SmartInfoEntity } from '../entities';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { Chunked, ChunkedArray, OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from '../infra.utils';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
const truncateMap: Record<TimeBucketSize, string> = {
|
||||
[TimeBucketSize.DAY]: 'day',
|
||||
@@ -50,6 +51,7 @@ const dateTrunc = (options: TimeBucketOptions) =>
|
||||
truncateMap[options.size]
|
||||
}', (asset."localDateTime" at time zone 'UTC')) at time zone 'UTC')::timestamptz`;
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class AssetRepository implements IAssetRepository {
|
||||
constructor(
|
||||
|
||||
@@ -2,7 +2,9 @@ import { AuditSearch, IAuditRepository } from '@app/domain';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { LessThan, MoreThan, Repository } from 'typeorm';
|
||||
import { AuditEntity } from '../entities';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
export class AuditRepository implements IAuditRepository {
|
||||
constructor(@InjectRepository(AuditEntity) private repository: Repository<AuditEntity>) {}
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@WebSocketGateway({
|
||||
cors: true,
|
||||
path: '/api/socket.io',
|
||||
|
||||
@@ -3,14 +3,26 @@ import { Injectable } from '@nestjs/common';
|
||||
import { compareSync, hash } from 'bcrypt';
|
||||
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class CryptoRepository implements ICryptoRepository {
|
||||
randomUUID = randomUUID;
|
||||
randomBytes = randomBytes;
|
||||
randomUUID() {
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
hashBcrypt = hash;
|
||||
compareBcrypt = compareSync;
|
||||
randomBytes(size: number) {
|
||||
return randomBytes(size);
|
||||
}
|
||||
|
||||
hashBcrypt(data: string | Buffer, saltOrRounds: string | number) {
|
||||
return hash(data, saltOrRounds);
|
||||
}
|
||||
|
||||
compareBcrypt(data: string | Buffer, encrypted: string) {
|
||||
return compareSync(data, encrypted);
|
||||
}
|
||||
|
||||
hashSha256(value: string) {
|
||||
return createHash('sha256').update(value).digest('base64');
|
||||
|
||||
@@ -15,8 +15,10 @@ import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import AsyncLock from 'async-lock';
|
||||
import { DataSource, EntityManager, QueryRunner } from 'typeorm';
|
||||
import { isValidInteger } from '../infra.utils';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
import { ImmichLogger } from '../logger';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class DatabaseRepository implements IDatabaseRepository {
|
||||
private logger = new ImmichLogger(DatabaseRepository.name);
|
||||
|
||||
@@ -13,23 +13,37 @@ import archiver from 'archiver';
|
||||
import chokidar, { WatchOptions } from 'chokidar';
|
||||
import { glob } from 'fast-glob';
|
||||
import { constants, createReadStream, existsSync, mkdirSync } from 'node:fs';
|
||||
import fs, { copyFile, readdir, rename, stat, utimes, writeFile } from 'node:fs/promises';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
export class FilesystemProvider implements IStorageRepository {
|
||||
private logger = new ImmichLogger(FilesystemProvider.name);
|
||||
|
||||
readdir = readdir;
|
||||
readdir(folder: string): Promise<string[]> {
|
||||
return fs.readdir(folder);
|
||||
}
|
||||
|
||||
copyFile = copyFile;
|
||||
copyFile(source: string, target: string) {
|
||||
return fs.copyFile(source, target);
|
||||
}
|
||||
|
||||
stat = stat;
|
||||
stat(filepath: string) {
|
||||
return fs.stat(filepath);
|
||||
}
|
||||
|
||||
writeFile = writeFile;
|
||||
writeFile(filepath: string, buffer: Buffer) {
|
||||
return fs.writeFile(filepath, buffer);
|
||||
}
|
||||
|
||||
rename = rename;
|
||||
rename(source: string, target: string) {
|
||||
return fs.rename(source, target);
|
||||
}
|
||||
|
||||
utimes = utimes;
|
||||
utimes(filepath: string, atime: Date, mtime: Date) {
|
||||
return fs.utimes(filepath, atime, mtime);
|
||||
}
|
||||
|
||||
createZipStream(): ImmichZipStream {
|
||||
const archive = archiver('zip', { store: true });
|
||||
|
||||
@@ -17,7 +17,9 @@ import { Job, JobsOptions, Processor, Queue, Worker, WorkerOptions } from 'bullm
|
||||
import { CronJob, CronTime } from 'cron';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { bullConfig } from '../infra.config';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class JobRepository implements IJobRepository {
|
||||
private workers: Partial<Record<QueueName, Worker>> = {};
|
||||
|
||||
@@ -5,7 +5,9 @@ import { IsNull, Not } from 'typeorm';
|
||||
import { Repository } from 'typeorm/repository/Repository.js';
|
||||
import { LibraryEntity, LibraryType } from '../entities';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class LibraryRepository implements ILibraryRepository {
|
||||
constructor(@InjectRepository(LibraryEntity) private repository: Repository<LibraryEntity>) {}
|
||||
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
} from '@app/domain';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
const errorPrefix = 'Machine learning request';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class MachineLearningRepository implements IMachineLearningRepository {
|
||||
private async predict<T>(url: string, input: TextModelInput | VisionModelInput, config: ModelConfig): Promise<T> {
|
||||
@@ -50,7 +52,7 @@ export class MachineLearningRepository implements IMachineLearningRepository {
|
||||
} as CLIPConfig);
|
||||
}
|
||||
|
||||
async getFormData(input: TextModelInput | VisionModelInput, config: ModelConfig): Promise<FormData> {
|
||||
private async getFormData(input: TextModelInput | VisionModelInput, config: ModelConfig): Promise<FormData> {
|
||||
const formData = new FormData();
|
||||
const { enabled, modelName, modelType, ...options } = config;
|
||||
if (!enabled) {
|
||||
|
||||
@@ -13,11 +13,13 @@ import fs from 'node:fs/promises';
|
||||
import { Writable } from 'node:stream';
|
||||
import { promisify } from 'node:util';
|
||||
import sharp from 'sharp';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
const probe = promisify<string, FfprobeData>(ffmpeg.ffprobe);
|
||||
sharp.concurrency(0);
|
||||
sharp.cache({ files: 0 });
|
||||
|
||||
@Instrumentation()
|
||||
export class MediaRepository implements IMediaRepository {
|
||||
private logger = new ImmichLogger(MediaRepository.name);
|
||||
|
||||
@@ -115,19 +117,6 @@ export class MediaRepository implements IMediaRepository {
|
||||
});
|
||||
}
|
||||
|
||||
configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) {
|
||||
return ffmpeg(input, { niceness: 10 })
|
||||
.inputOptions(options.inputOptions)
|
||||
.outputOptions(options.outputOptions)
|
||||
.output(output)
|
||||
.on('error', (error, stdout, stderr) => this.logger.error(stderr || error));
|
||||
}
|
||||
|
||||
chainPath(existing: string, path: string) {
|
||||
const separator = existing.endsWith(':') ? '' : ':';
|
||||
return `${existing}${separator}${path}`;
|
||||
}
|
||||
|
||||
async generateThumbhash(imagePath: string): Promise<Buffer> {
|
||||
const maxSize = 100;
|
||||
|
||||
@@ -140,4 +129,17 @@ export class MediaRepository implements IMediaRepository {
|
||||
const thumbhash = await import('thumbhash');
|
||||
return Buffer.from(thumbhash.rgbaToThumbHash(info.width, info.height, data));
|
||||
}
|
||||
|
||||
private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) {
|
||||
return ffmpeg(input, { niceness: 10 })
|
||||
.inputOptions(options.inputOptions)
|
||||
.outputOptions(options.outputOptions)
|
||||
.output(output)
|
||||
.on('error', (error, stdout, stderr) => this.logger.error(stderr || error));
|
||||
}
|
||||
|
||||
private chainPath(existing: string, path: string) {
|
||||
const separator = existing.endsWith(':') ? '' : ':';
|
||||
return `${existing}${separator}${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ import * as readLine from 'node:readline';
|
||||
import { DataSource, QueryRunner, Repository } from 'typeorm';
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
export class MetadataRepository implements IMetadataRepository {
|
||||
constructor(
|
||||
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
|
||||
|
||||
@@ -4,7 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { MoveEntity, PathType } from '../entities';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class MoveRepository implements IMoveRepository {
|
||||
constructor(@InjectRepository(MoveEntity) private repository: Repository<MoveEntity>) {}
|
||||
|
||||
@@ -3,7 +3,9 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DeepPartial, Repository } from 'typeorm';
|
||||
import { PartnerEntity } from '../entities';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class PartnerRepository implements IPartnerRepository {
|
||||
constructor(@InjectRepository(PartnerEntity) private readonly repository: Repository<PartnerEntity>) {}
|
||||
|
||||
@@ -15,7 +15,9 @@ import { FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repositor
|
||||
import { AssetEntity, AssetFaceEntity, PersonEntity } from '../entities';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { ChunkedArray, asVector, paginate } from '../infra.utils';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
export class PersonRepository implements IPersonRepository {
|
||||
constructor(
|
||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||
|
||||
@@ -26,7 +26,9 @@ import { Repository, SelectQueryBuilder } from 'typeorm';
|
||||
import { vectorExt } from '../database.config';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { asVector, isValidInteger, paginatedBuilder, searchAssetBuilder } from '../infra.utils';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class SearchRepository implements ISearchRepository {
|
||||
private logger = new ImmichLogger(SearchRepository.name);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { GitHubRelease, IServerInfoRepository } from '@app/domain';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class ServerInfoRepository implements IServerInfoRepository {
|
||||
async getGitHubRelease(): Promise<GitHubRelease> {
|
||||
|
||||
@@ -4,7 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { SharedLinkEntity } from '../entities';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class SharedLinkRepository implements ISharedLinkRepository {
|
||||
constructor(@InjectRepository(SharedLinkEntity) private repository: Repository<SharedLinkEntity>) {}
|
||||
|
||||
@@ -5,12 +5,15 @@ import { In, Repository } from 'typeorm';
|
||||
import { SystemConfigEntity } from '../entities';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { Chunked } from '../infra.utils';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
export class SystemConfigRepository implements ISystemConfigRepository {
|
||||
constructor(
|
||||
@InjectRepository(SystemConfigEntity)
|
||||
private repository: Repository<SystemConfigEntity>,
|
||||
) {}
|
||||
|
||||
async fetchStyle(url: string) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
||||
@@ -2,12 +2,15 @@ import { ISystemMetadataRepository } from '@app/domain/repositories/system-metad
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { SystemMetadata, SystemMetadataEntity } from '../entities';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
export class SystemMetadataRepository implements ISystemMetadataRepository {
|
||||
constructor(
|
||||
@InjectRepository(SystemMetadataEntity)
|
||||
private repository: Repository<SystemMetadataEntity>,
|
||||
) {}
|
||||
|
||||
async get<T extends keyof SystemMetadata>(key: T): Promise<SystemMetadata[T] | null> {
|
||||
const metadata = await this.repository.findOne({ where: { key } });
|
||||
if (!metadata) {
|
||||
|
||||
@@ -3,7 +3,9 @@ import { AssetEntity, TagEntity } from '@app/infra/entities';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class TagRepository implements ITagRepository {
|
||||
constructor(
|
||||
|
||||
@@ -4,7 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { UserTokenEntity } from '../entities';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class UserTokenRepository implements IUserTokenRepository {
|
||||
constructor(@InjectRepository(UserTokenEntity) private repository: Repository<UserTokenEntity>) {}
|
||||
|
||||
@@ -4,7 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
import { AssetEntity, UserEntity } from '../entities';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class UserRepository implements IUserRepository {
|
||||
constructor(
|
||||
|
||||
Reference in New Issue
Block a user