mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Move packages to huly.core
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# Huly Utils
|
||||
|
||||
Huly Utils is a monorepo managed by [Rush](https://rushjs.io/) that contains various packages and tools for engineering purposes. This repository is structured to support scalable development and efficient collaboration.
|
||||
Huly Utils is a monorepo managed by [Rush](https://rushjs.io/) that contains build tools to Huly platform.
|
||||
This repository is structured to support scalable development and efficient collaboration.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
@@ -13,10 +14,7 @@ common/
|
||||
scripts/ # Utility scripts for managing the repository
|
||||
temp/ # Temporary files and caches
|
||||
packages/
|
||||
measurements/ # Measurement utilities
|
||||
measurements-otlp/ # OpenTelemetry-based measurement utilities
|
||||
platform-rig/ # Platform rig utilities
|
||||
postgres-base/ # PostgreSQL base utilities
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
Generated
-1748
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
*
|
||||
!/lib/**
|
||||
!/types/**
|
||||
!/src/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__test__/
|
||||
/types/**/__test__/
|
||||
/src/**/__test__/
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"name": "@hcengineering/measurements-otlp",
|
||||
"entries": [
|
||||
{
|
||||
"version": "0.7.12",
|
||||
"tag": "@hcengineering/measurements-otlp_v0.7.12",
|
||||
"date": "Sat, 11 Oct 2025 19:18:56 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "rollback eslint"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/measurements\" from `^0.7.11` to `0.7.12`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.14` to `0.7.15`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.11",
|
||||
"tag": "@hcengineering/measurements-otlp_v0.7.11",
|
||||
"date": "Sat, 11 Oct 2025 17:58:53 GMT",
|
||||
"comments": {
|
||||
"none": [
|
||||
{
|
||||
"comment": "Add tests"
|
||||
}
|
||||
],
|
||||
"patch": [
|
||||
{
|
||||
"comment": "Fix eslint deps"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/measurements\" from `^0.7.10` to `0.7.11`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.12` to `0.7.13`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.10",
|
||||
"tag": "@hcengineering/measurements-otlp_v0.7.10",
|
||||
"date": "Fri, 10 Oct 2025 12:32:59 GMT",
|
||||
"comments": {
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.10` to `0.7.11`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
# Change Log - @hcengineering/measurements-otlp
|
||||
|
||||
This log was last generated on Sat, 11 Oct 2025 19:18:56 GMT and should not be manually modified.
|
||||
|
||||
## 0.7.12
|
||||
Sat, 11 Oct 2025 19:18:56 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- rollback eslint
|
||||
|
||||
## 0.7.11
|
||||
Sat, 11 Oct 2025 17:58:53 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- Fix eslint deps
|
||||
|
||||
## 0.7.10
|
||||
Fri, 10 Oct 2025 12:32:59 GMT
|
||||
|
||||
_Initial release_
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
{
|
||||
"name": "@hcengineering/measurements-otlp",
|
||||
"version": "0.7.12",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*",
|
||||
"!lib/**/__test__/**",
|
||||
"types/**/*",
|
||||
"!types/**/__test__/**",
|
||||
"src/**/*",
|
||||
"!src/**/__test__/**",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"test": "jest --passWithNoTests --silent",
|
||||
"format": "format src",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "workspace:^0.7.19",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/measurements": "workspace:^0.7.13",
|
||||
"@opentelemetry/sdk-node": "^0.203.0",
|
||||
"@opentelemetry/sdk-logs": "^0.203.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.62.0",
|
||||
"@opentelemetry/resources": "^2.0.1",
|
||||
"@opentelemetry/sdk-trace-node": "^2.0.1",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/core": "^2.0.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
|
||||
"@opentelemetry/otlp-exporter-base": "^0.203.0",
|
||||
"@opentelemetry/id-generator-aws-xray": "^2.0.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.203.0",
|
||||
"@opentelemetry/api-logs": "^0.203.0",
|
||||
"@opentelemetry/sdk-metrics": "^2.0.1"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.utils",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"require": "./lib/index.js",
|
||||
"import": "./lib/index.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
import { newMetrics, type MeasureLogger } from '@hcengineering/measurements'
|
||||
import { context, trace } from '@opentelemetry/api'
|
||||
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
|
||||
import { OpenTelemetryMetricsContext } from '../telemetry'
|
||||
|
||||
describe('telemetry', () => {
|
||||
let tracer: any
|
||||
let provider: NodeTracerProvider
|
||||
|
||||
beforeAll(() => {
|
||||
provider = new NodeTracerProvider()
|
||||
provider.register()
|
||||
tracer = trace.getTracer('test-tracer')
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
void provider.shutdown()
|
||||
})
|
||||
|
||||
describe('OpenTelemetryMetricsContext', () => {
|
||||
const mockLogger: MeasureLogger = {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
close: jest.fn(async () => {}),
|
||||
logOperation: jest.fn()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should create a new context with tracer', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'test',
|
||||
tracer,
|
||||
undefined,
|
||||
undefined,
|
||||
{ op: 'create' },
|
||||
{},
|
||||
metrics,
|
||||
mockLogger
|
||||
)
|
||||
|
||||
expect(ctx).toBeDefined()
|
||||
expect(ctx.metrics).toBe(metrics)
|
||||
expect(ctx.logger).toBe(mockLogger)
|
||||
})
|
||||
|
||||
it('should create child context with span', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'parent',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
metrics,
|
||||
mockLogger
|
||||
)
|
||||
|
||||
const child = ctx.newChild('child', { op: 'child' }, { span: true })
|
||||
|
||||
expect(child).toBeDefined()
|
||||
expect(child.parent).toBe(ctx)
|
||||
})
|
||||
|
||||
it('should create child context without span when span is false', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'parent',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
metrics,
|
||||
mockLogger
|
||||
)
|
||||
|
||||
const child = ctx.newChild('child', { op: 'child' }, { span: false })
|
||||
|
||||
expect(child).toBeDefined()
|
||||
})
|
||||
|
||||
it('should execute async operation with context', async () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'test',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
metrics,
|
||||
mockLogger
|
||||
)
|
||||
|
||||
let executed = false
|
||||
await ctx.with('operation', { op: 'test' }, async () => {
|
||||
executed = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
})
|
||||
|
||||
expect(executed).toBe(true)
|
||||
expect(metrics.measurements.operation).toBeDefined()
|
||||
})
|
||||
|
||||
it('should execute sync operation with context', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'test',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
metrics,
|
||||
mockLogger
|
||||
)
|
||||
|
||||
const result = ctx.withSync('operation', { op: 'test' }, () => {
|
||||
return 42
|
||||
})
|
||||
|
||||
expect(result).toBe(42)
|
||||
expect(metrics.measurements.operation).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle errors in async operations', async () => {
|
||||
const metrics = newMetrics()
|
||||
const span = tracer.startSpan('test-span')
|
||||
const ctx = new OpenTelemetryMetricsContext('test', tracer, context.active(), span, {}, {}, metrics, mockLogger)
|
||||
|
||||
await expect(
|
||||
ctx.with('operation', { op: 'test' }, async () => {
|
||||
throw new Error('Test error')
|
||||
})
|
||||
).rejects.toThrow('Test error')
|
||||
|
||||
expect(metrics.measurements.operation).toBeDefined()
|
||||
})
|
||||
|
||||
it('should measure custom value with meter', () => {
|
||||
const metrics = newMetrics()
|
||||
const mockMeter = {
|
||||
getCounter: jest.fn(() => ({
|
||||
counter: { record: jest.fn() },
|
||||
value: 0
|
||||
}))
|
||||
}
|
||||
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'test',
|
||||
tracer,
|
||||
undefined,
|
||||
undefined,
|
||||
{ op: 'test' },
|
||||
{},
|
||||
metrics,
|
||||
mockLogger,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
mockMeter as any
|
||||
)
|
||||
|
||||
ctx.measure('custom', 100)
|
||||
|
||||
expect(mockMeter.getCounter).toHaveBeenCalledWith('custom')
|
||||
})
|
||||
|
||||
it('should log info with OTLP logger', () => {
|
||||
const metrics = newMetrics()
|
||||
const mockOtlpLogger = {
|
||||
emit: jest.fn()
|
||||
}
|
||||
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'test',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{ op: 'test' },
|
||||
{},
|
||||
metrics,
|
||||
mockLogger,
|
||||
undefined,
|
||||
undefined,
|
||||
mockOtlpLogger as any
|
||||
)
|
||||
|
||||
ctx.info('Test message', { key: 'value' })
|
||||
|
||||
expect(mockOtlpLogger.emit).toHaveBeenCalled()
|
||||
expect(mockLogger.info).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should log error with OTLP logger', () => {
|
||||
const metrics = newMetrics()
|
||||
const mockOtlpLogger = {
|
||||
emit: jest.fn()
|
||||
}
|
||||
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'test',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{ op: 'test' },
|
||||
{},
|
||||
metrics,
|
||||
mockLogger,
|
||||
undefined,
|
||||
undefined,
|
||||
mockOtlpLogger as any
|
||||
)
|
||||
|
||||
ctx.error('Error message', { error: 'details' })
|
||||
|
||||
expect(mockOtlpLogger.emit).toHaveBeenCalled()
|
||||
expect(mockLogger.error).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should log warn with OTLP logger', () => {
|
||||
const metrics = newMetrics()
|
||||
const mockOtlpLogger = {
|
||||
emit: jest.fn()
|
||||
}
|
||||
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'test',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{ op: 'test' },
|
||||
{},
|
||||
metrics,
|
||||
mockLogger,
|
||||
undefined,
|
||||
undefined,
|
||||
mockOtlpLogger as any
|
||||
)
|
||||
|
||||
ctx.warn('Warning message', { warning: 'info' })
|
||||
|
||||
expect(mockOtlpLogger.emit).toHaveBeenCalled()
|
||||
expect(mockLogger.warn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should extract metadata from context', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'test',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
metrics,
|
||||
mockLogger
|
||||
)
|
||||
|
||||
const meta = ctx.extractMeta()
|
||||
|
||||
expect(meta).toBeDefined()
|
||||
expect(typeof meta).toBe('object')
|
||||
})
|
||||
|
||||
it('should get params', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'test',
|
||||
tracer,
|
||||
undefined,
|
||||
undefined,
|
||||
{ op: 'test', method: 'GET' },
|
||||
{},
|
||||
metrics,
|
||||
mockLogger
|
||||
)
|
||||
|
||||
const params = ctx.getParams()
|
||||
|
||||
expect(params).toEqual({ op: 'test', method: 'GET' })
|
||||
})
|
||||
|
||||
it('should share contextData with children', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'parent',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
metrics,
|
||||
mockLogger
|
||||
)
|
||||
ctx.contextData = { userId: '123' }
|
||||
|
||||
const child = ctx.newChild('child', {})
|
||||
|
||||
expect(child.contextData).toBe(ctx.contextData)
|
||||
})
|
||||
|
||||
it('should inherit params when inheritParams is true', async () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'parent',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{ parentKey: 'parentValue' },
|
||||
{},
|
||||
metrics,
|
||||
mockLogger
|
||||
)
|
||||
|
||||
await ctx.with(
|
||||
'child',
|
||||
{ childKey: 'childValue' },
|
||||
async (childCtx) => {
|
||||
expect(childCtx.getParams()).toEqual({ childKey: 'childValue' })
|
||||
},
|
||||
{},
|
||||
{ inheritParams: false }
|
||||
)
|
||||
|
||||
await ctx.with(
|
||||
'child',
|
||||
{ childKey: 'childValue' },
|
||||
async (childCtx) => {
|
||||
expect(childCtx.getParams()).toEqual({
|
||||
parentKey: 'parentValue',
|
||||
childKey: 'childValue'
|
||||
})
|
||||
},
|
||||
{},
|
||||
{ inheritParams: true }
|
||||
)
|
||||
})
|
||||
|
||||
it('should not call end() twice', () => {
|
||||
const metrics = newMetrics()
|
||||
const span = tracer.startSpan('test-span')
|
||||
const spanEndSpy = jest.spyOn(span, 'end')
|
||||
|
||||
const ctx = new OpenTelemetryMetricsContext('test', tracer, context.active(), span, {}, {}, metrics, mockLogger)
|
||||
|
||||
ctx.end()
|
||||
ctx.end() // Second call should be no-op
|
||||
|
||||
expect(spanEndSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should handle null return value', async () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'test',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
metrics,
|
||||
mockLogger
|
||||
)
|
||||
|
||||
const result = await ctx.with('operation', {}, () => null)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should log operation when log option is true', async () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'test',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
metrics,
|
||||
mockLogger
|
||||
)
|
||||
|
||||
await ctx.with(
|
||||
'operation',
|
||||
{ op: 'test' },
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
},
|
||||
{},
|
||||
{ log: true }
|
||||
)
|
||||
|
||||
expect(mockLogger.logOperation).toHaveBeenCalledWith(
|
||||
'operation',
|
||||
expect.any(Number),
|
||||
expect.objectContaining({ op: 'test' })
|
||||
)
|
||||
})
|
||||
|
||||
it('should suppress tracing when span is disable', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'parent',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
metrics,
|
||||
mockLogger
|
||||
)
|
||||
|
||||
const child = ctx.newChild('child', {}, { span: 'disable' })
|
||||
|
||||
expect(child).toBeDefined()
|
||||
})
|
||||
|
||||
it('should set span attributes from params', () => {
|
||||
const metrics = newMetrics()
|
||||
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
'parent',
|
||||
tracer,
|
||||
context.active(),
|
||||
undefined,
|
||||
{},
|
||||
{},
|
||||
metrics,
|
||||
mockLogger
|
||||
)
|
||||
|
||||
// When creating a child with span: true, attributes are set on the child's span
|
||||
const child = ctx.newChild(
|
||||
'child',
|
||||
{ key1: 'value1', key2: 'value2' },
|
||||
{ span: true }
|
||||
) as OpenTelemetryMetricsContext
|
||||
|
||||
// The child should have been created successfully
|
||||
expect(child).toBeDefined()
|
||||
expect(child.parent).toBe(ctx)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1 +0,0 @@
|
||||
export * from './telemetry'
|
||||
@@ -1,632 +0,0 @@
|
||||
import {
|
||||
childMetrics,
|
||||
consoleLogger,
|
||||
MeasureContext,
|
||||
MeasureMetricsContext,
|
||||
newMetrics,
|
||||
noParamsLogger,
|
||||
nullPromise,
|
||||
platformNow,
|
||||
platformNowDiff,
|
||||
updateMeasure,
|
||||
type FullParamsType,
|
||||
type MeasureLogger,
|
||||
type Metrics,
|
||||
type ParamsType,
|
||||
type WithOptions
|
||||
} from '@hcengineering/measurements'
|
||||
import {
|
||||
context,
|
||||
metrics as otelMetrics,
|
||||
propagation,
|
||||
Span,
|
||||
SpanStatusCode,
|
||||
trace,
|
||||
type Context,
|
||||
type Gauge,
|
||||
type Meter,
|
||||
type Tracer
|
||||
} from '@opentelemetry/api'
|
||||
import { Logger, SeverityNumber } from '@opentelemetry/api-logs'
|
||||
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
||||
import { suppressTracing } from '@opentelemetry/core'
|
||||
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'
|
||||
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
|
||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
|
||||
import { AWSXRayIdGenerator } from '@opentelemetry/id-generator-aws-xray'
|
||||
import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base'
|
||||
import { resourceFromAttributes } from '@opentelemetry/resources'
|
||||
import { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs'
|
||||
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
|
||||
import { NodeSDK } from '@opentelemetry/sdk-node'
|
||||
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
|
||||
|
||||
class MetricsContext {
|
||||
counters = new Map<string, { counter: Gauge, value: 0 }>()
|
||||
constructor (readonly meter?: Meter) {}
|
||||
|
||||
getCounter (name: string): { counter: Gauge, value: number } | undefined {
|
||||
if (this.meter === undefined) {
|
||||
return undefined
|
||||
}
|
||||
let counter = this.counters.get(name)
|
||||
if (counter === undefined) {
|
||||
counter = { counter: this.meter.createGauge(name), value: 0 }
|
||||
this.counters.set(name, counter)
|
||||
}
|
||||
return counter
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class OpenTelemetryMetricsContext implements MeasureContext {
|
||||
private readonly name: string
|
||||
private readonly params: ParamsType
|
||||
|
||||
private readonly fullParams: FullParamsType | (() => FullParamsType) = {}
|
||||
logger: MeasureLogger
|
||||
metrics: Metrics
|
||||
id?: string
|
||||
|
||||
st = platformNow()
|
||||
contextData: object = {}
|
||||
isDone = false
|
||||
doneTrace: string = ''
|
||||
|
||||
private done (value?: number, override?: boolean): void {
|
||||
if (!this.isDone) {
|
||||
this.doneTrace = new Error().stack ?? ''
|
||||
this.isDone = true
|
||||
updateMeasure(this.metrics, this.st, this.params, this.fullParams, (spend) => {}, value, override)
|
||||
this.span?.end()
|
||||
}
|
||||
}
|
||||
|
||||
constructor (
|
||||
name: string,
|
||||
readonly tracer: Tracer,
|
||||
readonly context: Context | undefined,
|
||||
readonly span: Span | undefined,
|
||||
params: ParamsType,
|
||||
fullParams: FullParamsType | (() => FullParamsType) = {},
|
||||
metrics: Metrics = newMetrics(),
|
||||
logger?: MeasureLogger,
|
||||
readonly parent?: MeasureContext,
|
||||
readonly logParams?: ParamsType,
|
||||
|
||||
readonly otlpLogger?: Logger,
|
||||
readonly meter?: MetricsContext
|
||||
) {
|
||||
this.name = name
|
||||
this.params = params
|
||||
this.fullParams = fullParams
|
||||
this.metrics = metrics
|
||||
this.metrics.namedParams = this.metrics.namedParams ?? {}
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (this.metrics.namedParams[k] !== v) {
|
||||
this.metrics.namedParams[k] = v
|
||||
} else {
|
||||
this.metrics.namedParams[k] = '*'
|
||||
}
|
||||
}
|
||||
|
||||
this.logger = logger ?? (this.logParams != null ? consoleLogger(this.logParams ?? {}) : noParamsLogger)
|
||||
}
|
||||
|
||||
measure (name: string, value: number, override?: boolean): void {
|
||||
const cnt = this.meter?.getCounter(name)
|
||||
if (cnt !== undefined) {
|
||||
if (cnt.value !== value) {
|
||||
cnt.counter.record(value, this.params)
|
||||
cnt.value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newChild (
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
opt?: {
|
||||
fullParams?: FullParamsType
|
||||
logger?: MeasureLogger
|
||||
span?: WithOptions['span'] // By default true
|
||||
meta?: Record<string, string | number | boolean>
|
||||
}
|
||||
): MeasureContext {
|
||||
let _span: Span | undefined
|
||||
let childContext: Context | undefined
|
||||
if (opt?.span === true || opt?.span === 'inherit') {
|
||||
childContext = opt?.span === 'inherit' ? context.active() : (this.context ?? context.active())
|
||||
|
||||
if (opt.meta !== undefined && Object.keys(opt.meta).length > 0) {
|
||||
// We need to set meta params
|
||||
childContext = propagation.extract(childContext ?? context.active(), opt.meta)
|
||||
}
|
||||
_span = this.tracer.startSpan(name, undefined, childContext)
|
||||
|
||||
const spanParams = [...Object.entries(params)]
|
||||
for (const [k, v] of spanParams) {
|
||||
_span?.setAttribute(k, v as any)
|
||||
}
|
||||
}
|
||||
if (opt?.span === 'disable') {
|
||||
childContext = suppressTracing(childContext ?? context.active())
|
||||
}
|
||||
if (childContext !== undefined && _span !== undefined) {
|
||||
childContext = trace.setSpan(childContext, _span)
|
||||
}
|
||||
|
||||
const result = new OpenTelemetryMetricsContext(
|
||||
name,
|
||||
this.tracer,
|
||||
childContext,
|
||||
_span,
|
||||
params,
|
||||
opt?.fullParams ?? {},
|
||||
childMetrics(this.metrics, [name]),
|
||||
opt?.logger ?? this.logger,
|
||||
this,
|
||||
this.logParams,
|
||||
this.otlpLogger,
|
||||
this.meter
|
||||
)
|
||||
result.id = this.id
|
||||
result.contextData = this.contextData
|
||||
return result
|
||||
}
|
||||
|
||||
extractMeta (): Record<string, string | number | boolean> {
|
||||
const headers: Record<string, string> = {}
|
||||
if (this.context !== undefined) {
|
||||
propagation.inject(this.context, headers)
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
with<T>(
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
op: (ctx: MeasureContext) => T | Promise<T>,
|
||||
fullParams?: ParamsType | (() => FullParamsType),
|
||||
opt?: WithOptions
|
||||
): Promise<T> {
|
||||
const c = this.newChild(name, opt?.inheritParams === true ? { ...this.params, ...params } : params, {
|
||||
fullParams,
|
||||
logger: this.logger,
|
||||
span: opt?.span ?? true,
|
||||
meta: opt?.meta
|
||||
})
|
||||
let needFinally = true
|
||||
try {
|
||||
const _context = (c as OpenTelemetryMetricsContext).context
|
||||
|
||||
const span = (c as OpenTelemetryMetricsContext).span
|
||||
|
||||
const value = _context !== undefined ? context.with(_context, () => op(c)) : op(c)
|
||||
if (value instanceof Promise) {
|
||||
needFinally = false
|
||||
if (span !== undefined) {
|
||||
void value.catch((err) => {
|
||||
span?.recordException(err)
|
||||
span?.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: err.message
|
||||
})
|
||||
})
|
||||
}
|
||||
return value.finally(() => {
|
||||
if (span !== undefined) {
|
||||
const fParams = typeof fullParams === 'function' ? fullParams() : fullParams
|
||||
const spanParams = [...Object.entries(params), ...Object.entries(fParams ?? {})]
|
||||
for (const [k, v] of spanParams) {
|
||||
span?.setAttribute(k, typeof v === 'object' ? JSON.stringify(v) : v)
|
||||
}
|
||||
}
|
||||
c.end()
|
||||
if (opt?.log === true) {
|
||||
this.logger.logOperation(name, platformNowDiff((c as OpenTelemetryMetricsContext).st), {
|
||||
...params,
|
||||
...fullParams
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (value == null) {
|
||||
return nullPromise as Promise<T>
|
||||
}
|
||||
return Promise.resolve(value)
|
||||
}
|
||||
} finally {
|
||||
if (needFinally) {
|
||||
c.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withSync<T>(
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
op: (ctx: MeasureContext) => T,
|
||||
fullParams?: ParamsType | (() => FullParamsType),
|
||||
opt?: WithOptions
|
||||
): T {
|
||||
const c = this.newChild(name, params, { fullParams, logger: this.logger, span: opt?.span ?? true })
|
||||
const _context = (c as OpenTelemetryMetricsContext).context
|
||||
try {
|
||||
return _context !== undefined ? context.with(_context, () => op(c)) : op(c)
|
||||
} finally {
|
||||
c.end()
|
||||
}
|
||||
}
|
||||
|
||||
error (message: string, args?: Record<string, any>): void {
|
||||
if (this.otlpLogger !== undefined) {
|
||||
this.otlpLogger.emit({
|
||||
severityNumber: SeverityNumber.ERROR,
|
||||
severityText: 'error',
|
||||
context: this.context,
|
||||
body: message,
|
||||
attributes: {
|
||||
'service.name': sdkServiceName,
|
||||
...(args ?? {})
|
||||
}
|
||||
})
|
||||
}
|
||||
this.logger.error(message, { ...this.params, ...args, ...(this.logParams ?? {}) })
|
||||
}
|
||||
|
||||
info (message: string, args?: Record<string, any>): void {
|
||||
if (this.otlpLogger !== undefined) {
|
||||
this.otlpLogger.emit({
|
||||
context: this.context,
|
||||
severityNumber: SeverityNumber.INFO,
|
||||
severityText: 'info',
|
||||
body: message,
|
||||
attributes: {
|
||||
'service.name': sdkServiceName,
|
||||
...(args ?? {})
|
||||
}
|
||||
})
|
||||
}
|
||||
this.logger.info(message, { ...this.params, ...args, ...(this.logParams ?? {}) })
|
||||
}
|
||||
|
||||
warn (message: string, args?: Record<string, any>): void {
|
||||
if (this.otlpLogger !== undefined) {
|
||||
this.otlpLogger.emit({
|
||||
severityNumber: SeverityNumber.WARN,
|
||||
severityText: 'warn',
|
||||
context: this.context,
|
||||
body: message,
|
||||
attributes: {
|
||||
'service.name': sdkServiceName,
|
||||
...(args ?? {})
|
||||
}
|
||||
})
|
||||
}
|
||||
this.logger.warn(message, { ...this.params, ...args, ...(this.logParams ?? {}) })
|
||||
}
|
||||
|
||||
end (): void {
|
||||
this.done()
|
||||
}
|
||||
|
||||
getParams (): ParamsType {
|
||||
return this.params
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse W3C baggage header format to Record<string, string>
|
||||
*
|
||||
* W3C baggage format: "key1=value1,key2=value2;property=value,key3=value3"
|
||||
* Returns only the key-value pairs, ignoring properties
|
||||
*/
|
||||
function parseBaggage (baggageHeader?: string): Record<string, string> {
|
||||
if (baggageHeader == null || typeof baggageHeader !== 'string') {
|
||||
return {}
|
||||
}
|
||||
|
||||
const result: Record<string, string> = {}
|
||||
|
||||
// Split by comma to get individual baggage members
|
||||
const members = baggageHeader.split(',')
|
||||
|
||||
for (const member of members) {
|
||||
const trimmedMember = member.trim()
|
||||
if (trimmedMember === '') continue
|
||||
|
||||
// Split by semicolon to separate key=value from properties
|
||||
const parts = trimmedMember.split(';')
|
||||
const keyValuePart = parts[0]?.trim()
|
||||
|
||||
if (keyValuePart == null) continue
|
||||
|
||||
// Split by equals to get key and value
|
||||
const equalIndex = keyValuePart.indexOf('=')
|
||||
if (equalIndex === -1) continue
|
||||
|
||||
const key = keyValuePart.substring(0, equalIndex).trim()
|
||||
const value = keyValuePart.substring(equalIndex + 1).trim()
|
||||
|
||||
if (key != null) {
|
||||
// URL decode the key and value
|
||||
try {
|
||||
const decodedKey = decodeURIComponent(key)
|
||||
const decodedValue = decodeURIComponent(value)
|
||||
result[decodedKey] = decodedValue
|
||||
} catch (error) {
|
||||
// If decoding fails, use the original values
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
let sdk: NodeSDK | undefined
|
||||
let sdkServiceName: string | undefined
|
||||
let sdkServiceVersion: string | undefined
|
||||
let loggerProvider: LoggerProvider | undefined
|
||||
|
||||
export function initOpenTelemetrySDK (serviceName: string, version: string): boolean {
|
||||
if (sdk !== undefined) {
|
||||
return true
|
||||
}
|
||||
process.env.OTEL_SERVICE_NAME = serviceName
|
||||
process.env.OTEL_SERVICE_VERSION = version
|
||||
|
||||
sdkServiceName = serviceName
|
||||
sdkServiceVersion = version
|
||||
const tracesUrl = getTracesUrl()
|
||||
|
||||
if (tracesUrl === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
const traceHeaders = parseTraceExporterHeaders()
|
||||
|
||||
const exporter = new OTLPTraceExporter({
|
||||
url: tracesUrl,
|
||||
headers: traceHeaders,
|
||||
compression:
|
||||
(process.env.OTEL_EXPORTER_OTLP_COMPRESSION as CompressionAlgorithm) ??
|
||||
(process.env.OTEL_EXPORTER_OTLP_TRACES_COMPRESSION as CompressionAlgorithm) ??
|
||||
CompressionAlgorithm.GZIP,
|
||||
keepAlive: true
|
||||
})
|
||||
|
||||
const batchSpanProcessor = new BatchSpanProcessor(exporter, {
|
||||
maxExportBatchSize: parseInt(process.env.OTEL_EXPORTER_OTLP_TRACES_MAX_EXPORT_BATCH_SIZE ?? '1000'),
|
||||
maxQueueSize: parseInt(process.env.OTEL_EXPORTER_OTLP_TRACES_MAX_QUEUE_SIZE ?? '1000')
|
||||
})
|
||||
|
||||
// Logs
|
||||
const logsEndpoint = getLogsUrl()
|
||||
const logHeaders = parseLogsExporterHeaders()
|
||||
const logExporter = new OTLPLogExporter({
|
||||
url: logsEndpoint,
|
||||
headers: logHeaders,
|
||||
compression:
|
||||
(process.env.OTEL_EXPORTER_OTLP_COMPRESSION as CompressionAlgorithm) ??
|
||||
(process.env.OTEL_EXPORTER_OTLP_LOGS_COMPRESSION as CompressionAlgorithm) ??
|
||||
CompressionAlgorithm.GZIP,
|
||||
keepAlive: true
|
||||
})
|
||||
|
||||
const batchLogProcessor = new BatchLogRecordProcessor(logExporter, {
|
||||
maxExportBatchSize: parseInt(process.env.OTEL_EXPORTER_OTLP_LOGS_MAX_EXPORT_BATCH_SIZE ?? '1000'),
|
||||
maxQueueSize: parseInt(process.env.OTEL_EXPORTER_OTLP_LOGS_MAX_QUEUE_SIZE ?? '1000')
|
||||
})
|
||||
|
||||
// Metrics
|
||||
|
||||
const metricsUrl = getMetricsUrl()
|
||||
const metricsHeaders = parseMetricsExporterHeaders()
|
||||
const metricsExporter = new OTLPMetricExporter({
|
||||
url: metricsUrl,
|
||||
headers: metricsHeaders
|
||||
})
|
||||
const metricReader = new PeriodicExportingMetricReader({
|
||||
exporter: metricsExporter,
|
||||
exportIntervalMillis: 15000
|
||||
})
|
||||
|
||||
// SDK
|
||||
|
||||
sdk = new NodeSDK({
|
||||
spanProcessors: [batchSpanProcessor],
|
||||
serviceName,
|
||||
traceExporter: exporter,
|
||||
resource: resourceFromAttributes({
|
||||
'service-name': serviceName,
|
||||
'service-version': version ?? '0.7',
|
||||
'deployment-environment': process.env.OTEL_ENVIRONMENT
|
||||
}),
|
||||
instrumentations: [getNodeAutoInstrumentations()],
|
||||
idGenerator: new AWSXRayIdGenerator(),
|
||||
logRecordProcessors: [batchLogProcessor],
|
||||
metricReader
|
||||
})
|
||||
|
||||
sdk.start()
|
||||
|
||||
loggerProvider = new LoggerProvider({
|
||||
processors: [batchLogProcessor]
|
||||
})
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
sdk
|
||||
?.shutdown()
|
||||
.then(() => {
|
||||
console.log('Tracing terminated')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('Error terminating tracing', error)
|
||||
})
|
||||
.finally(() => process.exit(0))
|
||||
})
|
||||
console.log('Using open telemetry metrics context', {
|
||||
traceEndpoint: tracesUrl,
|
||||
tracerHeadersSet: Array.from(Object.keys(traceHeaders)),
|
||||
logsEndpoint,
|
||||
logHeadersSet: Array.from(Object.keys(logHeaders))
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
export function reportOTELError (error: Error, attributes?: Record<string, any>): void {
|
||||
if (sdkServiceName !== undefined && sdkServiceVersion !== undefined && loggerProvider !== undefined) {
|
||||
const otlpLogger = loggerProvider?.getLogger(sdkServiceName, sdkServiceVersion)
|
||||
otlpLogger?.emit({
|
||||
severityNumber: SeverityNumber.ERROR,
|
||||
severityText: 'error',
|
||||
body: error.message,
|
||||
context: context.active(),
|
||||
attributes: {
|
||||
...attributes,
|
||||
'service.name': sdkServiceName,
|
||||
'service.version': sdkServiceVersion,
|
||||
'error.stack': error.stack
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function reportOTEL (
|
||||
severity: 'info' | 'warning',
|
||||
message: string,
|
||||
time: number,
|
||||
attributes?: Record<string, any>
|
||||
): void {
|
||||
if (sdkServiceName !== undefined && sdkServiceVersion !== undefined && loggerProvider !== undefined) {
|
||||
const otlpLogger = loggerProvider?.getLogger(sdkServiceName, sdkServiceVersion)
|
||||
otlpLogger?.emit({
|
||||
severityNumber: severity === 'info' ? SeverityNumber.INFO : SeverityNumber.WARN,
|
||||
body: message,
|
||||
severityText: severity,
|
||||
timestamp: time,
|
||||
context: context.active(),
|
||||
attributes: {
|
||||
...attributes,
|
||||
'service.name': sdkServiceName,
|
||||
'service.version': sdkServiceVersion
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function createOpenTelemetryMetricsContext (
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
fullParams: FullParamsType | (() => FullParamsType) = {},
|
||||
metrics: Metrics = newMetrics(),
|
||||
logger?: MeasureLogger,
|
||||
version?: string
|
||||
): MeasureContext {
|
||||
if (!initOpenTelemetrySDK(name, version ?? '')) {
|
||||
console.warn('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is not set, OpenTelemetry metrics will not be sent')
|
||||
return new MeasureMetricsContext(name, params, fullParams, metrics, logger)
|
||||
}
|
||||
|
||||
// Traces
|
||||
|
||||
const tracer = trace.getTracer(name)
|
||||
|
||||
const otlpLogger =
|
||||
process.env.OTEL_LOGGER_ENABLED === 'true' ? loggerProvider?.getLogger(sdkServiceName ?? name, version) : undefined
|
||||
|
||||
const meter = otelMetrics.getMeter(name, version)
|
||||
|
||||
const ctx = new OpenTelemetryMetricsContext(
|
||||
name,
|
||||
tracer,
|
||||
undefined,
|
||||
undefined,
|
||||
params,
|
||||
fullParams,
|
||||
metrics,
|
||||
logger,
|
||||
undefined,
|
||||
undefined,
|
||||
otlpLogger,
|
||||
new MetricsContext(meter)
|
||||
)
|
||||
return ctx
|
||||
}
|
||||
function parseTraceExporterHeaders (): Record<string, string> {
|
||||
const headers: Record<string, string> = parseBaggage(process.env.OTEL_EXPORTER_OTLP_HEADERS) ?? {}
|
||||
|
||||
if (process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS !== undefined) {
|
||||
const extraHeaders = parseBaggage(process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS)
|
||||
for (const [key, value] of Object.entries(extraHeaders)) {
|
||||
headers[key] = value
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
function getTracesUrl (): string | undefined {
|
||||
let tracesUrl = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
||||
|
||||
if (tracesUrl !== undefined && !tracesUrl.endsWith('/v1/traces')) {
|
||||
if (tracesUrl.endsWith('/')) {
|
||||
tracesUrl += 'v1/traces'
|
||||
} else {
|
||||
tracesUrl += '/v1/traces'
|
||||
}
|
||||
}
|
||||
return tracesUrl
|
||||
}
|
||||
|
||||
function getLogsUrl (): string | undefined {
|
||||
let logsUrl = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
||||
|
||||
if (logsUrl !== undefined && !logsUrl.endsWith('/v1/logs')) {
|
||||
if (logsUrl.endsWith('/')) {
|
||||
logsUrl += 'v1/logs'
|
||||
} else {
|
||||
logsUrl += '/v1/logs'
|
||||
}
|
||||
}
|
||||
return logsUrl
|
||||
}
|
||||
function parseLogsExporterHeaders (): Record<string, string> {
|
||||
const headers: Record<string, string> = parseBaggage(process.env.OTEL_EXPORTER_OTLP_HEADERS) ?? {}
|
||||
|
||||
if (process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS !== undefined) {
|
||||
const extraHeaders = parseBaggage(process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS)
|
||||
for (const [key, value] of Object.entries(extraHeaders)) {
|
||||
headers[key] = value
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
function getMetricsUrl (): string | undefined {
|
||||
let metricsUrl = process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
||||
|
||||
if (metricsUrl !== undefined && !metricsUrl.endsWith('/v1/metrics')) {
|
||||
if (metricsUrl.endsWith('/')) {
|
||||
metricsUrl += 'v1/metrics'
|
||||
} else {
|
||||
metricsUrl += '/v1/metrics'
|
||||
}
|
||||
}
|
||||
return metricsUrl
|
||||
}
|
||||
function parseMetricsExporterHeaders (): Record<string, string> {
|
||||
const headers: Record<string, string> = parseBaggage(process.env.OTEL_EXPORTER_OTLP_HEADERS) ?? {}
|
||||
|
||||
if (process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS !== undefined) {
|
||||
const extraHeaders = parseBaggage(process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS)
|
||||
for (const [key, value] of Object.entries(extraHeaders)) {
|
||||
headers[key] = value
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
*
|
||||
!/lib/**
|
||||
!/types/**
|
||||
!/src/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__test__/
|
||||
/types/**/__test__/
|
||||
/src/**/__test__/
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"name": "@hcengineering/measurements",
|
||||
"entries": [
|
||||
{
|
||||
"version": "0.7.13",
|
||||
"tag": "@hcengineering/measurements_v0.7.13",
|
||||
"date": "Tue, 14 Oct 2025 03:02:36 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "Remove zero value measure with override values"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.18` to `0.7.19`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.12",
|
||||
"tag": "@hcengineering/measurements_v0.7.12",
|
||||
"date": "Sat, 11 Oct 2025 19:18:56 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "rollback eslint"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.14` to `0.7.15`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.11",
|
||||
"tag": "@hcengineering/measurements_v0.7.11",
|
||||
"date": "Sat, 11 Oct 2025 17:58:53 GMT",
|
||||
"comments": {
|
||||
"none": [
|
||||
{
|
||||
"comment": "Add tests"
|
||||
}
|
||||
],
|
||||
"patch": [
|
||||
{
|
||||
"comment": "Fix eslint deps"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.12` to `0.7.13`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.10",
|
||||
"tag": "@hcengineering/measurements_v0.7.10",
|
||||
"date": "Fri, 10 Oct 2025 12:32:59 GMT",
|
||||
"comments": {
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.10` to `0.7.11`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
# Change Log - @hcengineering/measurements
|
||||
|
||||
This log was last generated on Tue, 14 Oct 2025 03:02:36 GMT and should not be manually modified.
|
||||
|
||||
## 0.7.13
|
||||
Tue, 14 Oct 2025 03:02:36 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- Remove zero value measure with override values
|
||||
|
||||
## 0.7.12
|
||||
Sat, 11 Oct 2025 19:18:56 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- rollback eslint
|
||||
|
||||
## 0.7.11
|
||||
Sat, 11 Oct 2025 17:58:53 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- Fix eslint deps
|
||||
|
||||
## 0.7.10
|
||||
Fri, 10 Oct 2025 12:32:59 GMT
|
||||
|
||||
_Initial release_
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"name": "@hcengineering/measurements",
|
||||
"version": "0.7.13",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*",
|
||||
"!lib/**/__test__/**",
|
||||
"types/**/*",
|
||||
"!types/**/__test__/**",
|
||||
"src/**/*",
|
||||
"!src/**/__test__/**",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"test": "jest --passWithNoTests --silent",
|
||||
"format": "format src",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "workspace:^0.7.19",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/platform",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"require": "./lib/index.js",
|
||||
"import": "./lib/index.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -1,467 +0,0 @@
|
||||
import {
|
||||
MeasureMetricsContext,
|
||||
NoMetricsContext,
|
||||
consoleLogger,
|
||||
noParamsLogger,
|
||||
withContext,
|
||||
setOperationLogProfiling,
|
||||
registerOperationLog,
|
||||
updateOperationLog,
|
||||
addOperation
|
||||
} from '../context'
|
||||
import { newMetrics } from '../metrics'
|
||||
import type { MeasureLogger, MeasureContext } from '../types'
|
||||
|
||||
describe('context', () => {
|
||||
describe('consoleLogger', () => {
|
||||
it('should create a logger with params', () => {
|
||||
const logger = consoleLogger({ service: 'test' })
|
||||
|
||||
expect(logger).toBeDefined()
|
||||
expect(typeof logger.info).toBe('function')
|
||||
expect(typeof logger.error).toBe('function')
|
||||
expect(typeof logger.warn).toBe('function')
|
||||
expect(typeof logger.close).toBe('function')
|
||||
})
|
||||
|
||||
it('should log info messages', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'info').mockImplementation()
|
||||
const logger = consoleLogger({ service: 'test' })
|
||||
|
||||
logger.info('Test message', { key: 'value' })
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled()
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should log error messages', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
||||
const logger = consoleLogger({ service: 'test' })
|
||||
|
||||
logger.error('Error message', { error: 'details' })
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled()
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should log warn messages', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
|
||||
const logger = consoleLogger({ service: 'test' })
|
||||
|
||||
logger.warn('Warning message', { warning: 'info' })
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled()
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should handle errors in params', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
||||
const logger = consoleLogger({})
|
||||
const error = new Error('Test error')
|
||||
|
||||
logger.error('Error occurred', { error })
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled()
|
||||
const call = consoleSpy.mock.calls[0]
|
||||
expect(call[0]).toBe('Error occurred')
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('MeasureMetricsContext', () => {
|
||||
let logger: MeasureLogger
|
||||
|
||||
beforeEach(() => {
|
||||
logger = noParamsLogger
|
||||
})
|
||||
|
||||
it('should create a new context', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', { op: 'create' }, {}, metrics, logger)
|
||||
|
||||
expect(ctx).toBeDefined()
|
||||
expect(ctx.metrics).toBe(metrics)
|
||||
expect(ctx.logger).toBe(logger)
|
||||
})
|
||||
|
||||
it('should measure operation duration', async () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', { op: 'create' }, {}, metrics, logger)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
ctx.end()
|
||||
|
||||
expect(metrics.operations).toBe(1)
|
||||
expect(metrics.value).toBeGreaterThan(40)
|
||||
})
|
||||
|
||||
it('should create child context', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('parent', { op: 'parent' }, {}, metrics, logger)
|
||||
const child = ctx.newChild('child', { op: 'child' })
|
||||
|
||||
expect(child).toBeDefined()
|
||||
expect(child.parent).toBe(ctx)
|
||||
})
|
||||
|
||||
it('should execute async operation with context', async () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', {}, {}, metrics, logger)
|
||||
|
||||
let executed = false
|
||||
await ctx.with('operation', { op: 'test' }, async () => {
|
||||
executed = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
})
|
||||
|
||||
expect(executed).toBe(true)
|
||||
expect(metrics.measurements.operation).toBeDefined()
|
||||
})
|
||||
|
||||
it('should execute sync operation with context', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', {}, {}, metrics, logger)
|
||||
|
||||
const result = ctx.withSync('operation', { op: 'test' }, () => {
|
||||
return 42
|
||||
})
|
||||
|
||||
expect(result).toBe(42)
|
||||
expect(metrics.measurements.operation).toBeDefined()
|
||||
})
|
||||
|
||||
it('should measure custom value', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', {}, {}, metrics, logger)
|
||||
|
||||
ctx.measure('custom', 100)
|
||||
|
||||
expect(metrics.measurements['#custom']).toBeDefined()
|
||||
expect(metrics.measurements['#custom'].value).toBe(100)
|
||||
})
|
||||
|
||||
it('should log info messages', () => {
|
||||
const mockLogger: MeasureLogger = {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
close: jest.fn(async () => {}),
|
||||
logOperation: jest.fn()
|
||||
}
|
||||
|
||||
const ctx = new MeasureMetricsContext('test', { op: 'test' }, {}, newMetrics(), mockLogger)
|
||||
ctx.info('Test message', { key: 'value' })
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
'Test message',
|
||||
expect.objectContaining({ key: 'value', op: 'test' })
|
||||
)
|
||||
})
|
||||
|
||||
it('should log error messages', () => {
|
||||
const mockLogger: MeasureLogger = {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
close: jest.fn(async () => {}),
|
||||
logOperation: jest.fn()
|
||||
}
|
||||
|
||||
const ctx = new MeasureMetricsContext('test', { op: 'test' }, {}, newMetrics(), mockLogger)
|
||||
ctx.error('Error message', { error: 'details' })
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
'Error message',
|
||||
expect.objectContaining({ error: 'details', op: 'test' })
|
||||
)
|
||||
})
|
||||
|
||||
it('should log warn messages', () => {
|
||||
const mockLogger: MeasureLogger = {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
close: jest.fn(async () => {}),
|
||||
logOperation: jest.fn()
|
||||
}
|
||||
|
||||
const ctx = new MeasureMetricsContext('test', { op: 'test' }, {}, newMetrics(), mockLogger)
|
||||
ctx.warn('Warning message', { warning: 'info' })
|
||||
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
'Warning message',
|
||||
expect.objectContaining({ warning: 'info', op: 'test' })
|
||||
)
|
||||
})
|
||||
|
||||
it('should get params', () => {
|
||||
const ctx = new MeasureMetricsContext('test', { op: 'test', method: 'GET' }, {}, newMetrics(), logger)
|
||||
const params = ctx.getParams()
|
||||
|
||||
expect(params).toEqual({ op: 'test', method: 'GET' })
|
||||
})
|
||||
|
||||
it('should share contextData with children', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('parent', {}, {}, metrics, logger)
|
||||
ctx.contextData = { userId: '123' }
|
||||
|
||||
const child = ctx.newChild('child', {})
|
||||
|
||||
expect(child.contextData).toBe(ctx.contextData)
|
||||
})
|
||||
|
||||
it('should handle named parameters', () => {
|
||||
const metrics = newMetrics()
|
||||
const r = new MeasureMetricsContext('test', { method: 'GET' }, {}, metrics, logger)
|
||||
expect(r).toBeDefined()
|
||||
expect(metrics.namedParams.method).toBe('GET')
|
||||
})
|
||||
|
||||
it('should update named parameters on multiple contexts', () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx1 = new MeasureMetricsContext('test1', { method: 'GET' }, {}, metrics, logger)
|
||||
expect(ctx1).toBeDefined()
|
||||
expect(metrics.namedParams.method).toBe('GET')
|
||||
|
||||
// Create another context with different value for same param
|
||||
const ctx2 = new MeasureMetricsContext('test2', { method: 'POST' }, {}, metrics, logger)
|
||||
expect(ctx2).toBeDefined()
|
||||
|
||||
// The second context will see existing value is different, so it will update to '*'
|
||||
// But this happens within the constructor logic
|
||||
expect(metrics.namedParams.method).toBe('POST')
|
||||
})
|
||||
})
|
||||
|
||||
describe('NoMetricsContext', () => {
|
||||
it('should create a no-op context', () => {
|
||||
const ctx = new NoMetricsContext()
|
||||
|
||||
expect(ctx).toBeDefined()
|
||||
expect(ctx.logger).toBeDefined()
|
||||
})
|
||||
|
||||
it('should execute operations without measuring', async () => {
|
||||
const ctx = new NoMetricsContext()
|
||||
|
||||
let executed = false
|
||||
await ctx.with('operation', {}, async () => {
|
||||
executed = true
|
||||
})
|
||||
|
||||
expect(executed).toBe(true)
|
||||
})
|
||||
|
||||
it('should create child contexts', () => {
|
||||
const ctx = new NoMetricsContext()
|
||||
const child = ctx.newChild('child', {})
|
||||
|
||||
expect(child).toBeDefined()
|
||||
expect(child).toBeInstanceOf(NoMetricsContext)
|
||||
})
|
||||
|
||||
it('should handle measure calls without error', () => {
|
||||
const ctx = new NoMetricsContext()
|
||||
|
||||
expect(() => {
|
||||
ctx.measure('test', 100)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle end calls without error', () => {
|
||||
const ctx = new NoMetricsContext()
|
||||
|
||||
expect(() => {
|
||||
ctx.end()
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should return empty params', () => {
|
||||
const ctx = new NoMetricsContext()
|
||||
const params = ctx.getParams()
|
||||
|
||||
expect(params).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('withContext decorator', () => {
|
||||
it('should wrap method with context', async () => {
|
||||
class TestClass {
|
||||
@withContext('testOperation', { service: 'test' })
|
||||
async testMethod (ctx: MeasureContext, value: number): Promise<number> {
|
||||
return value * 2
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new TestClass()
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('root', {}, {}, metrics, noParamsLogger)
|
||||
|
||||
const result = await instance.testMethod(ctx, 21)
|
||||
|
||||
expect(result).toBe(42)
|
||||
expect(metrics.measurements.testOperation).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('operation log profiling', () => {
|
||||
beforeEach(() => {
|
||||
setOperationLogProfiling(false)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
setOperationLogProfiling(false)
|
||||
})
|
||||
|
||||
it('should register operation log when profiling enabled', () => {
|
||||
setOperationLogProfiling(true)
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger)
|
||||
|
||||
const { opLogMetrics, op } = registerOperationLog(ctx)
|
||||
|
||||
expect(opLogMetrics).toBe(metrics)
|
||||
expect(op).toBeDefined()
|
||||
expect(ctx.id).toBeDefined()
|
||||
})
|
||||
|
||||
it('should not register operation log when profiling disabled', () => {
|
||||
setOperationLogProfiling(false)
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger)
|
||||
|
||||
const { opLogMetrics, op } = registerOperationLog(ctx)
|
||||
|
||||
expect(opLogMetrics).toBeUndefined()
|
||||
expect(op).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should update operation log', () => {
|
||||
setOperationLogProfiling(true)
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger)
|
||||
|
||||
const { opLogMetrics, op } = registerOperationLog(ctx)
|
||||
updateOperationLog(opLogMetrics, op)
|
||||
|
||||
expect(op?.end).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should add operation to log', async () => {
|
||||
setOperationLogProfiling(true)
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger)
|
||||
|
||||
registerOperationLog(ctx)
|
||||
|
||||
let executed = false
|
||||
await addOperation(ctx, 'asyncOp', { op: 'test' }, async () => {
|
||||
executed = true
|
||||
})
|
||||
|
||||
expect(executed).toBe(true)
|
||||
expect(metrics.opLog).toBeDefined()
|
||||
})
|
||||
|
||||
it('should limit operation log entries', () => {
|
||||
setOperationLogProfiling(true)
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger)
|
||||
|
||||
registerOperationLog(ctx)
|
||||
|
||||
// Create many operation log entries
|
||||
if (metrics.opLog != null && ctx.id != null) {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
metrics.opLog[ctx.id].ops.push({
|
||||
op: `test${i}`,
|
||||
start: i,
|
||||
end: i + 1,
|
||||
params: {}
|
||||
})
|
||||
}
|
||||
|
||||
const op = metrics.opLog[ctx.id]
|
||||
updateOperationLog(metrics, op)
|
||||
|
||||
expect(Object.keys(metrics.opLog).length).toBeLessThanOrEqual(31)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractMeta', () => {
|
||||
it('should return empty metadata', () => {
|
||||
const ctx = new MeasureMetricsContext('test', {}, {}, newMetrics(), noParamsLogger)
|
||||
const meta = ctx.extractMeta()
|
||||
|
||||
expect(meta).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('async operations', () => {
|
||||
it('should handle promise rejection', async () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger)
|
||||
|
||||
await expect(
|
||||
ctx.with('operation', { op: 'test' }, async () => {
|
||||
throw new Error('Test error')
|
||||
})
|
||||
).rejects.toThrow('Test error')
|
||||
|
||||
expect(metrics.measurements.operation).toBeDefined()
|
||||
})
|
||||
|
||||
it('should return null promise for null sync result', async () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger)
|
||||
|
||||
const result = await ctx.with('operation', { op: 'test' }, () => {
|
||||
return null
|
||||
})
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle sync return value', async () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', {}, {}, metrics, noParamsLogger)
|
||||
|
||||
const result = await ctx.with('operation', { op: 'test' }, () => {
|
||||
return 42
|
||||
})
|
||||
|
||||
expect(result).toBe(42)
|
||||
})
|
||||
|
||||
it('should log operation when log option is true', async () => {
|
||||
const mockLogger: MeasureLogger = {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
close: jest.fn(async () => {}),
|
||||
logOperation: jest.fn()
|
||||
}
|
||||
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('test', {}, {}, metrics, mockLogger)
|
||||
|
||||
await ctx.with(
|
||||
'operation',
|
||||
{ op: 'test' },
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
},
|
||||
{},
|
||||
{ log: true }
|
||||
)
|
||||
|
||||
expect(mockLogger.logOperation).toHaveBeenCalledWith(
|
||||
'operation',
|
||||
expect.any(Number),
|
||||
expect.objectContaining({ op: 'test' })
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,47 +0,0 @@
|
||||
import { platformNow, platformNowDiff } from '../index'
|
||||
|
||||
describe('index', () => {
|
||||
describe('platformNow', () => {
|
||||
it('should return a positive number', () => {
|
||||
const now = platformNow()
|
||||
expect(typeof now).toBe('number')
|
||||
expect(now).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should return increasing values over time', async () => {
|
||||
const first = platformNow()
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
const second = platformNow()
|
||||
expect(second).toBeGreaterThan(first)
|
||||
})
|
||||
})
|
||||
|
||||
describe('platformNowDiff', () => {
|
||||
it('should calculate the difference between timestamps', async () => {
|
||||
const start = platformNow()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
const diff = platformNowDiff(start)
|
||||
|
||||
expect(diff).toBeGreaterThan(40)
|
||||
expect(diff).toBeLessThan(100)
|
||||
})
|
||||
|
||||
it('should round to 2 decimal places', async () => {
|
||||
const start = platformNow()
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
const diff = platformNowDiff(start)
|
||||
|
||||
// Check that it has at most 2 decimal places
|
||||
const decimalPlaces = (diff.toString().split('.')[1] ?? '').length
|
||||
expect(decimalPlaces).toBeLessThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('should handle very small differences', () => {
|
||||
const start = platformNow()
|
||||
const diff = platformNowDiff(start)
|
||||
|
||||
expect(diff).toBeGreaterThanOrEqual(0)
|
||||
expect(typeof diff).toBe('number')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,297 +0,0 @@
|
||||
import {
|
||||
newMetrics,
|
||||
measure,
|
||||
childMetrics,
|
||||
metricsAggregate,
|
||||
metricsToString,
|
||||
metricsToJson,
|
||||
metricsToRows,
|
||||
updateMeasure
|
||||
} from '../metrics'
|
||||
import { platformNow } from '../index'
|
||||
|
||||
describe('metrics', () => {
|
||||
describe('newMetrics', () => {
|
||||
it('should create a new metrics object with default values', () => {
|
||||
const metrics = newMetrics()
|
||||
|
||||
expect(metrics.operations).toBe(0)
|
||||
expect(metrics.value).toBe(0)
|
||||
expect(metrics.measurements).toEqual({})
|
||||
expect(metrics.params).toEqual({})
|
||||
expect(metrics.namedParams).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('measure', () => {
|
||||
it('should measure operation duration', async () => {
|
||||
const metrics = newMetrics()
|
||||
const done = measure(metrics, { operation: 'test' })
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
done()
|
||||
|
||||
expect(metrics.operations).toBe(1)
|
||||
expect(metrics.value).toBeGreaterThan(40)
|
||||
})
|
||||
|
||||
it('should call endOp callback with duration', async () => {
|
||||
const metrics = newMetrics()
|
||||
let capturedSpend = 0
|
||||
const done = measure(metrics, { operation: 'test' }, {}, (spend) => {
|
||||
capturedSpend = spend
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
done()
|
||||
|
||||
expect(capturedSpend).toBeGreaterThan(40)
|
||||
})
|
||||
|
||||
it('should handle multiple operations', async () => {
|
||||
const metrics = newMetrics()
|
||||
|
||||
const done1 = measure(metrics, { operation: 'test' })
|
||||
await new Promise((resolve) => setTimeout(resolve, 20))
|
||||
done1()
|
||||
|
||||
const done2 = measure(metrics, { operation: 'test' })
|
||||
await new Promise((resolve) => setTimeout(resolve, 20))
|
||||
done2()
|
||||
|
||||
expect(metrics.operations).toBe(2)
|
||||
expect(metrics.value).toBeGreaterThan(30)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateMeasure', () => {
|
||||
it('should update metrics with custom value', () => {
|
||||
const metrics = newMetrics()
|
||||
const st = platformNow()
|
||||
|
||||
updateMeasure(metrics, st, { op: 'test' }, {}, undefined, 100)
|
||||
|
||||
expect(metrics.operations).toBe(1)
|
||||
expect(metrics.value).toBe(100)
|
||||
})
|
||||
|
||||
it('should override operations when override is true', () => {
|
||||
const metrics = newMetrics()
|
||||
const st = platformNow()
|
||||
|
||||
// First call without override - accumulates value and increments operations
|
||||
updateMeasure(metrics, st, { op: 'test' }, {}, undefined, 50, false)
|
||||
expect(metrics.value).toBe(50)
|
||||
expect(metrics.operations).toBe(1)
|
||||
|
||||
// Second call with override - sets operations, doesn't add to value
|
||||
updateMeasure(metrics, st, { op: 'test' }, {}, undefined, 100, true)
|
||||
|
||||
expect(metrics.operations).toBe(100) // overridden
|
||||
expect(metrics.value).toBe(50) // not changed when override=true
|
||||
})
|
||||
|
||||
it('should track parameters', () => {
|
||||
const metrics = newMetrics()
|
||||
const st = platformNow()
|
||||
|
||||
updateMeasure(metrics, st, { method: 'GET' }, {}, undefined, 100)
|
||||
updateMeasure(metrics, st, { method: 'POST' }, {}, undefined, 200)
|
||||
|
||||
expect(metrics.params.method).toBeDefined()
|
||||
expect(metrics.params.method.GET).toBeDefined()
|
||||
expect(metrics.params.method.POST).toBeDefined()
|
||||
expect(metrics.params.method.GET.value).toBe(100)
|
||||
expect(metrics.params.method.POST.value).toBe(200)
|
||||
})
|
||||
|
||||
it('should handle multiple parameters as counters', () => {
|
||||
const metrics = newMetrics()
|
||||
const st = platformNow()
|
||||
|
||||
updateMeasure(metrics, st, { method: 'GET', status: '200' }, {}, undefined, 100)
|
||||
updateMeasure(metrics, st, { method: 'GET', status: '404' }, {}, undefined, 50)
|
||||
|
||||
expect(metrics.params.method.GET).toBeDefined()
|
||||
expect(metrics.params.method.GET.topResult).toBeDefined()
|
||||
expect(metrics.params.method.GET.topResult?.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should update top results', () => {
|
||||
const metrics = newMetrics()
|
||||
const st = platformNow()
|
||||
|
||||
updateMeasure(metrics, st, {}, { request: 'slow' }, undefined, 100)
|
||||
updateMeasure(metrics, st, {}, { request: 'fast' }, undefined, 10)
|
||||
|
||||
expect(metrics.topResult).toBeDefined()
|
||||
expect(metrics.topResult?.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('childMetrics', () => {
|
||||
it('should create child metrics in hierarchy', () => {
|
||||
const root = newMetrics()
|
||||
const child = childMetrics(root, ['level1', 'level2'])
|
||||
|
||||
expect(root.measurements.level1).toBeDefined()
|
||||
expect(root.measurements.level1.measurements.level2).toBeDefined()
|
||||
expect(child).toBe(root.measurements.level1.measurements.level2)
|
||||
})
|
||||
|
||||
it('should reuse existing child metrics', () => {
|
||||
const root = newMetrics()
|
||||
const child1 = childMetrics(root, ['level1'])
|
||||
const child2 = childMetrics(root, ['level1'])
|
||||
|
||||
expect(child1).toBe(child2)
|
||||
})
|
||||
|
||||
it('should create nested paths', () => {
|
||||
const root = newMetrics()
|
||||
childMetrics(root, ['api', 'users', 'create'])
|
||||
|
||||
expect(root.measurements.api).toBeDefined()
|
||||
expect(root.measurements.api.measurements.users).toBeDefined()
|
||||
expect(root.measurements.api.measurements.users.measurements.create).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('metricsAggregate', () => {
|
||||
it('should aggregate metrics', () => {
|
||||
const metrics = newMetrics()
|
||||
metrics.value = 100
|
||||
metrics.operations = 10
|
||||
|
||||
const child1 = childMetrics(metrics, ['child1'])
|
||||
child1.value = 50
|
||||
child1.operations = 5
|
||||
|
||||
const child2 = childMetrics(metrics, ['child2'])
|
||||
child2.value = 30
|
||||
child2.operations = 3
|
||||
|
||||
const aggregated = metricsAggregate(metrics)
|
||||
|
||||
expect(aggregated.value).toBe(80) // child1 + child2
|
||||
expect(aggregated.operations).toBe(10)
|
||||
})
|
||||
|
||||
it('should limit number of child metrics', () => {
|
||||
const metrics = newMetrics()
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const child = childMetrics(metrics, [`child${i}`])
|
||||
child.value = i * 10
|
||||
}
|
||||
|
||||
const aggregated = metricsAggregate(metrics, 3)
|
||||
|
||||
expect(Object.keys(aggregated.measurements).length).toBe(3)
|
||||
})
|
||||
|
||||
it('should filter out metrics starting with #', () => {
|
||||
const metrics = newMetrics()
|
||||
|
||||
const child1 = childMetrics(metrics, ['normal'])
|
||||
child1.value = 50
|
||||
|
||||
const child2 = childMetrics(metrics, ['#internal'])
|
||||
child2.value = 30
|
||||
|
||||
const aggregated = metricsAggregate(metrics)
|
||||
|
||||
expect(aggregated.value).toBe(50) // only 'normal' counted
|
||||
})
|
||||
})
|
||||
|
||||
describe('metricsToString', () => {
|
||||
it('should convert metrics to string', () => {
|
||||
const metrics = newMetrics()
|
||||
metrics.value = 100
|
||||
metrics.operations = 10
|
||||
|
||||
const str = metricsToString(metrics, 'TestMetrics', 50)
|
||||
|
||||
expect(str).toContain('TestMetrics')
|
||||
expect(str).toContain('100')
|
||||
expect(str).toContain('10')
|
||||
})
|
||||
|
||||
it('should include child metrics in string', () => {
|
||||
const metrics = newMetrics()
|
||||
const child = childMetrics(metrics, ['operation'])
|
||||
child.value = 50
|
||||
child.operations = 5
|
||||
|
||||
const str = metricsToString(metrics, 'TestMetrics', 50)
|
||||
|
||||
expect(str).toContain('operation')
|
||||
})
|
||||
})
|
||||
|
||||
describe('metricsToJson', () => {
|
||||
it('should convert metrics to JSON', () => {
|
||||
const metrics = newMetrics()
|
||||
metrics.value = 100
|
||||
metrics.operations = 10
|
||||
|
||||
const json = metricsToJson(metrics)
|
||||
|
||||
// aggregated value is the total value when no children
|
||||
expect(json.$total).toBe(100)
|
||||
expect(json.$ops).toBe(10)
|
||||
})
|
||||
|
||||
it('should include child metrics in JSON', () => {
|
||||
const metrics = newMetrics()
|
||||
const child = childMetrics(metrics, ['operation'])
|
||||
child.value = 50
|
||||
child.operations = 5
|
||||
|
||||
const json = metricsToJson(metrics)
|
||||
|
||||
expect(json).toBeDefined()
|
||||
const keys = Object.keys(json)
|
||||
expect(keys.some((k) => k.includes('operation'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('metricsToRows', () => {
|
||||
it('should convert metrics to rows', () => {
|
||||
const metrics = newMetrics()
|
||||
metrics.value = 100
|
||||
metrics.operations = 10
|
||||
|
||||
const rows = metricsToRows(metrics, 'TestMetrics')
|
||||
|
||||
expect(Array.isArray(rows)).toBe(true)
|
||||
expect(rows.length).toBeGreaterThan(0)
|
||||
expect(rows[0]).toContain('TestMetrics')
|
||||
})
|
||||
|
||||
it('should include child metrics in rows', () => {
|
||||
const metrics = newMetrics()
|
||||
const child = childMetrics(metrics, ['operation'])
|
||||
child.value = 50
|
||||
child.operations = 5
|
||||
|
||||
const rows = metricsToRows(metrics, 'TestMetrics')
|
||||
|
||||
expect(rows.length).toBeGreaterThan(1)
|
||||
expect(rows.some((row) => row.includes('operation'))).toBe(true)
|
||||
})
|
||||
|
||||
it('should properly format row values', () => {
|
||||
const metrics = newMetrics()
|
||||
metrics.value = 100
|
||||
metrics.operations = 10
|
||||
|
||||
const rows = metricsToRows(metrics, 'TestMetrics')
|
||||
|
||||
expect(rows[0]).toHaveLength(5) // offset, name, avg, total, ops
|
||||
expect(typeof rows[0][0]).toBe('number') // offset
|
||||
expect(typeof rows[0][1]).toBe('string') // name
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,260 +0,0 @@
|
||||
import { MeasureMetricsContext, NoMetricsContext, noParamsLogger } from '../context'
|
||||
import { newMetrics, metricsAggregate } from '../metrics'
|
||||
import type { MeasureContext } from '../types'
|
||||
|
||||
describe('performance', () => {
|
||||
describe('overhead measurement', () => {
|
||||
// Reduced iterations to fit within 10 seconds total test time
|
||||
const iterations = 100
|
||||
const depth = 5
|
||||
|
||||
it('should measure overhead of with() vs raw execution', async () => {
|
||||
// Baseline: raw execution without measurement
|
||||
const baselineStart = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await simulateWork(1)
|
||||
}
|
||||
const baselineTime = performance.now() - baselineStart
|
||||
|
||||
// With measurement context
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('root', {}, {}, metrics, noParamsLogger)
|
||||
|
||||
const measuredStart = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await ctx.with('operation', { iteration: i }, async () => {
|
||||
await simulateWork(1)
|
||||
})
|
||||
}
|
||||
const measuredTime = performance.now() - measuredStart
|
||||
|
||||
const overhead = measuredTime - baselineTime
|
||||
const overheadPercentage = (overhead / baselineTime) * 100
|
||||
|
||||
console.log(`\n📊 Overhead Analysis (${iterations} iterations):`)
|
||||
console.log(` Baseline time: ${baselineTime.toFixed(2)}ms`)
|
||||
console.log(` Measured time: ${measuredTime.toFixed(2)}ms`)
|
||||
console.log(` Overhead: ${overhead.toFixed(2)}ms (${overheadPercentage.toFixed(2)}%)`)
|
||||
console.log(` Per operation: ${(overhead / iterations).toFixed(4)}ms`)
|
||||
|
||||
expect(metrics.measurements.operation).toBeDefined()
|
||||
expect(metrics.measurements.operation.operations).toBe(iterations)
|
||||
|
||||
// Overhead should be reasonable (typically < 50% for simple operations)
|
||||
// This is informational rather than a strict assertion
|
||||
expect(overheadPercentage).toBeLessThan(200)
|
||||
})
|
||||
|
||||
it('should measure overhead with deep nested contexts', async () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('root', {}, {}, metrics, noParamsLogger)
|
||||
|
||||
// Baseline: single level
|
||||
const singleLevelStart = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await ctx.with('shallow', {}, async () => {
|
||||
await simulateWork(1)
|
||||
})
|
||||
}
|
||||
const singleLevelTime = performance.now() - singleLevelStart
|
||||
|
||||
// Deep nesting
|
||||
const deepStart = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await deepNestedExecution(ctx, depth, 1)
|
||||
}
|
||||
const deepTime = performance.now() - deepStart
|
||||
|
||||
const nestingOverhead = deepTime - singleLevelTime
|
||||
const overheadPerLevel = nestingOverhead / (iterations * depth)
|
||||
|
||||
console.log(`\n📊 Deep Nesting Overhead (${iterations} iterations, depth=${depth}):`)
|
||||
console.log(` Single level: ${singleLevelTime.toFixed(2)}ms`)
|
||||
console.log(` Deep nested: ${deepTime.toFixed(2)}ms`)
|
||||
console.log(` Nesting overhead: ${nestingOverhead.toFixed(2)}ms`)
|
||||
console.log(` Per level: ${overheadPerLevel.toFixed(4)}ms`)
|
||||
|
||||
expect(metrics.measurements.shallow).toBeDefined()
|
||||
expect(metrics.measurements.level0).toBeDefined()
|
||||
})
|
||||
|
||||
it('should measure overhead with complex parameter tracking', async () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('root', {}, {}, metrics, noParamsLogger)
|
||||
|
||||
// Simple params
|
||||
const simpleStart = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await ctx.with('simple', { id: i }, async () => {
|
||||
await simulateWork(1)
|
||||
})
|
||||
}
|
||||
const simpleTime = performance.now() - simpleStart
|
||||
|
||||
// Complex params with multiple tracked values
|
||||
const complexStart = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await ctx.with(
|
||||
'complex',
|
||||
{
|
||||
method: i % 3 === 0 ? 'GET' : i % 3 === 1 ? 'POST' : 'PUT',
|
||||
status: i % 2 === 0 ? 200 : 404,
|
||||
cached: i % 4 === 0
|
||||
},
|
||||
async () => {
|
||||
await simulateWork(1)
|
||||
},
|
||||
{
|
||||
userId: `user_${i % 10}`,
|
||||
endpoint: `/api/v1/resource/${i % 5}`,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
)
|
||||
}
|
||||
const complexTime = performance.now() - complexStart
|
||||
|
||||
const paramOverhead = complexTime - simpleTime
|
||||
|
||||
console.log(`\n📊 Parameter Tracking Overhead (${iterations} iterations):`)
|
||||
console.log(` Simple params: ${simpleTime.toFixed(2)}ms`)
|
||||
console.log(` Complex params: ${complexTime.toFixed(2)}ms`)
|
||||
console.log(` Param overhead: ${paramOverhead.toFixed(2)}ms`)
|
||||
console.log(` Per operation: ${(paramOverhead / iterations).toFixed(4)}ms`)
|
||||
|
||||
expect(metrics.measurements.simple).toBeDefined()
|
||||
expect(metrics.measurements.complex).toBeDefined()
|
||||
expect(Object.keys(metrics.measurements.complex.params).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should measure overhead with NoMetricsContext', async () => {
|
||||
// NoMetricsContext should have minimal overhead
|
||||
const noMetricsCtx = new NoMetricsContext(noParamsLogger)
|
||||
|
||||
const start = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await noMetricsCtx.with('operation', { iteration: i }, async () => {
|
||||
await simulateWork(1)
|
||||
})
|
||||
}
|
||||
const noMetricsTime = performance.now() - start
|
||||
|
||||
// Compare with raw execution
|
||||
const rawStart = performance.now()
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await simulateWork(1)
|
||||
}
|
||||
const rawTime = performance.now() - rawStart
|
||||
|
||||
const overhead = noMetricsTime - rawTime
|
||||
const overheadPercentage = (overhead / rawTime) * 100
|
||||
|
||||
console.log(`\n📊 NoMetricsContext Overhead (${iterations} iterations):`)
|
||||
console.log(` Raw time: ${rawTime.toFixed(2)}ms`)
|
||||
console.log(` NoMetrics time: ${noMetricsTime.toFixed(2)}ms`)
|
||||
console.log(` Overhead: ${overhead.toFixed(2)}ms (${overheadPercentage.toFixed(2)}%)`)
|
||||
|
||||
// NoMetricsContext should have very low overhead
|
||||
expect(overheadPercentage).toBeLessThan(50)
|
||||
})
|
||||
})
|
||||
|
||||
describe('realistic workload simulation', () => {
|
||||
it('should measure overhead in complex realistic scenario', async () => {
|
||||
const metrics = newMetrics()
|
||||
const ctx = new MeasureMetricsContext('app', { service: 'api' }, {}, metrics, noParamsLogger)
|
||||
|
||||
const requests = 50
|
||||
const baselineStart = performance.now()
|
||||
|
||||
// Simulate without metrics
|
||||
for (let i = 0; i < requests; i++) {
|
||||
await simulateAPIRequest(null, i)
|
||||
}
|
||||
const baselineTime = performance.now() - baselineStart
|
||||
|
||||
// Reset for measured run
|
||||
const measuredStart = performance.now()
|
||||
|
||||
// Simulate with metrics
|
||||
for (let i = 0; i < requests; i++) {
|
||||
await simulateAPIRequest(ctx, i)
|
||||
}
|
||||
const measuredTime = performance.now() - measuredStart
|
||||
|
||||
const overhead = measuredTime - baselineTime
|
||||
const overheadPercentage = (overhead / baselineTime) * 100
|
||||
|
||||
console.log(`\n📊 Realistic Workload Analysis (${requests} API requests):`)
|
||||
console.log(` Baseline: ${baselineTime.toFixed(2)}ms`)
|
||||
console.log(` With metrics: ${measuredTime.toFixed(2)}ms`)
|
||||
console.log(` Overhead: ${overhead.toFixed(2)}ms (${overheadPercentage.toFixed(2)}%)`)
|
||||
console.log(` Per request: ${(overhead / requests).toFixed(4)}ms`)
|
||||
|
||||
// Check metrics structure
|
||||
const aggregated = metricsAggregate(metrics, 10)
|
||||
console.log(` Collected operations: ${aggregated.measurements.request?.operations ?? 0}`)
|
||||
|
||||
expect(aggregated.measurements.request).toBeDefined()
|
||||
expect(overheadPercentage).toBeLessThan(100) // Should be less than 100% overhead
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Helper functions
|
||||
|
||||
async function simulateWork (durationMs: number): Promise<void> {
|
||||
const end = performance.now() + durationMs
|
||||
while (performance.now() < end) {
|
||||
// Busy wait to simulate work
|
||||
const r = Math.random() * Math.random()
|
||||
expect(r).toBeGreaterThanOrEqual(0)
|
||||
}
|
||||
}
|
||||
|
||||
async function deepNestedExecution (
|
||||
ctx: MeasureContext,
|
||||
depth: number,
|
||||
workMs: number,
|
||||
currentLevel: number = 0
|
||||
): Promise<void> {
|
||||
if (currentLevel >= depth) {
|
||||
await simulateWork(workMs)
|
||||
return
|
||||
}
|
||||
|
||||
await ctx.with(`level${currentLevel}`, { level: currentLevel }, async (childCtx) => {
|
||||
await deepNestedExecution(childCtx, depth, workMs, currentLevel + 1)
|
||||
})
|
||||
}
|
||||
|
||||
async function simulateAPIRequest (ctx: MeasureContext | null, requestId: number): Promise<void> {
|
||||
const method = ['GET', 'POST', 'PUT'][requestId % 3]
|
||||
const endpoint = `/api/resource/${requestId % 10}`
|
||||
|
||||
if (ctx === null) {
|
||||
// No metrics version
|
||||
await simulateWork(1)
|
||||
// Simulate DB query
|
||||
await simulateWork(2)
|
||||
// Simulate processing
|
||||
await simulateWork(1)
|
||||
return
|
||||
}
|
||||
|
||||
// With metrics version
|
||||
await ctx.with('request', { method, endpoint }, async (reqCtx) => {
|
||||
await reqCtx.with('auth', { userId: `user_${requestId % 50}` }, async () => {
|
||||
await simulateWork(1)
|
||||
})
|
||||
|
||||
await reqCtx.with('database', { query: 'SELECT' }, async (dbCtx) => {
|
||||
await dbCtx.with('query_execution', {}, async () => {
|
||||
await simulateWork(2)
|
||||
})
|
||||
})
|
||||
|
||||
await reqCtx.with('processing', { items: requestId % 20 }, async () => {
|
||||
await simulateWork(1)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
// Basic performance metrics suite.
|
||||
|
||||
import { platformNow, platformNowDiff } from '.'
|
||||
import { childMetrics, newMetrics, updateMeasure } from './metrics'
|
||||
import {
|
||||
type FullParamsType,
|
||||
type MeasureContext,
|
||||
type MeasureLogger,
|
||||
type Metrics,
|
||||
type ParamsType,
|
||||
type OperationLog,
|
||||
type OperationLogEntry,
|
||||
type WithOptions
|
||||
} from './types'
|
||||
|
||||
const errorPrinter = ({ message, stack, ...rest }: Error): object => ({
|
||||
message,
|
||||
stack,
|
||||
...rest
|
||||
})
|
||||
function replacer (value: any): any {
|
||||
return value instanceof Error ? errorPrinter(value) : value
|
||||
}
|
||||
|
||||
export const consoleLogger = (logParams: Record<string, any>): MeasureLogger => ({
|
||||
info: (msg, args) => {
|
||||
console.info(
|
||||
msg,
|
||||
...Object.entries({ ...(args ?? {}), ...(logParams ?? {}) }).map(
|
||||
(it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}`
|
||||
)
|
||||
)
|
||||
},
|
||||
error: (msg, args) => {
|
||||
console.error(
|
||||
msg,
|
||||
...Object.entries({ ...(args ?? {}), ...(logParams ?? {}) }).map(
|
||||
(it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}`
|
||||
)
|
||||
)
|
||||
},
|
||||
warn: (msg, args) => {
|
||||
console.warn(msg, ...Object.entries(args ?? {}).map((it) => `${it[0]}=${JSON.stringify(replacer(it[1]))}`))
|
||||
},
|
||||
close: async () => {},
|
||||
logOperation: (operation, time, params) => {}
|
||||
})
|
||||
|
||||
export const noParamsLogger = consoleLogger({})
|
||||
|
||||
export const nullPromise = Promise.resolve()
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class MeasureMetricsContext implements MeasureContext {
|
||||
private readonly name: string
|
||||
private readonly params: ParamsType
|
||||
|
||||
private readonly fullParams: FullParamsType | (() => FullParamsType) = {}
|
||||
logger: MeasureLogger
|
||||
metrics: Metrics
|
||||
id?: string
|
||||
|
||||
st = platformNow()
|
||||
contextData: object = {}
|
||||
private done (value?: number, override?: boolean): void {
|
||||
updateMeasure(this.metrics, this.st, this.params, this.fullParams, (spend) => {}, value, override)
|
||||
}
|
||||
|
||||
constructor (
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
fullParams: FullParamsType | (() => FullParamsType) = {},
|
||||
metrics: Metrics = newMetrics(),
|
||||
logger?: MeasureLogger,
|
||||
readonly parent?: MeasureContext,
|
||||
readonly logParams?: ParamsType
|
||||
) {
|
||||
this.name = name
|
||||
this.params = params
|
||||
this.fullParams = fullParams
|
||||
this.metrics = metrics
|
||||
this.metrics.namedParams = this.metrics.namedParams ?? {}
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (this.metrics.namedParams[k] !== v) {
|
||||
this.metrics.namedParams[k] = v
|
||||
} else {
|
||||
this.metrics.namedParams[k] = '*'
|
||||
}
|
||||
}
|
||||
|
||||
this.logger = logger ?? (this.logParams != null ? consoleLogger(this.logParams ?? {}) : noParamsLogger)
|
||||
}
|
||||
|
||||
measure (name: string, value: number, override?: boolean): void {
|
||||
const c = new MeasureMetricsContext('#' + name, {}, {}, childMetrics(this.metrics, ['#' + name]), this.logger, this)
|
||||
c.contextData = this.contextData
|
||||
c.done(value, override)
|
||||
}
|
||||
|
||||
newChild (
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
opt?: {
|
||||
fullParams?: FullParamsType
|
||||
logger?: MeasureLogger
|
||||
span?: WithOptions['span'] // By default true
|
||||
}
|
||||
): MeasureContext {
|
||||
const result = new MeasureMetricsContext(
|
||||
name,
|
||||
params,
|
||||
opt?.fullParams ?? {},
|
||||
childMetrics(this.metrics, [name]),
|
||||
opt?.logger ?? this.logger,
|
||||
this,
|
||||
this.logParams
|
||||
)
|
||||
result.id = this.id
|
||||
result.contextData = this.contextData
|
||||
return result
|
||||
}
|
||||
|
||||
with<T>(
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
op: (ctx: MeasureContext) => T | Promise<T>,
|
||||
fullParams?: ParamsType | (() => FullParamsType),
|
||||
opt?: WithOptions
|
||||
): Promise<T> {
|
||||
const c = this.newChild(name, params, { fullParams, logger: this.logger })
|
||||
let needFinally = true
|
||||
try {
|
||||
const value = op(c)
|
||||
if (value instanceof Promise) {
|
||||
needFinally = false
|
||||
return value.finally(() => {
|
||||
c.end()
|
||||
if (opt?.log === true) {
|
||||
this.logger.logOperation(name, platformNowDiff((c as MeasureMetricsContext).st), {
|
||||
...params,
|
||||
...fullParams
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (value == null) {
|
||||
return nullPromise as Promise<T>
|
||||
}
|
||||
return Promise.resolve(value)
|
||||
}
|
||||
} finally {
|
||||
if (needFinally) {
|
||||
c.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extractMeta (): Record<string, string | number | boolean> {
|
||||
return {}
|
||||
}
|
||||
|
||||
withSync<T>(
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
op: (ctx: MeasureContext) => T,
|
||||
fullParams?: ParamsType | (() => FullParamsType)
|
||||
): T {
|
||||
const c = this.newChild(name, params, { fullParams, logger: this.logger })
|
||||
try {
|
||||
return op(c)
|
||||
} finally {
|
||||
c.end()
|
||||
}
|
||||
}
|
||||
|
||||
error (message: string, args?: Record<string, any>): void {
|
||||
this.logger.error(message, { ...this.params, ...args, ...(this.logParams ?? {}) })
|
||||
}
|
||||
|
||||
info (message: string, args?: Record<string, any>): void {
|
||||
this.logger.info(message, { ...this.params, ...args, ...(this.logParams ?? {}) })
|
||||
}
|
||||
|
||||
warn (message: string, args?: Record<string, any>): void {
|
||||
this.logger.warn(message, { ...this.params, ...args, ...(this.logParams ?? {}) })
|
||||
}
|
||||
|
||||
end (): void {
|
||||
this.done()
|
||||
}
|
||||
|
||||
getParams (): ParamsType {
|
||||
return this.params
|
||||
}
|
||||
}
|
||||
|
||||
export class NoMetricsContext implements MeasureContext {
|
||||
logger: MeasureLogger
|
||||
id?: string
|
||||
|
||||
contextData: object = {}
|
||||
|
||||
constructor (logger?: MeasureLogger) {
|
||||
this.logger = logger ?? consoleLogger({})
|
||||
}
|
||||
|
||||
measure (name: string, value: number, override?: boolean): void {}
|
||||
|
||||
newChild (
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
fullParams?: FullParamsType | (() => FullParamsType),
|
||||
logger?: MeasureLogger
|
||||
): MeasureContext {
|
||||
const result = new NoMetricsContext(logger ?? this.logger)
|
||||
result.id = this.id
|
||||
result.contextData = this.contextData
|
||||
return result
|
||||
}
|
||||
|
||||
with<T>(
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
op: (ctx: MeasureContext) => T | Promise<T>,
|
||||
fullParams?: ParamsType | (() => FullParamsType)
|
||||
): Promise<T> {
|
||||
const r = op(this.newChild(name, params, fullParams, this.logger))
|
||||
return r instanceof Promise ? r : Promise.resolve(r)
|
||||
}
|
||||
|
||||
extractMeta (): Record<string, string | number | boolean> {
|
||||
return {}
|
||||
}
|
||||
|
||||
withSync<T>(
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
op: (ctx: MeasureContext) => T,
|
||||
fullParams?: ParamsType | (() => FullParamsType)
|
||||
): T {
|
||||
const c = this.newChild(name, params, fullParams, this.logger)
|
||||
return op(c)
|
||||
}
|
||||
|
||||
withLog<T>(
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
op: (ctx: MeasureContext) => T | Promise<T>,
|
||||
fullParams?: ParamsType
|
||||
): Promise<T> {
|
||||
const r = op(this.newChild(name, params, fullParams, this.logger))
|
||||
return r instanceof Promise ? r : Promise.resolve(r)
|
||||
}
|
||||
|
||||
error (message: string, args?: Record<string, any>): void {
|
||||
this.logger.error(message, { ...args })
|
||||
}
|
||||
|
||||
info (message: string, args?: Record<string, any>): void {
|
||||
this.logger.info(message, { ...args })
|
||||
}
|
||||
|
||||
warn (message: string, args?: Record<string, any>): void {
|
||||
this.logger.warn(message, { ...args })
|
||||
}
|
||||
|
||||
end (): void {}
|
||||
|
||||
getParams (): ParamsType {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow to use decorator for context enabled functions
|
||||
*/
|
||||
export function withContext (name: string, params: ParamsType = {}): any {
|
||||
return (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
|
||||
const originalMethod = descriptor.value
|
||||
descriptor.value = function (...args: any[]): Promise<any> {
|
||||
const ctx = args[0] as MeasureContext
|
||||
return ctx.with(name, params, (ctx) => originalMethod.apply(this, [ctx, ...args.slice(1)]) as Promise<any>)
|
||||
}
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
let operationProfiling = false
|
||||
|
||||
export function setOperationLogProfiling (value: boolean): void {
|
||||
operationProfiling = value
|
||||
}
|
||||
|
||||
let globalId: number = 0
|
||||
|
||||
export function registerOperationLog (ctx: MeasureContext): { opLogMetrics?: Metrics, op?: OperationLog } {
|
||||
if (!operationProfiling) {
|
||||
return {}
|
||||
}
|
||||
const op: OperationLog = { start: platformNow(), ops: [], end: -1 }
|
||||
let opLogMetrics: Metrics | undefined
|
||||
|
||||
if (ctx.id === undefined) {
|
||||
ctx.id = 'op_' + (++globalId).toString(16)
|
||||
}
|
||||
if (ctx.metrics !== undefined) {
|
||||
if (ctx.metrics.opLog === undefined) {
|
||||
ctx.metrics.opLog = {}
|
||||
}
|
||||
ctx.metrics.opLog[ctx.id] = op
|
||||
opLogMetrics = ctx.metrics
|
||||
}
|
||||
return { opLogMetrics, op }
|
||||
}
|
||||
|
||||
export function updateOperationLog (opLogMetrics: Metrics | undefined, op: OperationLog | undefined): void {
|
||||
if (!operationProfiling) {
|
||||
return
|
||||
}
|
||||
if (op !== undefined) {
|
||||
op.end = platformNow()
|
||||
}
|
||||
// We should keep only longest one entry
|
||||
if (opLogMetrics?.opLog !== undefined) {
|
||||
const entries = Object.entries(opLogMetrics.opLog)
|
||||
|
||||
const incomplete = entries.filter((it) => it[1].end === -1)
|
||||
const complete = entries.filter((it) => it[1].end !== -1)
|
||||
complete.sort((a, b) => a[1].start - b[1].start)
|
||||
if (complete.length > 30) {
|
||||
complete.splice(0, complete.length - 30)
|
||||
}
|
||||
|
||||
opLogMetrics.opLog = Object.fromEntries(incomplete.concat(complete))
|
||||
}
|
||||
}
|
||||
|
||||
export function addOperation<T> (
|
||||
ctx: MeasureContext,
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
op: (ctx: MeasureContext) => Promise<T>,
|
||||
fullParams?: FullParamsType
|
||||
): Promise<T> {
|
||||
if (!operationProfiling) {
|
||||
return op(ctx)
|
||||
}
|
||||
let opEntry: OperationLogEntry | undefined
|
||||
|
||||
let p: MeasureContext | undefined = ctx
|
||||
let opLogMetrics: Metrics | undefined
|
||||
let id: string | undefined
|
||||
|
||||
while (p !== undefined) {
|
||||
if (p.metrics?.opLog !== undefined) {
|
||||
opLogMetrics = p.metrics
|
||||
}
|
||||
if (id === undefined && p.id !== undefined) {
|
||||
id = p.id
|
||||
}
|
||||
p = p.parent
|
||||
}
|
||||
const opLog = id !== undefined ? opLogMetrics?.opLog?.[id] : undefined
|
||||
|
||||
if (opLog !== undefined) {
|
||||
opEntry = {
|
||||
op: name,
|
||||
start: performance.now(),
|
||||
params: {},
|
||||
end: -1
|
||||
}
|
||||
}
|
||||
const result = op(ctx)
|
||||
if (opEntry !== undefined && opLog !== undefined) {
|
||||
void result.finally(() => {
|
||||
if (opEntry !== undefined && opLog !== undefined) {
|
||||
opEntry.end = performance.now()
|
||||
opEntry.params = { ...params, ...(typeof fullParams === 'function' ? fullParams() : fullParams) }
|
||||
opLog.ops.push(opEntry)
|
||||
}
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export * from './context'
|
||||
export * from './metrics'
|
||||
export type * from './types'
|
||||
|
||||
/**
|
||||
* Return a current performance timestamp
|
||||
*/
|
||||
export const platformNow: () => number = () => performance.now()
|
||||
|
||||
/**
|
||||
* Return a diff with previous performance snapshot with 2 digits after . max.
|
||||
*/
|
||||
export const platformNowDiff = (old: number): number => Math.round((performance.now() - old) * 100) / 100
|
||||
@@ -1,343 +0,0 @@
|
||||
// Basic performance metrics suite.
|
||||
|
||||
import { platformNow, type MetricsData } from '.'
|
||||
import { type FullParamsType, type Metrics, type ParamsType } from './types'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const globals: Metrics = newMetrics()
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @returns
|
||||
*/
|
||||
export function newMetrics (): Metrics {
|
||||
return {
|
||||
operations: 0,
|
||||
value: 0,
|
||||
measurements: {},
|
||||
params: {},
|
||||
namedParams: {}
|
||||
}
|
||||
}
|
||||
|
||||
function getUpdatedTopResult (
|
||||
current: Metrics['topResult'],
|
||||
time: number,
|
||||
params: FullParamsType
|
||||
): Metrics['topResult'] {
|
||||
if (time === 0) {
|
||||
return current
|
||||
}
|
||||
const result: Metrics['topResult'] = current ?? []
|
||||
|
||||
const newValue = {
|
||||
value: time,
|
||||
params
|
||||
}
|
||||
|
||||
if (result.length > 6) {
|
||||
if (result[0].value < newValue.value) {
|
||||
result[0] = newValue
|
||||
return result
|
||||
}
|
||||
if (result[result.length - 1].value > newValue.value) {
|
||||
result[result.length - 1] = newValue
|
||||
return result
|
||||
}
|
||||
|
||||
// Shift the middle
|
||||
return [result[0], newValue, ...result.slice(1, 3), result[5]]
|
||||
} else {
|
||||
result.push(newValue)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure with tree expansion. Operation counter will be added only to leaf's.
|
||||
* @public
|
||||
*/
|
||||
export function measure (
|
||||
metrics: Metrics,
|
||||
params: ParamsType,
|
||||
fullParams: FullParamsType | (() => FullParamsType) = {},
|
||||
endOp?: (spend: number) => void
|
||||
): () => void {
|
||||
const st = platformNow()
|
||||
return () => {
|
||||
updateMeasure(metrics, st, params, fullParams, endOp)
|
||||
}
|
||||
}
|
||||
export function updateMeasure (
|
||||
metrics: Metrics,
|
||||
st: number,
|
||||
params: ParamsType,
|
||||
fullParams: FullParamsType | (() => FullParamsType),
|
||||
endOp?: (spend: number) => void,
|
||||
value?: number,
|
||||
override?: boolean
|
||||
): void {
|
||||
const ed = platformNow()
|
||||
|
||||
const fParams = typeof fullParams === 'function' ? fullParams() : fullParams
|
||||
// Update params if required
|
||||
const pparams = Object.entries(params)
|
||||
if (pparams.length > 0) {
|
||||
const [k, v] = pparams[0]
|
||||
let params = metrics.params[k]
|
||||
if (params === undefined) {
|
||||
params = {}
|
||||
metrics.params[k] = params
|
||||
}
|
||||
const vKey = `${v?.toString() ?? ''}`
|
||||
let param = params[vKey]
|
||||
if (param === undefined) {
|
||||
param = {
|
||||
operations: 0,
|
||||
value: 0
|
||||
}
|
||||
params[vKey] = param
|
||||
}
|
||||
if (override === true) {
|
||||
if (value === 0) {
|
||||
// We need to delete value, to preserve sending zero values.
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete params[vKey]
|
||||
} else {
|
||||
param.operations = value ?? ed - st
|
||||
}
|
||||
} else {
|
||||
param.value += value ?? ed - st
|
||||
param.operations++
|
||||
}
|
||||
// Do not update top results for params.
|
||||
if (pparams.length > 1) {
|
||||
// We need to update all other params as counters.
|
||||
if (param.topResult === undefined) {
|
||||
param.topResult = []
|
||||
}
|
||||
for (const [, v] of pparams.slice(1)) {
|
||||
const r = (param.topResult ?? []).find((it) => it.params[`${v}`] === true)
|
||||
if (r !== undefined) {
|
||||
r.value += 1 // Counter of operations
|
||||
r.time = (r.time ?? 0) + (value ?? ed - st)
|
||||
} else {
|
||||
param.topResult.push({ params: { [`${v}`]: true }, value: 1, time: value ?? ed - st })
|
||||
}
|
||||
}
|
||||
param.topResult.sort((a, b) => b.value - a.value)
|
||||
}
|
||||
}
|
||||
// Update leaf data
|
||||
if (override === true) {
|
||||
metrics.operations = value ?? ed - st
|
||||
} else {
|
||||
metrics.value += value ?? ed - st
|
||||
metrics.operations++
|
||||
}
|
||||
|
||||
metrics.topResult = getUpdatedTopResult(metrics.topResult, ed - st, fParams)
|
||||
endOp?.(ed - st)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function childMetrics (root: Metrics, path: string[]): Metrics {
|
||||
const segments = path
|
||||
let oop = root
|
||||
for (const p of segments) {
|
||||
const v = oop.measurements[p] ?? { operations: 0, value: 0, measurements: {}, params: {} }
|
||||
oop.measurements[p] = v
|
||||
oop = v
|
||||
}
|
||||
return oop
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function metricsAggregate (m: Metrics, limit: number = -1, roundMath: boolean = false): Metrics {
|
||||
let ms = aggregateMetrics(m.measurements, limit)
|
||||
|
||||
// Use child overage, if there is no top level value specified.
|
||||
const me = Object.entries(ms)
|
||||
const sumVal: number =
|
||||
(me.length === 0 ? m.value : 0) +
|
||||
me
|
||||
.filter((it) => !it[0].startsWith('#'))
|
||||
.map((it) => it[1])
|
||||
.reduce((p, v) => {
|
||||
return p + v.value
|
||||
}, 0)
|
||||
|
||||
if (limit !== -1) {
|
||||
// We need to keep only top limit items in ms
|
||||
if (Object.keys(ms).length > 0) {
|
||||
const newMs: typeof ms = {}
|
||||
let added = 0
|
||||
for (const [k, v] of Object.entries(ms)) {
|
||||
newMs[k] = v
|
||||
added++
|
||||
if (added >= limit) {
|
||||
break
|
||||
}
|
||||
}
|
||||
ms = newMs
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
operations: m.operations,
|
||||
measurements: ms,
|
||||
params: m.params,
|
||||
value: sumVal,
|
||||
topResult: m.topResult,
|
||||
namedParams: m.namedParams,
|
||||
opLog: m.opLog
|
||||
}
|
||||
}
|
||||
|
||||
function aggregateMetrics (m: Record<string, Metrics>, limit: number = -1): Record<string, Metrics> {
|
||||
const result: Record<string, Metrics> = {}
|
||||
for (const [k, v] of Object.entries(m).sort((a, b) => b[1].value - a[1].value)) {
|
||||
result[k] = metricsAggregate(v, limit)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function toLen (val: string, sep: string, len: number): string {
|
||||
while (val.length < len) {
|
||||
val += sep
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
function printMetricsChildren (params: Record<string, Metrics>, offset: number, length: number): string {
|
||||
let r = ''
|
||||
if (Object.keys(params).length > 0) {
|
||||
r += '\n' + toLen('', ' ', offset)
|
||||
r += Object.entries(params)
|
||||
.filter((it) => it[1].value > 0.1)
|
||||
.map(([k, vv]) => toString(k, vv, offset, length))
|
||||
.join('\n' + toLen('', ' ', offset))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
function printMetricsParams (
|
||||
params: Record<string, Record<string, MetricsData>>,
|
||||
offset: number,
|
||||
length: number
|
||||
): string {
|
||||
let r = ''
|
||||
const joinP = (key: string, data: Record<string, MetricsData>): string[] => {
|
||||
return Object.entries(data)
|
||||
.filter((it) => it[1].value >= 0.1)
|
||||
.map(([k, vv]) =>
|
||||
`${toLen('', ' ', offset)}${toLen(key + '=' + k, '-', length - offset)}: avg ${
|
||||
Math.round((vv.value / (vv.operations > 0 ? vv.operations : 1)) * 100) / 100
|
||||
} total: ${Math.round(vv.value * 100) / 100} ops: ${vv.operations}`.trim()
|
||||
)
|
||||
}
|
||||
const joinParams = Object.entries(params).reduce<string[]>((p, c) => [...p, ...joinP(c[0], c[1])], [])
|
||||
if (Object.keys(joinParams).length > 0) {
|
||||
r += '\n' + toLen('', ' ', offset)
|
||||
r += joinParams.join('\n' + toLen('', ' ', offset))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
function toString (name: string, m: Metrics, offset: number, length: number): string {
|
||||
let r = `${toLen('', ' ', offset)}${toLen(name, '-', length - offset)}: avg ${
|
||||
Math.round((m.value / (m.operations > 0 ? m.operations : 1)) * 100) / 100
|
||||
} total: ${Math.round(m.value * 100) / 100} ops: ${m.operations}`.trim()
|
||||
r += printMetricsParams(m.params, offset + 4, length)
|
||||
r += printMetricsChildren(m.measurements, offset + 4, length)
|
||||
return r
|
||||
}
|
||||
|
||||
function toJson (m: Metrics): any {
|
||||
const obj: any = {
|
||||
$total: m.value,
|
||||
$ops: m.operations
|
||||
}
|
||||
if (m.operations > 1) {
|
||||
obj.avg = Math.round((m.value / (m.operations > 0 ? m.operations : 1)) * 100) / 100
|
||||
}
|
||||
if (Object.keys(m.params).length > 0) {
|
||||
obj.params = m.params
|
||||
}
|
||||
for (const [k, v] of Object.entries(m.measurements ?? {})) {
|
||||
obj[
|
||||
`${k} ${v.value} ${v.operations} ${
|
||||
v.operations > 1 ? Math.round((v.value / (v.operations > 0 ? m.operations : 1)) * 100) / 100 : ''
|
||||
}`
|
||||
] = toJson(v)
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function metricsToString (metrics: Metrics, name = 'System', length: number): string {
|
||||
return toString(name, metricsAggregate(metrics, 50, true), 0, length)
|
||||
}
|
||||
|
||||
export function metricsToJson (metrics: Metrics): any {
|
||||
return toJson(metricsAggregate(metrics))
|
||||
}
|
||||
|
||||
function printMetricsParamsRows (
|
||||
params: Record<string, Record<string, MetricsData>>,
|
||||
offset: number
|
||||
): (string | number)[][] {
|
||||
const r: (string | number)[][] = []
|
||||
function joinP (key: string, data: Record<string, MetricsData>): (string | number)[][] {
|
||||
return Object.entries(data).map(([k, vv]) => [
|
||||
offset,
|
||||
`${key}=${k}`,
|
||||
Math.round((vv.value / (vv.operations > 0 ? vv.operations : 1)) * 100) / 100,
|
||||
Math.round(vv.value * 100) / 100,
|
||||
vv.operations
|
||||
])
|
||||
}
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
r.push(...joinP(k, v))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
function printMetricsChildrenRows (params: Record<string, Metrics>, offset: number): (string | number)[][] {
|
||||
const r: (string | number)[][] = []
|
||||
if (Object.keys(params).length > 0) {
|
||||
Object.entries(params).forEach(([k, vv]) => r.push(...toStringRows(k, vv, offset)))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
function toStringRows (name: string, m: Metrics, offset: number): (number | string)[][] {
|
||||
const r: (number | string)[][] = [
|
||||
[
|
||||
offset,
|
||||
name,
|
||||
Math.round((m.value / (m.operations > 0 ? m.operations : 1)) * 100) / 100,
|
||||
Math.round(m.value * 100) / 100,
|
||||
m.operations
|
||||
]
|
||||
]
|
||||
r.push(...printMetricsParamsRows(m.params, offset + 1))
|
||||
r.push(...printMetricsChildrenRows(m.measurements, offset + 1))
|
||||
return r
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function metricsToRows (metrics: Metrics, name = 'System'): (number | string)[][] {
|
||||
return toStringRows(name, metricsAggregate(metrics, 50, true), 0)
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ParamType = string | number | boolean | undefined
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ParamsType = Record<string, ParamType>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type FullParamsType = Record<string, any>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface MetricsData {
|
||||
operations: number
|
||||
value: number
|
||||
topResult?: {
|
||||
value: number
|
||||
time?: number
|
||||
params: FullParamsType
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface OperationLogEntry {
|
||||
op: string
|
||||
params: ParamsType
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
export interface OperationLog {
|
||||
ops: OperationLogEntry[]
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Metrics extends MetricsData {
|
||||
namedParams: ParamsType
|
||||
params: Record<string, Record<string, MetricsData>>
|
||||
measurements: Record<string, Metrics>
|
||||
|
||||
opLog?: Record<string, OperationLog>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface MeasureLogger {
|
||||
info: (message: string, obj?: Record<string, any>) => void
|
||||
error: (message: string, obj?: Record<string, any>) => void
|
||||
|
||||
warn: (message: string, obj?: Record<string, any>) => void
|
||||
|
||||
logOperation: (operation: string, time: number, params: ParamsType) => void
|
||||
|
||||
childLogger?: (name: string, params: Record<string, any>) => MeasureLogger
|
||||
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
export interface WithOptions {
|
||||
span?: true | false | 'disable' | 'skip' | 'inherit' // 'none' means no span will be created, 'disable' means context will be tracing disabled
|
||||
log?: boolean
|
||||
inheritParams?: boolean
|
||||
|
||||
// Passed context metadata
|
||||
meta?: Record<string, string | number | boolean>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface MeasureContext<Q = any> {
|
||||
id?: string
|
||||
|
||||
// Context data will be copied referenced for all child contexts.
|
||||
contextData: Q
|
||||
// Create a child metrics context
|
||||
newChild: (
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
opt?: {
|
||||
fullParams?: FullParamsType
|
||||
logger?: MeasureLogger
|
||||
span?: WithOptions['span'] // By default true
|
||||
meta?: Record<string, string | number | boolean>
|
||||
}
|
||||
) => MeasureContext
|
||||
|
||||
metrics?: Metrics
|
||||
|
||||
with: <T>(
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
op: (ctx: MeasureContext<Q>) => T | Promise<T>,
|
||||
fullParams?: FullParamsType | (() => FullParamsType),
|
||||
opt?: WithOptions
|
||||
) => Promise<T>
|
||||
|
||||
withSync: <T>(
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
op: (ctx: MeasureContext<Q>) => T,
|
||||
fullParams?: FullParamsType | (() => FullParamsType),
|
||||
opt?: WithOptions
|
||||
) => T
|
||||
|
||||
extractMeta: () => Record<string, string | number | boolean>
|
||||
|
||||
logger: MeasureLogger
|
||||
|
||||
parent?: MeasureContext
|
||||
getParams: () => ParamsType
|
||||
|
||||
measure: (name: string, value: number, override?: boolean) => void
|
||||
|
||||
// Capture error
|
||||
error: (message: string, obj?: Record<string, any>) => void
|
||||
info: (message: string, obj?: Record<string, any>) => void
|
||||
warn: (message: string, obj?: Record<string, any>) => void
|
||||
|
||||
// Mark current context as complete
|
||||
// If no value is passed, time difference will be used.
|
||||
end: (value?: number) => void
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
*
|
||||
!/lib/**
|
||||
!/types/**
|
||||
!/src/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__test__/
|
||||
/types/**/__test__/
|
||||
/src/**/__test__/
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"name": "@hcengineering/postgres-base",
|
||||
"entries": [
|
||||
{
|
||||
"version": "0.7.12",
|
||||
"tag": "@hcengineering/postgres-base_v0.7.12",
|
||||
"date": "Sat, 11 Oct 2025 19:18:56 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "rollback eslint"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.14` to `0.7.15`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.11",
|
||||
"tag": "@hcengineering/postgres-base_v0.7.11",
|
||||
"date": "Sat, 11 Oct 2025 17:58:53 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "Fix eslint deps"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.12` to `0.7.13`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.10",
|
||||
"tag": "@hcengineering/postgres-base_v0.7.10",
|
||||
"date": "Fri, 10 Oct 2025 12:32:59 GMT",
|
||||
"comments": {
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform-rig\" from `^0.7.10` to `0.7.11`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
# Change Log - @hcengineering/postgres-base
|
||||
|
||||
This log was last generated on Sat, 11 Oct 2025 19:18:56 GMT and should not be manually modified.
|
||||
|
||||
## 0.7.12
|
||||
Sat, 11 Oct 2025 19:18:56 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- rollback eslint
|
||||
|
||||
## 0.7.11
|
||||
Sat, 11 Oct 2025 17:58:53 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- Fix eslint deps
|
||||
|
||||
## 0.7.10
|
||||
Fri, 10 Oct 2025 12:32:59 GMT
|
||||
|
||||
_Initial release_
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "node"
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
{
|
||||
"name": "@hcengineering/postgres-base",
|
||||
"version": "0.7.12",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"author": "Copyright © Hardcore Engineering Inc.",
|
||||
"template": "@hcengineering/node-package",
|
||||
"license": "EPL-2.0",
|
||||
"files": [
|
||||
"lib/**/*",
|
||||
"!lib/**/__test__/**",
|
||||
"types/**/*",
|
||||
"!types/**/__test__/**",
|
||||
"src/**/*",
|
||||
"!src/**/__test__/**",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"test": "jest --passWithNoTests --silent --forceExit",
|
||||
"format": "format src",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent --forceExit",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "workspace:^0.7.19",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/node": "^22.15.29",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"postgres": "^3.4.7"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.utils",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"require": "./lib/index.js",
|
||||
"import": "./lib/index.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -1,439 +0,0 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import postgres, { type Options, type ParameterOrJSON } from 'postgres'
|
||||
|
||||
const clientRefs = new Map<number, ClientRef>()
|
||||
|
||||
let clId = 0
|
||||
|
||||
export type DBResult = any[] & { count: number }
|
||||
export interface DBClient {
|
||||
execute: (query: string, parameters?: ParameterOrJSON<any>[] | undefined) => Promise<DBResult>
|
||||
|
||||
release: () => void
|
||||
|
||||
reserve: () => Promise<DBClient>
|
||||
|
||||
raw: () => postgres.Sql
|
||||
}
|
||||
|
||||
export function createDBClient (client: postgres.Sql, release: () => void = () => {}): DBClient {
|
||||
return {
|
||||
execute: (query, parameters) =>
|
||||
client.unsafe(query, doFetchTypes ? parameters : convertArrayParams(parameters), getPrepare()),
|
||||
release,
|
||||
reserve: async () => {
|
||||
const reserved = await client.reserve()
|
||||
return createDBClient(reserved, () => {
|
||||
reserved.release()
|
||||
})
|
||||
},
|
||||
raw: () => client
|
||||
}
|
||||
}
|
||||
|
||||
export function convertArrayParams (params?: unknown[]): any[] | undefined {
|
||||
if (params === undefined) return undefined
|
||||
|
||||
return params.map((param) => {
|
||||
if (!Array.isArray(param)) return param
|
||||
|
||||
if (param.length === 0) return '{}'
|
||||
|
||||
const sanitized = param.map((item) => {
|
||||
if (item === null || item === undefined) return 'NULL'
|
||||
|
||||
if (typeof item === 'number' || typeof item === 'boolean') {
|
||||
return String(item)
|
||||
}
|
||||
|
||||
if (typeof item === 'string') {
|
||||
const escaped = item.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
||||
return `"${escaped}"`
|
||||
}
|
||||
|
||||
const json = JSON.stringify(item)
|
||||
const escapedJson = json.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
||||
return `"${escapedJson}"`
|
||||
})
|
||||
|
||||
return `{${sanitized.join(',')}}`
|
||||
})
|
||||
}
|
||||
|
||||
export async function retryTxn (
|
||||
pool: postgres.Sql,
|
||||
operation: (client: postgres.TransactionSql) => Promise<any>
|
||||
): Promise<any> {
|
||||
await pool.begin(async (client) => {
|
||||
const result = await operation(client)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function shutdownPostgres (): Promise<void> {
|
||||
for (const c of connections.values()) {
|
||||
c.close(true)
|
||||
}
|
||||
connections.clear()
|
||||
}
|
||||
|
||||
export interface PostgresClientReference {
|
||||
getClient: () => Promise<postgres.Sql>
|
||||
|
||||
mgr: ConnectionMgr
|
||||
close: () => void
|
||||
url: () => string
|
||||
}
|
||||
|
||||
class PostgresClientReferenceImpl {
|
||||
count: number
|
||||
client: postgres.Sql
|
||||
|
||||
mgr: ConnectionMgr
|
||||
|
||||
constructor (
|
||||
readonly connectionString: string,
|
||||
client: postgres.Sql,
|
||||
readonly onclose: () => void
|
||||
) {
|
||||
this.count = 0
|
||||
this.client = client
|
||||
this.mgr = new ConnectionMgr(createDBClient(this.client))
|
||||
}
|
||||
|
||||
url (): string {
|
||||
return this.connectionString
|
||||
}
|
||||
|
||||
getClient (): postgres.Sql {
|
||||
return this.client
|
||||
}
|
||||
|
||||
close (force: boolean = false): void {
|
||||
this.count--
|
||||
if (this.count === 0 || force) {
|
||||
if (force) {
|
||||
this.count = 0
|
||||
}
|
||||
void (async () => {
|
||||
this.mgr.close()
|
||||
this.onclose()
|
||||
const cl = this.client
|
||||
await cl.end({ timeout: 1 })
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
addRef (): void {
|
||||
this.count++
|
||||
}
|
||||
}
|
||||
export class ClientRef implements PostgresClientReference {
|
||||
id = ++clId
|
||||
constructor (
|
||||
readonly client: PostgresClientReferenceImpl,
|
||||
readonly mgr: ConnectionMgr
|
||||
) {
|
||||
clientRefs.set(this.id, this)
|
||||
}
|
||||
|
||||
url (): string {
|
||||
return this.client.url()
|
||||
}
|
||||
|
||||
closed = false
|
||||
async getClient (): Promise<postgres.Sql> {
|
||||
if (!this.closed) {
|
||||
return this.client.getClient()
|
||||
} else {
|
||||
throw Error('DB client is already closed')
|
||||
}
|
||||
}
|
||||
|
||||
close (): void {
|
||||
// Do not allow double close of mongo connection client
|
||||
if (!this.closed) {
|
||||
clientRefs.delete(this.id)
|
||||
this.closed = true
|
||||
this.client.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export let dbExtraOptions: Partial<Options<any>> = {}
|
||||
export function setDBExtraOptions (options: Partial<Options<any>>): void {
|
||||
dbExtraOptions = options
|
||||
}
|
||||
|
||||
export function getPrepare (): { prepare: boolean } {
|
||||
return { prepare: dbExtraOptions.prepare ?? false }
|
||||
}
|
||||
|
||||
export const doFetchTypes = true
|
||||
|
||||
const connections = new Map<string, PostgresClientReferenceImpl>()
|
||||
|
||||
/**
|
||||
* Initialize a connection to DB
|
||||
* @public
|
||||
*/
|
||||
export function getDBClient (
|
||||
connectionString: string,
|
||||
database?: string,
|
||||
serviceName: string = 'transactor'
|
||||
): PostgresClientReference {
|
||||
const extraOptions = JSON.parse(process.env.POSTGRES_OPTIONS ?? '{}')
|
||||
const key = `${connectionString}${extraOptions}`
|
||||
|
||||
let existing = connections.get(key)
|
||||
|
||||
if (existing === undefined) {
|
||||
const sql = postgres(connectionString, {
|
||||
connection: {
|
||||
application_name: serviceName
|
||||
},
|
||||
database,
|
||||
max: 10,
|
||||
min: 2,
|
||||
connect_timeout: 30,
|
||||
idle_timeout: 0,
|
||||
transform: {
|
||||
undefined: null
|
||||
},
|
||||
debug: false,
|
||||
notice: false,
|
||||
onnotice (notice) {},
|
||||
onparameter (key, value) {},
|
||||
...dbExtraOptions,
|
||||
...extraOptions,
|
||||
fetch_types: doFetchTypes
|
||||
})
|
||||
|
||||
existing = new PostgresClientReferenceImpl(connectionString, sql, () => {
|
||||
connections.delete(key)
|
||||
})
|
||||
connections.set(key, existing)
|
||||
}
|
||||
// Add reference and return once closable
|
||||
existing.addRef()
|
||||
return new ClientRef(existing, existing.mgr)
|
||||
}
|
||||
|
||||
class ConnectionInfo {
|
||||
// It should preserve at least one available connection in pool, other connection should be closed
|
||||
available: DBClient[] = []
|
||||
|
||||
released: boolean = false
|
||||
|
||||
constructor (
|
||||
readonly connectionId: string,
|
||||
protected readonly client: DBClient,
|
||||
readonly managed: boolean,
|
||||
readonly mgrId: string
|
||||
) {}
|
||||
|
||||
async withReserve (action: (reservedClient: DBClient) => Promise<any>, forced: boolean = false): Promise<any> {
|
||||
let reserved: DBClient | undefined
|
||||
|
||||
// Check if we have at least one available connection and reserve one more if required.
|
||||
if (this.available.length === 0) {
|
||||
if (this.managed || forced) {
|
||||
reserved = await this.client.reserve()
|
||||
}
|
||||
} else {
|
||||
reserved = this.available.shift() as DBClient
|
||||
}
|
||||
|
||||
try {
|
||||
// Use reserved or pool
|
||||
return await action(reserved ?? this.client)
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
throw err
|
||||
} finally {
|
||||
if (this.released) {
|
||||
try {
|
||||
reserved?.release()
|
||||
} catch (err: any) {
|
||||
console.error('failed to release', err)
|
||||
}
|
||||
} else if (reserved !== undefined) {
|
||||
if (this.available.length > 0) {
|
||||
reserved?.release()
|
||||
} else {
|
||||
this.available.push(reserved)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
release (): void {
|
||||
for (const c of [...this.available]) {
|
||||
c.release()
|
||||
}
|
||||
this.available = []
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectionMgr {
|
||||
private readonly connections = new Map<string, ConnectionInfo>()
|
||||
constructor (protected readonly client: DBClient) {}
|
||||
|
||||
async write (id: string | undefined, mgrId: string, fn: (client: DBClient) => Promise<any>): Promise<void> {
|
||||
const backoffInterval = 25 // millis
|
||||
const maxTries = 5
|
||||
let tries = 0
|
||||
|
||||
const realId = id ?? `${++clId}`
|
||||
|
||||
const connection = this.getConnection(realId, mgrId, false)
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const retry: boolean | Error = await connection.withReserve(async (client) => {
|
||||
tries++
|
||||
try {
|
||||
await client.execute('BEGIN;')
|
||||
await fn(client)
|
||||
await client.execute('COMMIT;')
|
||||
return true
|
||||
} catch (err: any) {
|
||||
await client.execute('ROLLBACK;')
|
||||
console.error({ message: 'failed to process tx', error: err.message, cause: err })
|
||||
|
||||
if (!this.isRetryableError(err) || tries === maxTries) {
|
||||
return err
|
||||
} else {
|
||||
console.log('Transaction failed. Retrying.')
|
||||
console.log(err.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
if (retry === true) {
|
||||
break
|
||||
}
|
||||
if (retry instanceof Error) {
|
||||
// Pass it to exit
|
||||
throw retry
|
||||
}
|
||||
// Retry for a timeout
|
||||
await new Promise((resolve) => setTimeout(resolve, backoffInterval))
|
||||
}
|
||||
} finally {
|
||||
if (!connection.managed) {
|
||||
// We need to relase in case it temporaty connection was used
|
||||
connection.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async retry (id: string | undefined, mgrId: string, fn: (client: DBClient) => Promise<any>): Promise<any> {
|
||||
const backoffInterval = 25 // millis
|
||||
const maxTries = 5
|
||||
let tries = 0
|
||||
|
||||
const realId = id ?? `${++clId}`
|
||||
// Will reuse reserved if had and use new one if not
|
||||
const connection = this.getConnection(realId, mgrId, false)
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const retry: false | { result: any } | Error = await connection.withReserve(async (client) => {
|
||||
tries++
|
||||
try {
|
||||
return { result: await fn(client) }
|
||||
} catch (err: any) {
|
||||
console.error({ message: 'failed to process sql', error: err.message, cause: err })
|
||||
if (!this.isRetryableError(err) || tries === maxTries) {
|
||||
return err
|
||||
} else {
|
||||
console.log('Read Transaction failed. Retrying.')
|
||||
console.log(err.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
if (retry instanceof Error) {
|
||||
// Pass it to exit
|
||||
throw retry
|
||||
}
|
||||
if (retry === false) {
|
||||
// Retry for a timeout
|
||||
await new Promise((resolve) => setTimeout(resolve, backoffInterval))
|
||||
continue
|
||||
}
|
||||
return retry.result
|
||||
}
|
||||
} finally {
|
||||
if (!connection.managed) {
|
||||
// We need to relase in case it temporaty connection was used
|
||||
connection.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
release (id: string): void {
|
||||
const conn = this.connections.get(id)
|
||||
if (conn !== undefined) {
|
||||
conn.released = true
|
||||
this.connections.delete(id) // We need to delete first
|
||||
conn.release()
|
||||
}
|
||||
}
|
||||
|
||||
close (mgrId?: string): void {
|
||||
const cnts = this.connections
|
||||
for (const [k, conn] of Array.from(cnts.entries())) {
|
||||
if (mgrId !== undefined && conn.mgrId !== mgrId) {
|
||||
continue
|
||||
}
|
||||
cnts.delete(k)
|
||||
try {
|
||||
conn.release()
|
||||
} catch (err: any) {
|
||||
console.error('failed to release connection')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getConnection (id: string, mgrId: string, managed: boolean = true): ConnectionInfo {
|
||||
let conn = this.connections.get(id)
|
||||
if (conn === undefined) {
|
||||
conn = new ConnectionInfo(id, this.client, managed, mgrId)
|
||||
}
|
||||
if (managed) {
|
||||
this.connections.set(id, conn)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
private isRetryableError (err: any): boolean {
|
||||
const msg: string = err?.message ?? ''
|
||||
|
||||
return (
|
||||
err.code === '40001' || // Retry transaction
|
||||
err.code === '55P03' || // Lock not available
|
||||
err.code === 'CONNECTION_CLOSED' || // This error is thrown if the connection was closed without an error.
|
||||
err.code === 'CONNECTION_DESTROYED' || // This error is thrown for any queries that were pending when the timeout to sql.end({ timeout: X }) was reached. If the DB client is being closed completely retry will result in CONNECTION_ENDED which is not retried so should be fine.
|
||||
msg.includes('RETRY_SERIALIZABLE')
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
|
||||
}
|
||||
@@ -341,21 +341,6 @@
|
||||
"reviewCategory": "tools",
|
||||
"shouldPublish": true
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/measurements",
|
||||
"projectFolder": "packages/measurements",
|
||||
"shouldPublish": true
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/measurements-otlp",
|
||||
"projectFolder": "packages/measurements-otlp",
|
||||
"shouldPublish": true
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/postgres-base",
|
||||
"projectFolder": "packages/postgres-base",
|
||||
"shouldPublish": true
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/ui-test",
|
||||
"projectFolder": "packages/ui-test",
|
||||
|
||||
Reference in New Issue
Block a user