diff --git a/src/preset/bun-handler/index.ts b/src/preset/bun-handler/index.ts
index 95dc354..c722bec 100644
--- a/src/preset/bun-handler/index.ts
+++ b/src/preset/bun-handler/index.ts
@@ -11,6 +11,11 @@ import { providePageRender } from '../node/handler/providers';
import { SpecificExtras } from '../node/handler/utils';
import { HandlerProviders } from './providers';
+/**
+ * Возвращает preset с зависимостями для формирования обработчика входящего http-запроса.
+ * @param customize Получит функцию с помощью которой можно переопределить предустановленные провайдеры.
+ * @return Preset.
+ */
export function PresetBunHandler(customize?: PresetTuner) {
const preset = createPreset();
diff --git a/src/preset/bun-handler/providers/index.tsx b/src/preset/bun-handler/providers/index.tsx
index a86f30e..c4f3e9d 100644
--- a/src/preset/bun-handler/providers/index.tsx
+++ b/src/preset/bun-handler/providers/index.tsx
@@ -21,9 +21,10 @@ import {
getFetchExtraAborting,
getFetchLogging,
} from '../../isomorphic/utils';
-import { getForwardedHeaders, getPageResponseFormat } from '../../server/utils';
import { PageAssets } from '../../isomorphic/types';
import { RESPONSE_EVENT_TYPE } from '../../isomorphic/constants';
+import { getPageResponseFormat } from '../../server/utils/get-page-response-format';
+import { getForwardedHeaders } from '../../server/utils/get-forwarded-headers';
export const HandlerProviders = {
handlerMain(resolve: Resolve) {
diff --git a/src/preset/bun/index.ts b/src/preset/bun/index.ts
index 66a28eb..ec41e64 100644
--- a/src/preset/bun/index.ts
+++ b/src/preset/bun/index.ts
@@ -1,4 +1,3 @@
-/* eslint-disable require-jsdoc, jsdoc/require-jsdoc */
import { createPreset } from '../../di';
import { KnownToken } from '../../tokens';
import { PresetTuner } from '../isomorphic';
@@ -6,8 +5,13 @@ import { provideBaseConfig } from '../isomorphic/providers';
import { provideKnownHttpApiHosts, provideSsrBridgeServerSide } from '../node/node/providers';
import { BunProviders } from './providers';
-// @todo возможно стоит переименовать в PresetServer (так как в теории это можно использовать не только в Bun но и в Deno, Node.js)
+/**
+ * Возвращает preset с зависимостями для запуска приложения в Bun.
+ * @param customize Получит функцию с помощью которой можно переопределить предустановленные провайдеры.
+ * @return Preset.
+ */
export function PresetBun(customize?: PresetTuner) {
+ // @todo возможно стоит переименовать в PresetServer (так как в теории это можно использовать не только в Bun но и в Deno, Node.js)
const preset = createPreset();
// config
diff --git a/src/preset/bun/providers/index.ts b/src/preset/bun/providers/index.ts
index 851ecdd..347489c 100644
--- a/src/preset/bun/providers/index.ts
+++ b/src/preset/bun/providers/index.ts
@@ -10,15 +10,13 @@ import { getCurrentHub, init, runWithAsyncContext } from '@sentry/bun';
import { createSentryHandler } from '../../../log/handler/sentry';
import { provideFetch } from '../../isomorphic/providers';
import { ServerHandler, ServerMiddleware } from '../../server/types';
-import {
- applyServerMiddleware,
- healthCheck,
- getServeLogging,
- getServeErrorLogging,
- getServeMeasuring,
-} from '../../server/utils';
-import PromClient from 'prom-client';
import { statsHandler } from '../utils';
+import { getHealthCheck } from '../../server/utils/get-health-check';
+import { getServeLogging } from '../../server/utils/get-serve-logging';
+import { getServeErrorLogging } from '../../server/utils/get-serve-error-logging';
+import { getServeMeasuring } from '../../server/utils/get-serve-measuring';
+import { applyServerMiddleware } from '../../server/utils/apply-server-middleware';
+import PromClient from 'prom-client';
export const BunProviders = {
configSource(): ConfigSource {
@@ -88,7 +86,7 @@ export const BunProviders = {
serviceRoutes(): Array<[string, ServerHandler]> {
return [
// служебные маршруты (без промежуточных слоев)
- ['/healthcheck', healthCheck()],
+ ['/healthcheck', getHealthCheck()],
['/stats', statsHandler()],
];
},
diff --git a/src/preset/isomorphic/utils/__test__/hide-first-id.test.ts b/src/preset/isomorphic/utils/__test__/hide-first-id.test.ts
new file mode 100644
index 0000000..5e0610e
--- /dev/null
+++ b/src/preset/isomorphic/utils/__test__/hide-first-id.test.ts
@@ -0,0 +1,24 @@
+import { hideFirstId } from '../hide-first-id';
+
+describe('hideFirstId', () => {
+ const cases = [
+ {
+ input: '/api/v2/something/123456/some-bff/123456',
+ output: ['/api/v2/something/{id}/some-bff/123456', 123456],
+ },
+ {
+ input: '/api/v2/something/222/some-bff/333',
+ output: ['/api/v2/something/{id}/some-bff/333', 222],
+ },
+ {
+ input: '/api/v2/something/45320/some-bff',
+ output: ['/api/v2/something/{id}/some-bff', 45320],
+ },
+ ];
+
+ it('should replace first id only', () => {
+ cases.forEach(({ input, output }) => {
+ expect(hideFirstId(input)).toEqual(output);
+ });
+ });
+});
diff --git a/src/preset/isomorphic/utils/hide-first-id.ts b/src/preset/isomorphic/utils/hide-first-id.ts
new file mode 100644
index 0000000..e71eefe
--- /dev/null
+++ b/src/preset/isomorphic/utils/hide-first-id.ts
@@ -0,0 +1,14 @@
+/**
+ * Преобразует строку вида:
+ * "/api/v2/something/123456/some-bff/123456"
+ * в строку вида:
+ * "/api/v2/something/{id}/some-bff/123456"
+ * и возвращает кортеж с этой строкой и вырезанным числом в случае если оно найдено.
+ * @param url Url.
+ * @return Кортеж со строкой и результатом поиска числа.
+ */
+export function hideFirstId(url: string): [string, number | undefined] {
+ const found = /\d{2,}/.exec(url);
+
+ return found ? [url.replace(found[0], '{id}'), Number(found[0])] : [url, undefined];
+}
diff --git a/src/preset/node/handler/providers/index.tsx b/src/preset/node/handler/providers/index.tsx
index 414456e..fb5ab8d 100644
--- a/src/preset/node/handler/providers/index.tsx
+++ b/src/preset/node/handler/providers/index.tsx
@@ -28,7 +28,7 @@ import type { ConventionalJson } from '../../../isomorphic/types';
import { Fragment } from 'react';
import { HelmetContext, RegularHelmet, getPageResponseFormat } from '../utils';
import { renderToString } from 'react-dom/server';
-import { getFetchTracing } from '../../../server/utils';
+import { getFetchTracing } from '../../../server/utils/get-fetch-tracing';
/**
* Провайдер главной функции обработчика входящего http-запроса.
diff --git a/src/preset/node/node/utils/http-client/__test__/index.test.ts b/src/preset/node/node/utils/http-client/__test__/index.test.ts
index c63775e..e404ccc 100644
--- a/src/preset/node/node/utils/http-client/__test__/index.test.ts
+++ b/src/preset/node/node/utils/http-client/__test__/index.test.ts
@@ -1,9 +1,9 @@
import { Context } from '@opentelemetry/api';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import { Span, Tracer } from '@opentelemetry/sdk-trace-base';
-import { axiosTracingMiddleware, getRequestInfo, hideFirstId, getForwardedHeaders } from '..';
+import { axiosTracingMiddleware, getRequestInfo, getForwardedHeaders } from '..';
import { BaseConfig } from '../../../../../../config';
-import { Request } from 'express';
+import express from 'express';
describe('axiosTracingMiddleware', () => {
it('should handle success response', async () => {
@@ -144,29 +144,6 @@ describe('getRequestInfo', () => {
});
});
-describe('hideFirstId', () => {
- const cases = [
- {
- input: '/api/v2/something/123456/some-bff/123456',
- output: ['/api/v2/something/{id}/some-bff/123456', 123456],
- },
- {
- input: '/api/v2/something/222/some-bff/333',
- output: ['/api/v2/something/{id}/some-bff/333', 222],
- },
- {
- input: '/api/v2/something/45320/some-bff',
- output: ['/api/v2/something/{id}/some-bff', 45320],
- },
- ];
-
- it('should replace first id only', () => {
- cases.forEach(({ input, output }) => {
- expect(hideFirstId(input)).toEqual(output);
- });
- });
-});
-
describe('getRequestHeaders', () => {
it('should return headers', () => {
const config: BaseConfig = {
@@ -175,7 +152,7 @@ describe('getRequestHeaders', () => {
env: 'test',
};
- const request: Request = {
+ const request: express.Request = {
socket: {
remoteAddress: '127.0.0.1',
},
@@ -210,7 +187,7 @@ describe('getRequestHeaders', () => {
env: 'test',
};
- const request: Request = {
+ const request: express.Request = {
socket: {
remoteAddress: undefined,
},
diff --git a/src/preset/node/node/utils/http-client/index.ts b/src/preset/node/node/utils/http-client/index.ts
index 0490b91..01f6471 100644
--- a/src/preset/node/node/utils/http-client/index.ts
+++ b/src/preset/node/node/utils/http-client/index.ts
@@ -6,6 +6,7 @@ import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import { BaseConfig } from '../../../../../config';
import { getClientIp } from '../http-server';
import { displayUrl } from '../../../../isomorphic/utils';
+import { hideFirstId } from '../../../../isomorphic/utils/hide-first-id';
/**
* Возвращает новый middleware для трассировки исходящих запросов.
@@ -80,21 +81,6 @@ export function getRequestInfo(
};
}
-/**
- * Преобразует строку вида:
- * "/api/v2/something/123456/some-bff/123456"
- * в строку вида:
- * "/api/v2/something/{id}/some-bff/123456"
- * и возвращает кортеж с этой строкой и вырезанным числом в случае если оно найдено.
- * @param url Url.
- * @return Кортеж со строкой и результатом поиска числа.
- */
-export function hideFirstId(url: string): [string, number | undefined] {
- const found = /\d{2,}/.exec(url);
-
- return found ? [url.replace(found[0], '{id}'), Number(found[0])] : [url, undefined];
-}
-
/**
* Формирует заголовки для исходящих запросов с сервера по соглашению.
* @param config Конфиг.
diff --git a/src/preset/server/index.ts b/src/preset/server/index.ts
index 0b1b664..772ce33 100644
--- a/src/preset/server/index.ts
+++ b/src/preset/server/index.ts
@@ -1,2 +1,5 @@
export type { ServerHandler, ServerMiddleware, ServerHandlerContext } from './types';
-export { getClientIp, getForwardedHeaders, getPageResponseFormat, healthCheck } from './utils';
+export { getClientIp } from './utils/get-client-ip';
+export { getForwardedHeaders } from './utils/get-forwarded-headers';
+export { getHealthCheck } from './utils/get-health-check';
+export { getPageResponseFormat } from './utils/get-page-response-format';
diff --git a/src/preset/server/utils/__test__/apply-server-middleware.test.ts b/src/preset/server/utils/__test__/apply-server-middleware.test.ts
new file mode 100644
index 0000000..2b2c3d5
--- /dev/null
+++ b/src/preset/server/utils/__test__/apply-server-middleware.test.ts
@@ -0,0 +1,49 @@
+import { ServerMiddleware } from '../../types';
+import { applyServerMiddleware } from '../apply-server-middleware';
+
+describe('applyServerMiddleware', () => {
+ it('should compose middleware', async () => {
+ const log: string[] = [];
+
+ const foo: ServerMiddleware = async (request, next) => {
+ log.push('');
+ const result = await next(request);
+ log.push('');
+ return result;
+ };
+
+ const bar: ServerMiddleware = async (request, next) => {
+ log.push('');
+ const result = await next(request);
+ log.push('');
+ return result;
+ };
+
+ const baz: ServerMiddleware = async (request, next) => {
+ log.push('');
+ const result = await next(request);
+ log.push('');
+ return result;
+ };
+
+ const enhancer = applyServerMiddleware(foo, bar, baz);
+
+ const handler = enhancer(() => {
+ log.push('');
+ return new Response('Test');
+ });
+
+ await handler(new Request('http://test.com'), { events: new EventTarget() });
+
+ expect(log).toEqual([
+ // expected log
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ ]);
+ });
+});
diff --git a/src/preset/server/utils/__test__/get-client-ip.test.ts b/src/preset/server/utils/__test__/get-client-ip.test.ts
new file mode 100644
index 0000000..3c1d01f
--- /dev/null
+++ b/src/preset/server/utils/__test__/get-client-ip.test.ts
@@ -0,0 +1,23 @@
+import { getClientIp } from '../get-client-ip';
+
+describe('getClientIp', () => {
+ it('should handle "x-client-ip" header', () => {
+ const request = new Request('http://test.com', {
+ headers: {
+ 'x-client-ip': '127.1.2.3',
+ },
+ });
+
+ expect(getClientIp(request)).toBe('127.1.2.3');
+ });
+
+ it('should handle "x-forwarded-for" header', () => {
+ const request = new Request('http://test.com', {
+ headers: {
+ 'x-forwarded-for': '111.222.111.222',
+ },
+ });
+
+ expect(getClientIp(request)).toBe('111.222.111.222');
+ });
+});
diff --git a/src/preset/server/utils/__test__/get-fetch-tracing.test.ts b/src/preset/server/utils/__test__/get-fetch-tracing.test.ts
new file mode 100644
index 0000000..04979c7
--- /dev/null
+++ b/src/preset/server/utils/__test__/get-fetch-tracing.test.ts
@@ -0,0 +1,41 @@
+import { getFetchTracing } from '../get-fetch-tracing';
+
+describe('getFetchTracing', () => {
+ it('should trace fetch stages', async () => {
+ const tracer: any = {
+ startSpan: jest.fn(() => ({
+ setAttributes: jest.fn(),
+ setStatus: jest.fn(),
+ end: jest.fn(),
+ })),
+ };
+ const context: any = {};
+ const middleware = getFetchTracing(tracer, context);
+
+ const request = new Request('http://test.com/api/v2/product/1005002');
+
+ expect(
+ await Promise.resolve(
+ middleware(request, () => Promise.resolve(new Response('OK'))),
+ ).catch(() => 'catch!'),
+ ).not.toBe('catch!');
+ });
+
+ it('should handle error', async () => {
+ const tracer: any = {
+ startSpan: jest.fn(() => ({
+ setAttributes: jest.fn(),
+ setStatus: jest.fn(),
+ end: jest.fn(),
+ })),
+ };
+ const context: any = {};
+ const middleware = getFetchTracing(tracer, context);
+
+ const request = new Request('http://test.com/api/v2/product/1005002');
+
+ expect(
+ await Promise.resolve(middleware(request, () => Promise.reject('FAKE ERROR'))).catch(e => e),
+ ).toBe('FAKE ERROR');
+ });
+});
diff --git a/src/preset/server/utils/__test__/get-forwarded-headers.test.ts b/src/preset/server/utils/__test__/get-forwarded-headers.test.ts
new file mode 100644
index 0000000..4282764
--- /dev/null
+++ b/src/preset/server/utils/__test__/get-forwarded-headers.test.ts
@@ -0,0 +1,38 @@
+import { getForwardedHeaders } from '../get-forwarded-headers';
+
+describe('getForwardedHeaders', () => {
+ it('should contain headers by convention', () => {
+ const config = { appName: 'app_name', appVersion: 'version', env: 'env' };
+ const request = new Request('http://test.com', {
+ headers: {
+ 'x-client-ip': '127.0.0.89',
+ 'simaland-foo': 'hello',
+ 'simaland-bar': 'world',
+ 'simaland-params': JSON.stringify({ testParam: 123 }),
+ },
+ });
+ const result = getForwardedHeaders(config, request);
+
+ expect(result.get('user-agent')).toBe('simaland-app_name/version');
+ expect(result.get('x-client-ip')).toBe('127.0.0.89');
+ expect(result.get('simaland-foo')).toBe('hello');
+ expect(result.get('simaland-bar')).toBe('world');
+ expect(result.get('simaland-params')).toBe(null);
+ });
+
+ it('should not contain x-client-ip when client ip is not defined', () => {
+ const config = { appName: 'app_name', appVersion: 'version', env: 'env' };
+ const request = new Request('http://test.com', {
+ headers: {
+ 'simaland-foo': 'hello',
+ 'simaland-bar': 'world',
+ },
+ });
+ const result = getForwardedHeaders(config, request);
+
+ expect(result.get('user-agent')).toBe('simaland-app_name/version');
+ expect(result.get('x-client-ip')).toBe(null);
+ expect(result.get('simaland-foo')).toBe('hello');
+ expect(result.get('simaland-bar')).toBe('world');
+ });
+});
diff --git a/src/preset/server/utils/__test__/get-health-check.test.ts b/src/preset/server/utils/__test__/get-health-check.test.ts
new file mode 100644
index 0000000..c8ff793
--- /dev/null
+++ b/src/preset/server/utils/__test__/get-health-check.test.ts
@@ -0,0 +1,26 @@
+import { getHealthCheck } from '../get-health-check';
+
+describe('healthCheck', () => {
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it('handler should return response', async () => {
+ const handler = getHealthCheck();
+ const request = new Request('');
+
+ const res1 = await handler(request);
+ expect(res1.headers.get('content-type')).toBe('application/json');
+ expect(await res1.json()).toEqual({ uptime: 0 });
+
+ jest.advanceTimersByTime(1000);
+
+ const res2 = await handler(request);
+ expect(res2.headers.get('content-type')).toBe('application/json');
+ expect(await res2.json()).toEqual({ uptime: 1000 });
+ });
+});
diff --git a/src/preset/server/utils/__test__/get-page-response-format.test.ts b/src/preset/server/utils/__test__/get-page-response-format.test.ts
new file mode 100644
index 0000000..ba3e47e
--- /dev/null
+++ b/src/preset/server/utils/__test__/get-page-response-format.test.ts
@@ -0,0 +1,27 @@
+import { getPageResponseFormat } from '../get-page-response-format';
+
+describe('getPageResponseFormat', () => {
+ it('should return json', () => {
+ const request = new Request('http://test.com', {
+ headers: { accept: 'application/json' },
+ });
+
+ expect(getPageResponseFormat(request)).toBe('json');
+ });
+
+ it('should return html when no accept', () => {
+ const request = new Request('http://test.com', {
+ headers: { accept: '' },
+ });
+
+ expect(getPageResponseFormat(request)).toBe('html');
+ });
+
+ it('should return html', () => {
+ const request = new Request('http://test.com', {
+ headers: { accept: 'text/html' },
+ });
+
+ expect(getPageResponseFormat(request)).toBe('html');
+ });
+});
diff --git a/src/preset/server/utils/__test__/get-serve-error-logging.test.ts b/src/preset/server/utils/__test__/get-serve-error-logging.test.ts
new file mode 100644
index 0000000..ab23cbe
--- /dev/null
+++ b/src/preset/server/utils/__test__/get-serve-error-logging.test.ts
@@ -0,0 +1,35 @@
+import { DetailedError, createLogger } from '../../../../log';
+import { getServeErrorLogging } from '../get-serve-error-logging';
+
+describe('getServeErrorLogging', () => {
+ it('should log error', async () => {
+ const spy = jest.fn();
+ const logger = createLogger();
+
+ logger.subscribe(spy);
+
+ const request = new Request('http://test.ru');
+ const middleware = getServeErrorLogging(logger);
+
+ await Promise.resolve(middleware(request, () => Promise.reject('FAKE ERROR'))).catch(() => {});
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy.mock.calls[0][0]).toEqual({
+ type: 'error',
+ data: new DetailedError('FAKE ERROR', {
+ level: 'error',
+ context: [
+ {
+ key: 'Incoming request details',
+ data: {
+ url: 'http://test.ru',
+ method: 'GET',
+ headers: {},
+ params: {},
+ },
+ },
+ ],
+ }),
+ });
+ });
+});
diff --git a/src/preset/server/utils/__test__/get-serve-logging.test.ts b/src/preset/server/utils/__test__/get-serve-logging.test.ts
new file mode 100644
index 0000000..17d0913
--- /dev/null
+++ b/src/preset/server/utils/__test__/get-serve-logging.test.ts
@@ -0,0 +1,53 @@
+import { createLogger } from '../../../../log';
+import { getServeLogging } from '../get-serve-logging';
+
+describe('getServeLogging', () => {
+ it('should log request and response', async () => {
+ const spy = jest.fn();
+ const logger = createLogger();
+
+ logger.subscribe(spy);
+
+ const middleware = getServeLogging(logger);
+
+ const signal = new EventTarget();
+
+ const next = () =>
+ new Promise(resolve => {
+ signal.addEventListener(
+ 'response',
+ () => {
+ resolve(new Response('OK', { status: 200 }));
+ },
+ { once: true },
+ );
+ });
+
+ middleware(new Request('http://test.ru'), next);
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy.mock.calls[0][0]).toEqual({
+ type: 'info',
+ data: {
+ type: 'http.request[incoming]',
+ route: 'http://test.ru',
+ method: 'GET',
+ remote_ip: null,
+ },
+ });
+
+ signal.dispatchEvent(new Event('response'));
+ await Promise.resolve();
+ expect(spy).toHaveBeenCalledTimes(2);
+ expect(spy.mock.calls[1][0]).toEqual({
+ type: 'info',
+ data: {
+ type: 'http.response[outgoing]',
+ route: 'http://test.ru',
+ method: 'GET',
+ status: 200,
+ remote_ip: null,
+ latency: expect.any(Number),
+ },
+ });
+ });
+});
diff --git a/src/preset/server/utils/__test__/get-serve-measuring.test.ts b/src/preset/server/utils/__test__/get-serve-measuring.test.ts
new file mode 100644
index 0000000..abcae0f
--- /dev/null
+++ b/src/preset/server/utils/__test__/get-serve-measuring.test.ts
@@ -0,0 +1,30 @@
+import { BaseConfig } from '../../../../config';
+import { RESPONSE_EVENT_TYPE } from '../../../isomorphic/constants';
+import { getServeMeasuring } from '../get-serve-measuring';
+
+describe('getServeMeasuring', () => {
+ it('should collect metrics', async () => {
+ const config: BaseConfig = {
+ env: 'test',
+ appName: 'testApp',
+ appVersion: 'testVer',
+ };
+
+ const middleware = getServeMeasuring(config);
+ const events = new EventTarget();
+
+ expect(async () => {
+ await middleware(
+ new Request('http://test.com'),
+ () => {
+ events.dispatchEvent(new Event(RESPONSE_EVENT_TYPE.renderStart));
+ events.dispatchEvent(new Event(RESPONSE_EVENT_TYPE.renderFinish));
+ return Promise.resolve(new Response('OK'));
+ },
+ {
+ events,
+ },
+ );
+ }).not.toThrow();
+ });
+});
diff --git a/src/preset/server/utils/__test__/index.test.ts b/src/preset/server/utils/__test__/index.test.ts
deleted file mode 100644
index 9bb59d0..0000000
--- a/src/preset/server/utils/__test__/index.test.ts
+++ /dev/null
@@ -1,295 +0,0 @@
-import {
- healthCheck,
- getPageResponseFormat,
- getForwardedHeaders,
- getServeLogging,
- getServeErrorLogging,
- getServeMeasuring,
- getFetchTracing,
- applyServerMiddleware,
-} from '..';
-import { BaseConfig } from '../../../../config';
-import { DetailedError, createLogger } from '../../../../log';
-import { RESPONSE_EVENT_TYPE } from '../../../isomorphic/constants';
-import { ServerMiddleware } from '../../types';
-
-describe('getPageResponseFormat', () => {
- it('should return json', () => {
- const request = new Request('http://test.com', {
- headers: { accept: 'application/json' },
- });
-
- expect(getPageResponseFormat(request)).toBe('json');
- });
-
- it('should return html when no accept', () => {
- const request = new Request('http://test.com', {
- headers: { accept: '' },
- });
-
- expect(getPageResponseFormat(request)).toBe('html');
- });
-
- it('should return html', () => {
- const request = new Request('http://test.com', {
- headers: { accept: 'text/html' },
- });
-
- expect(getPageResponseFormat(request)).toBe('html');
- });
-});
-
-describe('getForwardedHeaders', () => {
- it('should contain headers by convention', () => {
- const config = { appName: 'app_name', appVersion: 'version', env: 'env' };
- const request = new Request('http://test.com', {
- headers: {
- 'x-client-ip': '127.0.0.89',
- 'simaland-foo': 'hello',
- 'simaland-bar': 'world',
- 'simaland-params': JSON.stringify({ testParam: 123 }),
- },
- });
- const result = getForwardedHeaders(config, request);
-
- expect(result.get('user-agent')).toBe('simaland-app_name/version');
- expect(result.get('x-client-ip')).toBe('127.0.0.89');
- expect(result.get('simaland-foo')).toBe('hello');
- expect(result.get('simaland-bar')).toBe('world');
- expect(result.get('simaland-params')).toBe(null);
- });
-
- it('should not contain x-client-ip when client ip is not defined', () => {
- const config = { appName: 'app_name', appVersion: 'version', env: 'env' };
- const request = new Request('http://test.com', {
- headers: {
- 'simaland-foo': 'hello',
- 'simaland-bar': 'world',
- },
- });
- const result = getForwardedHeaders(config, request);
-
- expect(result.get('user-agent')).toBe('simaland-app_name/version');
- expect(result.get('x-client-ip')).toBe(null);
- expect(result.get('simaland-foo')).toBe('hello');
- expect(result.get('simaland-bar')).toBe('world');
- });
-});
-
-describe('healthCheck', () => {
- beforeAll(() => {
- jest.useFakeTimers();
- });
-
- afterAll(() => {
- jest.useRealTimers();
- });
-
- it('handler should return response', async () => {
- const handler = healthCheck();
- const request = new Request('');
-
- const res1 = await handler(request);
- expect(res1.headers.get('content-type')).toBe('application/json');
- expect(await res1.json()).toEqual({ uptime: 0 });
-
- jest.advanceTimersByTime(1000);
-
- const res2 = await handler(request);
- expect(res2.headers.get('content-type')).toBe('application/json');
- expect(await res2.json()).toEqual({ uptime: 1000 });
- });
-});
-
-describe('getServeLogging', () => {
- it('should log request and response', async () => {
- const spy = jest.fn();
- const logger = createLogger();
-
- logger.subscribe(spy);
-
- const middleware = getServeLogging(logger);
-
- const signal = new EventTarget();
-
- const next = () =>
- new Promise(resolve => {
- signal.addEventListener(
- 'response',
- () => {
- resolve(new Response('OK', { status: 200 }));
- },
- { once: true },
- );
- });
-
- middleware(new Request('http://test.ru'), next);
- expect(spy).toHaveBeenCalledTimes(1);
- expect(spy.mock.calls[0][0]).toEqual({
- type: 'info',
- data: {
- type: 'http.request[incoming]',
- route: 'http://test.ru',
- method: 'GET',
- remote_ip: null,
- },
- });
-
- signal.dispatchEvent(new Event('response'));
- await Promise.resolve();
- expect(spy).toHaveBeenCalledTimes(2);
- expect(spy.mock.calls[1][0]).toEqual({
- type: 'info',
- data: {
- type: 'http.response[outgoing]',
- route: 'http://test.ru',
- method: 'GET',
- status: 200,
- remote_ip: null,
- latency: expect.any(Number),
- },
- });
- });
-});
-
-describe('getServeErrorLogging', () => {
- it('should log error', async () => {
- const spy = jest.fn();
- const logger = createLogger();
-
- logger.subscribe(spy);
-
- const request = new Request('http://test.ru');
- const middleware = getServeErrorLogging(logger);
-
- await Promise.resolve(middleware(request, () => Promise.reject('FAKE ERROR'))).catch(() => {});
-
- expect(spy).toHaveBeenCalledTimes(1);
- expect(spy.mock.calls[0][0]).toEqual({
- type: 'error',
- data: new DetailedError('FAKE ERROR', {
- level: 'error',
- context: [
- {
- key: 'Incoming request details',
- data: {
- url: 'http://test.ru',
- method: 'GET',
- headers: {},
- params: {},
- },
- },
- ],
- }),
- });
- });
-});
-
-describe('getServeMeasuring', () => {
- it('should collect metrics', async () => {
- const config: BaseConfig = { env: 'test', appName: 'testApp', appVersion: 'testVer' };
- const middleware = getServeMeasuring(config);
- const events = new EventTarget();
-
- expect(async () => {
- await middleware(
- new Request('http://test.com'),
- () => {
- events.dispatchEvent(new Event(RESPONSE_EVENT_TYPE.renderStart));
- events.dispatchEvent(new Event(RESPONSE_EVENT_TYPE.renderFinish));
- return Promise.resolve(new Response('OK'));
- },
- {
- events,
- },
- );
- }).not.toThrow();
- });
-});
-
-describe('getFetchTracing', () => {
- it('should trace fetch stages', async () => {
- const tracer: any = {
- startSpan: jest.fn(() => ({
- setAttributes: jest.fn(),
- setStatus: jest.fn(),
- end: jest.fn(),
- })),
- };
- const context: any = {};
- const middleware = getFetchTracing(tracer, context);
-
- const request = new Request('http://test.com/api/v2/product/1005002');
-
- expect(
- await Promise.resolve(
- middleware(request, () => Promise.resolve(new Response('OK'))),
- ).catch(() => 'catch!'),
- ).not.toBe('catch!');
- });
-
- it('should handle error', async () => {
- const tracer: any = {
- startSpan: jest.fn(() => ({
- setAttributes: jest.fn(),
- setStatus: jest.fn(),
- end: jest.fn(),
- })),
- };
- const context: any = {};
- const middleware = getFetchTracing(tracer, context);
-
- const request = new Request('http://test.com/api/v2/product/1005002');
-
- expect(
- await Promise.resolve(middleware(request, () => Promise.reject('FAKE ERROR'))).catch(e => e),
- ).toBe('FAKE ERROR');
- });
-});
-
-describe('applyServerMiddleware', () => {
- it('should compose middleware', async () => {
- const log: string[] = [];
-
- const foo: ServerMiddleware = async (request, next) => {
- log.push('');
- const result = await next(request);
- log.push('');
- return result;
- };
-
- const bar: ServerMiddleware = async (request, next) => {
- log.push('');
- const result = await next(request);
- log.push('');
- return result;
- };
-
- const baz: ServerMiddleware = async (request, next) => {
- log.push('');
- const result = await next(request);
- log.push('');
- return result;
- };
-
- const enhancer = applyServerMiddleware(foo, bar, baz);
-
- const handler = enhancer(() => {
- log.push('');
- return new Response('Test');
- });
-
- await handler(new Request('http://test.com'), { events: new EventTarget() });
-
- expect(log).toEqual([
- // expected log
- '',
- '',
- '',
- '',
- '',
- '',
- '',
- ]);
- });
-});
diff --git a/src/preset/server/utils/apply-server-middleware.ts b/src/preset/server/utils/apply-server-middleware.ts
new file mode 100644
index 0000000..adcc7ba
--- /dev/null
+++ b/src/preset/server/utils/apply-server-middleware.ts
@@ -0,0 +1,19 @@
+import type { ServerEnhancer, ServerMiddleware } from '../types';
+
+/**
+ * @inheritdoc
+ * @internal
+ */
+export function applyServerMiddleware(...list: ServerMiddleware[]): ServerEnhancer {
+ return handler => {
+ let result = handler;
+
+ for (const item of list.reverse()) {
+ const next = result;
+ result = async (request, context) =>
+ item(request, (req, ctx) => next(req, ctx ?? context), context);
+ }
+
+ return result;
+ };
+}
diff --git a/src/preset/server/utils/get-client-ip.ts b/src/preset/server/utils/get-client-ip.ts
new file mode 100644
index 0000000..4b82428
--- /dev/null
+++ b/src/preset/server/utils/get-client-ip.ts
@@ -0,0 +1,10 @@
+/**
+ * Вернет строку с IP на основе заголовков запроса.
+ * @param request Запрос.
+ * @return IP или null.
+ */
+export function getClientIp(request: Request): string | null {
+ const headerValue = request.headers.get('x-client-ip') || request.headers.get('x-forwarded-for');
+
+ return headerValue;
+}
diff --git a/src/preset/server/utils/get-fetch-tracing.ts b/src/preset/server/utils/get-fetch-tracing.ts
new file mode 100644
index 0000000..d6c0fc5
--- /dev/null
+++ b/src/preset/server/utils/get-fetch-tracing.ts
@@ -0,0 +1,45 @@
+import type { Middleware } from '../../../http';
+import { SpanStatusCode, type Context, type Tracer } from '@opentelemetry/api';
+import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
+import { hideFirstId } from '../../isomorphic/utils/hide-first-id';
+
+/**
+ * Вернет новый промежуточный слой трассировки для fetch.
+ * @param tracer Трассировщик.
+ * @param rootContext Контекст.
+ * @return Промежуточный слой трассировки.
+ */
+export function getFetchTracing(tracer: Tracer, rootContext: Context): Middleware {
+ return async (request, next) => {
+ const [url, foundId] = hideFirstId(new URL(request.url).pathname);
+ const span = tracer.startSpan(`HTTP ${request.method} ${url}`, undefined, rootContext);
+
+ span.setAttributes({
+ [SemanticAttributes.HTTP_URL]: url,
+ [SemanticAttributes.HTTP_METHOD]: request.method,
+ 'request.params': JSON.stringify(new URL(request.url).searchParams),
+ 'request.headers': JSON.stringify(request.headers),
+
+ // если нашли id - добавляем в теги
+ ...(foundId && { 'request.id': foundId }),
+ });
+
+ try {
+ const response = await next(request);
+
+ span.end();
+
+ return response;
+ } catch (error) {
+ span.setStatus({
+ code: SpanStatusCode.ERROR,
+ message: 'HTTP Request failed',
+ });
+
+ span.end();
+
+ // не прячем ошибку
+ throw error;
+ }
+ };
+}
diff --git a/src/preset/server/utils/get-forwarded-headers.ts b/src/preset/server/utils/get-forwarded-headers.ts
new file mode 100644
index 0000000..c743258
--- /dev/null
+++ b/src/preset/server/utils/get-forwarded-headers.ts
@@ -0,0 +1,34 @@
+import type { BaseConfig } from '../../../config';
+import { getClientIp } from './get-client-ip';
+
+/**
+ * Вернет заголовки, которые должны содержаться в исходящих http-запросах при обработке входящего http-запроса.
+ * @param config Конфигурация.
+ * @param request Входящий запрос.
+ * @return Заголовки.
+ */
+export function getForwardedHeaders(config: BaseConfig, request: Request): Headers {
+ const result = new Headers();
+
+ // user agent
+ result.set('User-Agent', `simaland-${config.appName}/${config.appVersion}`);
+
+ // client ip
+ const clientIp = getClientIp(request);
+
+ if (clientIp) {
+ result.set('X-Client-Ip', clientIp);
+ }
+
+ // service headers
+ request.headers.forEach((headerValue, headerName) => {
+ if (
+ headerName.toLowerCase().startsWith('simaland-') &&
+ headerName.toLowerCase() !== 'simaland-params'
+ ) {
+ result.set(headerName, headerValue);
+ }
+ });
+
+ return result;
+}
diff --git a/src/preset/server/utils/get-health-check.ts b/src/preset/server/utils/get-health-check.ts
new file mode 100644
index 0000000..2982164
--- /dev/null
+++ b/src/preset/server/utils/get-health-check.ts
@@ -0,0 +1,17 @@
+import type { Handler } from '../../../http';
+
+/**
+ * Возвращает новый обработчик входящих запросов.
+ * Обработчик возвращает ответ в формате JSON, тело - объект с полем "uptime" типа number.
+ * @return Обработчик.
+ */
+export function getHealthCheck(): Handler {
+ const startTime = Date.now();
+
+ return () =>
+ new Response(JSON.stringify({ uptime: Date.now() - startTime }), {
+ headers: {
+ 'content-type': 'application/json',
+ },
+ });
+}
diff --git a/src/preset/server/utils/get-page-response-format.ts b/src/preset/server/utils/get-page-response-format.ts
new file mode 100644
index 0000000..732e1cf
--- /dev/null
+++ b/src/preset/server/utils/get-page-response-format.ts
@@ -0,0 +1,16 @@
+/**
+ * Определяет формат ответа для страницы (html-верстки).
+ * Вернет "json" - если заголовок запроса "accept" содержит "application/json".
+ * Вернет "html" во всех остальных случаях.
+ * @param request Запрос.
+ * @return Формат.
+ */
+export function getPageResponseFormat(request: Request): 'html' | 'json' {
+ let result: 'html' | 'json' = 'html';
+
+ if ((request.headers.get('accept') || '').toLowerCase().includes('application/json')) {
+ result = 'json';
+ }
+
+ return result;
+}
diff --git a/src/preset/server/utils/get-serve-error-logging.ts b/src/preset/server/utils/get-serve-error-logging.ts
new file mode 100644
index 0000000..ffa53a5
--- /dev/null
+++ b/src/preset/server/utils/get-serve-error-logging.ts
@@ -0,0 +1,31 @@
+import { FetchUtil, log, type Middleware } from '../../../http';
+import { DetailedError, type Logger } from '../../../log';
+
+/**
+ * Возвращает новый промежуточный слой для логирования ошибки при обработке входящего http-запроса.
+ * @param logger Logger.
+ * @return Промежуточный слой.
+ */
+export function getServeErrorLogging(logger: Logger): Middleware {
+ return log({
+ onCatch: ({ request, error }) => {
+ logger.error(
+ new DetailedError(String(error), {
+ level: 'error',
+ context: [
+ {
+ key: 'Incoming request details',
+ data: {
+ url: FetchUtil.withoutParams(request.url),
+ method: request.method,
+ headers: Object.fromEntries(request.headers.entries()),
+ params: Object.fromEntries(new URL(request.url).searchParams.entries()),
+ // @todo data
+ },
+ },
+ ],
+ }),
+ );
+ },
+ });
+}
diff --git a/src/preset/server/utils/get-serve-logging.ts b/src/preset/server/utils/get-serve-logging.ts
new file mode 100644
index 0000000..8c97367
--- /dev/null
+++ b/src/preset/server/utils/get-serve-logging.ts
@@ -0,0 +1,38 @@
+import type { Middleware } from '../../../http';
+import type { Logger } from '../../../log';
+import { toMilliseconds } from '../../../utils';
+import { getClientIp } from './get-client-ip';
+
+/**
+ * Возвращает новый промежуточный слой для логирования обработки входящего http-запроса.
+ * @param logger Logger.
+ * @return Промежуточный слой.
+ */
+export function getServeLogging(logger: Logger): Middleware {
+ return async (request, next) => {
+ const start = process.hrtime.bigint();
+ const clientIp = getClientIp(request);
+
+ logger.info({
+ type: 'http.request[incoming]',
+ route: request.url,
+ method: request.method,
+ remote_ip: clientIp,
+ });
+
+ const response = await next(request);
+
+ const finish = process.hrtime.bigint();
+
+ logger.info({
+ type: 'http.response[outgoing]',
+ route: request.url,
+ method: request.method,
+ status: response.status,
+ remote_ip: clientIp,
+ latency: toMilliseconds(finish - start),
+ });
+
+ return response;
+ };
+}
diff --git a/src/preset/server/utils/get-serve-measuring.ts b/src/preset/server/utils/get-serve-measuring.ts
new file mode 100644
index 0000000..c1ccf9e
--- /dev/null
+++ b/src/preset/server/utils/get-serve-measuring.ts
@@ -0,0 +1,93 @@
+import type { BaseConfig } from '../../../config';
+import type { ServerMiddleware } from '../types';
+import { RESPONSE_EVENT_TYPE } from '../../isomorphic/constants';
+import { toMilliseconds } from '../../../utils';
+import PromClient from 'prom-client';
+
+/**
+ * Возвращает новый промежуточный слой метрки входящего http-запроса.
+ * @param config Конфигурация.
+ * @return Промежуточный слой.
+ */
+export function getServeMeasuring(config: BaseConfig): ServerMiddleware {
+ const ConventionalLabels = {
+ HTTP_RESPONSE: ['version', 'route', 'code', 'method'],
+ SSR: ['version', 'route', 'method'],
+ } as const;
+
+ // @todo скорее всего стоит сделать обертки над классами из PromClient чтобы вызывать у них методы замера а они уже сами вычисляли лейблы и тд
+ const requestCount = new PromClient.Counter({
+ name: 'http_request_count',
+ help: 'Incoming HTTP request count',
+ labelNames: ConventionalLabels.HTTP_RESPONSE,
+ });
+
+ const responseDuration = new PromClient.Histogram({
+ name: 'http_response_duration_ms',
+ help: 'Duration of incoming HTTP requests in ms',
+ labelNames: ConventionalLabels.HTTP_RESPONSE,
+ buckets: [30, 100, 200, 500, 1000, 2500, 5000, 10000],
+ });
+
+ const renderDuration = new PromClient.Histogram({
+ name: 'render_duration_ms',
+ help: 'Duration of SSR ms',
+ labelNames: ConventionalLabels.SSR,
+ buckets: [0.1, 15, 50, 100, 250, 500, 800, 1500],
+ });
+
+ /** @inheritdoc */
+ const getResponseLabels = (req: Request, res: Response) =>
+ ({
+ version: config.appVersion,
+ route: req.url,
+ code: res.status,
+ method: req.method,
+ }) satisfies Record<(typeof ConventionalLabels.HTTP_RESPONSE)[number], string | number>;
+
+ /** @inheritdoc */
+ const getRenderLabels = (request: Request) =>
+ ({
+ version: config.appVersion,
+ method: request.method,
+ route: request.url,
+ }) satisfies Record<(typeof ConventionalLabels.SSR)[number], string | number>;
+
+ return async (request, next, context) => {
+ const responseStart = process.hrtime.bigint();
+ let renderStart = 0n;
+
+ context.events.addEventListener(
+ RESPONSE_EVENT_TYPE.renderStart,
+ () => {
+ renderStart = process.hrtime.bigint();
+ },
+ { once: true },
+ );
+
+ context.events.addEventListener(
+ RESPONSE_EVENT_TYPE.renderFinish,
+ () => {
+ const renderFinish = process.hrtime.bigint();
+
+ renderDuration.observe(
+ getRenderLabels(request),
+ toMilliseconds(renderFinish - renderStart),
+ );
+ },
+ { once: true },
+ );
+
+ const response = await next(request);
+ const responseFinish = process.hrtime.bigint();
+
+ responseDuration.observe(
+ getResponseLabels(request, response),
+ toMilliseconds(responseFinish - responseStart),
+ );
+
+ requestCount.inc(getResponseLabels(request, response), 1);
+
+ return response;
+ };
+}
diff --git a/src/preset/server/utils/index.ts b/src/preset/server/utils/index.ts
deleted file mode 100644
index 38fd48b..0000000
--- a/src/preset/server/utils/index.ts
+++ /dev/null
@@ -1,295 +0,0 @@
-import type { BaseConfig } from '../../../config';
-import { Handler, Middleware, FetchUtil, log } from '../../../http';
-import type { ServerEnhancer, ServerMiddleware } from '../types';
-import { SpanStatusCode, type Context, type Tracer } from '@opentelemetry/api';
-import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
-import { hideFirstId } from '../../node/node/utils/http-client';
-import { DetailedError, Logger } from '../../../log';
-import { toMilliseconds } from '../../../utils';
-import { RESPONSE_EVENT_TYPE } from '../../isomorphic/constants';
-import PromClient from 'prom-client';
-
-/**
- * Определяет формат ответа для страницы (html-верстки).
- * Вернет "json" - если заголовок запроса "accept" содержит "application/json".
- * Вернет "html" во всех остальных случаях.
- * @param request Запрос.
- * @return Формат.
- */
-export function getPageResponseFormat(request: Request): 'html' | 'json' {
- let result: 'html' | 'json' = 'html';
-
- if ((request.headers.get('accept') || '').toLowerCase().includes('application/json')) {
- result = 'json';
- }
-
- return result;
-}
-
-/**
- * Вернет заголовки, которые должны содержаться в исходящих http-запросах при обработке входящего http-запроса.
- * @param config Конфигурация.
- * @param request Входящий запрос.
- * @return Заголовки.
- */
-export function getForwardedHeaders(config: BaseConfig, request: Request): Headers {
- const result = new Headers();
-
- // user agent
- result.set('User-Agent', `simaland-${config.appName}/${config.appVersion}`);
-
- // client ip
- const clientIp = getClientIp(request);
-
- if (clientIp) {
- result.set('X-Client-Ip', clientIp);
- }
-
- // service headers
- request.headers.forEach((headerValue, headerName) => {
- if (
- headerName.toLowerCase().startsWith('simaland-') &&
- headerName.toLowerCase() !== 'simaland-params'
- ) {
- result.set(headerName, headerValue);
- }
- });
-
- return result;
-}
-
-/**
- * Вернет строку с IP на основе заголовков запроса.
- * @param request Запрос.
- * @return IP или null.
- */
-export function getClientIp(request: Request): string | null {
- const headerValue = request.headers.get('x-client-ip') || request.headers.get('x-forwarded-for');
-
- return headerValue;
-}
-
-/**
- * Возвращает новый обработчик входящих запросов.
- * Обработчик возвращает ответ в формате JSON, тело - объект с полем "uptime" типа number.
- * @return Обработчик.
- */
-export function healthCheck(): Handler {
- const startTime = Date.now();
-
- return () =>
- new Response(JSON.stringify({ uptime: Date.now() - startTime }), {
- headers: {
- 'content-type': 'application/json',
- },
- });
-}
-
-/**
- * Возвращает новый промежуточный слой для логирования обработки входящего http-запроса.
- * @param logger Logger.
- * @return Промежуточный слой.
- */
-export function getServeLogging(logger: Logger): Middleware {
- return async (request, next) => {
- const start = process.hrtime.bigint();
-
- logger.info({
- type: 'http.request[incoming]',
- route: request.url,
- method: request.method,
- remote_ip: getClientIp(request),
- });
-
- const response = await next(request);
-
- const finish = process.hrtime.bigint();
-
- logger.info({
- type: 'http.response[outgoing]',
- route: request.url,
- method: request.method,
- status: response.status,
- remote_ip: getClientIp(request),
- latency: toMilliseconds(finish - start),
- });
-
- return response;
- };
-}
-
-/**
- * Возвращает новый промежуточный слой для логирования ошибки при обработке входящего http-запроса.
- * @param logger Logger.
- * @return Промежуточный слой.
- */
-export function getServeErrorLogging(logger: Logger) {
- return log({
- onCatch: ({ request, error }) => {
- logger.error(
- new DetailedError(String(error), {
- level: 'error',
- context: [
- {
- key: 'Incoming request details',
- data: {
- url: FetchUtil.withoutParams(request.url),
- method: request.method,
- headers: Object.fromEntries(request.headers.entries()),
- params: Object.fromEntries(new URL(request.url).searchParams.entries()),
- // @todo data
- },
- },
- ],
- }),
- );
- },
- });
-}
-
-/**
- * Возвращает новый промежуточный слой метрки входящего http-запроса.
- * @param config Конфигурация.
- * @return Промежуточный слой.
- */
-export function getServeMeasuring(config: BaseConfig): ServerMiddleware {
- const ConventionalLabels = {
- HTTP_RESPONSE: ['version', 'route', 'code', 'method'],
- SSR: ['version', 'route', 'method'],
- } as const;
-
- // @todo скорее всего стоит сделать обертки над классами из PromClient чтобы вызывать у них методы замера а они уже сами вычисляли лейблы и тд
- const requestCount = new PromClient.Counter({
- name: 'http_request_count',
- help: 'Incoming HTTP request count',
- labelNames: ConventionalLabels.HTTP_RESPONSE,
- });
-
- const responseDuration = new PromClient.Histogram({
- name: 'http_response_duration_ms',
- help: 'Duration of incoming HTTP requests in ms',
- labelNames: ConventionalLabels.HTTP_RESPONSE,
- buckets: [30, 100, 200, 500, 1000, 2500, 5000, 10000],
- });
-
- const renderDuration = new PromClient.Histogram({
- name: 'render_duration_ms',
- help: 'Duration of SSR ms',
- labelNames: ConventionalLabels.SSR,
- buckets: [0.1, 15, 50, 100, 250, 500, 800, 1500],
- });
-
- /** @inheritdoc */
- const getResponseLabels = (req: Request, res: Response) =>
- ({
- version: config.appVersion,
- route: req.url,
- code: res.status,
- method: req.method,
- }) satisfies Record<(typeof ConventionalLabels.HTTP_RESPONSE)[number], string | number>;
-
- /** @inheritdoc */
- const getRenderLabels = (request: Request) =>
- ({
- version: config.appVersion,
- method: request.method,
- route: request.url,
- }) satisfies Record<(typeof ConventionalLabels.SSR)[number], string | number>;
-
- return async (request, next, context) => {
- const responseStart = process.hrtime.bigint();
- let renderStart = 0n;
-
- context.events.addEventListener(
- RESPONSE_EVENT_TYPE.renderStart,
- () => {
- renderStart = process.hrtime.bigint();
- },
- { once: true },
- );
-
- context.events.addEventListener(
- RESPONSE_EVENT_TYPE.renderFinish,
- () => {
- const renderFinish = process.hrtime.bigint();
-
- renderDuration.observe(
- getRenderLabels(request),
- toMilliseconds(renderFinish - renderStart),
- );
- },
- { once: true },
- );
-
- const response = await next(request);
- const responseFinish = process.hrtime.bigint();
-
- responseDuration.observe(
- getResponseLabels(request, response),
- toMilliseconds(responseFinish - responseStart),
- );
-
- requestCount.inc(getResponseLabels(request, response), 1);
-
- return response;
- };
-}
-
-/**
- * Вернет новый промежуточный слой трассировки для fetch.
- * @param tracer Трассировщик.
- * @param rootContext Контекст.
- * @return Промежуточный слой трассировки.
- */
-export function getFetchTracing(tracer: Tracer, rootContext: Context): Middleware {
- return async (request, next) => {
- const [url, foundId] = hideFirstId(new URL(request.url).pathname);
- const span = tracer.startSpan(`HTTP ${request.method} ${url}`, undefined, rootContext);
-
- span.setAttributes({
- [SemanticAttributes.HTTP_URL]: url,
- [SemanticAttributes.HTTP_METHOD]: request.method,
- 'request.params': JSON.stringify(new URL(request.url).searchParams),
- 'request.headers': JSON.stringify(request.headers),
-
- // если нашли id - добавляем в теги
- ...(foundId && { 'request.id': foundId }),
- });
-
- try {
- const response = await next(request);
-
- span.end();
-
- return response;
- } catch (error) {
- span.setStatus({
- code: SpanStatusCode.ERROR,
- message: 'HTTP Request failed',
- });
-
- span.end();
-
- // не прячем ошибку
- throw error;
- }
- };
-}
-
-/**
- * @inheritdoc
- * @internal
- */
-export function applyServerMiddleware(...list: ServerMiddleware[]): ServerEnhancer {
- return handler => {
- let result = handler;
-
- for (const item of list.reverse()) {
- const next = result;
- result = async (request, context) =>
- item(request, (req, ctx) => next(req, ctx ?? context), context);
- }
-
- return result;
- };
-}