@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.
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-check.log +0 -0
- package/LICENSE +21 -0
- package/dist/core/errors.d.ts +8 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +108 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +8 -0
- package/dist/core/layers.d.ts +104 -0
- package/dist/core/layers.d.ts.map +1 -0
- package/dist/core/layers.js +110 -0
- package/dist/core/logging.d.ts +18 -0
- package/dist/core/logging.d.ts.map +1 -0
- package/dist/core/logging.js +41 -0
- package/dist/core/metrics.d.ts +37 -0
- package/dist/core/metrics.d.ts.map +1 -0
- package/dist/core/metrics.js +72 -0
- package/dist/core/testing.d.ts +43 -0
- package/dist/core/testing.d.ts.map +1 -0
- package/dist/core/testing.js +93 -0
- package/dist/core/tracing.d.ts +19 -0
- package/dist/core/tracing.d.ts.map +1 -0
- package/dist/core/tracing.js +43 -0
- package/dist/core/utilities.d.ts +11 -0
- package/dist/core/utilities.d.ts.map +1 -0
- package/dist/core/utilities.js +41 -0
- package/dist/flow/errors.d.ts +15 -0
- package/dist/flow/errors.d.ts.map +1 -0
- package/dist/flow/errors.js +66 -0
- package/dist/flow/index.d.ts +6 -0
- package/dist/flow/index.d.ts.map +1 -0
- package/dist/flow/index.js +6 -0
- package/dist/flow/layers.d.ts +40 -0
- package/dist/flow/layers.d.ts.map +1 -0
- package/dist/flow/layers.js +94 -0
- package/dist/flow/metrics.d.ts +52 -0
- package/dist/flow/metrics.d.ts.map +1 -0
- package/dist/flow/metrics.js +89 -0
- package/dist/flow/testing.d.ts +11 -0
- package/dist/flow/testing.d.ts.map +1 -0
- package/dist/flow/testing.js +27 -0
- package/dist/flow/tracing.d.ts +35 -0
- package/dist/flow/tracing.d.ts.map +1 -0
- package/dist/flow/tracing.js +42 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/service/metrics.d.ts +23 -0
- package/dist/service/metrics.d.ts.map +1 -0
- package/dist/service/metrics.js +17 -0
- package/dist/storage/azure.d.ts +47 -0
- package/dist/storage/azure.d.ts.map +1 -0
- package/dist/storage/azure.js +89 -0
- package/dist/storage/filesystem.d.ts +47 -0
- package/dist/storage/filesystem.d.ts.map +1 -0
- package/dist/storage/filesystem.js +70 -0
- package/dist/storage/gcs.d.ts +47 -0
- package/dist/storage/gcs.d.ts.map +1 -0
- package/dist/storage/gcs.js +90 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/s3.d.ts +47 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +67 -0
- package/dist/test-observability.d.ts +12 -0
- package/dist/test-observability.d.ts.map +1 -0
- package/dist/test-observability.js +153 -0
- package/dist/upload/errors.d.ts +16 -0
- package/dist/upload/errors.d.ts.map +1 -0
- package/dist/upload/errors.js +107 -0
- package/dist/upload/index.d.ts +6 -0
- package/dist/upload/index.d.ts.map +1 -0
- package/dist/upload/index.js +6 -0
- package/dist/upload/layers.d.ts +32 -0
- package/dist/upload/layers.d.ts.map +1 -0
- package/dist/upload/layers.js +63 -0
- package/dist/upload/metrics.d.ts +46 -0
- package/dist/upload/metrics.d.ts.map +1 -0
- package/dist/upload/metrics.js +80 -0
- package/dist/upload/testing.d.ts +32 -0
- package/dist/upload/testing.d.ts.map +1 -0
- package/dist/upload/testing.js +52 -0
- package/dist/upload/tracing.d.ts +25 -0
- package/dist/upload/tracing.d.ts.map +1 -0
- package/dist/upload/tracing.js +35 -0
- package/package.json +37 -0
- package/src/core/errors.ts +187 -0
- package/src/core/index.ts +9 -0
- package/src/core/layers.ts +205 -0
- package/src/core/logging.ts +81 -0
- package/src/core/metrics.ts +108 -0
- package/src/core/testing.ts +142 -0
- package/src/core/tracing.ts +67 -0
- package/src/core/utilities.ts +133 -0
- package/src/flow/errors.ts +95 -0
- package/src/flow/index.ts +17 -0
- package/src/flow/layers.ts +131 -0
- package/src/flow/metrics.ts +130 -0
- package/src/flow/testing.ts +33 -0
- package/src/flow/tracing.ts +72 -0
- package/src/index.ts +31 -0
- package/src/service/metrics.ts +31 -0
- package/src/storage/azure.ts +163 -0
- package/src/storage/filesystem.ts +153 -0
- package/src/storage/gcs.ts +161 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/s3.ts +136 -0
- package/src/test-observability.ts +234 -0
- package/src/upload/errors.ts +166 -0
- package/src/upload/index.ts +12 -0
- package/src/upload/layers.ts +88 -0
- package/src/upload/metrics.ts +118 -0
- package/src/upload/testing.ts +60 -0
- package/src/upload/tracing.ts +58 -0
- package/tsconfig.json +10 -0
- 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";
|