@codebaz/nextdoctor-agent 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.turbo/turbo-build.log +3 -0
  2. package/README.md +568 -0
  3. package/dist/detectors/__tests__/cold-start-threshold.test.d.ts +2 -0
  4. package/dist/detectors/__tests__/cold-start-threshold.test.d.ts.map +1 -0
  5. package/dist/detectors/__tests__/cold-start-threshold.test.js +156 -0
  6. package/dist/detectors/__tests__/cold-start-threshold.test.js.map +1 -0
  7. package/dist/detectors/__tests__/dynamic-route-candidate.test.d.ts +2 -0
  8. package/dist/detectors/__tests__/dynamic-route-candidate.test.d.ts.map +1 -0
  9. package/dist/detectors/__tests__/dynamic-route-candidate.test.js +318 -0
  10. package/dist/detectors/__tests__/dynamic-route-candidate.test.js.map +1 -0
  11. package/dist/detectors/__tests__/fetch-no-cache.test.d.ts +2 -0
  12. package/dist/detectors/__tests__/fetch-no-cache.test.d.ts.map +1 -0
  13. package/dist/detectors/__tests__/fetch-no-cache.test.js +199 -0
  14. package/dist/detectors/__tests__/fetch-no-cache.test.js.map +1 -0
  15. package/dist/detectors/base-detector.d.ts +17 -0
  16. package/dist/detectors/base-detector.d.ts.map +1 -0
  17. package/dist/detectors/base-detector.js +50 -0
  18. package/dist/detectors/base-detector.js.map +1 -0
  19. package/dist/detectors/cold-start-threshold.detector.d.ts +11 -0
  20. package/dist/detectors/cold-start-threshold.detector.d.ts.map +1 -0
  21. package/dist/detectors/cold-start-threshold.detector.js +87 -0
  22. package/dist/detectors/cold-start-threshold.detector.js.map +1 -0
  23. package/dist/detectors/dynamic-route-candidate.detector.d.ts +23 -0
  24. package/dist/detectors/dynamic-route-candidate.detector.d.ts.map +1 -0
  25. package/dist/detectors/dynamic-route-candidate.detector.js +96 -0
  26. package/dist/detectors/dynamic-route-candidate.detector.js.map +1 -0
  27. package/dist/detectors/fetch-no-cache.detector.d.ts +12 -0
  28. package/dist/detectors/fetch-no-cache.detector.d.ts.map +1 -0
  29. package/dist/detectors/fetch-no-cache.detector.js +178 -0
  30. package/dist/detectors/fetch-no-cache.detector.js.map +1 -0
  31. package/dist/detectors/index.d.ts +28 -0
  32. package/dist/detectors/index.d.ts.map +1 -0
  33. package/dist/detectors/index.js +97 -0
  34. package/dist/detectors/index.js.map +1 -0
  35. package/dist/detectors/types.d.ts +32 -0
  36. package/dist/detectors/types.d.ts.map +1 -0
  37. package/dist/detectors/types.js +2 -0
  38. package/dist/detectors/types.js.map +1 -0
  39. package/dist/index.d.ts +10 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +7 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/init.d.ts +133 -0
  44. package/dist/init.d.ts.map +1 -0
  45. package/dist/init.js +363 -0
  46. package/dist/init.js.map +1 -0
  47. package/dist/middleware.d.ts +10 -0
  48. package/dist/middleware.d.ts.map +1 -0
  49. package/dist/middleware.js +61 -0
  50. package/dist/middleware.js.map +1 -0
  51. package/dist/optimization.d.ts +43 -0
  52. package/dist/optimization.d.ts.map +1 -0
  53. package/dist/optimization.js +139 -0
  54. package/dist/optimization.js.map +1 -0
  55. package/dist/system-monitor.d.ts +124 -0
  56. package/dist/system-monitor.d.ts.map +1 -0
  57. package/dist/system-monitor.js +221 -0
  58. package/dist/system-monitor.js.map +1 -0
  59. package/dist/types.d.ts +61 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +14 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +55 -0
  64. package/src/detectors/__tests__/cold-start-threshold.test.ts +183 -0
  65. package/src/detectors/__tests__/dynamic-route-candidate.test.ts +365 -0
  66. package/src/detectors/__tests__/fetch-no-cache.test.ts +239 -0
  67. package/src/detectors/base-detector.ts +69 -0
  68. package/src/detectors/cold-start-threshold.detector.ts +95 -0
  69. package/src/detectors/dynamic-route-candidate.detector.ts +107 -0
  70. package/src/detectors/fetch-no-cache.detector.ts +204 -0
  71. package/src/detectors/index.ts +127 -0
  72. package/src/detectors/types.ts +38 -0
  73. package/src/index.ts +60 -0
  74. package/src/init.ts +424 -0
  75. package/src/middleware.ts +75 -0
  76. package/src/optimization.ts +164 -0
  77. package/src/system-monitor.ts +295 -0
  78. package/src/types.ts +66 -0
  79. package/tsconfig.json +11 -0
  80. package/tsconfig.tsbuildinfo +1 -0
package/src/init.ts ADDED
@@ -0,0 +1,424 @@
1
+ import { NodeSDK } from '@opentelemetry/sdk-node';
2
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
3
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-otlp-http';
4
+ import { trace } from '@opentelemetry/api';
5
+ import type {
6
+ NextDoctorConfig,
7
+ ExporterType,
8
+ RetryPolicy,
9
+ AgentHealth,
10
+ DetectedIssue,
11
+ LogLevel,
12
+ } from './types.js';
13
+ import { LogLevel as LogLevelEnum, ExporterType as ExporterTypeEnum } from './types.js';
14
+ import { SystemMonitor, type SystemMetrics } from './system-monitor.js';
15
+ import { detectionEngine } from './detectors/index.js';
16
+
17
+ class NextDoctorAgent {
18
+ private sdk: NodeSDK | null = null;
19
+ private config: NextDoctorConfig;
20
+ private systemMonitor: SystemMonitor;
21
+ private health: AgentHealth = {
22
+ initialized: false,
23
+ isHealthy: true,
24
+ exporterStatus: 'healthy',
25
+ bufferedSpans: 0,
26
+ errorCount: 0,
27
+ };
28
+ private retryPolicy: RetryPolicy;
29
+ private logLevel: LogLevel;
30
+ private initialized = false;
31
+ private detectedIssues: DetectedIssue[] = [];
32
+ private spansBuffer: any[] = [];
33
+ private lastAnalysisTime = Date.now();
34
+ private readonly analysisIntervalMs = 5000; // Analyze spans every 5 seconds
35
+ private startTime = Date.now();
36
+
37
+ constructor(config: NextDoctorConfig) {
38
+ this.validateConfig(config);
39
+ this.config = {
40
+ enabled: true,
41
+ serviceName: 'nextdoctor-app',
42
+ version: '0.1.0',
43
+ environment: 'production',
44
+ logLevel: LogLevelEnum.INFO,
45
+ samplingRate: 1.0,
46
+ timeout: 30000,
47
+ ...config,
48
+ };
49
+ this.logLevel = this.config.logLevel || LogLevelEnum.INFO;
50
+ this.retryPolicy = {
51
+ maxRetries: 5,
52
+ initialDelayMs: 100,
53
+ maxDelayMs: 30000,
54
+ backoffMultiplier: 2,
55
+ randomizationFactor: 0.1,
56
+ };
57
+ if (config.retryPolicy) {
58
+ this.retryPolicy = { ...this.retryPolicy, ...config.retryPolicy };
59
+ }
60
+ this.systemMonitor = new SystemMonitor((level, message, meta) => this.log(level, message, meta));
61
+ }
62
+
63
+ private validateConfig(config: Partial<NextDoctorConfig>): void {
64
+ if (!config.projectToken) {
65
+ throw new Error('NextDoctor: projectToken is required');
66
+ }
67
+ if (!config.endpoint) {
68
+ throw new Error('NextDoctor: endpoint is required');
69
+ }
70
+ if (config.samplingRate !== undefined && (config.samplingRate < 0 || config.samplingRate > 1)) {
71
+ throw new Error('NextDoctor: samplingRate must be between 0 and 1');
72
+ }
73
+ }
74
+
75
+ private log(level: LogLevel, message: string, meta?: any): void {
76
+ if (level < this.logLevel) return;
77
+
78
+ const timestamp = new Date().toISOString();
79
+ const prefix = `[NextDoctor ${timestamp}]`;
80
+
81
+ if (meta) {
82
+ console.log(`${prefix} ${message}`, meta);
83
+ } else {
84
+ console.log(`${prefix} ${message}`);
85
+ }
86
+ }
87
+
88
+ private createTraceExporter(): any {
89
+ const exporterConfig = (this.config.exporter || {}) as any;
90
+ const isVercel = exporterConfig.type === ExporterTypeEnum.VERCEL || this.isVercelEnvironment();
91
+
92
+ if (isVercel || !exporterConfig.url) {
93
+ this.log(LogLevelEnum.INFO, 'Using Vercel OTEL exporter');
94
+ return new OTLPTraceExporter({
95
+ url: this.config.endpoint,
96
+ headers: {
97
+ authorization: `Bearer ${this.config.projectToken}`,
98
+ 'content-type': 'application/json',
99
+ },
100
+ } as any);
101
+ }
102
+
103
+ return new OTLPTraceExporter({
104
+ url: exporterConfig.url || this.config.endpoint,
105
+ headers: {
106
+ ...exporterConfig.headers,
107
+ authorization: `Bearer ${this.config.projectToken}`,
108
+ 'content-type': 'application/json',
109
+ },
110
+ } as any);
111
+ }
112
+
113
+ private createResource(): any {
114
+ // Using any to avoid version mismatch issues with OpenTelemetry
115
+ const attributes: Record<string, string | number> = {
116
+ 'service.name': this.config.serviceName || 'nextdoctor-app',
117
+ 'service.version': this.config.version || '0.1.0',
118
+ 'deployment.environment': this.config.environment || 'production',
119
+ 'service.instance.id': this.generateInstanceId(),
120
+ };
121
+
122
+ return { attributes };
123
+ }
124
+
125
+ private generateInstanceId(): string {
126
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
127
+ }
128
+
129
+ private isVercelEnvironment(): boolean {
130
+ return !!(
131
+ typeof process !== 'undefined' &&
132
+ process.env.VERCEL === '1'
133
+ );
134
+ }
135
+
136
+ async initialize(): Promise<void> {
137
+ if (this.initialized) {
138
+ this.log(LogLevelEnum.WARN, 'NextDoctor agent already initialized');
139
+ return;
140
+ }
141
+
142
+ if (!this.config.enabled) {
143
+ this.log(LogLevelEnum.INFO, 'NextDoctor agent is disabled');
144
+ return;
145
+ }
146
+
147
+ try {
148
+ try {
149
+ this.log(LogLevelEnum.DEBUG, 'Initializing NextDoctor agent...');
150
+
151
+ const traceExporter = this.createTraceExporter();
152
+ const resource = this.createResource();
153
+
154
+ this.sdk = new NodeSDK({
155
+ resource: resource as any,
156
+ traceExporter,
157
+ instrumentations: [
158
+ getNodeAutoInstrumentations({
159
+ '@opentelemetry/instrumentation-http': {
160
+ enabled: true,
161
+ responseHook: (span: any, response: any) => {
162
+ if (response.statusCode) {
163
+ span.setAttribute('http.response.status', response.statusCode);
164
+ }
165
+ },
166
+ },
167
+ '@opentelemetry/instrumentation-fs': {
168
+ enabled: true,
169
+ },
170
+ '@opentelemetry/instrumentation-express': {
171
+ enabled: true,
172
+ },
173
+ }),
174
+ ],
175
+ });
176
+
177
+ await this.sdk.start();
178
+ this.health.initialized = true;
179
+ this.health.isHealthy = true;
180
+ this.log(LogLevelEnum.INFO, 'NextDoctor agent initialized successfully');
181
+ } catch (retryError: any) {
182
+ this.health.errorCount++;
183
+ if (this.health.errorCount < this.retryPolicy.maxRetries) {
184
+ this.log(LogLevelEnum.WARN, `Initialization attempt failed, retrying... (${this.health.errorCount}/${this.retryPolicy.maxRetries})`, {
185
+ error: retryError?.message,
186
+ });
187
+ await new Promise(resolve => setTimeout(resolve, 100 * Math.pow(2, this.health.errorCount)));
188
+ throw retryError;
189
+ }
190
+ throw retryError;
191
+ }
192
+
193
+ this.initialized = true;
194
+ } catch (error) {
195
+ this.health.initialized = false;
196
+ this.health.isHealthy = false;
197
+ this.health.exporterStatus = 'unreachable';
198
+ this.health.errorCount++;
199
+
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ this.log(LogLevelEnum.ERROR, 'Failed to initialize NextDoctor agent', { error: message });
202
+ throw error;
203
+ }
204
+ }
205
+
206
+ private analyzeSpan(span: any): void {
207
+ // Buffer span for batch analysis by detection engine
208
+ try {
209
+ if (!span) return;
210
+
211
+ // Add span to buffer with timestamp
212
+ this.spansBuffer.push({
213
+ ...span,
214
+ bufferedAt: Date.now(),
215
+ });
216
+
217
+ // Keep buffer size manageable (max 1000 spans)
218
+ if (this.spansBuffer.length > 1000) {
219
+ this.spansBuffer = this.spansBuffer.slice(-500);
220
+ }
221
+
222
+ // Run detection engine periodically
223
+ const now = Date.now();
224
+ if (now - this.lastAnalysisTime >= this.analysisIntervalMs) {
225
+ this.runDetectionEngine();
226
+ this.lastAnalysisTime = now;
227
+ }
228
+ } catch (error) {
229
+ this.log(LogLevelEnum.DEBUG, 'Error buffering span', { error });
230
+ }
231
+ }
232
+
233
+ private runDetectionEngine(): void {
234
+ // Run detection engine on buffered spans
235
+ try {
236
+ if (this.spansBuffer.length === 0) return;
237
+
238
+ // Extract context from spans
239
+ const firstSpan = this.spansBuffer[0];
240
+ const route = firstSpan?.attributes?.['http.route'] ||
241
+ firstSpan?.attributes?.['http.url'] ||
242
+ 'unknown';
243
+ const runtime = (process.env.NEXT_RUNTIME || 'nodejs') as 'nodejs' | 'edge';
244
+
245
+ // Calculate startup time if this is the first request
246
+ const startupTimeMs = Date.now() - this.startTime;
247
+
248
+ // Analyze spans with detection engine
249
+ const detectedIssues = detectionEngine.analyzeSpans(this.spansBuffer, {
250
+ route: String(route),
251
+ runtime,
252
+ startupTimeMs: startupTimeMs < 30000 ? startupTimeMs : undefined, // Only report first 30s
253
+ });
254
+
255
+ // Merge with existing detections (detection engine handles deduplication internally)
256
+ if (detectedIssues.length > 0) {
257
+ this.detectedIssues.push(...detectedIssues);
258
+
259
+ // Keep detected issues list manageable (max 500 most recent)
260
+ if (this.detectedIssues.length > 500) {
261
+ this.detectedIssues = this.detectedIssues.slice(-250);
262
+ }
263
+
264
+ this.log(LogLevelEnum.DEBUG, `Detection engine found ${detectedIssues.length} issues`, {
265
+ issues: detectedIssues.map(i => ({ id: i.id, severity: i.severity })),
266
+ });
267
+ }
268
+
269
+ // Clear buffer after analysis to avoid re-analyzing
270
+ this.spansBuffer = [];
271
+ } catch (error) {
272
+ this.log(LogLevelEnum.DEBUG, 'Error running detection engine', { error });
273
+ }
274
+ }
275
+
276
+ async shutdown(): Promise<void> {
277
+ if (!this.sdk) {
278
+ this.log(LogLevelEnum.WARN, 'NextDoctor agent not initialized');
279
+ return;
280
+ }
281
+
282
+ try {
283
+ // Run detection engine one final time for any remaining buffered spans
284
+ if (this.spansBuffer.length > 0) {
285
+ this.runDetectionEngine();
286
+ }
287
+
288
+ this.log(LogLevelEnum.INFO, 'Shutting down NextDoctor agent...');
289
+ await this.sdk.shutdown();
290
+ this.initialized = false;
291
+ this.health.initialized = false;
292
+ this.log(LogLevelEnum.INFO, 'NextDoctor agent shut down successfully');
293
+ } catch (error) {
294
+ const message = error instanceof Error ? error.message : String(error);
295
+ this.log(LogLevelEnum.ERROR, 'Error during shutdown', { error: message });
296
+ throw error;
297
+ }
298
+ }
299
+
300
+ getHealth(): AgentHealth {
301
+ return {
302
+ ...this.health,
303
+ errorCount: this.health.errorCount,
304
+ };
305
+ }
306
+
307
+ getDetectedIssues(): DetectedIssue[] {
308
+ return this.detectedIssues;
309
+ }
310
+
311
+ clearDetectedIssues(): void {
312
+ this.detectedIssues = [];
313
+ }
314
+
315
+ reportCustomMetric(name: string, value: number, attributes?: Record<string, any>): void {
316
+ if (!this.initialized) {
317
+ this.log(LogLevelEnum.WARN, 'Agent not initialized, metric not reported', { name });
318
+ return;
319
+ }
320
+
321
+ const tracer = trace.getTracer('nextdoctor');
322
+ const span = tracer.startSpan(`custom-metric: ${name}`);
323
+ span.setAttribute('metric.name', name);
324
+ span.setAttribute('metric.value', value);
325
+ if (attributes) {
326
+ Object.entries(attributes).forEach(([key, val]) => {
327
+ span.setAttribute(`metric.${key}`, val);
328
+ });
329
+ }
330
+ span.end();
331
+ }
332
+
333
+ private getUptime(): number {
334
+ return Date.now() - this.startTime;
335
+ }
336
+
337
+ getSystemMetrics(): SystemMetrics {
338
+ return this.systemMonitor.getSystemMetrics();
339
+ }
340
+
341
+ getSystemHealth(cpuThreshold: number = 80, memThreshold: number = 85) {
342
+ return this.systemMonitor.getSystemHealth(cpuThreshold, memThreshold);
343
+ }
344
+
345
+ getSystemSummary() {
346
+ return this.systemMonitor.getSummary();
347
+ }
348
+
349
+ getStats() {
350
+ return {
351
+ uptime: this.getUptime(),
352
+ initialized: this.initialized,
353
+ health: this.getHealth(),
354
+ detectedIssues: this.getDetectedIssues(),
355
+ system: this.getSystemSummary(),
356
+ };
357
+ }
358
+ }
359
+
360
+ // Global singleton instance
361
+ let agentInstance: NextDoctorAgent | null = null;
362
+
363
+ export async function initNextDoctor(config: NextDoctorConfig): Promise<void> {
364
+ if (agentInstance) {
365
+ console.warn('NextDoctor agent already initialized');
366
+ return;
367
+ }
368
+
369
+ agentInstance = new NextDoctorAgent(config);
370
+ await agentInstance.initialize();
371
+ }
372
+
373
+ export async function shutdownNextDoctor(): Promise<void> {
374
+ if (!agentInstance) {
375
+ console.warn('NextDoctor agent not initialized');
376
+ return;
377
+ }
378
+
379
+ await agentInstance.shutdown();
380
+ agentInstance = null;
381
+ }
382
+
383
+ export function getNextDoctorAgent(): NextDoctorAgent | null {
384
+ return agentInstance;
385
+ }
386
+
387
+ export function reportMetric(name: string, value: number, attributes?: Record<string, any>): void {
388
+ if (!agentInstance) {
389
+ console.warn('NextDoctor agent not initialized, metric not reported');
390
+ return;
391
+ }
392
+
393
+ agentInstance.reportCustomMetric(name, value, attributes);
394
+ }
395
+
396
+ export function getHealthStatus(): AgentHealth | null {
397
+ return agentInstance?.getHealth() || null;
398
+ }
399
+
400
+ export function getDetectedIssues(): DetectedIssue[] {
401
+ return agentInstance?.getDetectedIssues() || [];
402
+ }
403
+
404
+ export function getSystemMetrics(): SystemMetrics | null {
405
+ return agentInstance?.getSystemMetrics() || null;
406
+ }
407
+
408
+ export function getSystemHealth(cpuThreshold: number = 80, memThreshold: number = 85) {
409
+ if (!agentInstance) {
410
+ console.warn('NextDoctor agent not initialized, system health unavailable');
411
+ return null;
412
+ }
413
+
414
+ return agentInstance.getSystemHealth(cpuThreshold, memThreshold);
415
+ }
416
+
417
+ export function getSystemSummary() {
418
+ if (!agentInstance) {
419
+ console.warn('NextDoctor agent not initialized, system summary unavailable');
420
+ return null;
421
+ }
422
+
423
+ return agentInstance.getSystemSummary();
424
+ }
@@ -0,0 +1,75 @@
1
+ import { getNextDoctorAgent, reportMetric } from './init.js';
2
+
3
+ /**
4
+ * Middleware para Next.js API routes
5
+ * Captura automaticamente tempo de resposta e status
6
+ */
7
+ export function withNextDoctorMonitoring(
8
+ handler: (req: any, res: any) => Promise<void>,
9
+ ) {
10
+ return async (req: any, res: any) => {
11
+ const startTime = Date.now();
12
+ const originalStatusCode = res.statusCode;
13
+
14
+ // Wrap response.end to capture status
15
+ const originalEnd = res.end;
16
+ res.end = function (...args: any[]) {
17
+ const duration = Date.now() - startTime;
18
+ const agent = getNextDoctorAgent();
19
+
20
+ if (agent) {
21
+ reportMetric('api.request.duration', duration, {
22
+ method: req.method,
23
+ path: req.url,
24
+ status: res.statusCode,
25
+ });
26
+ }
27
+
28
+ return originalEnd.apply(res, args);
29
+ };
30
+
31
+ try {
32
+ await handler(req, res);
33
+ } catch (error) {
34
+ reportMetric('api.request.error', 1, {
35
+ method: req.method,
36
+ path: req.url,
37
+ error: error instanceof Error ? error.message : String(error),
38
+ });
39
+ throw error;
40
+ }
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Monitora operações async com timing e error tracking
46
+ */
47
+ export async function withNextDoctorTiming<T>(
48
+ name: string,
49
+ fn: () => Promise<T>,
50
+ meta?: Record<string, any>,
51
+ ): Promise<T> {
52
+ const startTime = Date.now();
53
+
54
+ try {
55
+ const result = await fn();
56
+ const duration = Date.now() - startTime;
57
+
58
+ reportMetric(`operation.${name}.duration`, duration, {
59
+ status: 'success',
60
+ ...meta,
61
+ });
62
+
63
+ return result;
64
+ } catch (error) {
65
+ const duration = Date.now() - startTime;
66
+
67
+ reportMetric(`operation.${name}.duration`, duration, {
68
+ status: 'error',
69
+ error: error instanceof Error ? error.message : String(error),
70
+ ...meta,
71
+ });
72
+
73
+ throw error;
74
+ }
75
+ }
@@ -0,0 +1,164 @@
1
+ import type { NextDoctorConfig } from './types.js';
2
+
3
+ /**
4
+ * Intelligent sampler para controlar volume de traces
5
+ */
6
+ export class IntelligentSampler {
7
+ private samplingRate: number;
8
+ private bucketsPerSecond = 1000;
9
+ private lastAdjustmentTime = Date.now();
10
+ private spanCount = 0;
11
+ private errorCount = 0;
12
+
13
+ constructor(initialRate: number = 1.0) {
14
+ this.samplingRate = Math.max(0, Math.min(1, initialRate));
15
+ }
16
+
17
+ shouldSample(spanName?: string): boolean {
18
+ // Always sample errors
19
+ if (spanName?.includes('error')) {
20
+ return true;
21
+ }
22
+
23
+ return Math.random() < this.samplingRate;
24
+ }
25
+
26
+ recordSpan(isError: boolean = false): void {
27
+ this.spanCount++;
28
+ if (isError) {
29
+ this.errorCount++;
30
+ }
31
+
32
+ // Adjust sampling rate every second
33
+ const now = Date.now();
34
+ if (now - this.lastAdjustmentTime > 1000) {
35
+ this.adjustSamplingRate();
36
+ this.lastAdjustmentTime = now;
37
+ this.spanCount = 0;
38
+ this.errorCount = 0;
39
+ }
40
+ }
41
+
42
+ private adjustSamplingRate(): void {
43
+ // If too many spans, reduce sampling
44
+ if (this.spanCount > this.bucketsPerSecond * 2) {
45
+ this.samplingRate *= 0.9;
46
+ }
47
+ // If too few spans, increase sampling
48
+ else if (this.spanCount < this.bucketsPerSecond * 0.5) {
49
+ this.samplingRate *= 1.1;
50
+ }
51
+
52
+ this.samplingRate = Math.max(0, Math.min(1, this.samplingRate));
53
+ }
54
+
55
+ getSamplingRate(): number {
56
+ return this.samplingRate;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Batch processor para otimização de memória
62
+ */
63
+ export class BatchProcessor {
64
+ private batch: any[] = [];
65
+ private batchSize: number;
66
+ private batchTimeoutMs: number;
67
+ private timeoutId: NodeJS.Timeout | null = null;
68
+ private onFlush: (batch: any[]) => Promise<void>;
69
+
70
+ constructor(
71
+ batchSize: number = 100,
72
+ batchTimeoutMs: number = 5000,
73
+ onFlush: (batch: any[]) => Promise<void>,
74
+ ) {
75
+ this.batchSize = batchSize;
76
+ this.batchTimeoutMs = batchTimeoutMs;
77
+ this.onFlush = onFlush;
78
+ }
79
+
80
+ add(item: any): void {
81
+ this.batch.push(item);
82
+
83
+ if (this.batch.length >= this.batchSize) {
84
+ this.flush();
85
+ } else if (!this.timeoutId) {
86
+ this.timeoutId = setTimeout(() => this.flush(), this.batchTimeoutMs);
87
+ }
88
+ }
89
+
90
+ async flush(): Promise<void> {
91
+ if (this.timeoutId) {
92
+ clearTimeout(this.timeoutId);
93
+ this.timeoutId = null;
94
+ }
95
+
96
+ if (this.batch.length === 0) {
97
+ return;
98
+ }
99
+
100
+ const itemsToFlush = [...this.batch];
101
+ this.batch = [];
102
+
103
+ try {
104
+ await this.onFlush(itemsToFlush);
105
+ } catch (error) {
106
+ console.error('Error flushing batch:', error);
107
+ // Re-add items if flush fails
108
+ this.batch = [...itemsToFlush, ...this.batch];
109
+ }
110
+ }
111
+
112
+ async destroy(): Promise<void> {
113
+ await this.flush();
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Circuit breaker para proteção contra exporters degradados
119
+ */
120
+ export class CircuitBreaker {
121
+ private failureCount = 0;
122
+ private failureThreshold = 5;
123
+ private resetTimeout = 60000; // 1 minute
124
+ private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
125
+ private lastFailureTime = 0;
126
+
127
+ async execute<T>(fn: () => Promise<T>): Promise<T | null> {
128
+ if (this.state === 'OPEN') {
129
+ if (Date.now() - this.lastFailureTime > this.resetTimeout) {
130
+ this.state = 'HALF_OPEN';
131
+ } else {
132
+ console.warn('Circuit breaker is OPEN, request rejected');
133
+ return null;
134
+ }
135
+ }
136
+
137
+ try {
138
+ const result = await fn();
139
+ if (this.state === 'HALF_OPEN') {
140
+ this.reset();
141
+ }
142
+ return result;
143
+ } catch (error) {
144
+ this.failureCount++;
145
+ this.lastFailureTime = Date.now();
146
+
147
+ if (this.failureCount >= this.failureThreshold) {
148
+ this.state = 'OPEN';
149
+ console.error('Circuit breaker opened due to excessive failures');
150
+ }
151
+
152
+ throw error;
153
+ }
154
+ }
155
+
156
+ private reset(): void {
157
+ this.failureCount = 0;
158
+ this.state = 'CLOSED';
159
+ }
160
+
161
+ getState(): string {
162
+ return this.state;
163
+ }
164
+ }