apitally 1.0.0-beta.3 → 1.0.0-beta.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"providers.cjs","names":["trace","SpanKind","TraceFlags","SamplingDecision","detectResources","envDetector","getConfig","defaultResource","resourceFromAttributes","randomUUID","getDistroVersion","NodeTracerProvider","AsyncLocalStorageContextManager","context","W3CTraceContextPropagator","MeterProvider","LoggerProvider","createContextKey"],"sources":["../src/providers.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n type Context,\n context,\n createContextKey,\n propagation,\n SpanKind,\n TraceFlags,\n trace,\n} from \"@opentelemetry/api\";\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport { W3CTraceContextPropagator } from \"@opentelemetry/core\";\nimport {\n defaultResource,\n detectResources,\n envDetector,\n type Resource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport { LoggerProvider, type LogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport { type IMetricReader, MeterProvider } from \"@opentelemetry/sdk-metrics\";\nimport {\n type Sampler,\n SamplingDecision,\n type SamplingResult,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\nimport { getConfig } from \"./config.js\";\nimport { logDebug, logWarning } from \"./logger.js\";\nimport { getDistroVersion } from \"./packageVersion.js\";\n\nconst MAX_ATTRIBUTE_VALUE_LENGTH = 65_536;\nconst DEPLOYMENT_ENVIRONMENT_NAME = \"deployment.environment.name\";\n\nclass RequestRootedSampler implements Sampler {\n shouldSample(\n parentContext: Context,\n _traceId: string,\n _spanName: string,\n spanKind: SpanKind,\n ): SamplingResult {\n const parent = trace.getSpanContext(parentContext);\n const shouldRecord =\n spanKind === SpanKind.SERVER ||\n (parent !== undefined && !parent.isRemote && (parent.traceFlags & TraceFlags.SAMPLED) !== 0);\n return {\n decision: shouldRecord ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD,\n };\n }\n\n toString(): string {\n return \"RequestRootedSampler\";\n }\n}\n\n// Apitally-Env and every signal's resource use the configured environment.\nexport function resolveEnvAndCreateResource(): { env: string; resource: Resource } {\n const environmentResource = detectResources({ detectors: [envDetector] });\n const env = getConfig().env;\n // defaultResource() ignores environment variables. Apitally attributes merge\n // last to keep the resource aligned with Apitally-Env.\n const resource = defaultResource()\n .merge(environmentResource)\n .merge(\n resourceFromAttributes({\n \"service.instance.id\": randomUUID(),\n [DEPLOYMENT_ENVIRONMENT_NAME]: env,\n \"telemetry.distro.name\": \"apitally-js\",\n \"telemetry.distro.version\": getDistroVersion(),\n }),\n );\n return { env, resource };\n}\n\nexport function setupTracerProvider(\n resource: Resource,\n spanProcessors: SpanProcessor[],\n): NodeTracerProvider | undefined {\n // Explicit sampler and length limits prevent OTel environment variables from\n // dropping upstream-unsampled SERVER spans or truncating long attributes.\n const provider = new NodeTracerProvider({\n sampler: new RequestRootedSampler(),\n resource,\n spanProcessors,\n generalLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n spanLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n });\n // OTel global setters reject duplicate or version-mismatched registrations,\n // preserving existing registrations.\n if (!trace.setGlobalTracerProvider(provider)) {\n return undefined;\n }\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n if (!context.setGlobalContextManager(contextManager)) {\n try {\n contextManager.disable();\n } catch (error) {\n logDebug(`Error disabling an unused OpenTelemetry context manager: ${String(error)}`);\n }\n warnIfContextDoesNotPropagate();\n }\n propagation.setGlobalPropagator(new W3CTraceContextPropagator());\n return provider;\n}\n\n// Meter and logger providers remain private because global registration could\n// replace or race a user's metrics or logs pipeline.\nexport function createMeterProvider(resource: Resource, readers: IMetricReader[]): MeterProvider {\n return new MeterProvider({ resource, readers });\n}\n\nexport function createLoggerProvider(\n resource: Resource,\n processors: LogRecordProcessor[],\n): LoggerProvider {\n return new LoggerProvider({ resource, processors });\n}\n\n// OTel exposes no context-manager getter, so a propagated probe verifies\n// registration. Without propagation, request contexts and suppression fail without an error.\nfunction warnIfContextDoesNotPropagate(): void {\n let isPropagated = false;\n try {\n const probeKey = createContextKey(\"apitally-context-probe\");\n isPropagated = context.with(\n context.active().setValue(probeKey, true),\n () => context.active().getValue(probeKey) === true,\n );\n } catch (error) {\n logDebug(`Error probing OpenTelemetry context propagation: ${String(error)}`);\n }\n if (!isPropagated) {\n logWarning(\n \"OpenTelemetry context propagation is not working, so Apitally cannot associate telemetry with requests. This can happen when conflicting versions of @opentelemetry/api are installed.\",\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAgCA,MAAM,6BAA6B;AACnC,MAAM,8BAA8B;AAEpC,IAAM,uBAAN,MAA8C;CAC5C,aACE,eACA,UACA,WACA,UACgB;EAChB,MAAM,SAASA,mBAAAA,MAAM,eAAe,aAAa;EAIjD,OAAO,EACL,UAHA,aAAaC,mBAAAA,SAAS,UACrB,WAAW,KAAA,KAAa,CAAC,OAAO,aAAa,OAAO,aAAaC,mBAAAA,WAAW,aAAa,IAEjEC,8BAAAA,iBAAiB,qBAAqBA,8BAAAA,iBAAiB,WAClF;CACF;CAEA,WAAmB;EACjB,OAAO;CACT;AACF;AAGA,SAAgB,8BAAmE;CACjF,MAAM,uBAAA,GAAsBC,yBAAAA,gBAAAA,CAAgB,EAAE,WAAW,CAACC,yBAAAA,WAAW,EAAE,CAAC;CACxE,MAAM,MAAMC,eAAAA,UAAU,CAAC,CAAC;CAaxB,OAAO;EAAE;EAAK,WAAA,GAVGC,yBAAAA,gBAAAA,CAAgB,CAAC,CAC/B,MAAM,mBAAmB,CAAC,CAC1B,OAAA,GACCC,yBAAAA,uBAAAA,CAAuB;GACrB,wBAAA,GAAuBC,YAAAA,WAAAA,CAAW;IACjC,8BAA8B;GAC/B,yBAAyB;GACzB,4BAA4BC,uBAAAA,iBAAiB;EAC/C,CAAC,CAEgB;CAAE;AACzB;AAEA,SAAgB,oBACd,UACA,gBACgC;CAGhC,MAAM,WAAW,IAAIC,8BAAAA,mBAAmB;EACtC,SAAS,IAAI,qBAAqB;EAClC;EACA;EACA,eAAe,EAAE,2BAA2B,2BAA2B;EACvE,YAAY,EAAE,2BAA2B,2BAA2B;CACtE,CAAC;CAGD,IAAI,CAACX,mBAAAA,MAAM,wBAAwB,QAAQ,GACzC;CAEF,MAAM,iBAAiB,IAAIY,mCAAAA,gCAAgC;CAC3D,eAAe,OAAO;CACtB,IAAI,CAACC,mBAAAA,QAAQ,wBAAwB,cAAc,GAAG;EACpD,IAAI;GACF,eAAe,QAAQ;EACzB,SAAS,OAAO;GACd,eAAA,SAAS,4DAA4D,OAAO,KAAK,GAAG;EACtF;EACA,8BAA8B;CAChC;CACA,mBAAA,YAAY,oBAAoB,IAAIC,oBAAAA,0BAA0B,CAAC;CAC/D,OAAO;AACT;AAIA,SAAgB,oBAAoB,UAAoB,SAAyC;CAC/F,OAAO,IAAIC,2BAAAA,cAAc;EAAE;EAAU;CAAQ,CAAC;AAChD;AAEA,SAAgB,qBACd,UACA,YACgB;CAChB,OAAO,IAAIC,wBAAAA,eAAe;EAAE;EAAU;CAAW,CAAC;AACpD;AAIA,SAAS,gCAAsC;CAC7C,IAAI,eAAe;CACnB,IAAI;EACF,MAAM,YAAA,GAAWC,mBAAAA,iBAAAA,CAAiB,wBAAwB;EAC1D,eAAeJ,mBAAAA,QAAQ,KACrBA,mBAAAA,QAAQ,OAAO,CAAC,CAAC,SAAS,UAAU,IAAI,SAClCA,mBAAAA,QAAQ,OAAO,CAAC,CAAC,SAAS,QAAQ,MAAM,IAChD;CACF,SAAS,OAAO;EACd,eAAA,SAAS,oDAAoD,OAAO,KAAK,GAAG;CAC9E;CACA,IAAI,CAAC,cACH,eAAA,WACE,wLACF;AAEJ"}
1
+ {"version":3,"file":"providers.cjs","names":["trace","SpanKind","TraceFlags","SamplingDecision","detectResources","envDetector","getConfig","defaultResource","resourceFromAttributes","randomUUID","getDistroVersion","NodeTracerProvider","AsyncLocalStorageContextManager","context","W3CTraceContextPropagator","LoggerProvider","createContextKey"],"sources":["../src/providers.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n type Context,\n context,\n createContextKey,\n propagation,\n SpanKind,\n TraceFlags,\n trace,\n} from \"@opentelemetry/api\";\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport { W3CTraceContextPropagator } from \"@opentelemetry/core\";\nimport {\n defaultResource,\n detectResources,\n envDetector,\n type Resource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport { LoggerProvider, type LogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport {\n type Sampler,\n SamplingDecision,\n type SamplingResult,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\nimport { getConfig } from \"./config.js\";\nimport { logDebug, logWarning } from \"./logger.js\";\nimport { getDistroVersion } from \"./packageVersion.js\";\n\nconst MAX_ATTRIBUTE_VALUE_LENGTH = 65_536;\nconst DEPLOYMENT_ENVIRONMENT_NAME = \"deployment.environment.name\";\n\nclass RequestRootedSampler implements Sampler {\n shouldSample(\n parentContext: Context,\n _traceId: string,\n _spanName: string,\n spanKind: SpanKind,\n ): SamplingResult {\n const parent = trace.getSpanContext(parentContext);\n const shouldRecord =\n spanKind === SpanKind.SERVER ||\n (parent !== undefined && !parent.isRemote && (parent.traceFlags & TraceFlags.SAMPLED) !== 0);\n return {\n decision: shouldRecord ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD,\n };\n }\n\n toString(): string {\n return \"RequestRootedSampler\";\n }\n}\n\n// Apitally-Env and every signal's resource use the configured environment.\nexport function resolveEnvAndCreateResource(): { env: string; resource: Resource } {\n const environmentResource = detectResources({ detectors: [envDetector] });\n const env = getConfig().env;\n // defaultResource() ignores environment variables. Apitally attributes merge\n // last to keep the resource aligned with Apitally-Env.\n const resource = defaultResource()\n .merge(environmentResource)\n .merge(\n resourceFromAttributes({\n \"service.instance.id\": randomUUID(),\n [DEPLOYMENT_ENVIRONMENT_NAME]: env,\n \"telemetry.distro.name\": \"apitally-js\",\n \"telemetry.distro.version\": getDistroVersion(),\n }),\n );\n return { env, resource };\n}\n\nexport function setupTracerProvider(\n resource: Resource,\n spanProcessors: SpanProcessor[],\n): NodeTracerProvider | undefined {\n // Explicit sampler and length limits prevent OTel environment variables from\n // dropping upstream-unsampled SERVER spans or truncating long attributes.\n const provider = new NodeTracerProvider({\n sampler: new RequestRootedSampler(),\n resource,\n spanProcessors,\n generalLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n spanLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n });\n // OTel global setters reject duplicate or version-mismatched registrations,\n // preserving existing registrations.\n if (!trace.setGlobalTracerProvider(provider)) {\n return undefined;\n }\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n if (!context.setGlobalContextManager(contextManager)) {\n try {\n contextManager.disable();\n } catch (error) {\n logDebug(`Error disabling an unused OpenTelemetry context manager: ${String(error)}`);\n }\n warnIfContextDoesNotPropagate();\n }\n propagation.setGlobalPropagator(new W3CTraceContextPropagator());\n return provider;\n}\n\n// The logger provider remains private because global registration could replace\n// or race a user's logs pipeline.\nexport function createLoggerProvider(\n resource: Resource,\n processors: LogRecordProcessor[],\n): LoggerProvider {\n return new LoggerProvider({ resource, processors });\n}\n\n// OTel exposes no context-manager getter, so a propagated probe verifies\n// registration. Without propagation, request contexts and suppression fail without an error.\nfunction warnIfContextDoesNotPropagate(): void {\n let isPropagated = false;\n try {\n const probeKey = createContextKey(\"apitally-context-probe\");\n isPropagated = context.with(\n context.active().setValue(probeKey, true),\n () => context.active().getValue(probeKey) === true,\n );\n } catch (error) {\n logDebug(`Error probing OpenTelemetry context propagation: ${String(error)}`);\n }\n if (!isPropagated) {\n logWarning(\n \"OpenTelemetry context propagation is not working, so Apitally cannot associate telemetry with requests. This can happen when conflicting versions of @opentelemetry/api are installed.\",\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;AA+BA,MAAM,6BAA6B;AACnC,MAAM,8BAA8B;AAEpC,IAAM,uBAAN,MAA8C;CAC5C,aACE,eACA,UACA,WACA,UACgB;EAChB,MAAM,SAASA,mBAAAA,MAAM,eAAe,aAAa;EAIjD,OAAO,EACL,UAHA,aAAaC,mBAAAA,SAAS,UACrB,WAAW,KAAA,KAAa,CAAC,OAAO,aAAa,OAAO,aAAaC,mBAAAA,WAAW,aAAa,IAEjEC,8BAAAA,iBAAiB,qBAAqBA,8BAAAA,iBAAiB,WAClF;CACF;CAEA,WAAmB;EACjB,OAAO;CACT;AACF;AAGA,SAAgB,8BAAmE;CACjF,MAAM,uBAAA,GAAsBC,yBAAAA,gBAAAA,CAAgB,EAAE,WAAW,CAACC,yBAAAA,WAAW,EAAE,CAAC;CACxE,MAAM,MAAMC,eAAAA,UAAU,CAAC,CAAC;CAaxB,OAAO;EAAE;EAAK,WAAA,GAVGC,yBAAAA,gBAAAA,CAAgB,CAAC,CAC/B,MAAM,mBAAmB,CAAC,CAC1B,OAAA,GACCC,yBAAAA,uBAAAA,CAAuB;GACrB,wBAAA,GAAuBC,YAAAA,WAAAA,CAAW;IACjC,8BAA8B;GAC/B,yBAAyB;GACzB,4BAA4BC,uBAAAA,iBAAiB;EAC/C,CAAC,CAEgB;CAAE;AACzB;AAEA,SAAgB,oBACd,UACA,gBACgC;CAGhC,MAAM,WAAW,IAAIC,8BAAAA,mBAAmB;EACtC,SAAS,IAAI,qBAAqB;EAClC;EACA;EACA,eAAe,EAAE,2BAA2B,2BAA2B;EACvE,YAAY,EAAE,2BAA2B,2BAA2B;CACtE,CAAC;CAGD,IAAI,CAACX,mBAAAA,MAAM,wBAAwB,QAAQ,GACzC;CAEF,MAAM,iBAAiB,IAAIY,mCAAAA,gCAAgC;CAC3D,eAAe,OAAO;CACtB,IAAI,CAACC,mBAAAA,QAAQ,wBAAwB,cAAc,GAAG;EACpD,IAAI;GACF,eAAe,QAAQ;EACzB,SAAS,OAAO;GACd,eAAA,SAAS,4DAA4D,OAAO,KAAK,GAAG;EACtF;EACA,8BAA8B;CAChC;CACA,mBAAA,YAAY,oBAAoB,IAAIC,oBAAAA,0BAA0B,CAAC;CAC/D,OAAO;AACT;AAIA,SAAgB,qBACd,UACA,YACgB;CAChB,OAAO,IAAIC,wBAAAA,eAAe;EAAE;EAAU;CAAW,CAAC;AACpD;AAIA,SAAS,gCAAsC;CAC7C,IAAI,eAAe;CACnB,IAAI;EACF,MAAM,YAAA,GAAWC,mBAAAA,iBAAAA,CAAiB,wBAAwB;EAC1D,eAAeH,mBAAAA,QAAQ,KACrBA,mBAAAA,QAAQ,OAAO,CAAC,CAAC,SAAS,UAAU,IAAI,SAClCA,mBAAAA,QAAQ,OAAO,CAAC,CAAC,SAAS,QAAQ,MAAM,IAChD;CACF,SAAS,OAAO;EACd,eAAA,SAAS,oDAAoD,OAAO,KAAK,GAAG;CAC9E;CACA,IAAI,CAAC,cACH,eAAA,WACE,wLACF;AAEJ"}
@@ -2,14 +2,12 @@ import { LogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
2
2
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
3
3
  import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
4
4
  import { Resource } from "@opentelemetry/resources";
5
- import { IMetricReader, MeterProvider } from "@opentelemetry/sdk-metrics";
6
5
  //#region src/providers.d.ts
7
6
  export declare function resolveEnvAndCreateResource(): {
8
7
  env: string;
9
8
  resource: Resource;
10
9
  };
11
10
  export declare function setupTracerProvider(resource: Resource, spanProcessors: SpanProcessor[]): NodeTracerProvider | undefined;
12
- export declare function createMeterProvider(resource: Resource, readers: IMetricReader[]): MeterProvider;
13
11
  export declare function createLoggerProvider(resource: Resource, processors: LogRecordProcessor[]): LoggerProvider;
14
12
  //#endregion
15
13
  //# sourceMappingURL=providers.d.cts.map
@@ -1,6 +1,5 @@
1
1
  import { LogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
2
2
  import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
3
- import { IMetricReader, MeterProvider } from "@opentelemetry/sdk-metrics";
4
3
  import { Resource } from "@opentelemetry/resources";
5
4
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
6
5
  //#region src/providers.d.ts
@@ -9,7 +8,6 @@ export declare function resolveEnvAndCreateResource(): {
9
8
  resource: Resource;
10
9
  };
11
10
  export declare function setupTracerProvider(resource: Resource, spanProcessors: SpanProcessor[]): NodeTracerProvider | undefined;
12
- export declare function createMeterProvider(resource: Resource, readers: IMetricReader[]): MeterProvider;
13
11
  export declare function createLoggerProvider(resource: Resource, processors: LogRecordProcessor[]): LoggerProvider;
14
12
  //#endregion
15
13
  //# sourceMappingURL=providers.d.ts.map
package/dist/providers.js CHANGED
@@ -5,7 +5,6 @@ import { SpanKind, TraceFlags, context, createContextKey, propagation, trace } f
5
5
  import { LoggerProvider } from "@opentelemetry/sdk-logs";
6
6
  import { SamplingDecision } from "@opentelemetry/sdk-trace-base";
7
7
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
8
- import { MeterProvider } from "@opentelemetry/sdk-metrics";
9
8
  import { randomUUID } from "node:crypto";
10
9
  import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
11
10
  import { defaultResource, detectResources, envDetector, resourceFromAttributes } from "@opentelemetry/resources";
@@ -57,12 +56,6 @@ function setupTracerProvider(resource, spanProcessors) {
57
56
  propagation.setGlobalPropagator(new W3CTraceContextPropagator());
58
57
  return provider;
59
58
  }
60
- function createMeterProvider(resource, readers) {
61
- return new MeterProvider({
62
- resource,
63
- readers
64
- });
65
- }
66
59
  function createLoggerProvider(resource, processors) {
67
60
  return new LoggerProvider({
68
61
  resource,
@@ -80,6 +73,6 @@ function warnIfContextDoesNotPropagate() {
80
73
  if (!isPropagated) logWarning("OpenTelemetry context propagation is not working, so Apitally cannot associate telemetry with requests. This can happen when conflicting versions of @opentelemetry/api are installed.");
81
74
  }
82
75
  //#endregion
83
- export { createLoggerProvider, createMeterProvider, resolveEnvAndCreateResource, setupTracerProvider };
76
+ export { createLoggerProvider, resolveEnvAndCreateResource, setupTracerProvider };
84
77
 
85
78
  //# sourceMappingURL=providers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"providers.js","names":[],"sources":["../src/providers.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n type Context,\n context,\n createContextKey,\n propagation,\n SpanKind,\n TraceFlags,\n trace,\n} from \"@opentelemetry/api\";\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport { W3CTraceContextPropagator } from \"@opentelemetry/core\";\nimport {\n defaultResource,\n detectResources,\n envDetector,\n type Resource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport { LoggerProvider, type LogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport { type IMetricReader, MeterProvider } from \"@opentelemetry/sdk-metrics\";\nimport {\n type Sampler,\n SamplingDecision,\n type SamplingResult,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\nimport { getConfig } from \"./config.js\";\nimport { logDebug, logWarning } from \"./logger.js\";\nimport { getDistroVersion } from \"./packageVersion.js\";\n\nconst MAX_ATTRIBUTE_VALUE_LENGTH = 65_536;\nconst DEPLOYMENT_ENVIRONMENT_NAME = \"deployment.environment.name\";\n\nclass RequestRootedSampler implements Sampler {\n shouldSample(\n parentContext: Context,\n _traceId: string,\n _spanName: string,\n spanKind: SpanKind,\n ): SamplingResult {\n const parent = trace.getSpanContext(parentContext);\n const shouldRecord =\n spanKind === SpanKind.SERVER ||\n (parent !== undefined && !parent.isRemote && (parent.traceFlags & TraceFlags.SAMPLED) !== 0);\n return {\n decision: shouldRecord ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD,\n };\n }\n\n toString(): string {\n return \"RequestRootedSampler\";\n }\n}\n\n// Apitally-Env and every signal's resource use the configured environment.\nexport function resolveEnvAndCreateResource(): { env: string; resource: Resource } {\n const environmentResource = detectResources({ detectors: [envDetector] });\n const env = getConfig().env;\n // defaultResource() ignores environment variables. Apitally attributes merge\n // last to keep the resource aligned with Apitally-Env.\n const resource = defaultResource()\n .merge(environmentResource)\n .merge(\n resourceFromAttributes({\n \"service.instance.id\": randomUUID(),\n [DEPLOYMENT_ENVIRONMENT_NAME]: env,\n \"telemetry.distro.name\": \"apitally-js\",\n \"telemetry.distro.version\": getDistroVersion(),\n }),\n );\n return { env, resource };\n}\n\nexport function setupTracerProvider(\n resource: Resource,\n spanProcessors: SpanProcessor[],\n): NodeTracerProvider | undefined {\n // Explicit sampler and length limits prevent OTel environment variables from\n // dropping upstream-unsampled SERVER spans or truncating long attributes.\n const provider = new NodeTracerProvider({\n sampler: new RequestRootedSampler(),\n resource,\n spanProcessors,\n generalLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n spanLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n });\n // OTel global setters reject duplicate or version-mismatched registrations,\n // preserving existing registrations.\n if (!trace.setGlobalTracerProvider(provider)) {\n return undefined;\n }\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n if (!context.setGlobalContextManager(contextManager)) {\n try {\n contextManager.disable();\n } catch (error) {\n logDebug(`Error disabling an unused OpenTelemetry context manager: ${String(error)}`);\n }\n warnIfContextDoesNotPropagate();\n }\n propagation.setGlobalPropagator(new W3CTraceContextPropagator());\n return provider;\n}\n\n// Meter and logger providers remain private because global registration could\n// replace or race a user's metrics or logs pipeline.\nexport function createMeterProvider(resource: Resource, readers: IMetricReader[]): MeterProvider {\n return new MeterProvider({ resource, readers });\n}\n\nexport function createLoggerProvider(\n resource: Resource,\n processors: LogRecordProcessor[],\n): LoggerProvider {\n return new LoggerProvider({ resource, processors });\n}\n\n// OTel exposes no context-manager getter, so a propagated probe verifies\n// registration. Without propagation, request contexts and suppression fail without an error.\nfunction warnIfContextDoesNotPropagate(): void {\n let isPropagated = false;\n try {\n const probeKey = createContextKey(\"apitally-context-probe\");\n isPropagated = context.with(\n context.active().setValue(probeKey, true),\n () => context.active().getValue(probeKey) === true,\n );\n } catch (error) {\n logDebug(`Error probing OpenTelemetry context propagation: ${String(error)}`);\n }\n if (!isPropagated) {\n logWarning(\n \"OpenTelemetry context propagation is not working, so Apitally cannot associate telemetry with requests. This can happen when conflicting versions of @opentelemetry/api are installed.\",\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAgCA,MAAM,6BAA6B;AACnC,MAAM,8BAA8B;AAEpC,IAAM,uBAAN,MAA8C;CAC5C,aACE,eACA,UACA,WACA,UACgB;EAChB,MAAM,SAAS,MAAM,eAAe,aAAa;EAIjD,OAAO,EACL,UAHA,aAAa,SAAS,UACrB,WAAW,KAAA,KAAa,CAAC,OAAO,aAAa,OAAO,aAAa,WAAW,aAAa,IAEjE,iBAAiB,qBAAqB,iBAAiB,WAClF;CACF;CAEA,WAAmB;EACjB,OAAO;CACT;AACF;AAGA,SAAgB,8BAAmE;CACjF,MAAM,sBAAsB,gBAAgB,EAAE,WAAW,CAAC,WAAW,EAAE,CAAC;CACxE,MAAM,MAAM,UAAU,CAAC,CAAC;CAaxB,OAAO;EAAE;EAAK,UAVG,gBAAgB,CAAC,CAC/B,MAAM,mBAAmB,CAAC,CAC1B,MACC,uBAAuB;GACrB,uBAAuB,WAAW;IACjC,8BAA8B;GAC/B,yBAAyB;GACzB,4BAA4B,iBAAiB;EAC/C,CAAC,CAEgB;CAAE;AACzB;AAEA,SAAgB,oBACd,UACA,gBACgC;CAGhC,MAAM,WAAW,IAAI,mBAAmB;EACtC,SAAS,IAAI,qBAAqB;EAClC;EACA;EACA,eAAe,EAAE,2BAA2B,2BAA2B;EACvE,YAAY,EAAE,2BAA2B,2BAA2B;CACtE,CAAC;CAGD,IAAI,CAAC,MAAM,wBAAwB,QAAQ,GACzC;CAEF,MAAM,iBAAiB,IAAI,gCAAgC;CAC3D,eAAe,OAAO;CACtB,IAAI,CAAC,QAAQ,wBAAwB,cAAc,GAAG;EACpD,IAAI;GACF,eAAe,QAAQ;EACzB,SAAS,OAAO;GACd,SAAS,4DAA4D,OAAO,KAAK,GAAG;EACtF;EACA,8BAA8B;CAChC;CACA,YAAY,oBAAoB,IAAI,0BAA0B,CAAC;CAC/D,OAAO;AACT;AAIA,SAAgB,oBAAoB,UAAoB,SAAyC;CAC/F,OAAO,IAAI,cAAc;EAAE;EAAU;CAAQ,CAAC;AAChD;AAEA,SAAgB,qBACd,UACA,YACgB;CAChB,OAAO,IAAI,eAAe;EAAE;EAAU;CAAW,CAAC;AACpD;AAIA,SAAS,gCAAsC;CAC7C,IAAI,eAAe;CACnB,IAAI;EACF,MAAM,WAAW,iBAAiB,wBAAwB;EAC1D,eAAe,QAAQ,KACrB,QAAQ,OAAO,CAAC,CAAC,SAAS,UAAU,IAAI,SAClC,QAAQ,OAAO,CAAC,CAAC,SAAS,QAAQ,MAAM,IAChD;CACF,SAAS,OAAO;EACd,SAAS,oDAAoD,OAAO,KAAK,GAAG;CAC9E;CACA,IAAI,CAAC,cACH,WACE,wLACF;AAEJ"}
1
+ {"version":3,"file":"providers.js","names":[],"sources":["../src/providers.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n type Context,\n context,\n createContextKey,\n propagation,\n SpanKind,\n TraceFlags,\n trace,\n} from \"@opentelemetry/api\";\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport { W3CTraceContextPropagator } from \"@opentelemetry/core\";\nimport {\n defaultResource,\n detectResources,\n envDetector,\n type Resource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport { LoggerProvider, type LogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport {\n type Sampler,\n SamplingDecision,\n type SamplingResult,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\nimport { getConfig } from \"./config.js\";\nimport { logDebug, logWarning } from \"./logger.js\";\nimport { getDistroVersion } from \"./packageVersion.js\";\n\nconst MAX_ATTRIBUTE_VALUE_LENGTH = 65_536;\nconst DEPLOYMENT_ENVIRONMENT_NAME = \"deployment.environment.name\";\n\nclass RequestRootedSampler implements Sampler {\n shouldSample(\n parentContext: Context,\n _traceId: string,\n _spanName: string,\n spanKind: SpanKind,\n ): SamplingResult {\n const parent = trace.getSpanContext(parentContext);\n const shouldRecord =\n spanKind === SpanKind.SERVER ||\n (parent !== undefined && !parent.isRemote && (parent.traceFlags & TraceFlags.SAMPLED) !== 0);\n return {\n decision: shouldRecord ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD,\n };\n }\n\n toString(): string {\n return \"RequestRootedSampler\";\n }\n}\n\n// Apitally-Env and every signal's resource use the configured environment.\nexport function resolveEnvAndCreateResource(): { env: string; resource: Resource } {\n const environmentResource = detectResources({ detectors: [envDetector] });\n const env = getConfig().env;\n // defaultResource() ignores environment variables. Apitally attributes merge\n // last to keep the resource aligned with Apitally-Env.\n const resource = defaultResource()\n .merge(environmentResource)\n .merge(\n resourceFromAttributes({\n \"service.instance.id\": randomUUID(),\n [DEPLOYMENT_ENVIRONMENT_NAME]: env,\n \"telemetry.distro.name\": \"apitally-js\",\n \"telemetry.distro.version\": getDistroVersion(),\n }),\n );\n return { env, resource };\n}\n\nexport function setupTracerProvider(\n resource: Resource,\n spanProcessors: SpanProcessor[],\n): NodeTracerProvider | undefined {\n // Explicit sampler and length limits prevent OTel environment variables from\n // dropping upstream-unsampled SERVER spans or truncating long attributes.\n const provider = new NodeTracerProvider({\n sampler: new RequestRootedSampler(),\n resource,\n spanProcessors,\n generalLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n spanLimits: { attributeValueLengthLimit: MAX_ATTRIBUTE_VALUE_LENGTH },\n });\n // OTel global setters reject duplicate or version-mismatched registrations,\n // preserving existing registrations.\n if (!trace.setGlobalTracerProvider(provider)) {\n return undefined;\n }\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n if (!context.setGlobalContextManager(contextManager)) {\n try {\n contextManager.disable();\n } catch (error) {\n logDebug(`Error disabling an unused OpenTelemetry context manager: ${String(error)}`);\n }\n warnIfContextDoesNotPropagate();\n }\n propagation.setGlobalPropagator(new W3CTraceContextPropagator());\n return provider;\n}\n\n// The logger provider remains private because global registration could replace\n// or race a user's logs pipeline.\nexport function createLoggerProvider(\n resource: Resource,\n processors: LogRecordProcessor[],\n): LoggerProvider {\n return new LoggerProvider({ resource, processors });\n}\n\n// OTel exposes no context-manager getter, so a propagated probe verifies\n// registration. Without propagation, request contexts and suppression fail without an error.\nfunction warnIfContextDoesNotPropagate(): void {\n let isPropagated = false;\n try {\n const probeKey = createContextKey(\"apitally-context-probe\");\n isPropagated = context.with(\n context.active().setValue(probeKey, true),\n () => context.active().getValue(probeKey) === true,\n );\n } catch (error) {\n logDebug(`Error probing OpenTelemetry context propagation: ${String(error)}`);\n }\n if (!isPropagated) {\n logWarning(\n \"OpenTelemetry context propagation is not working, so Apitally cannot associate telemetry with requests. This can happen when conflicting versions of @opentelemetry/api are installed.\",\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;AA+BA,MAAM,6BAA6B;AACnC,MAAM,8BAA8B;AAEpC,IAAM,uBAAN,MAA8C;CAC5C,aACE,eACA,UACA,WACA,UACgB;EAChB,MAAM,SAAS,MAAM,eAAe,aAAa;EAIjD,OAAO,EACL,UAHA,aAAa,SAAS,UACrB,WAAW,KAAA,KAAa,CAAC,OAAO,aAAa,OAAO,aAAa,WAAW,aAAa,IAEjE,iBAAiB,qBAAqB,iBAAiB,WAClF;CACF;CAEA,WAAmB;EACjB,OAAO;CACT;AACF;AAGA,SAAgB,8BAAmE;CACjF,MAAM,sBAAsB,gBAAgB,EAAE,WAAW,CAAC,WAAW,EAAE,CAAC;CACxE,MAAM,MAAM,UAAU,CAAC,CAAC;CAaxB,OAAO;EAAE;EAAK,UAVG,gBAAgB,CAAC,CAC/B,MAAM,mBAAmB,CAAC,CAC1B,MACC,uBAAuB;GACrB,uBAAuB,WAAW;IACjC,8BAA8B;GAC/B,yBAAyB;GACzB,4BAA4B,iBAAiB;EAC/C,CAAC,CAEgB;CAAE;AACzB;AAEA,SAAgB,oBACd,UACA,gBACgC;CAGhC,MAAM,WAAW,IAAI,mBAAmB;EACtC,SAAS,IAAI,qBAAqB;EAClC;EACA;EACA,eAAe,EAAE,2BAA2B,2BAA2B;EACvE,YAAY,EAAE,2BAA2B,2BAA2B;CACtE,CAAC;CAGD,IAAI,CAAC,MAAM,wBAAwB,QAAQ,GACzC;CAEF,MAAM,iBAAiB,IAAI,gCAAgC;CAC3D,eAAe,OAAO;CACtB,IAAI,CAAC,QAAQ,wBAAwB,cAAc,GAAG;EACpD,IAAI;GACF,eAAe,QAAQ;EACzB,SAAS,OAAO;GACd,SAAS,4DAA4D,OAAO,KAAK,GAAG;EACtF;EACA,8BAA8B;CAChC;CACA,YAAY,oBAAoB,IAAI,0BAA0B,CAAC;CAC/D,OAAO;AACT;AAIA,SAAgB,qBACd,UACA,YACgB;CAChB,OAAO,IAAI,eAAe;EAAE;EAAU;CAAW,CAAC;AACpD;AAIA,SAAS,gCAAsC;CAC7C,IAAI,eAAe;CACnB,IAAI;EACF,MAAM,WAAW,iBAAiB,wBAAwB;EAC1D,eAAe,QAAQ,KACrB,QAAQ,OAAO,CAAC,CAAC,SAAS,UAAU,IAAI,SAClC,QAAQ,OAAO,CAAC,CAAC,SAAS,QAAQ,MAAM,IAChD;CACF,SAAS,OAAO;EACd,SAAS,oDAAoD,OAAO,KAAK,GAAG;CAC9E;CACA,IAAI,CAAC,cACH,WACE,wLACF;AAEJ"}
@@ -2,19 +2,18 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_exceptions = require("./exceptions.cjs");
3
3
  //#region src/serverErrors.ts
4
4
  const SERVER_ERROR_EVENT_NAME = "apitally.request.server_error";
5
- const MAX_GROUPS = 100;
5
+ const MAX_ERRORS = 100;
6
6
  const MAX_CONSUMER_LENGTH = 128;
7
7
  const MAX_PATH_LENGTH = 2e3;
8
8
  const MAX_TYPE_LENGTH = 256;
9
9
  const MAX_MESSAGE_LENGTH = 2048;
10
10
  const MAX_STACKTRACE_LENGTH = 65536;
11
- const MAX_COUNT = 2 ** 32 - 1;
12
11
  const MESSAGE_TRUNCATION_SUFFIX = "... (truncated)";
13
12
  const STACKTRACE_TRUNCATION_SUFFIX = "\n... (truncated) ...";
14
- const SERVER_ERROR_GROUPS_KEY = Symbol.for("apitally.serverErrorGroups");
15
- const serverErrorGroupsHolder = globalThis;
16
- const serverErrorGroups = serverErrorGroupsHolder[SERVER_ERROR_GROUPS_KEY] ?? /* @__PURE__ */ new Map();
17
- serverErrorGroupsHolder[SERVER_ERROR_GROUPS_KEY] = serverErrorGroups;
13
+ const SERVER_ERROR_AGGREGATES_KEY = Symbol.for("apitally.serverErrorAggregates");
14
+ const serverErrorAggregatesHolder = globalThis;
15
+ const serverErrorAggregates = serverErrorAggregatesHolder[SERVER_ERROR_AGGREGATES_KEY] ?? /* @__PURE__ */ new Map();
16
+ serverErrorAggregatesHolder[SERVER_ERROR_AGGREGATES_KEY] = serverErrorAggregates;
18
17
  function addServerError(consumer, method, path, error, sentryEventId) {
19
18
  method = method.toUpperCase();
20
19
  if (method === "OPTIONS" || !path) return;
@@ -24,37 +23,40 @@ function addServerError(consumer, method, path, error, sentryEventId) {
24
23
  message: exception,
25
24
  stack: ""
26
25
  } : exception;
27
- const group = {
26
+ const fields = {
28
27
  method,
29
28
  path: path.slice(0, MAX_PATH_LENGTH),
30
29
  type: String(name ?? "").slice(0, MAX_TYPE_LENGTH),
31
30
  message: formatMessage(message),
32
- stacktrace: formatStacktrace(stack),
33
- count: 1
31
+ stacktrace: formatStacktrace(stack)
34
32
  };
35
- if (consumer !== void 0) group.consumer = consumer.slice(0, MAX_CONSUMER_LENGTH);
36
- if (sentryEventId !== void 0) group.sentry_event_id = sentryEventId;
37
- const key = [
38
- group.consumer ?? "",
39
- group.method,
40
- group.path,
41
- group.type,
42
- group.message,
43
- group.stacktrace
44
- ].join("\0");
45
- const existing = serverErrorGroups.get(key);
46
- if (existing) {
47
- existing.count = Math.min(existing.count + 1, MAX_COUNT);
48
- existing.sentry_event_id = group.sentry_event_id ?? existing.sentry_event_id;
49
- } else if (serverErrorGroups.size < MAX_GROUPS) serverErrorGroups.set(key, group);
33
+ const key = Object.values(fields).join("\0");
34
+ let aggregate = serverErrorAggregates.get(key);
35
+ if (!aggregate) {
36
+ if (serverErrorAggregates.size >= MAX_ERRORS) return;
37
+ aggregate = {
38
+ ...fields,
39
+ counts: /* @__PURE__ */ new Map()
40
+ };
41
+ serverErrorAggregates.set(key, aggregate);
42
+ }
43
+ consumer = consumer?.slice(0, MAX_CONSUMER_LENGTH);
44
+ aggregate.counts.set(consumer, (aggregate.counts.get(consumer) ?? 0) + 1);
45
+ if (sentryEventId !== void 0) aggregate.sentry_event_id = sentryEventId;
50
46
  }
51
47
  function drainServerErrors() {
52
- const groups = [...serverErrorGroups.values()];
53
- serverErrorGroups.clear();
54
- return groups;
48
+ const aggregates = [...serverErrorAggregates.values()];
49
+ serverErrorAggregates.clear();
50
+ return aggregates.map(({ counts, ...fields }) => ({
51
+ ...fields,
52
+ counts: [...counts].map(([consumer, count]) => consumer === void 0 ? { count } : {
53
+ consumer,
54
+ count
55
+ })
56
+ }));
55
57
  }
56
58
  function resetServerErrors() {
57
- serverErrorGroups.clear();
59
+ serverErrorAggregates.clear();
58
60
  }
59
61
  function formatMessage(message) {
60
62
  const text = String(message ?? "").trim();
@@ -1 +1 @@
1
- {"version":3,"file":"serverErrors.cjs","names":["coerceToException"],"sources":["../src/serverErrors.ts"],"sourcesContent":["import type { AnyValueMap } from \"@opentelemetry/api-logs\";\nimport { coerceToException } from \"./exceptions.js\";\n\nexport const SERVER_ERROR_EVENT_NAME = \"apitally.request.server_error\";\n\nconst MAX_GROUPS = 100;\nconst MAX_CONSUMER_LENGTH = 128;\nconst MAX_PATH_LENGTH = 2_000;\nconst MAX_TYPE_LENGTH = 256;\nconst MAX_MESSAGE_LENGTH = 2_048;\nconst MAX_STACKTRACE_LENGTH = 65_536;\nconst MAX_COUNT = 2 ** 32 - 1;\nconst MESSAGE_TRUNCATION_SUFFIX = \"... (truncated)\";\nconst STACKTRACE_TRUNCATION_SUFFIX = \"\\n... (truncated) ...\";\n\ntype ServerErrorGroup = {\n consumer?: string;\n method: string;\n path: string;\n type: string;\n message: string;\n stacktrace: string;\n count: number;\n sentry_event_id?: string;\n};\n\nconst SERVER_ERROR_GROUPS_KEY = Symbol.for(\"apitally.serverErrorGroups\");\nconst serverErrorGroupsHolder = globalThis as Record<\n symbol,\n Map<string, ServerErrorGroup> | undefined\n>;\nconst serverErrorGroups =\n serverErrorGroupsHolder[SERVER_ERROR_GROUPS_KEY] ?? new Map<string, ServerErrorGroup>();\nserverErrorGroupsHolder[SERVER_ERROR_GROUPS_KEY] = serverErrorGroups;\n\nexport function addServerError(\n consumer: string | undefined,\n method: string,\n path: string,\n error: unknown,\n sentryEventId: string | undefined,\n): void {\n method = method.toUpperCase();\n if (method === \"OPTIONS\" || !path) {\n return;\n }\n const exception = coerceToException(error);\n const { name, message, stack } =\n typeof exception === \"string\"\n ? { name: \"\", message: exception, stack: \"\" }\n : (exception as { name?: unknown; message?: unknown; stack?: unknown });\n const group: ServerErrorGroup = {\n method,\n path: path.slice(0, MAX_PATH_LENGTH),\n type: String(name ?? \"\").slice(0, MAX_TYPE_LENGTH),\n message: formatMessage(message),\n stacktrace: formatStacktrace(stack),\n count: 1,\n };\n if (consumer !== undefined) {\n group.consumer = consumer.slice(0, MAX_CONSUMER_LENGTH);\n }\n if (sentryEventId !== undefined) {\n group.sentry_event_id = sentryEventId;\n }\n const key = [\n group.consumer ?? \"\",\n group.method,\n group.path,\n group.type,\n group.message,\n group.stacktrace,\n ].join(\"\\0\");\n const existing = serverErrorGroups.get(key);\n if (existing) {\n existing.count = Math.min(existing.count + 1, MAX_COUNT);\n existing.sentry_event_id = group.sentry_event_id ?? existing.sentry_event_id;\n } else if (serverErrorGroups.size < MAX_GROUPS) {\n serverErrorGroups.set(key, group);\n }\n}\n\nexport function drainServerErrors(): AnyValueMap[] {\n const groups = [...serverErrorGroups.values()];\n serverErrorGroups.clear();\n return groups;\n}\n\nexport function resetServerErrors(): void {\n serverErrorGroups.clear();\n}\n\nfunction formatMessage(message: unknown): string {\n const text = String(message ?? \"\").trim();\n if (text.length <= MAX_MESSAGE_LENGTH) {\n return text;\n }\n return (\n text.slice(0, MAX_MESSAGE_LENGTH - MESSAGE_TRUNCATION_SUFFIX.length) + MESSAGE_TRUNCATION_SUFFIX\n );\n}\n\n// The error line and innermost frames come first in a JS stack, so the head is kept.\nfunction formatStacktrace(stack: unknown): string {\n const text = typeof stack === \"string\" ? stack.trim() : \"\";\n if (text.length <= MAX_STACKTRACE_LENGTH) {\n return text;\n }\n const cutoff = MAX_STACKTRACE_LENGTH - STACKTRACE_TRUNCATION_SUFFIX.length;\n const lines: string[] = [];\n let length = 0;\n for (const line of text.split(\"\\n\")) {\n if (length + line.length + 1 > cutoff) {\n break;\n }\n lines.push(line);\n length += line.length + 1;\n }\n return lines.join(\"\\n\") + STACKTRACE_TRUNCATION_SUFFIX;\n}\n"],"mappings":";;;AAGA,MAAa,0BAA0B;AAEvC,MAAM,aAAa;AACnB,MAAM,sBAAsB;AAC5B,MAAM,kBAAkB;AACxB,MAAM,kBAAkB;AACxB,MAAM,qBAAqB;AAC3B,MAAM,wBAAwB;AAC9B,MAAM,YAAY,KAAK,KAAK;AAC5B,MAAM,4BAA4B;AAClC,MAAM,+BAA+B;AAarC,MAAM,0BAA0B,OAAO,IAAI,4BAA4B;AACvE,MAAM,0BAA0B;AAIhC,MAAM,oBACJ,wBAAwB,4CAA4B,IAAI,IAA8B;AACxF,wBAAwB,2BAA2B;AAEnD,SAAgB,eACd,UACA,QACA,MACA,OACA,eACM;CACN,SAAS,OAAO,YAAY;CAC5B,IAAI,WAAW,aAAa,CAAC,MAC3B;CAEF,MAAM,YAAYA,mBAAAA,kBAAkB,KAAK;CACzC,MAAM,EAAE,MAAM,SAAS,UACrB,OAAO,cAAc,WACjB;EAAE,MAAM;EAAI,SAAS;EAAW,OAAO;CAAG,IACzC;CACP,MAAM,QAA0B;EAC9B;EACA,MAAM,KAAK,MAAM,GAAG,eAAe;EACnC,MAAM,OAAO,QAAQ,EAAE,CAAC,CAAC,MAAM,GAAG,eAAe;EACjD,SAAS,cAAc,OAAO;EAC9B,YAAY,iBAAiB,KAAK;EAClC,OAAO;CACT;CACA,IAAI,aAAa,KAAA,GACf,MAAM,WAAW,SAAS,MAAM,GAAG,mBAAmB;CAExD,IAAI,kBAAkB,KAAA,GACpB,MAAM,kBAAkB;CAE1B,MAAM,MAAM;EACV,MAAM,YAAY;EAClB,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM;CACR,CAAC,CAAC,KAAK,IAAI;CACX,MAAM,WAAW,kBAAkB,IAAI,GAAG;CAC1C,IAAI,UAAU;EACZ,SAAS,QAAQ,KAAK,IAAI,SAAS,QAAQ,GAAG,SAAS;EACvD,SAAS,kBAAkB,MAAM,mBAAmB,SAAS;CAC/D,OAAO,IAAI,kBAAkB,OAAO,YAClC,kBAAkB,IAAI,KAAK,KAAK;AAEpC;AAEA,SAAgB,oBAAmC;CACjD,MAAM,SAAS,CAAC,GAAG,kBAAkB,OAAO,CAAC;CAC7C,kBAAkB,MAAM;CACxB,OAAO;AACT;AAEA,SAAgB,oBAA0B;CACxC,kBAAkB,MAAM;AAC1B;AAEA,SAAS,cAAc,SAA0B;CAC/C,MAAM,OAAO,OAAO,WAAW,EAAE,CAAC,CAAC,KAAK;CACxC,IAAI,KAAK,UAAU,oBACjB,OAAO;CAET,OACE,KAAK,MAAM,GAAG,IAAqD,IAAI;AAE3E;AAGA,SAAS,iBAAiB,OAAwB;CAChD,MAAM,OAAO,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;CACxD,IAAI,KAAK,UAAU,uBACjB,OAAO;CAET,MAAM,SAAS;CACf,MAAM,QAAkB,CAAC;CACzB,IAAI,SAAS;CACb,KAAK,MAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;EACnC,IAAI,SAAS,KAAK,SAAS,IAAI,QAC7B;EAEF,MAAM,KAAK,IAAI;EACf,UAAU,KAAK,SAAS;CAC1B;CACA,OAAO,MAAM,KAAK,IAAI,IAAI;AAC5B"}
1
+ {"version":3,"file":"serverErrors.cjs","names":["coerceToException"],"sources":["../src/serverErrors.ts"],"sourcesContent":["import type { AnyValueMap } from \"@opentelemetry/api-logs\";\nimport { coerceToException } from \"./exceptions.js\";\n\nexport const SERVER_ERROR_EVENT_NAME = \"apitally.request.server_error\";\n\nconst MAX_ERRORS = 100;\nconst MAX_CONSUMER_LENGTH = 128;\nconst MAX_PATH_LENGTH = 2_000;\nconst MAX_TYPE_LENGTH = 256;\nconst MAX_MESSAGE_LENGTH = 2_048;\nconst MAX_STACKTRACE_LENGTH = 65_536;\nconst MESSAGE_TRUNCATION_SUFFIX = \"... (truncated)\";\nconst STACKTRACE_TRUNCATION_SUFFIX = \"\\n... (truncated) ...\";\n\ntype ServerErrorAggregate = {\n method: string;\n path: string;\n type: string;\n message: string;\n stacktrace: string;\n sentry_event_id?: string;\n counts: Map<string | undefined, number>;\n};\n\nconst SERVER_ERROR_AGGREGATES_KEY = Symbol.for(\"apitally.serverErrorAggregates\");\nconst serverErrorAggregatesHolder = globalThis as Record<\n symbol,\n Map<string, ServerErrorAggregate> | undefined\n>;\nconst serverErrorAggregates =\n serverErrorAggregatesHolder[SERVER_ERROR_AGGREGATES_KEY] ??\n new Map<string, ServerErrorAggregate>();\nserverErrorAggregatesHolder[SERVER_ERROR_AGGREGATES_KEY] = serverErrorAggregates;\n\nexport function addServerError(\n consumer: string | undefined,\n method: string,\n path: string,\n error: unknown,\n sentryEventId: string | undefined,\n): void {\n method = method.toUpperCase();\n if (method === \"OPTIONS\" || !path) {\n return;\n }\n const exception = coerceToException(error);\n const { name, message, stack } =\n typeof exception === \"string\"\n ? { name: \"\", message: exception, stack: \"\" }\n : (exception as { name?: unknown; message?: unknown; stack?: unknown });\n const fields = {\n method,\n path: path.slice(0, MAX_PATH_LENGTH),\n type: String(name ?? \"\").slice(0, MAX_TYPE_LENGTH),\n message: formatMessage(message),\n stacktrace: formatStacktrace(stack),\n };\n const key = Object.values(fields).join(\"\\0\");\n let aggregate = serverErrorAggregates.get(key);\n if (!aggregate) {\n if (serverErrorAggregates.size >= MAX_ERRORS) {\n return;\n }\n aggregate = { ...fields, counts: new Map() };\n serverErrorAggregates.set(key, aggregate);\n }\n consumer = consumer?.slice(0, MAX_CONSUMER_LENGTH);\n aggregate.counts.set(consumer, (aggregate.counts.get(consumer) ?? 0) + 1);\n if (sentryEventId !== undefined) {\n aggregate.sentry_event_id = sentryEventId;\n }\n}\n\nexport function drainServerErrors(): AnyValueMap[] {\n const aggregates = [...serverErrorAggregates.values()];\n serverErrorAggregates.clear();\n return aggregates.map(({ counts, ...fields }) => ({\n ...fields,\n counts: [...counts].map(([consumer, count]) =>\n consumer === undefined ? { count } : { consumer, count },\n ),\n }));\n}\n\nexport function resetServerErrors(): void {\n serverErrorAggregates.clear();\n}\n\nfunction formatMessage(message: unknown): string {\n const text = String(message ?? \"\").trim();\n if (text.length <= MAX_MESSAGE_LENGTH) {\n return text;\n }\n return (\n text.slice(0, MAX_MESSAGE_LENGTH - MESSAGE_TRUNCATION_SUFFIX.length) + MESSAGE_TRUNCATION_SUFFIX\n );\n}\n\n// The error line and innermost frames come first in a JS stack, so the head is kept.\nfunction formatStacktrace(stack: unknown): string {\n const text = typeof stack === \"string\" ? stack.trim() : \"\";\n if (text.length <= MAX_STACKTRACE_LENGTH) {\n return text;\n }\n const cutoff = MAX_STACKTRACE_LENGTH - STACKTRACE_TRUNCATION_SUFFIX.length;\n const lines: string[] = [];\n let length = 0;\n for (const line of text.split(\"\\n\")) {\n if (length + line.length + 1 > cutoff) {\n break;\n }\n lines.push(line);\n length += line.length + 1;\n }\n return lines.join(\"\\n\") + STACKTRACE_TRUNCATION_SUFFIX;\n}\n"],"mappings":";;;AAGA,MAAa,0BAA0B;AAEvC,MAAM,aAAa;AACnB,MAAM,sBAAsB;AAC5B,MAAM,kBAAkB;AACxB,MAAM,kBAAkB;AACxB,MAAM,qBAAqB;AAC3B,MAAM,wBAAwB;AAC9B,MAAM,4BAA4B;AAClC,MAAM,+BAA+B;AAYrC,MAAM,8BAA8B,OAAO,IAAI,gCAAgC;AAC/E,MAAM,8BAA8B;AAIpC,MAAM,wBACJ,4BAA4B,gDAC5B,IAAI,IAAkC;AACxC,4BAA4B,+BAA+B;AAE3D,SAAgB,eACd,UACA,QACA,MACA,OACA,eACM;CACN,SAAS,OAAO,YAAY;CAC5B,IAAI,WAAW,aAAa,CAAC,MAC3B;CAEF,MAAM,YAAYA,mBAAAA,kBAAkB,KAAK;CACzC,MAAM,EAAE,MAAM,SAAS,UACrB,OAAO,cAAc,WACjB;EAAE,MAAM;EAAI,SAAS;EAAW,OAAO;CAAG,IACzC;CACP,MAAM,SAAS;EACb;EACA,MAAM,KAAK,MAAM,GAAG,eAAe;EACnC,MAAM,OAAO,QAAQ,EAAE,CAAC,CAAC,MAAM,GAAG,eAAe;EACjD,SAAS,cAAc,OAAO;EAC9B,YAAY,iBAAiB,KAAK;CACpC;CACA,MAAM,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,KAAK,IAAI;CAC3C,IAAI,YAAY,sBAAsB,IAAI,GAAG;CAC7C,IAAI,CAAC,WAAW;EACd,IAAI,sBAAsB,QAAQ,YAChC;EAEF,YAAY;GAAE,GAAG;GAAQ,wBAAQ,IAAI,IAAI;EAAE;EAC3C,sBAAsB,IAAI,KAAK,SAAS;CAC1C;CACA,WAAW,UAAU,MAAM,GAAG,mBAAmB;CACjD,UAAU,OAAO,IAAI,WAAW,UAAU,OAAO,IAAI,QAAQ,KAAK,KAAK,CAAC;CACxE,IAAI,kBAAkB,KAAA,GACpB,UAAU,kBAAkB;AAEhC;AAEA,SAAgB,oBAAmC;CACjD,MAAM,aAAa,CAAC,GAAG,sBAAsB,OAAO,CAAC;CACrD,sBAAsB,MAAM;CAC5B,OAAO,WAAW,KAAK,EAAE,QAAQ,GAAG,cAAc;EAChD,GAAG;EACH,QAAQ,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK,CAAC,UAAU,WAClC,aAAa,KAAA,IAAY,EAAE,MAAM,IAAI;GAAE;GAAU;EAAM,CACzD;CACF,EAAE;AACJ;AAEA,SAAgB,oBAA0B;CACxC,sBAAsB,MAAM;AAC9B;AAEA,SAAS,cAAc,SAA0B;CAC/C,MAAM,OAAO,OAAO,WAAW,EAAE,CAAC,CAAC,KAAK;CACxC,IAAI,KAAK,UAAU,oBACjB,OAAO;CAET,OACE,KAAK,MAAM,GAAG,IAAqD,IAAI;AAE3E;AAGA,SAAS,iBAAiB,OAAwB;CAChD,MAAM,OAAO,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;CACxD,IAAI,KAAK,UAAU,uBACjB,OAAO;CAET,MAAM,SAAS;CACf,MAAM,QAAkB,CAAC;CACzB,IAAI,SAAS;CACb,KAAK,MAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;EACnC,IAAI,SAAS,KAAK,SAAS,IAAI,QAC7B;EAEF,MAAM,KAAK,IAAI;EACf,UAAU,KAAK,SAAS;CAC1B;CACA,OAAO,MAAM,KAAK,IAAI,IAAI;AAC5B"}
@@ -1,19 +1,18 @@
1
1
  import { coerceToException } from "./exceptions.js";
2
2
  //#region src/serverErrors.ts
3
3
  const SERVER_ERROR_EVENT_NAME = "apitally.request.server_error";
4
- const MAX_GROUPS = 100;
4
+ const MAX_ERRORS = 100;
5
5
  const MAX_CONSUMER_LENGTH = 128;
6
6
  const MAX_PATH_LENGTH = 2e3;
7
7
  const MAX_TYPE_LENGTH = 256;
8
8
  const MAX_MESSAGE_LENGTH = 2048;
9
9
  const MAX_STACKTRACE_LENGTH = 65536;
10
- const MAX_COUNT = 2 ** 32 - 1;
11
10
  const MESSAGE_TRUNCATION_SUFFIX = "... (truncated)";
12
11
  const STACKTRACE_TRUNCATION_SUFFIX = "\n... (truncated) ...";
13
- const SERVER_ERROR_GROUPS_KEY = Symbol.for("apitally.serverErrorGroups");
14
- const serverErrorGroupsHolder = globalThis;
15
- const serverErrorGroups = serverErrorGroupsHolder[SERVER_ERROR_GROUPS_KEY] ?? /* @__PURE__ */ new Map();
16
- serverErrorGroupsHolder[SERVER_ERROR_GROUPS_KEY] = serverErrorGroups;
12
+ const SERVER_ERROR_AGGREGATES_KEY = Symbol.for("apitally.serverErrorAggregates");
13
+ const serverErrorAggregatesHolder = globalThis;
14
+ const serverErrorAggregates = serverErrorAggregatesHolder[SERVER_ERROR_AGGREGATES_KEY] ?? /* @__PURE__ */ new Map();
15
+ serverErrorAggregatesHolder[SERVER_ERROR_AGGREGATES_KEY] = serverErrorAggregates;
17
16
  function addServerError(consumer, method, path, error, sentryEventId) {
18
17
  method = method.toUpperCase();
19
18
  if (method === "OPTIONS" || !path) return;
@@ -23,37 +22,40 @@ function addServerError(consumer, method, path, error, sentryEventId) {
23
22
  message: exception,
24
23
  stack: ""
25
24
  } : exception;
26
- const group = {
25
+ const fields = {
27
26
  method,
28
27
  path: path.slice(0, MAX_PATH_LENGTH),
29
28
  type: String(name ?? "").slice(0, MAX_TYPE_LENGTH),
30
29
  message: formatMessage(message),
31
- stacktrace: formatStacktrace(stack),
32
- count: 1
30
+ stacktrace: formatStacktrace(stack)
33
31
  };
34
- if (consumer !== void 0) group.consumer = consumer.slice(0, MAX_CONSUMER_LENGTH);
35
- if (sentryEventId !== void 0) group.sentry_event_id = sentryEventId;
36
- const key = [
37
- group.consumer ?? "",
38
- group.method,
39
- group.path,
40
- group.type,
41
- group.message,
42
- group.stacktrace
43
- ].join("\0");
44
- const existing = serverErrorGroups.get(key);
45
- if (existing) {
46
- existing.count = Math.min(existing.count + 1, MAX_COUNT);
47
- existing.sentry_event_id = group.sentry_event_id ?? existing.sentry_event_id;
48
- } else if (serverErrorGroups.size < MAX_GROUPS) serverErrorGroups.set(key, group);
32
+ const key = Object.values(fields).join("\0");
33
+ let aggregate = serverErrorAggregates.get(key);
34
+ if (!aggregate) {
35
+ if (serverErrorAggregates.size >= MAX_ERRORS) return;
36
+ aggregate = {
37
+ ...fields,
38
+ counts: /* @__PURE__ */ new Map()
39
+ };
40
+ serverErrorAggregates.set(key, aggregate);
41
+ }
42
+ consumer = consumer?.slice(0, MAX_CONSUMER_LENGTH);
43
+ aggregate.counts.set(consumer, (aggregate.counts.get(consumer) ?? 0) + 1);
44
+ if (sentryEventId !== void 0) aggregate.sentry_event_id = sentryEventId;
49
45
  }
50
46
  function drainServerErrors() {
51
- const groups = [...serverErrorGroups.values()];
52
- serverErrorGroups.clear();
53
- return groups;
47
+ const aggregates = [...serverErrorAggregates.values()];
48
+ serverErrorAggregates.clear();
49
+ return aggregates.map(({ counts, ...fields }) => ({
50
+ ...fields,
51
+ counts: [...counts].map(([consumer, count]) => consumer === void 0 ? { count } : {
52
+ consumer,
53
+ count
54
+ })
55
+ }));
54
56
  }
55
57
  function resetServerErrors() {
56
- serverErrorGroups.clear();
58
+ serverErrorAggregates.clear();
57
59
  }
58
60
  function formatMessage(message) {
59
61
  const text = String(message ?? "").trim();
@@ -1 +1 @@
1
- {"version":3,"file":"serverErrors.js","names":[],"sources":["../src/serverErrors.ts"],"sourcesContent":["import type { AnyValueMap } from \"@opentelemetry/api-logs\";\nimport { coerceToException } from \"./exceptions.js\";\n\nexport const SERVER_ERROR_EVENT_NAME = \"apitally.request.server_error\";\n\nconst MAX_GROUPS = 100;\nconst MAX_CONSUMER_LENGTH = 128;\nconst MAX_PATH_LENGTH = 2_000;\nconst MAX_TYPE_LENGTH = 256;\nconst MAX_MESSAGE_LENGTH = 2_048;\nconst MAX_STACKTRACE_LENGTH = 65_536;\nconst MAX_COUNT = 2 ** 32 - 1;\nconst MESSAGE_TRUNCATION_SUFFIX = \"... (truncated)\";\nconst STACKTRACE_TRUNCATION_SUFFIX = \"\\n... (truncated) ...\";\n\ntype ServerErrorGroup = {\n consumer?: string;\n method: string;\n path: string;\n type: string;\n message: string;\n stacktrace: string;\n count: number;\n sentry_event_id?: string;\n};\n\nconst SERVER_ERROR_GROUPS_KEY = Symbol.for(\"apitally.serverErrorGroups\");\nconst serverErrorGroupsHolder = globalThis as Record<\n symbol,\n Map<string, ServerErrorGroup> | undefined\n>;\nconst serverErrorGroups =\n serverErrorGroupsHolder[SERVER_ERROR_GROUPS_KEY] ?? new Map<string, ServerErrorGroup>();\nserverErrorGroupsHolder[SERVER_ERROR_GROUPS_KEY] = serverErrorGroups;\n\nexport function addServerError(\n consumer: string | undefined,\n method: string,\n path: string,\n error: unknown,\n sentryEventId: string | undefined,\n): void {\n method = method.toUpperCase();\n if (method === \"OPTIONS\" || !path) {\n return;\n }\n const exception = coerceToException(error);\n const { name, message, stack } =\n typeof exception === \"string\"\n ? { name: \"\", message: exception, stack: \"\" }\n : (exception as { name?: unknown; message?: unknown; stack?: unknown });\n const group: ServerErrorGroup = {\n method,\n path: path.slice(0, MAX_PATH_LENGTH),\n type: String(name ?? \"\").slice(0, MAX_TYPE_LENGTH),\n message: formatMessage(message),\n stacktrace: formatStacktrace(stack),\n count: 1,\n };\n if (consumer !== undefined) {\n group.consumer = consumer.slice(0, MAX_CONSUMER_LENGTH);\n }\n if (sentryEventId !== undefined) {\n group.sentry_event_id = sentryEventId;\n }\n const key = [\n group.consumer ?? \"\",\n group.method,\n group.path,\n group.type,\n group.message,\n group.stacktrace,\n ].join(\"\\0\");\n const existing = serverErrorGroups.get(key);\n if (existing) {\n existing.count = Math.min(existing.count + 1, MAX_COUNT);\n existing.sentry_event_id = group.sentry_event_id ?? existing.sentry_event_id;\n } else if (serverErrorGroups.size < MAX_GROUPS) {\n serverErrorGroups.set(key, group);\n }\n}\n\nexport function drainServerErrors(): AnyValueMap[] {\n const groups = [...serverErrorGroups.values()];\n serverErrorGroups.clear();\n return groups;\n}\n\nexport function resetServerErrors(): void {\n serverErrorGroups.clear();\n}\n\nfunction formatMessage(message: unknown): string {\n const text = String(message ?? \"\").trim();\n if (text.length <= MAX_MESSAGE_LENGTH) {\n return text;\n }\n return (\n text.slice(0, MAX_MESSAGE_LENGTH - MESSAGE_TRUNCATION_SUFFIX.length) + MESSAGE_TRUNCATION_SUFFIX\n );\n}\n\n// The error line and innermost frames come first in a JS stack, so the head is kept.\nfunction formatStacktrace(stack: unknown): string {\n const text = typeof stack === \"string\" ? stack.trim() : \"\";\n if (text.length <= MAX_STACKTRACE_LENGTH) {\n return text;\n }\n const cutoff = MAX_STACKTRACE_LENGTH - STACKTRACE_TRUNCATION_SUFFIX.length;\n const lines: string[] = [];\n let length = 0;\n for (const line of text.split(\"\\n\")) {\n if (length + line.length + 1 > cutoff) {\n break;\n }\n lines.push(line);\n length += line.length + 1;\n }\n return lines.join(\"\\n\") + STACKTRACE_TRUNCATION_SUFFIX;\n}\n"],"mappings":";;AAGA,MAAa,0BAA0B;AAEvC,MAAM,aAAa;AACnB,MAAM,sBAAsB;AAC5B,MAAM,kBAAkB;AACxB,MAAM,kBAAkB;AACxB,MAAM,qBAAqB;AAC3B,MAAM,wBAAwB;AAC9B,MAAM,YAAY,KAAK,KAAK;AAC5B,MAAM,4BAA4B;AAClC,MAAM,+BAA+B;AAarC,MAAM,0BAA0B,OAAO,IAAI,4BAA4B;AACvE,MAAM,0BAA0B;AAIhC,MAAM,oBACJ,wBAAwB,4CAA4B,IAAI,IAA8B;AACxF,wBAAwB,2BAA2B;AAEnD,SAAgB,eACd,UACA,QACA,MACA,OACA,eACM;CACN,SAAS,OAAO,YAAY;CAC5B,IAAI,WAAW,aAAa,CAAC,MAC3B;CAEF,MAAM,YAAY,kBAAkB,KAAK;CACzC,MAAM,EAAE,MAAM,SAAS,UACrB,OAAO,cAAc,WACjB;EAAE,MAAM;EAAI,SAAS;EAAW,OAAO;CAAG,IACzC;CACP,MAAM,QAA0B;EAC9B;EACA,MAAM,KAAK,MAAM,GAAG,eAAe;EACnC,MAAM,OAAO,QAAQ,EAAE,CAAC,CAAC,MAAM,GAAG,eAAe;EACjD,SAAS,cAAc,OAAO;EAC9B,YAAY,iBAAiB,KAAK;EAClC,OAAO;CACT;CACA,IAAI,aAAa,KAAA,GACf,MAAM,WAAW,SAAS,MAAM,GAAG,mBAAmB;CAExD,IAAI,kBAAkB,KAAA,GACpB,MAAM,kBAAkB;CAE1B,MAAM,MAAM;EACV,MAAM,YAAY;EAClB,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM;CACR,CAAC,CAAC,KAAK,IAAI;CACX,MAAM,WAAW,kBAAkB,IAAI,GAAG;CAC1C,IAAI,UAAU;EACZ,SAAS,QAAQ,KAAK,IAAI,SAAS,QAAQ,GAAG,SAAS;EACvD,SAAS,kBAAkB,MAAM,mBAAmB,SAAS;CAC/D,OAAO,IAAI,kBAAkB,OAAO,YAClC,kBAAkB,IAAI,KAAK,KAAK;AAEpC;AAEA,SAAgB,oBAAmC;CACjD,MAAM,SAAS,CAAC,GAAG,kBAAkB,OAAO,CAAC;CAC7C,kBAAkB,MAAM;CACxB,OAAO;AACT;AAEA,SAAgB,oBAA0B;CACxC,kBAAkB,MAAM;AAC1B;AAEA,SAAS,cAAc,SAA0B;CAC/C,MAAM,OAAO,OAAO,WAAW,EAAE,CAAC,CAAC,KAAK;CACxC,IAAI,KAAK,UAAU,oBACjB,OAAO;CAET,OACE,KAAK,MAAM,GAAG,IAAqD,IAAI;AAE3E;AAGA,SAAS,iBAAiB,OAAwB;CAChD,MAAM,OAAO,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;CACxD,IAAI,KAAK,UAAU,uBACjB,OAAO;CAET,MAAM,SAAS;CACf,MAAM,QAAkB,CAAC;CACzB,IAAI,SAAS;CACb,KAAK,MAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;EACnC,IAAI,SAAS,KAAK,SAAS,IAAI,QAC7B;EAEF,MAAM,KAAK,IAAI;EACf,UAAU,KAAK,SAAS;CAC1B;CACA,OAAO,MAAM,KAAK,IAAI,IAAI;AAC5B"}
1
+ {"version":3,"file":"serverErrors.js","names":[],"sources":["../src/serverErrors.ts"],"sourcesContent":["import type { AnyValueMap } from \"@opentelemetry/api-logs\";\nimport { coerceToException } from \"./exceptions.js\";\n\nexport const SERVER_ERROR_EVENT_NAME = \"apitally.request.server_error\";\n\nconst MAX_ERRORS = 100;\nconst MAX_CONSUMER_LENGTH = 128;\nconst MAX_PATH_LENGTH = 2_000;\nconst MAX_TYPE_LENGTH = 256;\nconst MAX_MESSAGE_LENGTH = 2_048;\nconst MAX_STACKTRACE_LENGTH = 65_536;\nconst MESSAGE_TRUNCATION_SUFFIX = \"... (truncated)\";\nconst STACKTRACE_TRUNCATION_SUFFIX = \"\\n... (truncated) ...\";\n\ntype ServerErrorAggregate = {\n method: string;\n path: string;\n type: string;\n message: string;\n stacktrace: string;\n sentry_event_id?: string;\n counts: Map<string | undefined, number>;\n};\n\nconst SERVER_ERROR_AGGREGATES_KEY = Symbol.for(\"apitally.serverErrorAggregates\");\nconst serverErrorAggregatesHolder = globalThis as Record<\n symbol,\n Map<string, ServerErrorAggregate> | undefined\n>;\nconst serverErrorAggregates =\n serverErrorAggregatesHolder[SERVER_ERROR_AGGREGATES_KEY] ??\n new Map<string, ServerErrorAggregate>();\nserverErrorAggregatesHolder[SERVER_ERROR_AGGREGATES_KEY] = serverErrorAggregates;\n\nexport function addServerError(\n consumer: string | undefined,\n method: string,\n path: string,\n error: unknown,\n sentryEventId: string | undefined,\n): void {\n method = method.toUpperCase();\n if (method === \"OPTIONS\" || !path) {\n return;\n }\n const exception = coerceToException(error);\n const { name, message, stack } =\n typeof exception === \"string\"\n ? { name: \"\", message: exception, stack: \"\" }\n : (exception as { name?: unknown; message?: unknown; stack?: unknown });\n const fields = {\n method,\n path: path.slice(0, MAX_PATH_LENGTH),\n type: String(name ?? \"\").slice(0, MAX_TYPE_LENGTH),\n message: formatMessage(message),\n stacktrace: formatStacktrace(stack),\n };\n const key = Object.values(fields).join(\"\\0\");\n let aggregate = serverErrorAggregates.get(key);\n if (!aggregate) {\n if (serverErrorAggregates.size >= MAX_ERRORS) {\n return;\n }\n aggregate = { ...fields, counts: new Map() };\n serverErrorAggregates.set(key, aggregate);\n }\n consumer = consumer?.slice(0, MAX_CONSUMER_LENGTH);\n aggregate.counts.set(consumer, (aggregate.counts.get(consumer) ?? 0) + 1);\n if (sentryEventId !== undefined) {\n aggregate.sentry_event_id = sentryEventId;\n }\n}\n\nexport function drainServerErrors(): AnyValueMap[] {\n const aggregates = [...serverErrorAggregates.values()];\n serverErrorAggregates.clear();\n return aggregates.map(({ counts, ...fields }) => ({\n ...fields,\n counts: [...counts].map(([consumer, count]) =>\n consumer === undefined ? { count } : { consumer, count },\n ),\n }));\n}\n\nexport function resetServerErrors(): void {\n serverErrorAggregates.clear();\n}\n\nfunction formatMessage(message: unknown): string {\n const text = String(message ?? \"\").trim();\n if (text.length <= MAX_MESSAGE_LENGTH) {\n return text;\n }\n return (\n text.slice(0, MAX_MESSAGE_LENGTH - MESSAGE_TRUNCATION_SUFFIX.length) + MESSAGE_TRUNCATION_SUFFIX\n );\n}\n\n// The error line and innermost frames come first in a JS stack, so the head is kept.\nfunction formatStacktrace(stack: unknown): string {\n const text = typeof stack === \"string\" ? stack.trim() : \"\";\n if (text.length <= MAX_STACKTRACE_LENGTH) {\n return text;\n }\n const cutoff = MAX_STACKTRACE_LENGTH - STACKTRACE_TRUNCATION_SUFFIX.length;\n const lines: string[] = [];\n let length = 0;\n for (const line of text.split(\"\\n\")) {\n if (length + line.length + 1 > cutoff) {\n break;\n }\n lines.push(line);\n length += line.length + 1;\n }\n return lines.join(\"\\n\") + STACKTRACE_TRUNCATION_SUFFIX;\n}\n"],"mappings":";;AAGA,MAAa,0BAA0B;AAEvC,MAAM,aAAa;AACnB,MAAM,sBAAsB;AAC5B,MAAM,kBAAkB;AACxB,MAAM,kBAAkB;AACxB,MAAM,qBAAqB;AAC3B,MAAM,wBAAwB;AAC9B,MAAM,4BAA4B;AAClC,MAAM,+BAA+B;AAYrC,MAAM,8BAA8B,OAAO,IAAI,gCAAgC;AAC/E,MAAM,8BAA8B;AAIpC,MAAM,wBACJ,4BAA4B,gDAC5B,IAAI,IAAkC;AACxC,4BAA4B,+BAA+B;AAE3D,SAAgB,eACd,UACA,QACA,MACA,OACA,eACM;CACN,SAAS,OAAO,YAAY;CAC5B,IAAI,WAAW,aAAa,CAAC,MAC3B;CAEF,MAAM,YAAY,kBAAkB,KAAK;CACzC,MAAM,EAAE,MAAM,SAAS,UACrB,OAAO,cAAc,WACjB;EAAE,MAAM;EAAI,SAAS;EAAW,OAAO;CAAG,IACzC;CACP,MAAM,SAAS;EACb;EACA,MAAM,KAAK,MAAM,GAAG,eAAe;EACnC,MAAM,OAAO,QAAQ,EAAE,CAAC,CAAC,MAAM,GAAG,eAAe;EACjD,SAAS,cAAc,OAAO;EAC9B,YAAY,iBAAiB,KAAK;CACpC;CACA,MAAM,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,KAAK,IAAI;CAC3C,IAAI,YAAY,sBAAsB,IAAI,GAAG;CAC7C,IAAI,CAAC,WAAW;EACd,IAAI,sBAAsB,QAAQ,YAChC;EAEF,YAAY;GAAE,GAAG;GAAQ,wBAAQ,IAAI,IAAI;EAAE;EAC3C,sBAAsB,IAAI,KAAK,SAAS;CAC1C;CACA,WAAW,UAAU,MAAM,GAAG,mBAAmB;CACjD,UAAU,OAAO,IAAI,WAAW,UAAU,OAAO,IAAI,QAAQ,KAAK,KAAK,CAAC;CACxE,IAAI,kBAAkB,KAAA,GACpB,UAAU,kBAAkB;AAEhC;AAEA,SAAgB,oBAAmC;CACjD,MAAM,aAAa,CAAC,GAAG,sBAAsB,OAAO,CAAC;CACrD,sBAAsB,MAAM;CAC5B,OAAO,WAAW,KAAK,EAAE,QAAQ,GAAG,cAAc;EAChD,GAAG;EACH,QAAQ,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK,CAAC,UAAU,WAClC,aAAa,KAAA,IAAY,EAAE,MAAM,IAAI;GAAE;GAAU;EAAM,CACzD;CACF,EAAE;AACJ;AAEA,SAAgB,oBAA0B;CACxC,sBAAsB,MAAM;AAC9B;AAEA,SAAS,cAAc,SAA0B;CAC/C,MAAM,OAAO,OAAO,WAAW,EAAE,CAAC,CAAC,KAAK;CACxC,IAAI,KAAK,UAAU,oBACjB,OAAO;CAET,OACE,KAAK,MAAM,GAAG,IAAqD,IAAI;AAE3E;AAGA,SAAS,iBAAiB,OAAwB;CAChD,MAAM,OAAO,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;CACxD,IAAI,KAAK,UAAU,uBACjB,OAAO;CAET,MAAM,SAAS;CACf,MAAM,QAAkB,CAAC;CACzB,IAAI,SAAS;CACb,KAAK,MAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;EACnC,IAAI,SAAS,KAAK,SAAS,IAAI,QAC7B;EAEF,MAAM,KAAK,IAAI;EACf,UAAU,KAAK,SAAS;CAC1B;CACA,OAAO,MAAM,KAAK,IAAI,IAAI;AAC5B"}
package/dist/spool.cjs CHANGED
@@ -24,6 +24,7 @@ var Spool = class {
24
24
  maxSize;
25
25
  current = /* @__PURE__ */ new Map();
26
26
  closed = [];
27
+ filesClosedSinceExportRotation = 0;
27
28
  tempDir;
28
29
  queues = {
29
30
  traces: Promise.resolve(),
@@ -63,6 +64,9 @@ var Spool = class {
63
64
  if (this.current.has(signal) && !this.closed.some((file) => file.signal === signal)) await this.closeCurrentFile(signal);
64
65
  });
65
66
  await this.evict();
67
+ const closedFileCount = this.filesClosedSinceExportRotation;
68
+ this.filesClosedSinceExportRotation = 0;
69
+ return closedFileCount;
66
70
  }
67
71
  async closeCurrentFiles() {
68
72
  for (const signal of SIGNALS) await this.enqueue(signal, () => this.closeCurrentFile(signal));
@@ -100,6 +104,7 @@ var Spool = class {
100
104
  try {
101
105
  await file.close();
102
106
  this.closed.push(file);
107
+ this.filesClosedSinceExportRotation += 1;
103
108
  } catch {
104
109
  require_logger.logWarning(WRITE_FAILURE_WARNING);
105
110
  await file.delete();
@@ -1 +1 @@
1
- {"version":3,"file":"spool.cjs","names":["tmpdir","createGzip","join","randomUUID","openSync","createWriteStream","readFile","unlink","readdirSync","statSync"],"sources":["../src/spool.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n createWriteStream,\n openSync,\n readdirSync,\n statSync,\n unlinkSync,\n utimesSync,\n type WriteStream,\n writeFileSync,\n} from \"node:fs\";\nimport { readFile, unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createGzip, type Gzip } from \"node:zlib\";\nimport { logWarning, resetWarning } from \"./logger.js\";\n\nconst WRITE_FAILURE_WARNING = \"Error writing to disk, dropping buffered telemetry\";\n\nconst SIGNALS = [\"traces\", \"logs\", \"metrics\"] as const;\nexport type Signal = (typeof SIGNALS)[number];\n\nconst MAX_UNCOMPRESSED_FILE_SIZE = 4_000_000;\nconst MAX_RETRY_TIME_AFTER_FIRST_ATTEMPT_MILLIS = 59 * 60 * 1000;\nconst MAX_SPOOL_SIZE_DISK = 50_000_000;\nconst MAX_SPOOL_SIZE_MEMORY = 10_000_000;\nconst MAX_UNTOUCHED_FILE_AGE_MILLIS = 2 * 60 * 60 * 1000;\nconst SPOOL_FILE_NAME_PATTERN = /^apitally-.*\\.gz$/;\n\n// Operations for each signal are serialized because gzip and file streams\n// complete asynchronously.\nexport class Spool {\n readonly inMemory: boolean;\n maxSize: number;\n readonly current = new Map<Signal, SpoolFile>();\n private readonly closed: SpoolFile[] = [];\n private readonly tempDir: string;\n private readonly queues: Record<Signal, Promise<void>> = {\n traces: Promise.resolve(),\n logs: Promise.resolve(),\n metrics: Promise.resolve(),\n };\n\n constructor(tempDir: string = tmpdir()) {\n this.tempDir = tempDir;\n this.inMemory = !isTempDirWritable(tempDir);\n if (this.inMemory) {\n logWarning(\n `Unable to create temporary files, buffering telemetry in memory (max ${MAX_SPOOL_SIZE_MEMORY / 1_000_000} MB)`,\n );\n } else {\n cleanupOrphanedFiles(tempDir);\n }\n this.maxSize = this.inMemory ? MAX_SPOOL_SIZE_MEMORY : MAX_SPOOL_SIZE_DISK;\n }\n\n append(signal: Signal, payload: Uint8Array): Promise<void> {\n return this.enqueue(signal, async () => {\n let file = this.current.get(signal);\n if (file && file.uncompressedSize + payload.length > MAX_UNCOMPRESSED_FILE_SIZE) {\n await this.closeCurrentFile(signal);\n file = undefined;\n }\n try {\n if (!file) {\n file = new SpoolFile(signal, this.inMemory ? undefined : this.tempDir);\n this.current.set(signal, file);\n }\n await file.write(payload);\n // Writes recovered; the write-failure warning may fire again later.\n resetWarning(WRITE_FAILURE_WARNING);\n } catch {\n logWarning(WRITE_FAILURE_WARNING);\n await this.discardCurrentFile(signal);\n }\n await this.evict();\n });\n }\n\n // Current files close only when no backlog exists, avoiding one file per cycle.\n // Sequential rotation preserves send order.\n async rotateForExport(): Promise<void> {\n for (const signal of SIGNALS) {\n await this.enqueue(signal, async () => {\n if (this.current.has(signal) && !this.closed.some((file) => file.signal === signal)) {\n await this.closeCurrentFile(signal);\n }\n });\n }\n await this.evict();\n }\n\n async closeCurrentFiles(): Promise<void> {\n for (const signal of SIGNALS) {\n await this.enqueue(signal, () => this.closeCurrentFile(signal));\n }\n }\n\n pendingFiles(): SpoolFile[] {\n return [...this.closed];\n }\n\n async deleteFile(file: SpoolFile): Promise<void> {\n const index = this.closed.indexOf(file);\n if (index >= 0) {\n this.closed.splice(index, 1);\n }\n await file.delete();\n }\n\n // Apitally ingest deduplicates for one hour; a later retry could ingest the file twice.\n deleteIfExpired(file: SpoolFile): Promise<void> | undefined {\n if (!file.isExpired()) {\n return undefined;\n }\n logWarning(`Buffered ${file.signal} could not be delivered within an hour and were dropped`);\n return this.deleteFile(file);\n }\n\n // Refresh mtimes so orphan cleanup in another process preserves live files.\n touchFiles(): void {\n for (const file of [...this.current.values(), ...this.closed]) {\n file.touch();\n }\n }\n\n async clear(): Promise<void> {\n await Promise.all(\n SIGNALS.map((signal) => this.enqueue(signal, () => this.discardCurrentFile(signal))),\n );\n const files = this.closed.splice(0);\n await Promise.all(files.map((file) => file.delete()));\n }\n\n private enqueue(signal: Signal, operation: () => Promise<void>): Promise<void> {\n const result = this.queues[signal].then(operation);\n this.queues[signal] = result.catch(() => undefined);\n return result;\n }\n\n private async closeCurrentFile(signal: Signal): Promise<void> {\n const file = this.current.get(signal);\n if (!file) {\n return;\n }\n this.current.delete(signal);\n try {\n await file.close();\n this.closed.push(file);\n } catch {\n logWarning(WRITE_FAILURE_WARNING);\n await file.delete();\n }\n }\n\n private async discardCurrentFile(signal: Signal): Promise<void> {\n const file = this.current.get(signal);\n if (file) {\n this.current.delete(signal);\n await file.delete();\n }\n }\n\n private async evict(): Promise<void> {\n const deletions = [...this.closed].map((file) => this.deleteIfExpired(file));\n while (this.totalSize() > this.maxSize) {\n // Prefer retaining metrics, but enforce the size limit when only metrics remain.\n const oldest = this.closed.find((file) => file.signal !== \"metrics\") ?? this.closed[0];\n if (!oldest) {\n break;\n }\n logWarning(`Buffer size limit reached, dropping oldest buffered ${oldest.signal}`);\n deletions.push(this.deleteFile(oldest));\n }\n await Promise.all(deletions);\n }\n\n private totalSize(): number {\n let total = 0;\n for (const file of [...this.closed, ...this.current.values()]) {\n total += file.storedSize;\n }\n return total;\n }\n}\n\n// One gzip stream of concatenated OTLP request payloads for a single signal,\n// written through to a temp file or, as fallback, an in-memory buffer.\nexport class SpoolFile {\n readonly signal: Signal;\n readonly path?: string;\n firstAttemptAtMillis?: number;\n uncompressedSize = 0;\n readonly gzip: Gzip;\n private readonly fileStream?: WriteStream;\n private readonly memoryChunks?: Buffer[];\n private memorySize = 0;\n private streamError?: Error;\n private readonly closedPromise: Promise<void>;\n\n constructor(signal: Signal, tempDir?: string) {\n this.signal = signal;\n this.gzip = createGzip();\n let closeTarget: Gzip | WriteStream;\n let closeEvent: string;\n if (tempDir === undefined) {\n const chunks: Buffer[] = [];\n this.memoryChunks = chunks;\n this.gzip.on(\"data\", (chunk: Buffer) => {\n chunks.push(chunk);\n this.memorySize += chunk.length;\n });\n closeTarget = this.gzip;\n closeEvent = \"end\";\n } else {\n this.path = join(tempDir, `apitally-${randomUUID()}.gz`);\n // A synchronous exclusive open reports creation errors to the triggering append.\n const fd = openSync(this.path, \"wx\", 0o600);\n this.fileStream = createWriteStream(this.path, { fd });\n this.gzip.pipe(this.fileStream);\n closeTarget = this.fileStream;\n closeEvent = \"close\";\n }\n const streams: (Gzip | WriteStream)[] = this.fileStream\n ? [this.gzip, this.fileStream]\n : [this.gzip];\n this.closedPromise = new Promise((resolve, reject) => {\n closeTarget.once(closeEvent, resolve);\n for (const stream of streams) {\n stream.once(\"error\", reject);\n }\n });\n // Stream errors can occur outside an awaited operation; rejection handling\n // prevents an unhandled rejection.\n this.closedPromise.catch(() => undefined);\n for (const stream of streams) {\n stream.on(\"error\", (error: Error) => {\n this.streamError ??= error;\n });\n }\n }\n\n write(payload: Uint8Array): Promise<void> {\n return new Promise((resolve, reject) => {\n if (this.streamError) {\n reject(this.streamError);\n return;\n }\n this.gzip.write(payload, (error) => {\n const failure = error ?? this.streamError;\n if (failure) {\n reject(failure);\n } else {\n this.uncompressedSize += payload.length;\n resolve();\n }\n });\n });\n }\n\n async close(): Promise<void> {\n if (this.streamError) {\n throw this.streamError;\n }\n this.gzip.end();\n await this.closedPromise;\n }\n\n get storedSize(): number {\n return this.fileStream ? this.fileStream.bytesWritten : this.memorySize;\n }\n\n markAttempt(): void {\n this.firstAttemptAtMillis ??= performance.now();\n }\n\n isExpired(): boolean {\n return (\n this.firstAttemptAtMillis !== undefined &&\n performance.now() - this.firstAttemptAtMillis > MAX_RETRY_TIME_AFTER_FIRST_ATTEMPT_MILLIS\n );\n }\n\n touch(): void {\n if (this.path) {\n try {\n const now = new Date();\n utimesSync(this.path, now, now);\n } catch {\n // The file may already be gone; orphan cleanup only targets stale files.\n }\n }\n }\n\n readStoredBytes(): Promise<Buffer> {\n if (this.path) {\n return readFile(this.path);\n }\n return Promise.resolve(Buffer.concat(this.memoryChunks ?? []));\n }\n\n async delete(): Promise<void> {\n this.gzip.destroy();\n this.fileStream?.destroy();\n if (this.path) {\n await unlink(this.path).catch(() => undefined);\n }\n }\n}\n\nfunction isTempDirWritable(tempDir: string): boolean {\n const probePath = join(tempDir, `apitally-${randomUUID()}.gz`);\n try {\n writeFileSync(probePath, \"\", { flag: \"wx\", mode: 0o600 });\n unlinkSync(probePath);\n return true;\n } catch {\n return false;\n }\n}\n\n// Live processes refresh mtimes each cycle, so only stale spool files from dead\n// processes are removed.\nfunction cleanupOrphanedFiles(tempDir: string): void {\n const cutoff = Date.now() - MAX_UNTOUCHED_FILE_AGE_MILLIS;\n try {\n for (const name of readdirSync(tempDir)) {\n if (!SPOOL_FILE_NAME_PATTERN.test(name)) {\n continue;\n }\n try {\n const filePath = join(tempDir, name);\n if (statSync(filePath).mtimeMs < cutoff) {\n unlinkSync(filePath);\n }\n } catch {\n // Another process may have removed the file first.\n }\n }\n } catch {\n // Reading the temporary directory is best-effort.\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"spool.cjs","names":["tmpdir","createGzip","join","randomUUID","openSync","createWriteStream","readFile","unlink","readdirSync","statSync"],"sources":["../src/spool.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport {\n createWriteStream,\n openSync,\n readdirSync,\n statSync,\n unlinkSync,\n utimesSync,\n type WriteStream,\n writeFileSync,\n} from \"node:fs\";\nimport { readFile, unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createGzip, type Gzip } from \"node:zlib\";\nimport { logWarning, resetWarning } from \"./logger.js\";\n\nconst WRITE_FAILURE_WARNING = \"Error writing to disk, dropping buffered telemetry\";\n\nconst SIGNALS = [\"traces\", \"logs\", \"metrics\"] as const;\nexport type Signal = (typeof SIGNALS)[number];\n\nconst MAX_UNCOMPRESSED_FILE_SIZE = 4_000_000;\nconst MAX_RETRY_TIME_AFTER_FIRST_ATTEMPT_MILLIS = 59 * 60 * 1000;\nconst MAX_SPOOL_SIZE_DISK = 50_000_000;\nconst MAX_SPOOL_SIZE_MEMORY = 10_000_000;\nconst MAX_UNTOUCHED_FILE_AGE_MILLIS = 2 * 60 * 60 * 1000;\nconst SPOOL_FILE_NAME_PATTERN = /^apitally-.*\\.gz$/;\n\n// Operations for each signal are serialized because gzip and file streams\n// complete asynchronously.\nexport class Spool {\n readonly inMemory: boolean;\n maxSize: number;\n readonly current = new Map<Signal, SpoolFile>();\n private readonly closed: SpoolFile[] = [];\n private filesClosedSinceExportRotation = 0;\n private readonly tempDir: string;\n private readonly queues: Record<Signal, Promise<void>> = {\n traces: Promise.resolve(),\n logs: Promise.resolve(),\n metrics: Promise.resolve(),\n };\n\n constructor(tempDir: string = tmpdir()) {\n this.tempDir = tempDir;\n this.inMemory = !isTempDirWritable(tempDir);\n if (this.inMemory) {\n logWarning(\n `Unable to create temporary files, buffering telemetry in memory (max ${MAX_SPOOL_SIZE_MEMORY / 1_000_000} MB)`,\n );\n } else {\n cleanupOrphanedFiles(tempDir);\n }\n this.maxSize = this.inMemory ? MAX_SPOOL_SIZE_MEMORY : MAX_SPOOL_SIZE_DISK;\n }\n\n append(signal: Signal, payload: Uint8Array): Promise<void> {\n return this.enqueue(signal, async () => {\n let file = this.current.get(signal);\n if (file && file.uncompressedSize + payload.length > MAX_UNCOMPRESSED_FILE_SIZE) {\n await this.closeCurrentFile(signal);\n file = undefined;\n }\n try {\n if (!file) {\n file = new SpoolFile(signal, this.inMemory ? undefined : this.tempDir);\n this.current.set(signal, file);\n }\n await file.write(payload);\n // Writes recovered; the write-failure warning may fire again later.\n resetWarning(WRITE_FAILURE_WARNING);\n } catch {\n logWarning(WRITE_FAILURE_WARNING);\n await this.discardCurrentFile(signal);\n }\n await this.evict();\n });\n }\n\n // Current files close only when no backlog exists, avoiding one file per cycle.\n // Sequential rotation preserves send order. Returns the number of files closed\n // since the previous call, including files closed at the size limit.\n async rotateForExport(): Promise<number> {\n for (const signal of SIGNALS) {\n await this.enqueue(signal, async () => {\n if (this.current.has(signal) && !this.closed.some((file) => file.signal === signal)) {\n await this.closeCurrentFile(signal);\n }\n });\n }\n await this.evict();\n const closedFileCount = this.filesClosedSinceExportRotation;\n this.filesClosedSinceExportRotation = 0;\n return closedFileCount;\n }\n\n async closeCurrentFiles(): Promise<void> {\n for (const signal of SIGNALS) {\n await this.enqueue(signal, () => this.closeCurrentFile(signal));\n }\n }\n\n pendingFiles(): SpoolFile[] {\n return [...this.closed];\n }\n\n async deleteFile(file: SpoolFile): Promise<void> {\n const index = this.closed.indexOf(file);\n if (index >= 0) {\n this.closed.splice(index, 1);\n }\n await file.delete();\n }\n\n // Apitally ingest deduplicates for one hour; a later retry could ingest the file twice.\n deleteIfExpired(file: SpoolFile): Promise<void> | undefined {\n if (!file.isExpired()) {\n return undefined;\n }\n logWarning(`Buffered ${file.signal} could not be delivered within an hour and were dropped`);\n return this.deleteFile(file);\n }\n\n // Refresh mtimes so orphan cleanup in another process preserves live files.\n touchFiles(): void {\n for (const file of [...this.current.values(), ...this.closed]) {\n file.touch();\n }\n }\n\n async clear(): Promise<void> {\n await Promise.all(\n SIGNALS.map((signal) => this.enqueue(signal, () => this.discardCurrentFile(signal))),\n );\n const files = this.closed.splice(0);\n await Promise.all(files.map((file) => file.delete()));\n }\n\n private enqueue(signal: Signal, operation: () => Promise<void>): Promise<void> {\n const result = this.queues[signal].then(operation);\n this.queues[signal] = result.catch(() => undefined);\n return result;\n }\n\n private async closeCurrentFile(signal: Signal): Promise<void> {\n const file = this.current.get(signal);\n if (!file) {\n return;\n }\n this.current.delete(signal);\n try {\n await file.close();\n this.closed.push(file);\n this.filesClosedSinceExportRotation += 1;\n } catch {\n logWarning(WRITE_FAILURE_WARNING);\n await file.delete();\n }\n }\n\n private async discardCurrentFile(signal: Signal): Promise<void> {\n const file = this.current.get(signal);\n if (file) {\n this.current.delete(signal);\n await file.delete();\n }\n }\n\n private async evict(): Promise<void> {\n const deletions = [...this.closed].map((file) => this.deleteIfExpired(file));\n while (this.totalSize() > this.maxSize) {\n // Prefer retaining metrics, but enforce the size limit when only metrics remain.\n const oldest = this.closed.find((file) => file.signal !== \"metrics\") ?? this.closed[0];\n if (!oldest) {\n break;\n }\n logWarning(`Buffer size limit reached, dropping oldest buffered ${oldest.signal}`);\n deletions.push(this.deleteFile(oldest));\n }\n await Promise.all(deletions);\n }\n\n private totalSize(): number {\n let total = 0;\n for (const file of [...this.closed, ...this.current.values()]) {\n total += file.storedSize;\n }\n return total;\n }\n}\n\n// One gzip stream of concatenated OTLP request payloads for a single signal,\n// written through to a temp file or, as fallback, an in-memory buffer.\nexport class SpoolFile {\n readonly signal: Signal;\n readonly path?: string;\n firstAttemptAtMillis?: number;\n uncompressedSize = 0;\n readonly gzip: Gzip;\n private readonly fileStream?: WriteStream;\n private readonly memoryChunks?: Buffer[];\n private memorySize = 0;\n private streamError?: Error;\n private readonly closedPromise: Promise<void>;\n\n constructor(signal: Signal, tempDir?: string) {\n this.signal = signal;\n this.gzip = createGzip();\n let closeTarget: Gzip | WriteStream;\n let closeEvent: string;\n if (tempDir === undefined) {\n const chunks: Buffer[] = [];\n this.memoryChunks = chunks;\n this.gzip.on(\"data\", (chunk: Buffer) => {\n chunks.push(chunk);\n this.memorySize += chunk.length;\n });\n closeTarget = this.gzip;\n closeEvent = \"end\";\n } else {\n this.path = join(tempDir, `apitally-${randomUUID()}.gz`);\n // A synchronous exclusive open reports creation errors to the triggering append.\n const fd = openSync(this.path, \"wx\", 0o600);\n this.fileStream = createWriteStream(this.path, { fd });\n this.gzip.pipe(this.fileStream);\n closeTarget = this.fileStream;\n closeEvent = \"close\";\n }\n const streams: (Gzip | WriteStream)[] = this.fileStream\n ? [this.gzip, this.fileStream]\n : [this.gzip];\n this.closedPromise = new Promise((resolve, reject) => {\n closeTarget.once(closeEvent, resolve);\n for (const stream of streams) {\n stream.once(\"error\", reject);\n }\n });\n // Stream errors can occur outside an awaited operation; rejection handling\n // prevents an unhandled rejection.\n this.closedPromise.catch(() => undefined);\n for (const stream of streams) {\n stream.on(\"error\", (error: Error) => {\n this.streamError ??= error;\n });\n }\n }\n\n write(payload: Uint8Array): Promise<void> {\n return new Promise((resolve, reject) => {\n if (this.streamError) {\n reject(this.streamError);\n return;\n }\n this.gzip.write(payload, (error) => {\n const failure = error ?? this.streamError;\n if (failure) {\n reject(failure);\n } else {\n this.uncompressedSize += payload.length;\n resolve();\n }\n });\n });\n }\n\n async close(): Promise<void> {\n if (this.streamError) {\n throw this.streamError;\n }\n this.gzip.end();\n await this.closedPromise;\n }\n\n get storedSize(): number {\n return this.fileStream ? this.fileStream.bytesWritten : this.memorySize;\n }\n\n markAttempt(): void {\n this.firstAttemptAtMillis ??= performance.now();\n }\n\n isExpired(): boolean {\n return (\n this.firstAttemptAtMillis !== undefined &&\n performance.now() - this.firstAttemptAtMillis > MAX_RETRY_TIME_AFTER_FIRST_ATTEMPT_MILLIS\n );\n }\n\n touch(): void {\n if (this.path) {\n try {\n const now = new Date();\n utimesSync(this.path, now, now);\n } catch {\n // The file may already be gone; orphan cleanup only targets stale files.\n }\n }\n }\n\n readStoredBytes(): Promise<Buffer> {\n if (this.path) {\n return readFile(this.path);\n }\n return Promise.resolve(Buffer.concat(this.memoryChunks ?? []));\n }\n\n async delete(): Promise<void> {\n this.gzip.destroy();\n this.fileStream?.destroy();\n if (this.path) {\n await unlink(this.path).catch(() => undefined);\n }\n }\n}\n\nfunction isTempDirWritable(tempDir: string): boolean {\n const probePath = join(tempDir, `apitally-${randomUUID()}.gz`);\n try {\n writeFileSync(probePath, \"\", { flag: \"wx\", mode: 0o600 });\n unlinkSync(probePath);\n return true;\n } catch {\n return false;\n }\n}\n\n// Live processes refresh mtimes each cycle, so only stale spool files from dead\n// processes are removed.\nfunction cleanupOrphanedFiles(tempDir: string): void {\n const cutoff = Date.now() - MAX_UNTOUCHED_FILE_AGE_MILLIS;\n try {\n for (const name of readdirSync(tempDir)) {\n if (!SPOOL_FILE_NAME_PATTERN.test(name)) {\n continue;\n }\n try {\n const filePath = join(tempDir, name);\n if (statSync(filePath).mtimeMs < cutoff) {\n unlinkSync(filePath);\n }\n } catch {\n // Another process may have removed the file first.\n }\n }\n } catch {\n // Reading the temporary directory is best-effort.\n }\n}\n"],"mappings":"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"}
package/dist/spool.d.cts CHANGED
@@ -7,11 +7,12 @@ export declare class Spool {
7
7
  maxSize: number;
8
8
  readonly current: Map<"traces" | "logs" | "metrics", SpoolFile>;
9
9
  private readonly closed;
10
+ private filesClosedSinceExportRotation;
10
11
  private readonly tempDir;
11
12
  private readonly queues;
12
13
  constructor(tempDir?: string);
13
14
  append(signal: Signal, payload: Uint8Array): Promise<void>;
14
- rotateForExport(): Promise<void>;
15
+ rotateForExport(): Promise<number>;
15
16
  closeCurrentFiles(): Promise<void>;
16
17
  pendingFiles(): SpoolFile[];
17
18
  deleteFile(file: SpoolFile): Promise<void>;
package/dist/spool.d.ts CHANGED
@@ -7,11 +7,12 @@ export declare class Spool {
7
7
  maxSize: number;
8
8
  readonly current: Map<"traces" | "logs" | "metrics", SpoolFile>;
9
9
  private readonly closed;
10
+ private filesClosedSinceExportRotation;
10
11
  private readonly tempDir;
11
12
  private readonly queues;
12
13
  constructor(tempDir?: string);
13
14
  append(signal: Signal, payload: Uint8Array): Promise<void>;
14
- rotateForExport(): Promise<void>;
15
+ rotateForExport(): Promise<number>;
15
16
  closeCurrentFiles(): Promise<void>;
16
17
  pendingFiles(): SpoolFile[];
17
18
  deleteFile(file: SpoolFile): Promise<void>;
package/dist/spool.js CHANGED
@@ -23,6 +23,7 @@ var Spool = class {
23
23
  maxSize;
24
24
  current = /* @__PURE__ */ new Map();
25
25
  closed = [];
26
+ filesClosedSinceExportRotation = 0;
26
27
  tempDir;
27
28
  queues = {
28
29
  traces: Promise.resolve(),
@@ -62,6 +63,9 @@ var Spool = class {
62
63
  if (this.current.has(signal) && !this.closed.some((file) => file.signal === signal)) await this.closeCurrentFile(signal);
63
64
  });
64
65
  await this.evict();
66
+ const closedFileCount = this.filesClosedSinceExportRotation;
67
+ this.filesClosedSinceExportRotation = 0;
68
+ return closedFileCount;
65
69
  }
66
70
  async closeCurrentFiles() {
67
71
  for (const signal of SIGNALS) await this.enqueue(signal, () => this.closeCurrentFile(signal));
@@ -99,6 +103,7 @@ var Spool = class {
99
103
  try {
100
104
  await file.close();
101
105
  this.closed.push(file);
106
+ this.filesClosedSinceExportRotation += 1;
102
107
  } catch {
103
108
  logWarning(WRITE_FAILURE_WARNING);
104
109
  await file.delete();