@uploadista/observability 0.0.3

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 (118) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-check.log +0 -0
  3. package/LICENSE +21 -0
  4. package/dist/core/errors.d.ts +8 -0
  5. package/dist/core/errors.d.ts.map +1 -0
  6. package/dist/core/errors.js +108 -0
  7. package/dist/core/index.d.ts +8 -0
  8. package/dist/core/index.d.ts.map +1 -0
  9. package/dist/core/index.js +8 -0
  10. package/dist/core/layers.d.ts +104 -0
  11. package/dist/core/layers.d.ts.map +1 -0
  12. package/dist/core/layers.js +110 -0
  13. package/dist/core/logging.d.ts +18 -0
  14. package/dist/core/logging.d.ts.map +1 -0
  15. package/dist/core/logging.js +41 -0
  16. package/dist/core/metrics.d.ts +37 -0
  17. package/dist/core/metrics.d.ts.map +1 -0
  18. package/dist/core/metrics.js +72 -0
  19. package/dist/core/testing.d.ts +43 -0
  20. package/dist/core/testing.d.ts.map +1 -0
  21. package/dist/core/testing.js +93 -0
  22. package/dist/core/tracing.d.ts +19 -0
  23. package/dist/core/tracing.d.ts.map +1 -0
  24. package/dist/core/tracing.js +43 -0
  25. package/dist/core/utilities.d.ts +11 -0
  26. package/dist/core/utilities.d.ts.map +1 -0
  27. package/dist/core/utilities.js +41 -0
  28. package/dist/flow/errors.d.ts +15 -0
  29. package/dist/flow/errors.d.ts.map +1 -0
  30. package/dist/flow/errors.js +66 -0
  31. package/dist/flow/index.d.ts +6 -0
  32. package/dist/flow/index.d.ts.map +1 -0
  33. package/dist/flow/index.js +6 -0
  34. package/dist/flow/layers.d.ts +40 -0
  35. package/dist/flow/layers.d.ts.map +1 -0
  36. package/dist/flow/layers.js +94 -0
  37. package/dist/flow/metrics.d.ts +52 -0
  38. package/dist/flow/metrics.d.ts.map +1 -0
  39. package/dist/flow/metrics.js +89 -0
  40. package/dist/flow/testing.d.ts +11 -0
  41. package/dist/flow/testing.d.ts.map +1 -0
  42. package/dist/flow/testing.js +27 -0
  43. package/dist/flow/tracing.d.ts +35 -0
  44. package/dist/flow/tracing.d.ts.map +1 -0
  45. package/dist/flow/tracing.js +42 -0
  46. package/dist/index.d.ts +8 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +14 -0
  49. package/dist/service/metrics.d.ts +23 -0
  50. package/dist/service/metrics.d.ts.map +1 -0
  51. package/dist/service/metrics.js +17 -0
  52. package/dist/storage/azure.d.ts +47 -0
  53. package/dist/storage/azure.d.ts.map +1 -0
  54. package/dist/storage/azure.js +89 -0
  55. package/dist/storage/filesystem.d.ts +47 -0
  56. package/dist/storage/filesystem.d.ts.map +1 -0
  57. package/dist/storage/filesystem.js +70 -0
  58. package/dist/storage/gcs.d.ts +47 -0
  59. package/dist/storage/gcs.d.ts.map +1 -0
  60. package/dist/storage/gcs.js +90 -0
  61. package/dist/storage/index.d.ts +5 -0
  62. package/dist/storage/index.d.ts.map +1 -0
  63. package/dist/storage/index.js +5 -0
  64. package/dist/storage/s3.d.ts +47 -0
  65. package/dist/storage/s3.d.ts.map +1 -0
  66. package/dist/storage/s3.js +67 -0
  67. package/dist/test-observability.d.ts +12 -0
  68. package/dist/test-observability.d.ts.map +1 -0
  69. package/dist/test-observability.js +153 -0
  70. package/dist/upload/errors.d.ts +16 -0
  71. package/dist/upload/errors.d.ts.map +1 -0
  72. package/dist/upload/errors.js +107 -0
  73. package/dist/upload/index.d.ts +6 -0
  74. package/dist/upload/index.d.ts.map +1 -0
  75. package/dist/upload/index.js +6 -0
  76. package/dist/upload/layers.d.ts +32 -0
  77. package/dist/upload/layers.d.ts.map +1 -0
  78. package/dist/upload/layers.js +63 -0
  79. package/dist/upload/metrics.d.ts +46 -0
  80. package/dist/upload/metrics.d.ts.map +1 -0
  81. package/dist/upload/metrics.js +80 -0
  82. package/dist/upload/testing.d.ts +32 -0
  83. package/dist/upload/testing.d.ts.map +1 -0
  84. package/dist/upload/testing.js +52 -0
  85. package/dist/upload/tracing.d.ts +25 -0
  86. package/dist/upload/tracing.d.ts.map +1 -0
  87. package/dist/upload/tracing.js +35 -0
  88. package/package.json +37 -0
  89. package/src/core/errors.ts +187 -0
  90. package/src/core/index.ts +9 -0
  91. package/src/core/layers.ts +205 -0
  92. package/src/core/logging.ts +81 -0
  93. package/src/core/metrics.ts +108 -0
  94. package/src/core/testing.ts +142 -0
  95. package/src/core/tracing.ts +67 -0
  96. package/src/core/utilities.ts +133 -0
  97. package/src/flow/errors.ts +95 -0
  98. package/src/flow/index.ts +17 -0
  99. package/src/flow/layers.ts +131 -0
  100. package/src/flow/metrics.ts +130 -0
  101. package/src/flow/testing.ts +33 -0
  102. package/src/flow/tracing.ts +72 -0
  103. package/src/index.ts +31 -0
  104. package/src/service/metrics.ts +31 -0
  105. package/src/storage/azure.ts +163 -0
  106. package/src/storage/filesystem.ts +153 -0
  107. package/src/storage/gcs.ts +161 -0
  108. package/src/storage/index.ts +5 -0
  109. package/src/storage/s3.ts +136 -0
  110. package/src/test-observability.ts +234 -0
  111. package/src/upload/errors.ts +166 -0
  112. package/src/upload/index.ts +12 -0
  113. package/src/upload/layers.ts +88 -0
  114. package/src/upload/metrics.ts +118 -0
  115. package/src/upload/testing.ts +60 -0
  116. package/src/upload/tracing.ts +58 -0
  117. package/tsconfig.json +10 -0
  118. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,81 @@
1
+ import { Effect } from "effect";
2
+
3
+ // ============================================================================
4
+ // Enhanced Logging Helpers (Storage-agnostic)
5
+ // ============================================================================
6
+
7
+ export const logWithContext = (
8
+ message: string,
9
+ context: Record<string, unknown>,
10
+ ) => Effect.log(message).pipe(Effect.annotateLogs(context));
11
+
12
+ export const logUploadProgress = (
13
+ storageType: string,
14
+ uploadId: string,
15
+ progress: {
16
+ uploadedBytes: number;
17
+ totalBytes: number;
18
+ partNumber?: number;
19
+ speed?: number;
20
+ },
21
+ ) =>
22
+ logWithContext("Upload progress", {
23
+ storage_type: storageType,
24
+ upload_id: uploadId,
25
+ uploaded_bytes: progress.uploadedBytes,
26
+ total_bytes: progress.totalBytes,
27
+ progress_percentage: Math.round(
28
+ (progress.uploadedBytes / progress.totalBytes) * 100,
29
+ ),
30
+ ...(progress.partNumber && { part_number: progress.partNumber }),
31
+ ...(progress.speed && { upload_speed_bps: progress.speed }),
32
+ });
33
+
34
+ export const logStorageOperation = (
35
+ storageType: string,
36
+ operation: string,
37
+ uploadId: string,
38
+ metadata?: Record<string, unknown>,
39
+ ) =>
40
+ logWithContext(`${storageType.toUpperCase()} ${operation}`, {
41
+ storage_type: storageType,
42
+ operation,
43
+ upload_id: uploadId,
44
+ ...metadata,
45
+ });
46
+
47
+ export const logUploadCompletion = (
48
+ storageType: string,
49
+ uploadId: string,
50
+ metrics: {
51
+ fileSize: number;
52
+ totalDurationMs: number;
53
+ partsCount?: number;
54
+ averagePartSize?: number;
55
+ throughputBps?: number;
56
+ retryCount?: number;
57
+ },
58
+ ) => {
59
+ const throughputMBps = metrics.throughputBps
60
+ ? metrics.throughputBps / (1024 * 1024)
61
+ : 0;
62
+
63
+ return logWithContext(`${storageType.toUpperCase()} upload completed`, {
64
+ storage_type: storageType,
65
+ upload_id: uploadId,
66
+ file_size_bytes: metrics.fileSize,
67
+ file_size_mb: Math.round((metrics.fileSize / (1024 * 1024)) * 100) / 100,
68
+ total_duration_ms: metrics.totalDurationMs,
69
+ total_duration_seconds:
70
+ Math.round((metrics.totalDurationMs / 1000) * 100) / 100,
71
+ throughput_bps: metrics.throughputBps,
72
+ throughput_mbps: Math.round(throughputMBps * 100) / 100,
73
+ ...(metrics.partsCount && { parts_count: metrics.partsCount }),
74
+ ...(metrics.averagePartSize && {
75
+ average_part_size_bytes: metrics.averagePartSize,
76
+ average_part_size_mb:
77
+ Math.round((metrics.averagePartSize / (1024 * 1024)) * 100) / 100,
78
+ }),
79
+ ...(metrics.retryCount && { retry_count: metrics.retryCount }),
80
+ });
81
+ };
@@ -0,0 +1,108 @@
1
+ import { Metric, MetricBoundaries } from "effect";
2
+
3
+ // ============================================================================
4
+ // Core Storage Metrics (reusable across all storage types)
5
+ // ============================================================================
6
+
7
+ // Counter metrics
8
+ export const createUploadMetrics = (storageType: string) => ({
9
+ uploadRequestsTotal: Metric.counter(`${storageType}_upload_requests_total`, {
10
+ description: `Total number of upload requests for ${storageType}`,
11
+ }),
12
+
13
+ uploadPartsTotal: Metric.counter(`${storageType}_upload_parts_total`, {
14
+ description: `Total number of individual parts uploaded for ${storageType}`,
15
+ }),
16
+
17
+ uploadSuccessTotal: Metric.counter(`${storageType}_upload_success_total`, {
18
+ description: `Total number of successful uploads for ${storageType}`,
19
+ }),
20
+
21
+ uploadErrorsTotal: Metric.counter(`${storageType}_upload_errors_total`, {
22
+ description: `Total number of upload errors for ${storageType}`,
23
+ }),
24
+
25
+ apiCallsTotal: Metric.counter(`${storageType}_api_calls_total`, {
26
+ description: `Total number of API calls for ${storageType}`,
27
+ }),
28
+ });
29
+
30
+ // Histogram metrics for timing and sizes (reusable)
31
+ export const createUploadHistograms = (storageType: string) => ({
32
+ uploadDurationHistogram: Metric.histogram(
33
+ `${storageType}_upload_duration_seconds`,
34
+ MetricBoundaries.exponential({
35
+ start: 0.01, // 10ms
36
+ factor: 2,
37
+ count: 20, // Up to ~10 seconds
38
+ }),
39
+ `Duration of upload operations in seconds for ${storageType}`,
40
+ ),
41
+
42
+ partUploadDurationHistogram: Metric.histogram(
43
+ `${storageType}_part_upload_duration_seconds`,
44
+ MetricBoundaries.exponential({
45
+ start: 0.001, // 1ms
46
+ factor: 2,
47
+ count: 15, // Up to ~32 seconds
48
+ }),
49
+ `Duration of individual part uploads in seconds for ${storageType}`,
50
+ ),
51
+
52
+ fileSizeHistogram: Metric.histogram(
53
+ `${storageType}_file_size_bytes`,
54
+ MetricBoundaries.exponential({
55
+ start: 1024, // 1KB
56
+ factor: 2,
57
+ count: 25, // Up to ~33GB
58
+ }),
59
+ `Size of uploaded files in bytes for ${storageType}`,
60
+ ),
61
+
62
+ partSizeHistogram: Metric.histogram(
63
+ `${storageType}_part_size_bytes`,
64
+ MetricBoundaries.linear({
65
+ start: 5_242_880, // 5MB (minimum part size)
66
+ width: 1_048_576, // 1MB increments
67
+ count: 20, // Up to ~25MB
68
+ }),
69
+ `Size of upload parts in bytes for ${storageType}`,
70
+ ),
71
+ });
72
+
73
+ // Gauge metrics for current state (reusable)
74
+ export const createUploadGauges = (storageType: string) => ({
75
+ activeUploadsGauge: Metric.gauge(`${storageType}_active_uploads`, {
76
+ description: `Number of currently active uploads for ${storageType}`,
77
+ }),
78
+
79
+ uploadThroughputGauge: Metric.gauge(
80
+ `${storageType}_upload_throughput_bytes_per_second`,
81
+ {
82
+ description: `Current upload throughput in bytes per second for ${storageType}`,
83
+ },
84
+ ),
85
+ });
86
+
87
+ // Summary metrics for percentiles (reusable)
88
+ export const createUploadSummaries = (storageType: string) => ({
89
+ uploadLatencySummary: Metric.summary({
90
+ name: `${storageType}_upload_latency_seconds`,
91
+ maxAge: "10 minutes",
92
+ maxSize: 1000,
93
+ error: 0.01,
94
+ quantiles: [0.5, 0.9, 0.95, 0.99],
95
+ description: `Upload latency percentiles for ${storageType}`,
96
+ }),
97
+ });
98
+
99
+ // Combined metrics factory
100
+ export const createStorageMetrics = (storageType: string) => ({
101
+ ...createUploadMetrics(storageType),
102
+ ...createUploadHistograms(storageType),
103
+ ...createUploadGauges(storageType),
104
+ ...createUploadSummaries(storageType),
105
+ });
106
+
107
+ // Type for storage metrics
108
+ export type StorageMetrics = ReturnType<typeof createStorageMetrics>;
@@ -0,0 +1,142 @@
1
+ import { Effect, Layer, Metric } from "effect";
2
+ import type {
3
+ FlowObservabilityService,
4
+ StorageObservabilityService,
5
+ UploadObservabilityService,
6
+ } from "./layers.js";
7
+ import {
8
+ FlowObservability,
9
+ StorageObservability,
10
+ UploadObservability,
11
+ } from "./layers.js";
12
+ import { createStorageMetrics } from "./metrics.js";
13
+
14
+ // ============================================================================
15
+ // Test Observability Layers
16
+ // ============================================================================
17
+
18
+ /**
19
+ * Mock storage observability for testing
20
+ */
21
+ export const makeTestStorageObservability = (
22
+ storageType: string,
23
+ ): Layer.Layer<StorageObservability> => {
24
+ const metrics = createStorageMetrics(storageType);
25
+ const service: StorageObservabilityService = {
26
+ serviceName: `test-${storageType}-store`,
27
+ storageType,
28
+ metrics,
29
+ enabled: true,
30
+ };
31
+ return Layer.succeed(StorageObservability, service);
32
+ };
33
+
34
+ /**
35
+ * Mock upload observability for testing
36
+ */
37
+ export const makeTestUploadObservability =
38
+ (): Layer.Layer<UploadObservability> => {
39
+ const service: UploadObservabilityService = {
40
+ serviceName: "test-upload-server",
41
+ enabled: true,
42
+ metrics: {
43
+ uploadCreated: Effect.void,
44
+ uploadCompleted: Effect.void,
45
+ uploadFailed: Effect.void,
46
+ chunkUploaded: Effect.void,
47
+ },
48
+ };
49
+ return Layer.succeed(UploadObservability, service);
50
+ };
51
+
52
+ /**
53
+ * Mock flow observability for testing
54
+ */
55
+ export const makeTestFlowObservability = (): Layer.Layer<FlowObservability> => {
56
+ const service: FlowObservabilityService = {
57
+ serviceName: "test-flow-engine",
58
+ enabled: true,
59
+ metrics: {
60
+ flowStarted: Effect.void,
61
+ flowCompleted: Effect.void,
62
+ flowFailed: Effect.void,
63
+ nodeExecuted: Effect.void,
64
+ },
65
+ };
66
+ return Layer.succeed(FlowObservability, service);
67
+ };
68
+
69
+ // ============================================================================
70
+ // Test Utilities
71
+ // ============================================================================
72
+
73
+ /**
74
+ * Capture metrics snapshot from an effect for testing
75
+ * Note: Metric snapshots are simplified - for full metric testing,
76
+ * use Effect's built-in metric testing utilities
77
+ */
78
+ export const captureMetrics = <A, E, R>(
79
+ effect: Effect.Effect<A, E, R>,
80
+ ): Effect.Effect<A, E, R> =>
81
+ Effect.gen(function* () {
82
+ const result = yield* effect;
83
+ // Metrics are automatically captured by Effect runtime
84
+ yield* Metric.snapshot;
85
+ return result;
86
+ });
87
+
88
+ /**
89
+ * Test helper to capture metrics around effect execution
90
+ * This is a simplified version - for production testing,
91
+ * use Effect's metric testing utilities
92
+ */
93
+ export const withMetricTracking = <A, E, R>(
94
+ effect: Effect.Effect<A, E, R>,
95
+ ): Effect.Effect<A, E, R> =>
96
+ Effect.gen(function* () {
97
+ // Track metric before execution
98
+ yield* Metric.snapshot;
99
+ const result = yield* effect;
100
+ // Track metric after execution
101
+ yield* Metric.snapshot;
102
+ return result;
103
+ });
104
+
105
+ /**
106
+ * Test fixture for observability testing
107
+ */
108
+ export interface ObservabilityTestFixture {
109
+ readonly storageObservability: Layer.Layer<StorageObservability>;
110
+ readonly uploadObservability: Layer.Layer<UploadObservability>;
111
+ readonly flowObservability: Layer.Layer<FlowObservability>;
112
+ }
113
+
114
+ /**
115
+ * Create a complete test fixture with all observability layers
116
+ */
117
+ export const createTestFixture = (
118
+ storageType = "test-storage",
119
+ ): ObservabilityTestFixture => ({
120
+ storageObservability: makeTestStorageObservability(storageType),
121
+ uploadObservability: makeTestUploadObservability(),
122
+ flowObservability: makeTestFlowObservability(),
123
+ });
124
+
125
+ /**
126
+ * Run an effect with test observability layers
127
+ */
128
+ export const runWithTestObservability = <A, E>(
129
+ effect: Effect.Effect<
130
+ A,
131
+ E,
132
+ StorageObservability | UploadObservability | FlowObservability
133
+ >,
134
+ storageType = "test-storage",
135
+ ): Effect.Effect<A, E> => {
136
+ const fixture = createTestFixture(storageType);
137
+ return effect.pipe(
138
+ Effect.provide(fixture.storageObservability),
139
+ Effect.provide(fixture.uploadObservability),
140
+ Effect.provide(fixture.flowObservability),
141
+ );
142
+ };
@@ -0,0 +1,67 @@
1
+ import { NodeSdk, WebSdk } from "@effect/opentelemetry";
2
+ import {
3
+ BatchSpanProcessor,
4
+ ConsoleSpanExporter,
5
+ } from "@opentelemetry/sdk-trace-base";
6
+ import { Context, Effect, Layer } from "effect";
7
+
8
+ // ============================================================================
9
+ // Universal Tracing (Environment-agnostic)
10
+ // ============================================================================
11
+
12
+ // Generic service tag for tracing context
13
+ export const TracingService = Context.GenericTag<{ serviceName: string }>(
14
+ "TracingService",
15
+ );
16
+
17
+ // Create a tracing layer using Effect's native tracing (works in all environments)
18
+ export const createTracingLayer = (options?: { serviceName?: string }) => {
19
+ const serviceName = options?.serviceName ?? "uploadista-storage";
20
+
21
+ // Return a layer that provides tracing service context
22
+ return Layer.succeed(TracingService, { serviceName });
23
+ };
24
+
25
+ // Storage-specific tracing layers
26
+ export const createStorageTracingLayer = (storageType: string) =>
27
+ createTracingLayer({
28
+ serviceName: `uploadista-${storageType}-store`,
29
+ });
30
+
31
+ // Utility to add storage context to spans
32
+ export const withStorageSpan =
33
+ <A, E, R>(
34
+ operation: string,
35
+ storageType: string,
36
+ attributes?: Record<string, unknown>,
37
+ ) =>
38
+ (effect: Effect.Effect<A, E, R>) =>
39
+ effect.pipe(
40
+ Effect.withSpan(`${storageType}-${operation}`, {
41
+ attributes: {
42
+ "storage.type": storageType,
43
+ operation: operation,
44
+ ...attributes,
45
+ },
46
+ }),
47
+ );
48
+
49
+ // Set up tracing with the OpenTelemetry SDK
50
+ export const WebSdkLive = WebSdk.layer(() => ({
51
+ resource: { serviceName: "uploadista-storage" },
52
+ // Export span data to the console
53
+ spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()),
54
+ }));
55
+
56
+ export const NodeSdkLive = NodeSdk.layer(() => ({
57
+ resource: { serviceName: "uploadista-storage" },
58
+ // Export span data to the console
59
+ spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()),
60
+ }));
61
+
62
+ // Cloudflare Workers SDK (uses WebSdk as base)
63
+ export const WorkersSdkLive = WebSdk.layer(() => ({
64
+ resource: { serviceName: "uploadista-storage-workers" },
65
+ // Export span data to the console in Workers environment
66
+ spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter()),
67
+ }));
@@ -0,0 +1,133 @@
1
+ import { Effect, Metric } from "effect";
2
+ import type { StorageMetrics } from "./metrics.js";
3
+
4
+ // ============================================================================
5
+ // Storage Observability Utility Functions
6
+ // ============================================================================
7
+
8
+ // Generic upload metrics wrapper
9
+ export const withUploadMetrics = <A, E, R>(
10
+ metrics: StorageMetrics,
11
+ uploadId: string,
12
+ effect: Effect.Effect<A, E, R>,
13
+ ): Effect.Effect<A, E, R> =>
14
+ effect.pipe(
15
+ Effect.tap(() =>
16
+ metrics.uploadRequestsTotal.pipe(Metric.tagged("upload_id", uploadId))(
17
+ Effect.succeed(1),
18
+ ),
19
+ ),
20
+ Effect.tapError(() =>
21
+ metrics.uploadErrorsTotal.pipe(Metric.tagged("upload_id", uploadId))(
22
+ Effect.succeed(1),
23
+ ),
24
+ ),
25
+ Effect.tap(() =>
26
+ metrics.uploadSuccessTotal.pipe(Metric.tagged("upload_id", uploadId))(
27
+ Effect.succeed(1),
28
+ ),
29
+ ),
30
+ );
31
+
32
+ // Generic API call metrics wrapper
33
+ export const withApiMetrics = <A, E, R>(
34
+ metrics: StorageMetrics,
35
+ operation: string,
36
+ effect: Effect.Effect<A, E, R>,
37
+ ): Effect.Effect<A, E, R> =>
38
+ effect.pipe(
39
+ Effect.tap(() =>
40
+ metrics.apiCallsTotal.pipe(Metric.tagged("operation", operation))(
41
+ Effect.succeed(1),
42
+ ),
43
+ ),
44
+ );
45
+
46
+ // Generic timing metrics wrapper
47
+ export const withTimingMetrics = <A, E, R>(
48
+ metric: Metric.Metric.Histogram<number>,
49
+ effect: Effect.Effect<A, E, R>,
50
+ ): Effect.Effect<A, E, R> =>
51
+ Effect.gen(function* () {
52
+ const startTime = yield* Effect.sync(() => Date.now());
53
+ const result = yield* effect;
54
+ const endTime = yield* Effect.sync(() => Date.now());
55
+ const duration = (endTime - startTime) / 1000; // Convert to seconds
56
+
57
+ yield* metric(Effect.succeed(duration));
58
+
59
+ return result;
60
+ });
61
+
62
+ // File size tracking
63
+ export const trackFileSize = <A, E, R>(
64
+ metrics: StorageMetrics,
65
+ fileSize: number,
66
+ effect: Effect.Effect<A, E, R>,
67
+ ): Effect.Effect<A, E, R> =>
68
+ effect.pipe(
69
+ Effect.tap(() => metrics.fileSizeHistogram(Effect.succeed(fileSize))),
70
+ );
71
+
72
+ // Part size tracking
73
+ export const trackPartSize = <A, E, R>(
74
+ metrics: StorageMetrics,
75
+ partSize: number,
76
+ effect: Effect.Effect<A, E, R>,
77
+ ): Effect.Effect<A, E, R> =>
78
+ effect.pipe(
79
+ Effect.tap(() => metrics.partSizeHistogram(Effect.succeed(partSize))),
80
+ );
81
+
82
+ // Active uploads tracking
83
+ export const withActiveUploadTracking = <A, E, R>(
84
+ metrics: StorageMetrics,
85
+ effect: Effect.Effect<A, E, R>,
86
+ ): Effect.Effect<A, E, R> =>
87
+ effect.pipe(
88
+ Effect.tap(() => metrics.activeUploadsGauge(Effect.succeed(1))),
89
+ Effect.ensuring(metrics.activeUploadsGauge(Effect.succeed(-1))),
90
+ );
91
+
92
+ // Throughput calculation and tracking
93
+ export const withThroughputTracking = <A, E, R>(
94
+ metrics: StorageMetrics,
95
+ bytes: number,
96
+ effect: Effect.Effect<A, E, R>,
97
+ ): Effect.Effect<A, E, R> =>
98
+ Effect.gen(function* () {
99
+ const startTime = yield* Effect.sync(() => Date.now());
100
+ const result = yield* effect;
101
+ const endTime = yield* Effect.sync(() => Date.now());
102
+ const durationSeconds = (endTime - startTime) / 1000;
103
+ const throughputBps = durationSeconds > 0 ? bytes / durationSeconds : 0;
104
+
105
+ yield* metrics.uploadThroughputGauge(Effect.succeed(throughputBps));
106
+
107
+ return result;
108
+ });
109
+
110
+ // Combined metrics wrapper for common upload operations
111
+ export const withStorageOperationMetrics = <A, E, R>(
112
+ metrics: StorageMetrics,
113
+ operation: string,
114
+ uploadId: string,
115
+ effect: Effect.Effect<A, E, R>,
116
+ fileSize?: number,
117
+ ): Effect.Effect<A, E, R> => {
118
+ let wrappedEffect = effect.pipe(
119
+ (eff) => withApiMetrics(metrics, operation, eff),
120
+ (eff) => withUploadMetrics(metrics, uploadId, eff),
121
+ (eff) => withTimingMetrics(metrics.uploadDurationHistogram, eff),
122
+ (eff) => withActiveUploadTracking(metrics, eff),
123
+ );
124
+
125
+ if (fileSize !== undefined) {
126
+ wrappedEffect = wrappedEffect.pipe(
127
+ (eff) => trackFileSize(metrics, fileSize, eff),
128
+ (eff) => withThroughputTracking(metrics, fileSize, eff),
129
+ );
130
+ }
131
+
132
+ return wrappedEffect;
133
+ };
@@ -0,0 +1,95 @@
1
+ import { Effect, Metric } from "effect";
2
+ import { createFlowMetrics } from "./metrics.js";
3
+
4
+ // ============================================================================
5
+ // Flow Error Classification
6
+ // ============================================================================
7
+
8
+ export type FlowErrorCategory =
9
+ | "flow_validation_error"
10
+ | "node_execution_error"
11
+ | "node_not_found_error"
12
+ | "flow_timeout_error"
13
+ | "flow_cancelled_error"
14
+ | "unknown_flow_error";
15
+
16
+ /**
17
+ * Classify flow execution errors
18
+ */
19
+ export const classifyFlowError = (error: unknown): FlowErrorCategory => {
20
+ if (!error || typeof error !== "object") return "unknown_flow_error";
21
+
22
+ const errorCode = "code" in error ? error.code : undefined;
23
+ if (!errorCode) return "unknown_flow_error";
24
+
25
+ // Flow-specific error codes
26
+ switch (errorCode) {
27
+ case "FLOW_VALIDATION_ERROR":
28
+ case "FLOW_INVALID_INPUT":
29
+ case "FLOW_INVALID_OUTPUT":
30
+ return "flow_validation_error";
31
+ case "FLOW_NODE_NOT_FOUND":
32
+ case "FLOW_EDGE_INVALID":
33
+ return "node_not_found_error";
34
+ case "FLOW_NODE_EXECUTION_FAILED":
35
+ case "FLOW_NODE_ERROR":
36
+ return "node_execution_error";
37
+ case "FLOW_TIMEOUT":
38
+ return "flow_timeout_error";
39
+ case "FLOW_CANCELLED":
40
+ case "ABORTED":
41
+ return "flow_cancelled_error";
42
+ default:
43
+ return "unknown_flow_error";
44
+ }
45
+ };
46
+
47
+ /**
48
+ * Track flow errors with classification
49
+ */
50
+ export const trackFlowError = <E>(
51
+ error: E,
52
+ ): Effect.Effect<void, never, never> => {
53
+ const metrics = createFlowMetrics();
54
+ const category = classifyFlowError(error);
55
+
56
+ return Effect.gen(function* () {
57
+ // Increment total failed flows
58
+ yield* Metric.increment(metrics.flowFailedTotal);
59
+
60
+ // Log error with classification
61
+ yield* Effect.logError("Flow execution failed").pipe(
62
+ Effect.annotateLogs({
63
+ "error.category": category,
64
+ "error.message": String(error),
65
+ }),
66
+ );
67
+ });
68
+ };
69
+
70
+ /**
71
+ * Track node errors with classification
72
+ */
73
+ export const trackNodeError = <E>(
74
+ nodeId: string,
75
+ nodeType: string,
76
+ error: E,
77
+ ): Effect.Effect<void, never, never> => {
78
+ const metrics = createFlowMetrics();
79
+ const category = classifyFlowError(error);
80
+
81
+ return Effect.gen(function* () {
82
+ // Increment node failed counter
83
+ yield* Metric.increment(metrics.nodeFailedTotal);
84
+
85
+ // Log error with node context
86
+ yield* Effect.logError("Node execution failed").pipe(
87
+ Effect.annotateLogs({
88
+ "node.id": nodeId,
89
+ "node.type": nodeType,
90
+ "error.category": category,
91
+ "error.message": String(error),
92
+ }),
93
+ );
94
+ });
95
+ };
@@ -0,0 +1,17 @@
1
+ // Flow observability exports
2
+ export * from "./metrics.js";
3
+ export * from "./tracing.js";
4
+ export {
5
+ makeFlowObservabilityLive,
6
+ FlowObservabilityLive,
7
+ getFlowMetrics,
8
+ withFlowDuration,
9
+ withNodeDuration,
10
+ trackActiveFlow,
11
+ trackActiveNode,
12
+ } from "./layers.js";
13
+ export * from "./errors.js";
14
+ export {
15
+ makeTestFlowObservability as makeTestFlowObservabilityUtil,
16
+ runWithTestFlowObservability,
17
+ } from "./testing.js";