BREAKING CHANGE(core): Refactor to v3: introduce modular core/domain architecture, plugin system, observability and strict TypeScript configuration; remove legacy classes

This commit is contained in:
2025-11-29 18:32:00 +00:00
parent 53673e37cb
commit 7e89b6ebf5
68 changed files with 17020 additions and 720 deletions

View File

@@ -0,0 +1,543 @@
/**
* Label map for metrics
*/
export type Labels = Record<string, string | number>;
/**
* Metric types
*/
export enum MetricType {
COUNTER = 'counter',
GAUGE = 'gauge',
HISTOGRAM = 'histogram',
}
/**
* Histogram bucket configuration
*/
export interface HistogramBuckets {
buckets: number[];
counts: Map<number, number>;
sum: number;
count: number;
}
/**
* Base metric class
*/
abstract class Metric {
constructor(
public readonly name: string,
public readonly type: MetricType,
public readonly help: string,
public readonly labels: string[] = []
) {}
abstract getValue(labels?: Labels): number | HistogramBuckets;
abstract reset(): void;
}
/**
* Counter metric - monotonically increasing value
*
* @example
* ```typescript
* const requestCounter = new Counter('http_requests_total', 'Total HTTP requests', ['method', 'status']);
* requestCounter.inc({ method: 'GET', status: '200' });
* requestCounter.inc({ method: 'POST', status: '201' }, 5);
* ```
*/
export class Counter extends Metric {
private values: Map<string, number> = new Map();
constructor(name: string, help: string, labels: string[] = []) {
super(name, MetricType.COUNTER, help, labels);
}
/**
* Increment counter
*/
inc(labels: Labels = {}, value: number = 1): void {
if (value < 0) {
throw new Error('Counter can only be incremented with positive values');
}
const key = this.getKey(labels);
const current = this.values.get(key) || 0;
this.values.set(key, current + value);
}
/**
* Get current value
*/
getValue(labels: Labels = {}): number {
const key = this.getKey(labels);
return this.values.get(key) || 0;
}
/**
* Reset counter
*/
reset(): void {
this.values.clear();
}
/**
* Get all values with labels
*/
getAll(): Array<{ labels: Labels; value: number }> {
const results: Array<{ labels: Labels; value: number }> = [];
for (const [key, value] of this.values.entries()) {
const labels = this.parseKey(key);
results.push({ labels, value });
}
return results;
}
private getKey(labels: Labels): string {
const labelStr = this.labels
.map((label) => `${label}=${labels[label] || ''}`)
.join(',');
return labelStr || 'default';
}
private parseKey(key: string): Labels {
if (key === 'default') return {};
const labels: Labels = {};
const pairs = key.split(',');
for (const pair of pairs) {
const [name, value] = pair.split('=');
if (name && value !== undefined) {
labels[name] = value;
}
}
return labels;
}
}
/**
* Gauge metric - value that can go up and down
*
* @example
* ```typescript
* const activeConnections = new Gauge('active_connections', 'Number of active connections');
* activeConnections.set(42);
* activeConnections.inc();
* activeConnections.dec(5);
* ```
*/
export class Gauge extends Metric {
private values: Map<string, number> = new Map();
constructor(name: string, help: string, labels: string[] = []) {
super(name, MetricType.GAUGE, help, labels);
}
/**
* Set gauge to a specific value
*/
set(value: number, labels: Labels = {}): void {
const key = this.getKey(labels);
this.values.set(key, value);
}
/**
* Increment gauge
*/
inc(labels: Labels = {}, value: number = 1): void {
const key = this.getKey(labels);
const current = this.values.get(key) || 0;
this.values.set(key, current + value);
}
/**
* Decrement gauge
*/
dec(labels: Labels = {}, value: number = 1): void {
this.inc(labels, -value);
}
/**
* Get current value
*/
getValue(labels: Labels = {}): number {
const key = this.getKey(labels);
return this.values.get(key) || 0;
}
/**
* Reset gauge
*/
reset(): void {
this.values.clear();
}
/**
* Get all values with labels
*/
getAll(): Array<{ labels: Labels; value: number }> {
const results: Array<{ labels: Labels; value: number }> = [];
for (const [key, value] of this.values.entries()) {
const labels = this.parseKey(key);
results.push({ labels, value });
}
return results;
}
private getKey(labels: Labels): string {
const labelStr = this.labels
.map((label) => `${label}=${labels[label] || ''}`)
.join(',');
return labelStr || 'default';
}
private parseKey(key: string): Labels {
if (key === 'default') return {};
const labels: Labels = {};
const pairs = key.split(',');
for (const pair of pairs) {
const [name, value] = pair.split('=');
if (name && value !== undefined) {
labels[name] = value;
}
}
return labels;
}
}
/**
* Histogram metric - tracks distribution of values
*
* @example
* ```typescript
* const latency = new Histogram('request_duration_seconds', 'Request latency', ['endpoint'], [0.1, 0.5, 1, 2, 5]);
* latency.observe(0.234, { endpoint: '/api/users' });
* latency.observe(1.567, { endpoint: '/api/users' });
* ```
*/
export class Histogram extends Metric {
private buckets: number[];
private values: Map<string, HistogramBuckets> = new Map();
constructor(
name: string,
help: string,
labels: string[] = [],
buckets: number[] = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
) {
super(name, MetricType.HISTOGRAM, help, labels);
this.buckets = [...buckets].sort((a, b) => a - b);
}
/**
* Observe a value
*/
observe(value: number, labels: Labels = {}): void {
const key = this.getKey(labels);
let bucketData = this.values.get(key);
if (!bucketData) {
bucketData = {
buckets: this.buckets,
counts: new Map(this.buckets.map((b) => [b, 0])),
sum: 0,
count: 0,
};
this.values.set(key, bucketData);
}
// Update bucket counts
for (const bucket of this.buckets) {
if (value <= bucket) {
const current = bucketData.counts.get(bucket) || 0;
bucketData.counts.set(bucket, current + 1);
}
}
bucketData.sum += value;
bucketData.count++;
}
/**
* Get histogram data
*/
getValue(labels: Labels = {}): HistogramBuckets {
const key = this.getKey(labels);
return (
this.values.get(key) || {
buckets: this.buckets,
counts: new Map(this.buckets.map((b) => [b, 0])),
sum: 0,
count: 0,
}
);
}
/**
* Reset histogram
*/
reset(): void {
this.values.clear();
}
/**
* Get all histogram data with labels
*/
getAll(): Array<{ labels: Labels; value: HistogramBuckets }> {
const results: Array<{ labels: Labels; value: HistogramBuckets }> = [];
for (const [key, value] of this.values.entries()) {
const labels = this.parseKey(key);
results.push({ labels, value });
}
return results;
}
private getKey(labels: Labels): string {
const labelStr = this.labels
.map((label) => `${label}=${labels[label] || ''}`)
.join(',');
return labelStr || 'default';
}
private parseKey(key: string): Labels {
if (key === 'default') return {};
const labels: Labels = {};
const pairs = key.split(',');
for (const pair of pairs) {
const [name, value] = pair.split('=');
if (name && value !== undefined) {
labels[name] = value;
}
}
return labels;
}
}
/**
* Metrics registry
*/
export class MetricsRegistry {
private metrics: Map<string, Metric> = new Map();
/**
* Register a metric
*/
register(metric: Metric): void {
if (this.metrics.has(metric.name)) {
throw new Error(`Metric ${metric.name} already registered`);
}
this.metrics.set(metric.name, metric);
}
/**
* Get a metric by name
*/
get(name: string): Metric | undefined {
return this.metrics.get(name);
}
/**
* Get all metrics
*/
getAll(): Metric[] {
return Array.from(this.metrics.values());
}
/**
* Clear all metrics
*/
clear(): void {
this.metrics.clear();
}
/**
* Reset all metric values
*/
reset(): void {
for (const metric of this.metrics.values()) {
metric.reset();
}
}
/**
* Export metrics in Prometheus text format
*/
export(): string {
const lines: string[] = [];
for (const metric of this.metrics.values()) {
// Add help text
lines.push(`# HELP ${metric.name} ${metric.help}`);
lines.push(`# TYPE ${metric.name} ${metric.type}`);
if (metric instanceof Counter || metric instanceof Gauge) {
const all = metric.getAll();
for (const { labels, value } of all) {
const labelStr = Object.entries(labels)
.map(([k, v]) => `${k}="${v}"`)
.join(',');
const metricLine = labelStr
? `${metric.name}{${labelStr}} ${value}`
: `${metric.name} ${value}`;
lines.push(metricLine);
}
} else if (metric instanceof Histogram) {
const all = metric.getAll();
for (const { labels, value } of all) {
const labelStr = Object.entries(labels)
.map(([k, v]) => `${k}="${v}"`)
.join(',');
const labelPrefix = labelStr ? `{${labelStr}}` : '';
// Export bucket counts
for (const [bucket, count] of value.counts.entries()) {
const bucketLabel = labelStr
? `{${labelStr},le="${bucket}"}`
: `{le="${bucket}"}`;
lines.push(`${metric.name}_bucket${bucketLabel} ${count}`);
}
// Export +Inf bucket
const infLabel = labelStr ? `{${labelStr},le="+Inf"}` : `{le="+Inf"}`;
lines.push(`${metric.name}_bucket${infLabel} ${value.count}`);
// Export sum and count
lines.push(`${metric.name}_sum${labelPrefix} ${value.sum}`);
lines.push(`${metric.name}_count${labelPrefix} ${value.count}`);
}
}
lines.push(''); // Empty line between metrics
}
return lines.join('\n');
}
}
/**
* Default metrics registry
*/
export const defaultRegistry = new MetricsRegistry();
/**
* Metrics collector for Elasticsearch client
*/
export class MetricsCollector {
public readonly registry: MetricsRegistry;
// Standard metrics
public readonly requestsTotal: Counter;
public readonly requestDuration: Histogram;
public readonly requestErrors: Counter;
public readonly activeConnections: Gauge;
public readonly bulkOperations: Counter;
public readonly bulkDocuments: Counter;
public readonly retries: Counter;
constructor(registry: MetricsRegistry = defaultRegistry) {
this.registry = registry;
// Initialize standard metrics
this.requestsTotal = new Counter(
'elasticsearch_requests_total',
'Total number of Elasticsearch requests',
['operation', 'index']
);
this.registry.register(this.requestsTotal);
this.requestDuration = new Histogram(
'elasticsearch_request_duration_seconds',
'Elasticsearch request duration in seconds',
['operation', 'index']
);
this.registry.register(this.requestDuration);
this.requestErrors = new Counter(
'elasticsearch_request_errors_total',
'Total number of Elasticsearch request errors',
['operation', 'index', 'error_code']
);
this.registry.register(this.requestErrors);
this.activeConnections = new Gauge(
'elasticsearch_active_connections',
'Number of active Elasticsearch connections'
);
this.registry.register(this.activeConnections);
this.bulkOperations = new Counter(
'elasticsearch_bulk_operations_total',
'Total number of bulk operations',
['index']
);
this.registry.register(this.bulkOperations);
this.bulkDocuments = new Counter(
'elasticsearch_bulk_documents_total',
'Total number of documents in bulk operations',
['index', 'status']
);
this.registry.register(this.bulkDocuments);
this.retries = new Counter(
'elasticsearch_retries_total',
'Total number of retry attempts',
['operation']
);
this.registry.register(this.retries);
}
/**
* Create a custom counter
*/
counter(name: string, help: string, labels?: string[]): Counter {
const counter = new Counter(name, help, labels);
this.registry.register(counter);
return counter;
}
/**
* Create a custom gauge
*/
gauge(name: string, help: string, labels?: string[]): Gauge {
const gauge = new Gauge(name, help, labels);
this.registry.register(gauge);
return gauge;
}
/**
* Create a custom histogram
*/
histogram(name: string, help: string, labels?: string[], buckets?: number[]): Histogram {
const histogram = new Histogram(name, help, labels, buckets);
this.registry.register(histogram);
return histogram;
}
/**
* Export all metrics in Prometheus format
*/
export(): string {
return this.registry.export();
}
}
/**
* Default metrics collector
*/
export const defaultMetricsCollector = new MetricsCollector();