@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,166 @@
|
|
|
1
|
+
import { Effect, Metric } from "effect";
|
|
2
|
+
import type { UploadServerMetrics } from "./metrics.js";
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Upload Error Classification and Tracking
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
export type UploadErrorCategory =
|
|
9
|
+
| "network_error"
|
|
10
|
+
| "authentication_error"
|
|
11
|
+
| "authorization_error"
|
|
12
|
+
| "validation_error"
|
|
13
|
+
| "size_limit_error"
|
|
14
|
+
| "storage_error"
|
|
15
|
+
| "abort_error"
|
|
16
|
+
| "unknown_error";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Classify upload errors into standard categories
|
|
20
|
+
*/
|
|
21
|
+
export const classifyUploadError = (error: unknown): UploadErrorCategory => {
|
|
22
|
+
if (!error || typeof error !== "object") return "unknown_error";
|
|
23
|
+
|
|
24
|
+
const errorCode = "code" in error ? error.code : undefined;
|
|
25
|
+
const errorName = "name" in error ? error.name : undefined;
|
|
26
|
+
const errorMessage =
|
|
27
|
+
error instanceof Error ? error.message.toLowerCase() : "";
|
|
28
|
+
|
|
29
|
+
// Abort errors
|
|
30
|
+
if (
|
|
31
|
+
errorCode === "ABORTED" ||
|
|
32
|
+
errorName === "AbortError" ||
|
|
33
|
+
errorMessage.includes("abort")
|
|
34
|
+
) {
|
|
35
|
+
return "abort_error";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Size limit errors
|
|
39
|
+
if (
|
|
40
|
+
errorCode === "FILE_TOO_LARGE" ||
|
|
41
|
+
errorCode === "LIMIT_FILE_SIZE" ||
|
|
42
|
+
errorCode === "RequestEntityTooLarge" ||
|
|
43
|
+
errorMessage.includes("too large") ||
|
|
44
|
+
errorMessage.includes("size limit") ||
|
|
45
|
+
errorMessage.includes("max size")
|
|
46
|
+
) {
|
|
47
|
+
return "size_limit_error";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Validation errors
|
|
51
|
+
if (
|
|
52
|
+
errorCode === "INVALID_FILE" ||
|
|
53
|
+
errorCode === "INVALID_METADATA" ||
|
|
54
|
+
errorCode === "VALIDATION_ERROR" ||
|
|
55
|
+
errorMessage.includes("validation") ||
|
|
56
|
+
errorMessage.includes("invalid")
|
|
57
|
+
) {
|
|
58
|
+
return "validation_error";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Network errors
|
|
62
|
+
if (
|
|
63
|
+
errorCode === "NetworkError" ||
|
|
64
|
+
errorCode === "ECONNRESET" ||
|
|
65
|
+
errorCode === "ENOTFOUND" ||
|
|
66
|
+
errorCode === "ETIMEDOUT" ||
|
|
67
|
+
errorMessage.includes("network") ||
|
|
68
|
+
errorMessage.includes("timeout")
|
|
69
|
+
) {
|
|
70
|
+
return "network_error";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Authentication errors
|
|
74
|
+
if (
|
|
75
|
+
errorCode === "UNAUTHORIZED" ||
|
|
76
|
+
errorCode === "AuthenticationFailed" ||
|
|
77
|
+
errorName === "AuthenticationError" ||
|
|
78
|
+
errorMessage.includes("authentication") ||
|
|
79
|
+
errorMessage.includes("unauthorized")
|
|
80
|
+
) {
|
|
81
|
+
return "authentication_error";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Authorization errors
|
|
85
|
+
if (
|
|
86
|
+
errorCode === "FORBIDDEN" ||
|
|
87
|
+
errorCode === "AccessDenied" ||
|
|
88
|
+
errorName === "AuthorizationError" ||
|
|
89
|
+
errorMessage.includes("forbidden") ||
|
|
90
|
+
errorMessage.includes("permission")
|
|
91
|
+
) {
|
|
92
|
+
return "authorization_error";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Storage errors
|
|
96
|
+
if (
|
|
97
|
+
errorCode === "FILE_WRITE_ERROR" ||
|
|
98
|
+
errorCode === "STORAGE_ERROR" ||
|
|
99
|
+
errorMessage.includes("storage") ||
|
|
100
|
+
errorMessage.includes("write error")
|
|
101
|
+
) {
|
|
102
|
+
return "storage_error";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return "unknown_error";
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Track upload errors with metrics and structured logging
|
|
110
|
+
*/
|
|
111
|
+
export const trackUploadError = (
|
|
112
|
+
metrics: UploadServerMetrics,
|
|
113
|
+
operation: string,
|
|
114
|
+
error: unknown,
|
|
115
|
+
context: Record<string, unknown> = {},
|
|
116
|
+
) =>
|
|
117
|
+
Effect.gen(function* () {
|
|
118
|
+
const errorCategory = classifyUploadError(error);
|
|
119
|
+
|
|
120
|
+
// Record error metrics
|
|
121
|
+
const errorMetric = metrics.uploadFailedTotal.pipe(
|
|
122
|
+
Metric.tagged("operation", operation),
|
|
123
|
+
Metric.tagged("error_category", errorCategory),
|
|
124
|
+
);
|
|
125
|
+
yield* errorMetric(Effect.succeed(1));
|
|
126
|
+
|
|
127
|
+
// Create detailed error context
|
|
128
|
+
const errorDetails = {
|
|
129
|
+
operation,
|
|
130
|
+
error_category: errorCategory,
|
|
131
|
+
error_type: typeof error,
|
|
132
|
+
error_message: error instanceof Error ? error.message : String(error),
|
|
133
|
+
error_code:
|
|
134
|
+
error && typeof error === "object" && "code" in error
|
|
135
|
+
? String(error.code)
|
|
136
|
+
: undefined,
|
|
137
|
+
error_name:
|
|
138
|
+
error && typeof error === "object" && "name" in error
|
|
139
|
+
? String(error.name)
|
|
140
|
+
: undefined,
|
|
141
|
+
...context,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Log structured error
|
|
145
|
+
yield* Effect.logError(`Upload ${operation} failed`).pipe(
|
|
146
|
+
Effect.annotateLogs(errorDetails),
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create a custom error classifier for upload operations
|
|
152
|
+
*/
|
|
153
|
+
export const createUploadErrorClassifier = (
|
|
154
|
+
customErrorMapping?: (error: unknown) => UploadErrorCategory | null,
|
|
155
|
+
) => {
|
|
156
|
+
return (error: unknown): UploadErrorCategory => {
|
|
157
|
+
// Try custom mapping first
|
|
158
|
+
if (customErrorMapping) {
|
|
159
|
+
const customResult = customErrorMapping(error);
|
|
160
|
+
if (customResult !== null) return customResult;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Fall back to generic classification
|
|
164
|
+
return classifyUploadError(error);
|
|
165
|
+
};
|
|
166
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Upload observability exports
|
|
2
|
+
export * from "./metrics.js";
|
|
3
|
+
export * from "./tracing.js";
|
|
4
|
+
export {
|
|
5
|
+
makeUploadObservabilityLive,
|
|
6
|
+
UploadObservabilityLive,
|
|
7
|
+
getUploadMetrics,
|
|
8
|
+
withUploadDuration,
|
|
9
|
+
withChunkDuration,
|
|
10
|
+
} from "./layers.js";
|
|
11
|
+
export * from "./errors.js";
|
|
12
|
+
export * from "./testing.js";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Effect, Layer, Metric } from "effect";
|
|
2
|
+
import {
|
|
3
|
+
makeUploadObservabilityLayer,
|
|
4
|
+
UploadObservability,
|
|
5
|
+
} from "../core/layers.js";
|
|
6
|
+
import { createUploadServerMetrics } from "./metrics.js";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Upload Observability Layer Implementation
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a live upload observability layer with full metrics
|
|
14
|
+
*/
|
|
15
|
+
export const makeUploadObservabilityLive = (
|
|
16
|
+
serviceName = "uploadista-upload-server",
|
|
17
|
+
): Layer.Layer<UploadObservability> => {
|
|
18
|
+
const metrics = createUploadServerMetrics();
|
|
19
|
+
|
|
20
|
+
return Layer.succeed(UploadObservability, {
|
|
21
|
+
serviceName,
|
|
22
|
+
enabled: true,
|
|
23
|
+
metrics: {
|
|
24
|
+
uploadCreated: Effect.succeed(metrics.uploadCreatedTotal).pipe(
|
|
25
|
+
Effect.flatMap((metric) => Metric.increment(metric)),
|
|
26
|
+
),
|
|
27
|
+
uploadCompleted: Effect.succeed(metrics.uploadCompletedTotal).pipe(
|
|
28
|
+
Effect.flatMap((metric) => Metric.increment(metric)),
|
|
29
|
+
),
|
|
30
|
+
uploadFailed: Effect.succeed(metrics.uploadFailedTotal).pipe(
|
|
31
|
+
Effect.flatMap((metric) => Metric.increment(metric)),
|
|
32
|
+
),
|
|
33
|
+
chunkUploaded: Effect.succeed(metrics.chunkUploadedTotal).pipe(
|
|
34
|
+
Effect.flatMap((metric) => Metric.increment(metric)),
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Default live upload observability layer
|
|
42
|
+
*/
|
|
43
|
+
export const UploadObservabilityLive = makeUploadObservabilityLive();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* No-op upload observability layer (for testing or disabled observability)
|
|
47
|
+
*/
|
|
48
|
+
export const UploadObservabilityDisabled = makeUploadObservabilityLayer(false);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Helper to get upload metrics from context
|
|
52
|
+
*/
|
|
53
|
+
export const getUploadMetrics = Effect.gen(function* () {
|
|
54
|
+
const obs = yield* UploadObservability;
|
|
55
|
+
return obs.metrics;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Helper to track upload duration
|
|
60
|
+
*/
|
|
61
|
+
export const withUploadDuration = <A, E, R>(
|
|
62
|
+
effect: Effect.Effect<A, E, R>,
|
|
63
|
+
): Effect.Effect<A, E, R | UploadObservability> => {
|
|
64
|
+
const metrics = createUploadServerMetrics();
|
|
65
|
+
return Effect.gen(function* () {
|
|
66
|
+
const startTime = Date.now();
|
|
67
|
+
const result = yield* effect;
|
|
68
|
+
const duration = (Date.now() - startTime) / 1000; // Convert to seconds
|
|
69
|
+
yield* Metric.update(metrics.uploadDurationHistogram, duration);
|
|
70
|
+
return result;
|
|
71
|
+
}).pipe(Effect.withSpan("upload-operation"));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Helper to track chunk upload duration
|
|
76
|
+
*/
|
|
77
|
+
export const withChunkDuration = <A, E, R>(
|
|
78
|
+
effect: Effect.Effect<A, E, R>,
|
|
79
|
+
): Effect.Effect<A, E, R> => {
|
|
80
|
+
const metrics = createUploadServerMetrics();
|
|
81
|
+
return Effect.gen(function* () {
|
|
82
|
+
const startTime = Date.now();
|
|
83
|
+
const result = yield* effect;
|
|
84
|
+
const duration = (Date.now() - startTime) / 1000; // Convert to seconds
|
|
85
|
+
yield* Metric.update(metrics.chunkUploadDurationHistogram, duration);
|
|
86
|
+
return result;
|
|
87
|
+
}).pipe(Effect.withSpan("chunk-upload"));
|
|
88
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Metric, MetricBoundaries } from "effect";
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Upload Server Metrics
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Upload server metrics for tracking upload operations
|
|
9
|
+
*/
|
|
10
|
+
export const createUploadServerMetrics = () => ({
|
|
11
|
+
// Counter metrics
|
|
12
|
+
uploadCreatedTotal: Metric.counter("upload_created_total", {
|
|
13
|
+
description: "Total number of uploads created",
|
|
14
|
+
}),
|
|
15
|
+
|
|
16
|
+
uploadCompletedTotal: Metric.counter("upload_completed_total", {
|
|
17
|
+
description: "Total number of uploads completed successfully",
|
|
18
|
+
}),
|
|
19
|
+
|
|
20
|
+
uploadFailedTotal: Metric.counter("upload_failed_total", {
|
|
21
|
+
description: "Total number of uploads that failed",
|
|
22
|
+
}),
|
|
23
|
+
|
|
24
|
+
chunkUploadedTotal: Metric.counter("chunk_uploaded_total", {
|
|
25
|
+
description: "Total number of chunks uploaded",
|
|
26
|
+
}),
|
|
27
|
+
|
|
28
|
+
uploadFromUrlTotal: Metric.counter("upload_from_url_total", {
|
|
29
|
+
description: "Total number of URL-based uploads",
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
uploadFromUrlSuccessTotal: Metric.counter("upload_from_url_success_total", {
|
|
33
|
+
description: "Total number of successful URL-based uploads",
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
uploadFromUrlFailedTotal: Metric.counter("upload_from_url_failed_total", {
|
|
37
|
+
description: "Total number of failed URL-based uploads",
|
|
38
|
+
}),
|
|
39
|
+
|
|
40
|
+
// Histogram metrics
|
|
41
|
+
uploadDurationHistogram: Metric.histogram(
|
|
42
|
+
"upload_duration_seconds",
|
|
43
|
+
MetricBoundaries.exponential({
|
|
44
|
+
start: 0.01, // 10ms
|
|
45
|
+
factor: 2,
|
|
46
|
+
count: 20, // Up to ~10 seconds
|
|
47
|
+
}),
|
|
48
|
+
"Duration of complete upload operations in seconds",
|
|
49
|
+
),
|
|
50
|
+
|
|
51
|
+
chunkUploadDurationHistogram: Metric.histogram(
|
|
52
|
+
"chunk_upload_duration_seconds",
|
|
53
|
+
MetricBoundaries.exponential({
|
|
54
|
+
start: 0.001, // 1ms
|
|
55
|
+
factor: 2,
|
|
56
|
+
count: 15, // Up to ~32 seconds
|
|
57
|
+
}),
|
|
58
|
+
"Duration of individual chunk uploads in seconds",
|
|
59
|
+
),
|
|
60
|
+
|
|
61
|
+
uploadFileSizeHistogram: Metric.histogram(
|
|
62
|
+
"upload_file_size_bytes",
|
|
63
|
+
MetricBoundaries.exponential({
|
|
64
|
+
start: 1024, // 1KB
|
|
65
|
+
factor: 2,
|
|
66
|
+
count: 25, // Up to ~33GB
|
|
67
|
+
}),
|
|
68
|
+
"Size of uploaded files in bytes",
|
|
69
|
+
),
|
|
70
|
+
|
|
71
|
+
chunkSizeHistogram: Metric.histogram(
|
|
72
|
+
"chunk_size_bytes",
|
|
73
|
+
MetricBoundaries.linear({
|
|
74
|
+
start: 262_144, // 256KB
|
|
75
|
+
width: 262_144, // 256KB increments
|
|
76
|
+
count: 20, // Up to ~5MB
|
|
77
|
+
}),
|
|
78
|
+
"Size of uploaded chunks in bytes",
|
|
79
|
+
),
|
|
80
|
+
|
|
81
|
+
// Gauge metrics
|
|
82
|
+
activeUploadsGauge: Metric.gauge("active_uploads", {
|
|
83
|
+
description: "Number of currently active uploads",
|
|
84
|
+
}),
|
|
85
|
+
|
|
86
|
+
uploadThroughputGauge: Metric.gauge("upload_throughput_bytes_per_second", {
|
|
87
|
+
description: "Current upload throughput in bytes per second",
|
|
88
|
+
}),
|
|
89
|
+
|
|
90
|
+
// Summary metrics for latency percentiles
|
|
91
|
+
uploadLatencySummary: Metric.summary({
|
|
92
|
+
name: "upload_latency_seconds",
|
|
93
|
+
maxAge: "10 minutes",
|
|
94
|
+
maxSize: 1000,
|
|
95
|
+
error: 0.01,
|
|
96
|
+
quantiles: [0.5, 0.9, 0.95, 0.99],
|
|
97
|
+
description: "Upload operation latency percentiles",
|
|
98
|
+
}),
|
|
99
|
+
|
|
100
|
+
chunkLatencySummary: Metric.summary({
|
|
101
|
+
name: "chunk_latency_seconds",
|
|
102
|
+
maxAge: "10 minutes",
|
|
103
|
+
maxSize: 1000,
|
|
104
|
+
error: 0.01,
|
|
105
|
+
quantiles: [0.5, 0.9, 0.95, 0.99],
|
|
106
|
+
description: "Chunk upload latency percentiles",
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Type for upload server metrics
|
|
112
|
+
*/
|
|
113
|
+
export type UploadServerMetrics = ReturnType<typeof createUploadServerMetrics>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Default upload server metrics instance
|
|
117
|
+
*/
|
|
118
|
+
export const uploadServerMetrics = createUploadServerMetrics();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import { UploadObservability } from "../core/layers.js";
|
|
3
|
+
import { createUploadServerMetrics } from "./metrics.js";
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Upload Observability Testing Utilities
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a test upload observability layer that doesn't actually emit metrics
|
|
11
|
+
* but validates that the observability system is wired correctly
|
|
12
|
+
*/
|
|
13
|
+
export const UploadObservabilityTest = Layer.succeed(UploadObservability, {
|
|
14
|
+
serviceName: "uploadista-upload-server-test",
|
|
15
|
+
enabled: true,
|
|
16
|
+
metrics: {
|
|
17
|
+
uploadCreated: () => Promise.resolve(),
|
|
18
|
+
uploadCompleted: () => Promise.resolve(),
|
|
19
|
+
uploadFailed: () => Promise.resolve(),
|
|
20
|
+
chunkUploaded: () => Promise.resolve(),
|
|
21
|
+
} as any,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get metrics for validation (useful for testing metric definitions)
|
|
26
|
+
*/
|
|
27
|
+
export const getTestMetrics = () => createUploadServerMetrics();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validate that all required metrics exist
|
|
31
|
+
*/
|
|
32
|
+
export const validateMetricsExist = () => {
|
|
33
|
+
const metrics = getTestMetrics();
|
|
34
|
+
|
|
35
|
+
const requiredMetrics = [
|
|
36
|
+
"uploadCreatedTotal",
|
|
37
|
+
"uploadCompletedTotal",
|
|
38
|
+
"uploadFailedTotal",
|
|
39
|
+
"chunkUploadedTotal",
|
|
40
|
+
"uploadFromUrlTotal",
|
|
41
|
+
"uploadFromUrlSuccessTotal",
|
|
42
|
+
"uploadFromUrlFailedTotal",
|
|
43
|
+
"uploadDurationHistogram",
|
|
44
|
+
"chunkUploadDurationHistogram",
|
|
45
|
+
"uploadFileSizeHistogram",
|
|
46
|
+
"chunkSizeHistogram",
|
|
47
|
+
"activeUploadsGauge",
|
|
48
|
+
"uploadThroughputGauge",
|
|
49
|
+
"uploadLatencySummary",
|
|
50
|
+
"chunkLatencySummary",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const missingMetrics = requiredMetrics.filter((name) => !(name in metrics));
|
|
54
|
+
|
|
55
|
+
if (missingMetrics.length > 0) {
|
|
56
|
+
throw new Error(`Missing required metrics: ${missingMetrics.join(", ")}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Upload Tracing Utilities
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Wrap an Effect with an upload operation span
|
|
9
|
+
*/
|
|
10
|
+
export const withUploadSpan =
|
|
11
|
+
<A, E, R>(operation: string, attributes?: Record<string, unknown>) =>
|
|
12
|
+
(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
13
|
+
effect.pipe(
|
|
14
|
+
Effect.withSpan(`upload-${operation}`, {
|
|
15
|
+
attributes: {
|
|
16
|
+
"upload.operation": operation,
|
|
17
|
+
...attributes,
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Add upload context to the current span
|
|
24
|
+
*/
|
|
25
|
+
export const withUploadContext = (context: {
|
|
26
|
+
uploadId?: string;
|
|
27
|
+
fileName?: string;
|
|
28
|
+
fileSize?: number;
|
|
29
|
+
storageId?: string;
|
|
30
|
+
mimeType?: string;
|
|
31
|
+
}) =>
|
|
32
|
+
Effect.annotateCurrentSpan({
|
|
33
|
+
"upload.id": context.uploadId ?? "unknown",
|
|
34
|
+
"upload.file_name": context.fileName ?? "unknown",
|
|
35
|
+
"upload.file_size": context.fileSize?.toString() ?? "0",
|
|
36
|
+
"upload.storage_id": context.storageId ?? "unknown",
|
|
37
|
+
"upload.mime_type": context.mimeType ?? "unknown",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Add chunk context to the current span
|
|
42
|
+
*/
|
|
43
|
+
export const withChunkContext = (context: {
|
|
44
|
+
uploadId: string;
|
|
45
|
+
chunkSize: number;
|
|
46
|
+
offset: number;
|
|
47
|
+
totalSize?: number;
|
|
48
|
+
}) =>
|
|
49
|
+
Effect.annotateCurrentSpan({
|
|
50
|
+
"chunk.upload_id": context.uploadId,
|
|
51
|
+
"chunk.size": context.chunkSize.toString(),
|
|
52
|
+
"chunk.offset": context.offset.toString(),
|
|
53
|
+
"chunk.total_size": context.totalSize?.toString() ?? "0",
|
|
54
|
+
"chunk.progress":
|
|
55
|
+
context.totalSize && context.totalSize > 0
|
|
56
|
+
? ((context.offset / context.totalSize) * 100).toFixed(2)
|
|
57
|
+
: "0",
|
|
58
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/index.ts","./src/test-observability.ts","./src/core/errors.ts","./src/core/index.ts","./src/core/layers.ts","./src/core/logging.ts","./src/core/metrics.ts","./src/core/testing.ts","./src/core/tracing.ts","./src/core/utilities.ts","./src/flow/errors.ts","./src/flow/index.ts","./src/flow/layers.ts","./src/flow/metrics.ts","./src/flow/testing.ts","./src/flow/tracing.ts","./src/service/metrics.ts","./src/storage/azure.ts","./src/storage/filesystem.ts","./src/storage/gcs.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/upload/errors.ts","./src/upload/index.ts","./src/upload/layers.ts","./src/upload/metrics.ts","./src/upload/testing.ts","./src/upload/tracing.ts"],"version":"5.9.3"}
|