@oas-tools/oas-telemetry 0.7.1 → 0.8.0-alpha.0

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 (167) hide show
  1. package/.env.example +21 -2
  2. package/README.md +1 -2
  3. package/dist/cjs/config/bootConfig.cjs +19 -14
  4. package/dist/cjs/config/config.cjs +112 -125
  5. package/dist/cjs/config/config.types.cjs +1 -4
  6. package/dist/cjs/docs/openapi.yaml +158 -4
  7. package/dist/cjs/index.cjs +27 -30
  8. package/dist/cjs/routesManager.cjs +62 -70
  9. package/dist/cjs/telemetry/custom-implementations/exporters/InMemoryDbLogExporter.cjs +202 -190
  10. package/dist/cjs/telemetry/custom-implementations/exporters/InMemoryDbMetricExporter.cjs +204 -99
  11. package/dist/cjs/telemetry/custom-implementations/exporters/InMemoryDbSpanExporter.cjs +152 -116
  12. package/dist/cjs/telemetry/custom-implementations/instrumentations/logsInstrumentation.cjs +92 -0
  13. package/dist/cjs/telemetry/custom-implementations/metrics/tsdb/Chunk.cjs +159 -0
  14. package/dist/cjs/telemetry/custom-implementations/metrics/tsdb/Series.cjs +168 -0
  15. package/dist/cjs/telemetry/custom-implementations/metrics/tsdb/SeriesRegistry.cjs +389 -0
  16. package/dist/cjs/telemetry/custom-implementations/metrics/tsdb/types.cjs +2 -0
  17. package/dist/cjs/telemetry/custom-implementations/metrics/tsdb/utils.cjs +77 -0
  18. package/dist/cjs/telemetry/custom-implementations/processors/dynamicMultiLogProcessor.cjs +65 -63
  19. package/dist/cjs/telemetry/custom-implementations/processors/dynamicMultiSpanProcessor.cjs +63 -62
  20. package/dist/cjs/telemetry/custom-implementations/utils/circular.cjs +47 -47
  21. package/dist/cjs/telemetry/custom-implementations/utils/storagePath.cjs +39 -0
  22. package/dist/cjs/telemetry/custom-implementations/wrappers.cjs +141 -138
  23. package/dist/cjs/telemetry/initializeTelemetry.cjs +35 -91
  24. package/dist/cjs/telemetry/telemetryConfigurator.cjs +70 -72
  25. package/dist/cjs/telemetry/telemetryRegistry.cjs +45 -31
  26. package/dist/cjs/tlm-ai/agent.cjs +49 -64
  27. package/dist/cjs/tlm-ai/aiController.cjs +54 -76
  28. package/dist/cjs/tlm-ai/aiRoutes.cjs +17 -20
  29. package/dist/cjs/tlm-ai/aiService.cjs +91 -95
  30. package/dist/cjs/tlm-ai/tools.cjs +177 -174
  31. package/dist/cjs/tlm-auth/authController.cjs +80 -123
  32. package/dist/cjs/tlm-auth/authMiddleware.cjs +25 -30
  33. package/dist/cjs/tlm-auth/authRoutes.cjs +11 -14
  34. package/dist/cjs/tlm-log/logController.cjs +171 -116
  35. package/dist/cjs/tlm-log/logRoutes.cjs +20 -20
  36. package/dist/cjs/tlm-metric/metricsController.cjs +211 -121
  37. package/dist/cjs/tlm-metric/metricsRoutes.cjs +23 -20
  38. package/dist/cjs/tlm-plugin/pluginController.cjs +128 -140
  39. package/dist/cjs/tlm-plugin/pluginProcess.cjs +89 -94
  40. package/dist/cjs/tlm-plugin/pluginRoutes.cjs +11 -14
  41. package/dist/cjs/tlm-plugin/pluginService.cjs +73 -74
  42. package/dist/cjs/tlm-trace/traceController.cjs +169 -117
  43. package/dist/cjs/tlm-trace/traceRoutes.cjs +20 -20
  44. package/dist/cjs/tlm-ui/uiRoutes.cjs +63 -32
  45. package/dist/cjs/tlm-util/utilController.cjs +68 -70
  46. package/dist/cjs/tlm-util/utilRoutes.cjs +51 -63
  47. package/dist/cjs/types/index.cjs +2 -5
  48. package/dist/cjs/utils/logger.cjs +38 -43
  49. package/dist/cjs/utils/regexUtils.cjs +22 -22
  50. package/dist/esm/config/bootConfig.js +6 -0
  51. package/dist/esm/config/config.js +1 -2
  52. package/dist/esm/docs/openapi.yaml +158 -4
  53. package/dist/esm/index.js +9 -8
  54. package/dist/esm/routesManager.js +6 -10
  55. package/dist/esm/telemetry/custom-implementations/exporters/InMemoryDbLogExporter.js +47 -8
  56. package/dist/esm/telemetry/custom-implementations/exporters/InMemoryDbMetricExporter.js +164 -48
  57. package/dist/esm/telemetry/custom-implementations/exporters/InMemoryDbSpanExporter.js +69 -29
  58. package/dist/esm/telemetry/custom-implementations/instrumentations/logsInstrumentation.js +85 -0
  59. package/dist/esm/telemetry/custom-implementations/metrics/tsdb/Chunk.js +155 -0
  60. package/dist/esm/telemetry/custom-implementations/metrics/tsdb/Series.js +164 -0
  61. package/dist/esm/telemetry/custom-implementations/metrics/tsdb/SeriesRegistry.js +382 -0
  62. package/dist/esm/telemetry/custom-implementations/metrics/tsdb/types.js +1 -0
  63. package/dist/esm/telemetry/custom-implementations/metrics/tsdb/utils.js +74 -0
  64. package/dist/esm/telemetry/custom-implementations/processors/dynamicMultiLogProcessor.js +2 -1
  65. package/dist/esm/telemetry/custom-implementations/processors/dynamicMultiSpanProcessor.js +1 -1
  66. package/dist/esm/telemetry/custom-implementations/utils/storagePath.js +33 -0
  67. package/dist/esm/telemetry/custom-implementations/wrappers.js +5 -2
  68. package/dist/esm/telemetry/initializeTelemetry.js +27 -69
  69. package/dist/esm/telemetry/telemetryConfigurator.js +42 -40
  70. package/dist/esm/telemetry/telemetryRegistry.js +12 -1
  71. package/dist/esm/tlm-ai/agent.js +5 -3
  72. package/dist/esm/tlm-ai/aiController.js +3 -3
  73. package/dist/esm/tlm-ai/aiService.js +6 -2
  74. package/dist/esm/tlm-ai/tools.js +5 -9
  75. package/dist/esm/tlm-auth/authController.js +3 -2
  76. package/dist/esm/tlm-log/logController.js +84 -4
  77. package/dist/esm/tlm-log/logRoutes.js +5 -2
  78. package/dist/esm/tlm-metric/metricsController.js +172 -49
  79. package/dist/esm/tlm-metric/metricsRoutes.js +10 -4
  80. package/dist/esm/tlm-plugin/pluginController.js +6 -11
  81. package/dist/esm/tlm-plugin/pluginService.js +2 -4
  82. package/dist/esm/tlm-trace/traceController.js +102 -16
  83. package/dist/esm/tlm-trace/traceRoutes.js +5 -2
  84. package/dist/esm/tlm-ui/uiRoutes.js +5 -5
  85. package/dist/esm/tlm-util/utilController.js +3 -9
  86. package/dist/esm/tlm-util/utilRoutes.js +2 -2
  87. package/dist/types/config/bootConfig.d.ts +4 -0
  88. package/dist/types/config/config.d.ts +36 -7
  89. package/dist/types/config/config.types.d.ts +6 -0
  90. package/dist/types/index.d.ts +2 -3
  91. package/dist/types/telemetry/custom-implementations/exporters/InMemoryDbLogExporter.d.ts +4 -1
  92. package/dist/types/telemetry/custom-implementations/exporters/InMemoryDbMetricExporter.d.ts +60 -15
  93. package/dist/types/telemetry/custom-implementations/exporters/InMemoryDbSpanExporter.d.ts +9 -4
  94. package/dist/types/telemetry/custom-implementations/instrumentations/logsInstrumentation.d.ts +23 -0
  95. package/dist/types/telemetry/custom-implementations/metrics/tsdb/Chunk.d.ts +49 -0
  96. package/dist/types/telemetry/custom-implementations/metrics/tsdb/Series.d.ts +67 -0
  97. package/dist/types/telemetry/custom-implementations/metrics/tsdb/SeriesRegistry.d.ts +69 -0
  98. package/dist/types/telemetry/custom-implementations/metrics/tsdb/types.d.ts +68 -0
  99. package/dist/types/telemetry/custom-implementations/metrics/tsdb/utils.d.ts +21 -0
  100. package/dist/types/telemetry/custom-implementations/processors/dynamicMultiLogProcessor.d.ts +2 -2
  101. package/dist/types/telemetry/custom-implementations/utils/storagePath.d.ts +12 -0
  102. package/dist/types/telemetry/custom-implementations/wrappers.d.ts +1 -1
  103. package/dist/types/telemetry/telemetryConfigurator.d.ts +1 -1
  104. package/dist/types/telemetry/telemetryRegistry.d.ts +8 -0
  105. package/dist/types/tlm-ai/agent.d.ts +1 -1
  106. package/dist/types/tlm-ai/aiService.d.ts +1 -1
  107. package/dist/types/tlm-log/logController.d.ts +2 -0
  108. package/dist/types/tlm-metric/metricsController.d.ts +16 -2
  109. package/dist/types/tlm-trace/traceController.d.ts +3 -1
  110. package/dist/types/types/index.d.ts +2 -2
  111. package/dist/ui/assets/{ApiDocsPage-C_VVPPHa.js → ApiDocsPage-BFUrXE5F.js} +2 -2
  112. package/dist/ui/assets/CollapsibleCard-STA1GVQO.js +1 -0
  113. package/dist/ui/assets/DevToolsPage-BRSfZqO_.js +1 -0
  114. package/dist/ui/assets/LandingPage-DzeDy7q7.js +6 -0
  115. package/dist/ui/assets/LogsPage-BeiFrV2X.js +1 -0
  116. package/dist/ui/assets/{NotFoundPage-B3quk3P1.js → NotFoundPage-fRNOatbM.js} +1 -1
  117. package/dist/ui/assets/PluginCreatePage-Ch_RXsdf.js +50 -0
  118. package/dist/ui/assets/PluginPage-Cl65ZZ_n.js +27 -0
  119. package/dist/ui/assets/TraceSpansPage-BoK4M5Hh.js +6 -0
  120. package/dist/ui/assets/VirtualizedListPanel-zcj0v7DL.js +16 -0
  121. package/dist/ui/assets/alert-BkNVKxJN.js +1133 -0
  122. package/dist/ui/assets/badge-CN7FeufU.js +1 -0
  123. package/dist/ui/assets/{chevron-down-CPsvsmqj.js → chevron-down-CG--ounh.js} +1 -1
  124. package/dist/ui/assets/{chevron-up-Df9jMo1X.js → chevron-up-B6tzMAOm.js} +1 -1
  125. package/dist/ui/assets/{circle-alert-DOPQPvU8.js → circle-alert-BDF8Tq9y.js} +1 -1
  126. package/dist/ui/assets/dialog-BrpWNk36.js +15 -0
  127. package/dist/ui/assets/index-6xOVKwKn.js +305 -0
  128. package/dist/ui/assets/index-D6f1KjWV.css +1 -0
  129. package/dist/ui/assets/index-D96rVSkR.js +1 -0
  130. package/dist/ui/assets/info-99kuqpbx.js +6 -0
  131. package/dist/ui/assets/{input-Dzvg_ZEZ.js → input-B-01QDg_.js} +1 -1
  132. package/dist/ui/assets/label-CQLeZjM1.js +1 -0
  133. package/dist/ui/assets/{loader-circle-CrvlRy5o.js → loader-circle-BoDGk-BO.js} +1 -1
  134. package/dist/ui/assets/{loginPage-qa4V-B70.js → loginPage-8F4EEd1B.js} +1 -1
  135. package/dist/ui/assets/metrics-page-D1GxaB_c.css +1 -0
  136. package/dist/ui/assets/metrics-page-DPtteXqY.js +31 -0
  137. package/dist/ui/assets/popover-DS_8DYYt.js +11 -0
  138. package/dist/ui/assets/select-DYjegiXi.js +6 -0
  139. package/dist/ui/assets/separator-DGsRxIrl.js +6 -0
  140. package/dist/ui/assets/severityOptions-DEOvJqC9.js +11 -0
  141. package/dist/ui/assets/square-pen-DPhgYz6O.js +6 -0
  142. package/dist/ui/assets/switch-Di9NJH2A.js +1 -0
  143. package/dist/ui/assets/trace-DJq1miYa.js +1 -0
  144. package/dist/ui/assets/upload-BiLTpCnX.js +11 -0
  145. package/dist/ui/assets/{utilService-DNyqzwj0.js → utilService-CNZOmadC.js} +1 -1
  146. package/dist/ui/assets/wand-sparkles-CPoBNFFg.js +6 -0
  147. package/dist/ui/index.html +2 -2
  148. package/package.json +44 -48
  149. package/dist/ui/assets/CollapsibleCard-B3KR_8mL.js +0 -1
  150. package/dist/ui/assets/DevToolsPage-OyZcDcmw.js +0 -1
  151. package/dist/ui/assets/LandingPage-CppFBA6K.js +0 -6
  152. package/dist/ui/assets/LogsPage-9Fq8GArS.js +0 -26
  153. package/dist/ui/assets/PluginCreatePage-X_aCH4t4.js +0 -50
  154. package/dist/ui/assets/PluginPage-DMDSihrZ.js +0 -27
  155. package/dist/ui/assets/alert-jQ9HCPIf.js +0 -1133
  156. package/dist/ui/assets/badge-CNq0-mH5.js +0 -1
  157. package/dist/ui/assets/card-DFAwwhN3.js +0 -1
  158. package/dist/ui/assets/index-BkD6DijD.js +0 -15
  159. package/dist/ui/assets/index-CERGVYZK.js +0 -292
  160. package/dist/ui/assets/index-CSIPf9qw.css +0 -1
  161. package/dist/ui/assets/label-DuVnkZ4q.js +0 -1
  162. package/dist/ui/assets/select-DhS8YUtJ.js +0 -1
  163. package/dist/ui/assets/separator-isK4chBP.js +0 -6
  164. package/dist/ui/assets/severityOptions-O38dSOfk.js +0 -11
  165. package/dist/ui/assets/switch-Z3mImG9n.js +0 -1
  166. package/dist/ui/assets/tabs-_77MUUQe.js +0 -16
  167. package/dist/ui/assets/upload-C1LT4Gkb.js +0 -16
@@ -4,18 +4,31 @@ import logger from '../../../utils/logger.js';
4
4
  import { applyNesting, removeCircularRefs } from '../utils/circular.js';
5
5
  import { Enabler } from '../wrappers.js';
6
6
  import { pluginService } from '../../../tlm-plugin/pluginService.js';
7
+ import { getStoragePath } from '../utils/storagePath.js';
7
8
  export class InMemoryDbSpanExporter extends Enabler {
9
+ _spans = null;
10
+ _retentionTimeInSeconds;
11
+ _storagePath = null;
12
+ _initialized = false;
8
13
  constructor(retentionTimeInSeconds = 3600) {
9
14
  super();
10
- this._baseUrl = '/telemetry'; // Default base URL, can be overridden by the config
11
15
  this._retentionTimeInSeconds = retentionTimeInSeconds;
12
- this._spans = new dataStore({ timestampData: true });
13
- this._spans.ensureIndex({ fieldName: 'createdAt' });
16
+ this._storagePath = getStoragePath('traces');
14
17
  this._startCleanupJob();
15
18
  }
16
19
  ;
17
- set baseUrl(baseUrl) {
18
- this._baseUrl = baseUrl;
20
+ _ensureInitialized() {
21
+ if (this._initialized)
22
+ return;
23
+ this._initialized = true;
24
+ this._spans = new dataStore(this._storagePath ? { filename: this._storagePath, timestampData: true, autoload: true } : { timestampData: true });
25
+ this._spans.ensureIndex({ fieldName: 'createdAt' });
26
+ if (this._storagePath) {
27
+ logger.info(`[SpanExporter] Disk storage enabled at: ${this._storagePath}`);
28
+ }
29
+ else {
30
+ logger.info(`[SpanExporter] Using in-memory storage`);
31
+ }
19
32
  }
20
33
  set retentionTimeInSeconds(retentionTimeInSeconds) {
21
34
  this._retentionTimeInSeconds = retentionTimeInSeconds;
@@ -25,32 +38,26 @@ export class InMemoryDbSpanExporter extends Enabler {
25
38
  return this._retentionTimeInSeconds;
26
39
  }
27
40
  export(readableSpans, resultCallback) {
41
+ this._ensureInitialized();
28
42
  logger.debug('InMemoryDbSpanExporter.export called with spans: ', readableSpans.length);
29
43
  try {
30
44
  // Prepare spans to be inserted into the in-memory database (remove circular references and convert to nested objects)
31
45
  const cleanSpans = readableSpans
32
46
  .map(nestedSpan => removeCircularRefs(nestedSpan)) // to avoid JSON parsing error
33
- .map(span => applyNesting(span)) // to avoid dot notation in keys (neDB does not support dot notation in keys)
34
- .filter(span => {
35
- const target = span?.attributes?.http?.target;
36
- // Exclude spans where target includes 'telemetry' but NOT 'telemetry/utils/generate-log' or 'telemetry/utils/generate-wait'
37
- if (target && target.includes(this._baseUrl)) {
38
- return (target.includes("generate"));
39
- }
40
- return true;
41
- });
47
+ .map(span => applyNesting(span)); // to avoid dot notation in keys (neDB does not support dot notation in keys)
42
48
  cleanSpans.forEach(span => {
43
49
  pluginService.broadcastTrace(span);
44
50
  });
45
- //
46
51
  if (this.isEnabled()) {
47
52
  // Insert spans into the in-memory database
48
- this._spans.insert(cleanSpans, (err, _newDoc) => {
49
- if (err) {
50
- logger.error(err);
51
- return;
52
- }
53
- });
53
+ if (this._spans) {
54
+ this._spans.insert(cleanSpans, (err, _newDoc) => {
55
+ if (err) {
56
+ logger.error(err);
57
+ return;
58
+ }
59
+ });
60
+ }
54
61
  }
55
62
  return resultCallback({ code: ExportResultCode.SUCCESS });
56
63
  }
@@ -64,10 +71,22 @@ export class InMemoryDbSpanExporter extends Enabler {
64
71
  }
65
72
  ;
66
73
  shutdown() {
67
- this._spans = new dataStore();
74
+ this._spans = null;
68
75
  return this.forceFlush();
69
76
  }
70
77
  ;
78
+ reset() {
79
+ this._ensureInitialized();
80
+ // Remove all spans from database but keep persistence enabled
81
+ this._spans.remove({}, { multi: true }, (err) => {
82
+ if (err) {
83
+ logger.error(`[SpanExporter] Error during reset: ${err.message}`);
84
+ }
85
+ else {
86
+ logger.info(`[SpanExporter] Reset - all spans cleared`);
87
+ }
88
+ });
89
+ }
71
90
  /**
72
91
  * Exports any pending spans in the exporter
73
92
  */
@@ -75,15 +94,30 @@ export class InMemoryDbSpanExporter extends Enabler {
75
94
  return Promise.resolve();
76
95
  }
77
96
  ;
78
- //err,docs
79
- find(search, callback) {
80
- this._spans.find(search, callback);
81
- }
82
- reset() {
83
- this._spans = new dataStore();
97
+ async find(findConfig) {
98
+ this._ensureInitialized();
99
+ const { query, limit, sortOrder } = findConfig;
100
+ const effectiveSortOrder = sortOrder || { timestamp: -1 };
101
+ const docs = await new Promise((resolve, reject) => {
102
+ let query_exec = this._spans.find(query)
103
+ .sort(effectiveSortOrder);
104
+ // Only apply limit if provided
105
+ if (limit !== undefined) {
106
+ query_exec = query_exec.limit(limit);
107
+ }
108
+ query_exec.exec((err, docs) => {
109
+ if (err)
110
+ reject(err);
111
+ else
112
+ resolve(docs);
113
+ });
114
+ });
115
+ return docs;
84
116
  }
85
- ;
86
117
  getFinishedSpans() {
118
+ this._ensureInitialized();
119
+ if (!this._spans)
120
+ return [];
87
121
  return this._spans.getAllData();
88
122
  }
89
123
  ;
@@ -93,11 +127,17 @@ export class InMemoryDbSpanExporter extends Enabler {
93
127
  * @param callback - The callback to execute after insertion.
94
128
  */
95
129
  insert(spans, callback) {
130
+ this._ensureInitialized();
131
+ if (!this._spans) {
132
+ return callback(new Error('Spans database not initialized'), []);
133
+ }
96
134
  this._spans.insert(spans, callback);
97
135
  }
98
136
  _startCleanupJob() {
99
137
  const interval = 1000;
100
138
  setInterval(() => {
139
+ if (!this._spans)
140
+ return; // Safety check - not initialized yet
101
141
  const expirationDate = new Date(Date.now() - this._retentionTimeInSeconds * 1000);
102
142
  this._spans.remove({ createdAt: { $lt: expirationDate } }, { multi: true }, (err, numRemoved) => {
103
143
  if (err) {
@@ -0,0 +1,85 @@
1
+ import { InstrumentationBase } from '@opentelemetry/instrumentation';
2
+ import { SeverityNumber } from '@opentelemetry/api-logs';
3
+ import util from 'util';
4
+ import { originalConsoleMethods } from '../../telemetryRegistry.js';
5
+ export class LogsInstrumentation extends InstrumentationBase {
6
+ _loggerProvider;
7
+ _otelLogger;
8
+ constructor(config = {}) {
9
+ super('@oas-telemetry/logs-instrumentation', '1.0.0', config);
10
+ }
11
+ /**
12
+ * No-op: this instrumentation does not patch modules loaded via require().
13
+ */
14
+ init() {
15
+ return [];
16
+ }
17
+ /**
18
+ * Called by the SDK when the LoggerProvider is available.
19
+ * This is the correct way for an instrumentation to receive a logger provider.
20
+ */
21
+ setLoggerProvider(provider) {
22
+ this._loggerProvider = provider;
23
+ this._otelLogger = provider.getLogger(this.instrumentationName);
24
+ // If already enabled, re-apply patches with the new logger provider.
25
+ if (this.isEnabled()) {
26
+ this._unpatchConsole();
27
+ this._patchConsole();
28
+ }
29
+ }
30
+ /**
31
+ * Called by the SDK after all providers have been registered.
32
+ */
33
+ enable() {
34
+ super.enable();
35
+ // Fallback: if no logger provider has been set yet, we do nothing.
36
+ if (!this._loggerProvider) {
37
+ return;
38
+ }
39
+ this._patchConsole();
40
+ }
41
+ disable() {
42
+ super.disable();
43
+ this._unpatchConsole();
44
+ }
45
+ _patchConsole() {
46
+ if (!this._otelLogger)
47
+ return;
48
+ Object.keys(originalConsoleMethods).forEach((method) => {
49
+ const original = originalConsoleMethods[method];
50
+ console[method] = (...args) => {
51
+ const { number, text } = getSeverityForMethod(method);
52
+ this._otelLogger.emit({
53
+ severityNumber: number,
54
+ severityText: text,
55
+ body: util.format(...args),
56
+ attributes: {
57
+ source: `console.${method}`,
58
+ library: this.instrumentationName,
59
+ },
60
+ });
61
+ original(...args);
62
+ };
63
+ });
64
+ }
65
+ _unpatchConsole() {
66
+ Object.keys(originalConsoleMethods).forEach((method) => {
67
+ console[method] = originalConsoleMethods[method];
68
+ });
69
+ }
70
+ }
71
+ function getSeverityForMethod(method) {
72
+ switch (method) {
73
+ case "log":
74
+ case "info":
75
+ return { number: SeverityNumber.INFO, text: "INFO" };
76
+ case "debug":
77
+ return { number: SeverityNumber.DEBUG, text: "DEBUG" };
78
+ case "warn":
79
+ return { number: SeverityNumber.WARN, text: "WARN" };
80
+ case "error":
81
+ return { number: SeverityNumber.ERROR, text: "ERROR" };
82
+ default:
83
+ return { number: SeverityNumber.INFO, text: "INFO" };
84
+ }
85
+ }
@@ -0,0 +1,155 @@
1
+ export class Chunk {
2
+ maxSamples;
3
+ startTimes;
4
+ endTimes;
5
+ values;
6
+ histograms;
7
+ cursor = 0;
8
+ minEndTime = 0;
9
+ maxEndTime = 0;
10
+ isHistogram;
11
+ constructor(maxSamples = 120, isHistogram = false) {
12
+ this.maxSamples = maxSamples;
13
+ this.startTimes = new Float64Array(maxSamples);
14
+ this.endTimes = new Float64Array(maxSamples);
15
+ this.values = new Float64Array(maxSamples);
16
+ this.histograms = new Array(maxSamples).fill(null);
17
+ this.isHistogram = isHistogram;
18
+ }
19
+ /**
20
+ * Append a sample to the chunk.
21
+ * Returns true if successful or false if the chunk is full.
22
+ */
23
+ append(startTime, endTime, value) {
24
+ if (this.cursor >= this.maxSamples) {
25
+ return false;
26
+ }
27
+ if (this.cursor === 0) {
28
+ this.minEndTime = endTime;
29
+ }
30
+ this.startTimes[this.cursor] = startTime;
31
+ this.endTimes[this.cursor] = endTime;
32
+ if (this.isHistogram && typeof value === 'object') {
33
+ this.values[this.cursor] = value.count;
34
+ this.histograms[this.cursor] = value;
35
+ }
36
+ else if (typeof value === 'number') {
37
+ this.values[this.cursor] = value;
38
+ this.histograms[this.cursor] = null;
39
+ }
40
+ else {
41
+ const hv = value;
42
+ this.values[this.cursor] = hv.count;
43
+ this.histograms[this.cursor] = hv;
44
+ }
45
+ this.maxEndTime = endTime;
46
+ this.cursor++;
47
+ return true;
48
+ }
49
+ /**
50
+ * Binary search for first index with endTime >= targetTime
51
+ */
52
+ _binarySearchStart(targetTime) {
53
+ let left = 0;
54
+ let right = this.cursor - 1;
55
+ let result = this.cursor;
56
+ while (left <= right) {
57
+ const mid = (left + right) >> 1;
58
+ if (this.endTimes[mid] >= targetTime) {
59
+ result = mid;
60
+ right = mid - 1;
61
+ }
62
+ else {
63
+ left = mid + 1;
64
+ }
65
+ }
66
+ return result;
67
+ }
68
+ /**
69
+ * Binary search for last index with endTime <= targetTime
70
+ */
71
+ _binarySearchEnd(targetTime, startIdx = 0) {
72
+ let left = startIdx;
73
+ let right = this.cursor - 1;
74
+ let result = startIdx - 1;
75
+ while (left <= right) {
76
+ const mid = (left + right) >> 1;
77
+ if (this.endTimes[mid] <= targetTime) {
78
+ result = mid;
79
+ left = mid + 1;
80
+ }
81
+ else {
82
+ right = mid - 1;
83
+ }
84
+ }
85
+ return result;
86
+ }
87
+ /**
88
+ * Efficient slicing method for bulk operations.
89
+ * Always returns slices. Values can be either a Float64Array (numeric)
90
+ * or a JS array of HistogramValue objects (histogram series).
91
+ */
92
+ getSlices(startTime, endTime) {
93
+ const emptyResult = {
94
+ startTimes: new Float64Array(0),
95
+ endTimes: new Float64Array(0),
96
+ values: this.isHistogram ? [] : new Float64Array(0),
97
+ };
98
+ const start = startTime ?? 0;
99
+ const end = endTime ?? Number.MAX_SAFE_INTEGER;
100
+ if (this.cursor === 0 || this.maxEndTime < start || this.minEndTime > end) {
101
+ return emptyResult;
102
+ }
103
+ const startIdx = this._binarySearchStart(start);
104
+ const endIdx = this._binarySearchEnd(end, startIdx);
105
+ if (startIdx > endIdx) {
106
+ return emptyResult;
107
+ }
108
+ const startTimesNew = this.startTimes.subarray(startIdx, endIdx + 1);
109
+ const endTimesNew = this.endTimes.subarray(startIdx, endIdx + 1);
110
+ if (this.isHistogram) {
111
+ // Return histogram objects as a simple JS array slice
112
+ return {
113
+ startTimes: startTimesNew,
114
+ endTimes: endTimesNew,
115
+ values: this.histograms.slice(startIdx, endIdx + 1),
116
+ };
117
+ }
118
+ return {
119
+ startTimes: startTimesNew,
120
+ endTimes: endTimesNew,
121
+ values: this.values.subarray(startIdx, endIdx + 1),
122
+ };
123
+ }
124
+ isFull() {
125
+ return this.cursor >= this.maxSamples;
126
+ }
127
+ overlaps(startTime, endTime) {
128
+ if (this.cursor === 0)
129
+ return false;
130
+ return !(this.maxEndTime < startTime || this.minEndTime > endTime);
131
+ }
132
+ getStats() {
133
+ return {
134
+ samples: this.cursor,
135
+ maxSamples: this.maxSamples,
136
+ minEndTime: this.minEndTime,
137
+ maxEndTime: this.maxEndTime,
138
+ memoryBytes: this.getMemoryUsage(),
139
+ };
140
+ }
141
+ getMemoryUsage() {
142
+ const arrayMemory = this.maxSamples * 8 * 3;
143
+ const histogramMemory = this.histograms.filter(h => h !== null).length * 200;
144
+ return arrayMemory + histogramMemory;
145
+ }
146
+ getMinTime() {
147
+ return this.minEndTime;
148
+ }
149
+ getMaxTime() {
150
+ return this.maxEndTime;
151
+ }
152
+ size() {
153
+ return this.cursor;
154
+ }
155
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Series: Time-series storage with metadata and chunks
3
+ * One series per unique metric+labels combination
4
+ */
5
+ import { Chunk } from './Chunk.js';
6
+ export class Series {
7
+ labelSet;
8
+ metadata;
9
+ chunks = [];
10
+ maxChunks;
11
+ chunkSize;
12
+ isHistogram;
13
+ constructor(labelSet, metadata, chunkSize = 120, maxChunks = 60 // ~1 hour at 1min intervals with 120 samples/chunk
14
+ ) {
15
+ this.labelSet = labelSet;
16
+ this.metadata = metadata;
17
+ this.chunkSize = chunkSize;
18
+ this.maxChunks = maxChunks;
19
+ this.isHistogram = metadata.dataPointType === 0; // HISTOGRAM = 0
20
+ }
21
+ /**
22
+ * Append a sample to the series
23
+ */
24
+ append(startTime, endTime, value) {
25
+ // Get or create current chunk
26
+ let currentChunk = this.chunks[this.chunks.length - 1];
27
+ if (!currentChunk || currentChunk.isFull()) {
28
+ // Create new chunk
29
+ currentChunk = new Chunk(this.chunkSize, this.isHistogram);
30
+ this.chunks.push(currentChunk);
31
+ // Enforce max chunks limit (circular buffer behavior)
32
+ if (this.chunks.length > this.maxChunks) {
33
+ this.chunks.shift(); // Remove oldest chunk
34
+ }
35
+ }
36
+ currentChunk.append(startTime, endTime, value);
37
+ }
38
+ /**
39
+ * Query slices with options object. Example:
40
+ * querySlices({ startTime, endTime, includeStartTimes: true })
41
+ * includeStartTimes defaults to false.
42
+ * If both startTime and endTime are undefined, returns all data (no time filtering).
43
+ */
44
+ querySlices(options) {
45
+ const start = options?.startTime ?? 0;
46
+ const end = options?.endTime ?? Number.MAX_VALUE;
47
+ const includeStartTimes = options?.includeStartTimes ?? false;
48
+ let totalLength = 0;
49
+ for (const chunk of this.chunks) {
50
+ if (!chunk.overlaps(start, end))
51
+ continue;
52
+ const slice = chunk.getSlices(start, end);
53
+ totalLength += slice.startTimes.length;
54
+ }
55
+ if (totalLength === 0) {
56
+ const empty = {
57
+ endTimes: new Float64Array(0),
58
+ values: this.isHistogram ? [] : new Float64Array(0),
59
+ };
60
+ if (includeStartTimes) {
61
+ empty.startTimes = new Float64Array(0);
62
+ }
63
+ return empty;
64
+ }
65
+ let resultStart;
66
+ if (includeStartTimes) {
67
+ resultStart = new Float64Array(totalLength);
68
+ }
69
+ const resultEnd = new Float64Array(totalLength);
70
+ const resultValues = this.isHistogram
71
+ ? new Array(totalLength)
72
+ : new Float64Array(totalLength);
73
+ let offset = 0;
74
+ for (const chunk of this.chunks) {
75
+ if (!chunk.overlaps(start, end))
76
+ continue;
77
+ const slice = chunk.getSlices(start, end);
78
+ const len = slice.startTimes.length;
79
+ if (len === 0)
80
+ continue;
81
+ // Copy start and end times
82
+ if (includeStartTimes && resultStart) {
83
+ resultStart.set(slice.startTimes, offset);
84
+ }
85
+ resultEnd.set(slice.endTimes, offset);
86
+ // Copy values (different type depending on metric)
87
+ if (this.isHistogram) {
88
+ resultValues.splice(offset, len, ...slice.values);
89
+ }
90
+ else {
91
+ resultValues.set(slice.values, offset);
92
+ }
93
+ offset += len;
94
+ }
95
+ const result = {
96
+ endTimes: resultEnd,
97
+ values: resultValues
98
+ };
99
+ if (includeStartTimes && resultStart) {
100
+ result.startTimes = resultStart;
101
+ }
102
+ return result;
103
+ }
104
+ /**
105
+ * Remove chunks older than threshold
106
+ */
107
+ evictOldChunks(thresholdTime) {
108
+ let evicted = 0;
109
+ while (this.chunks.length > 0 && this.chunks[0].getMaxTime() < thresholdTime) {
110
+ this.chunks.shift();
111
+ evicted++;
112
+ }
113
+ return evicted;
114
+ }
115
+ /**
116
+ * Get series metadata
117
+ */
118
+ getMetadata() {
119
+ return this.metadata;
120
+ }
121
+ getLabels() {
122
+ return this.labelSet.labels;
123
+ }
124
+ getOriginalAttributes() {
125
+ return this.labelSet.originalAttributes;
126
+ }
127
+ /**
128
+ * Get label hash
129
+ */
130
+ getLabelHash() {
131
+ return this.labelSet.hash;
132
+ }
133
+ getStats() {
134
+ const totalSamples = this.chunks.reduce((sum, chunk) => sum + chunk.size(), 0);
135
+ const memoryBytes = this.chunks.reduce((sum, chunk) => sum + chunk.getMemoryUsage(), 0);
136
+ return {
137
+ metricName: this.metadata?.descriptor?.name ?? 'unknown',
138
+ labels: this.labelSet?.labels ?? {},
139
+ chunks: this.chunks.length,
140
+ samples: totalSamples,
141
+ memoryBytes,
142
+ oldestTime: this.chunks[0]?.getMinTime() ?? 0,
143
+ newestTime: this.chunks[this.chunks.length - 1]?.getMaxTime() ?? 0
144
+ };
145
+ }
146
+ /**
147
+ * Check if series has any samples
148
+ */
149
+ isEmpty() {
150
+ return this.chunks.length === 0 || this.chunks.every(c => c.size() === 0);
151
+ }
152
+ /**
153
+ * Get time range covered by this series
154
+ */
155
+ getTimeRange() {
156
+ if (this.chunks.length === 0) {
157
+ return { min: 0, max: 0 };
158
+ }
159
+ return {
160
+ min: this.chunks[0].getMinTime(),
161
+ max: this.chunks[this.chunks.length - 1].getMaxTime()
162
+ };
163
+ }
164
+ }