Skip to content

Commit

Permalink
Refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
myrotvorets-team committed Oct 14, 2023
1 parent 7b7cd3c commit fd2a3ac
Show file tree
Hide file tree
Showing 15 changed files with 656 additions and 466 deletions.
1 change: 1 addition & 0 deletions .mocharc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = {
recursive: true,
extension: ['.test.mts'],
'node-option': ['loader=ts-node/esm', 'no-warnings'],
require: 'mocha.setup.mjs',
reporter: 'mocha-multi',
'reporter-option': [
'spec=-',
Expand Down
14 changes: 14 additions & 0 deletions mocha.setup.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const env = { ...process.env };
process.env = {
NODE_ENV: 'test',
OTEL_SDK_DISABLED: 'true',
KNEX_DRIVER: 'better-sqlite3',
KNEX_DATABASE: ':memory:',
};

/** @type {import('mocha').RootHookObject} */
export const mochaHooks = {
afterAll() {
process.env = { ...env };
},
};
668 changes: 351 additions & 317 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,23 @@
"license": "MIT",
"dependencies": {
"@cloudnative/health-connect": "^2.1.0",
"@myrotvorets/create-server": "^2.2.0",
"@myrotvorets/envalidators": "^2.1.0",
"@myrotvorets/express-async-middleware-wrapper": "^2.2.0",
"@myrotvorets/express-microservice-middlewares": "^1.6.0",
"@myrotvorets/oav-installer": "^4.1.0",
"@myrotvorets/opentelemetry-configurator": "^6.4.1",
"@myrotvorets/express-microservice-middlewares": "^2.1.0",
"@myrotvorets/express-request-logger": "^1.2.0",
"@myrotvorets/oav-installer": "^4.1.1",
"@myrotvorets/opentelemetry-configurator": "^7.0.0",
"@myrotvorets/opentelemetry-plugin-knex": "^0.30.0",
"@myrotvorets/otel-utils": "^0.0.10",
"@opentelemetry/api": "^1.6.0",
"@opentelemetry/core": "^1.17.1",
"@opentelemetry/semantic-conventions": "^1.17.1",
"awilix": "^9.0.0",
"envalid": "^8.0.0",
"express": "^4.18.2",
"express-openapi-validator": "^5.0.6",
"inet_xtoy": "^1.2.5",
"knex": "^2.0.0",
"morgan": "^1.10.0",
"mysql2": "^3.6.1",
"objection": "^3.1.2"
},
Expand All @@ -42,8 +46,6 @@
"@types/express": "^4.17.19",
"@types/mocha": "^10.0.2",
"@types/mock-knex": "^0.4.6",
"@types/morgan": "^1.9.6",
"@types/multer": "^1.4.8",
"@types/node": "^20.8.6",
"@types/supertest": "^2.0.14",
"better-sqlite3": "^9.0.0",
Expand Down
24 changes: 20 additions & 4 deletions src/index.mts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
/* c8 ignore start */
import { configure } from './lib/tracing.mjs';
import { run } from './server.mjs';
import { OpenTelemetryConfigurator, getExpressInstrumentations } from '@myrotvorets/opentelemetry-configurator';
import { KnexInstrumentation } from '@myrotvorets/opentelemetry-plugin-knex';
import { initProcessMetrics } from '@myrotvorets/otel-utils';

configure();
run().catch((e) => console.error(e));
process.env['OTEL_SERVICE_NAME'] = 'psb-api-identigraf-auth';

export const configurator = new OpenTelemetryConfigurator({
serviceName: process.env['OTEL_SERVICE_NAME'],
instrumentations: [...getExpressInstrumentations(), new KnexInstrumentation()],
});

configurator.start();

await initProcessMetrics();

try {
const { run } = await import('./server.mjs');
await run();
} catch (e) {
console.error(e);
}
/* c8 ignore stop */
75 changes: 48 additions & 27 deletions src/knexfile.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,72 @@ import type { Knex } from 'knex';

interface DbEnv {
NODE_ENV: string;
MYSQL_DATABASE: string;
MYSQL_HOST: string;
MYSQL_USER: string;
MYSQL_PASSWORD: string;
MYSQL_CONN_LIMIT: number;
KNEX_DRIVER: string;
KNEX_DATABASE: string;
KNEX_HOST: string;
KNEX_USER: string;
KNEX_PASSWORD: string;
KNEX_CONN_LIMIT: number;
}

function getEnvironment(environment: NodeJS.Dict<string>): Readonly<DbEnv> {
return cleanEnv(environment, {
NODE_ENV: str({ default: 'development' }),
MYSQL_DATABASE: str(),
MYSQL_HOST: str({ default: 'localhost' }),
MYSQL_USER: str({ default: '' }),
MYSQL_PASSWORD: str({ default: '' }),
MYSQL_CONN_LIMIT: num({ default: 2 }),
KNEX_DRIVER: str({ default: 'mysql2', choices: ['better-sqlite3', 'mysql2'] }), // Run `npm i driver` if any other driver is needed
KNEX_DATABASE: str(),
KNEX_HOST: str({ default: 'localhost' }),
KNEX_USER: str({ default: '' }),
KNEX_PASSWORD: str({ default: '' }),
KNEX_CONN_LIMIT: num({ default: 2 }),
});
}

export function buildKnexConfig(environment: NodeJS.Dict<string> = process.env): Knex.Config {
const base = dirname(fileURLToPath(import.meta.url));
const env = getEnvironment(environment);

return {
client: 'mysql2',
asyncStackTraces: env.NODE_ENV === 'development',
connection: {
database: env.MYSQL_DATABASE,
host: env.MYSQL_HOST,
user: env.MYSQL_USER,
password: env.MYSQL_PASSWORD,
dateStrings: true,
charset: 'utf8mb4',
},
pool: {
min: 0,
max: env.MYSQL_CONN_LIMIT,
},
let config: Knex.Config = {
client: env.KNEX_DRIVER,
asyncStackTraces: ['development', 'test'].includes(env.NODE_ENV),
migrations: {
tableName: 'knex_migrations_identigraf_auth',
directory: join(dirname(fileURLToPath(import.meta.url)), '..', 'test', 'migrations'),
directory: join(base, '..', 'test', 'migrations'),
loadExtensions: ['.mts'],
},
seeds: {
directory: join(dirname(fileURLToPath(import.meta.url)), '..', 'test', 'seeds'),
directory: join(base, '..', 'test', 'seeds'),
loadExtensions: ['.mts'],
},
};

if (env.KNEX_DRIVER === 'mysql2') {
config = {
...config,
connection: {
database: env.KNEX_DATABASE,
host: env.KNEX_HOST,
user: env.KNEX_USER,
password: env.KNEX_PASSWORD,
dateStrings: true,
charset: 'utf8mb4',
},
pool: {
min: 0,
max: env.KNEX_CONN_LIMIT,
},
};
} else if (env.KNEX_DRIVER === 'better-sqlite3') {
config = {
...config,
useNullAsDefault: true,
connection: {
filename: env.KNEX_DATABASE,
},
};
} else {
throw new Error(`Unsupported driver ${env.KNEX_DRIVER}`);
}

return config;
}
/* c8 ignore stop */
74 changes: 74 additions & 0 deletions src/lib/container.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { AwilixContainer, asFunction, asValue, createContainer } from 'awilix';
import type { NextFunction, Request, Response } from 'express';
import * as knexpkg from 'knex';
import { type Logger, type Meter, getLogger, getMeter } from '@myrotvorets/otel-utils';
import { Model } from 'objection';
import { environment } from './environment.mjs';
import { buildKnexConfig } from '../knexfile.mjs';

export interface Container {
environment: ReturnType<typeof environment>;
logger: Logger;
meter: Meter;
db: knexpkg.Knex;
}

export interface RequestContainer {
req: Request;
}

export const container = createContainer<Container>();

function createEnvironment(): ReturnType<typeof environment> {
return environment(true);
}

function createLogger({ req }: Partial<RequestContainer>): Logger {
const logger = getLogger();
logger.clearAttributes();
if (req) {
logger.setAttribute('ip', req.ip);
logger.setAttribute('request', `${req.method} ${req.url}`);
}

return logger;
}

function createMeter(): Meter {
return getMeter();
}

function createDatabase(): knexpkg.Knex {
const { knex } = knexpkg.default;
const db = knex(buildKnexConfig());
Model.knex(db);
return db;
}

export type LocalsWithContainer = Record<'container', AwilixContainer<RequestContainer & Container>>;

export function initializeContainer(): typeof container {
container.register({
environment: asFunction(createEnvironment).singleton(),
logger: asFunction(createLogger).scoped(),
meter: asFunction(createMeter).singleton(),
db: asFunction(createDatabase).singleton(),
});

container.register('req', asValue(undefined));

return container;
}

export function scopedContainerMiddleware(
req: Request,
res: Response<unknown, LocalsWithContainer>,
next: NextFunction,
): void {
res.locals.container = container.createScope<RequestContainer>();
res.locals.container.register({
req: asValue(req),
});

next();
}
13 changes: 13 additions & 0 deletions src/lib/metrics.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* c8 ignore start */
import { ValueType } from '@opentelemetry/api';
import { getMeter } from '@myrotvorets/otel-utils';

const meter = getMeter();

export const requestDurationHistogram = meter.createHistogram('psbapi.request.duration', {
description: 'Measures the duration of requests.',
unit: 'ms',
valueType: ValueType.DOUBLE,
});

/* c8 ignore stop */
13 changes: 0 additions & 13 deletions src/lib/tracing.mts

This file was deleted.

39 changes: 39 additions & 0 deletions src/middleware/duration.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { RequestHandler } from 'express';
import { hrTime, hrTimeDuration, hrTimeToMilliseconds } from '@opentelemetry/core';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import type { OpenApiRequest } from '@myrotvorets/oav-installer';
import { requestDurationHistogram } from '../lib/metrics.mjs';

export const requestDurationMiddleware: RequestHandler = (req, res, next): void => {
const start = hrTime();
const recordDurarion = (): void => {
res.removeListener('error', recordDurarion);
res.removeListener('finish', recordDurarion);
const end = hrTime();
const duration = hrTimeDuration(start, end);

let route: string | undefined;
if ('openapi' in req && req.openapi) {
const r = req as OpenApiRequest;
route = r.openapi!.openApiRoute || r.openapi!.expressRoute;
}

if (!route && req.route) {
route = (req.route as Record<'path', string>).path;
}

if (!route) {
route = '<unknown>';
}

requestDurationHistogram.record(hrTimeToMilliseconds(duration), {
[SemanticAttributes.HTTP_METHOD]: req.method,
[SemanticAttributes.HTTP_ROUTE]: route,
[SemanticAttributes.HTTP_STATUS_CODE]: res.statusCode,
});
};

res.prependOnceListener('error', recordDurarion);
res.prependOnceListener('finish', recordDurarion);
next();
};
25 changes: 25 additions & 0 deletions src/middleware/logger.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { requestLogger } from '@myrotvorets/express-request-logger';
import type { NextFunction, Request, Response } from 'express';
import type { LocalsWithContainer } from '../lib/container.mjs';

export const loggerMiddleware =
process.env['NODE_ENV'] !== 'test' /* c8 ignore start */
? requestLogger<never, never, never, never, LocalsWithContainer>({
format: '[PSBAPI-identigraf-auth] :method\t:url\t:status :res[content-length]\t:date[iso]\t:total-time',
beforeLogHook: (err, _req, res, line, tokens): string => {
const { status } = tokens;
const message = `Status: ${status} len: ${tokens['res[content-length]']} time: ${tokens['total-time']}`;
const logger = res.locals.container.resolve('logger');
if (+(status ?? '') >= 500 || err) {
logger.error(message);
} else if (+(status ?? '') >= 400) {
logger.warning(message);
} else {
logger.info(message);
}

return line;
},
})
: /* c8 ignore stop */
(_req: Request, _res: Response, next: NextFunction): void => next();
Loading

0 comments on commit fd2a3ac

Please sign in to comment.