@objectstack/service-storage 5.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -56,10 +56,11 @@ async function streamToBuffer(stream) {
56
56
  }
57
57
  return Buffer.concat(chunks);
58
58
  }
59
- var S3StorageAdapter;
59
+ var import_observability2, S3StorageAdapter;
60
60
  var init_s3_storage_adapter = __esm({
61
61
  "src/s3-storage-adapter.ts"() {
62
62
  "use strict";
63
+ import_observability2 = require("@objectstack/observability");
63
64
  S3StorageAdapter = class {
64
65
  constructor(options) {
65
66
  this.options = options;
@@ -72,6 +73,34 @@ var init_s3_storage_adapter = __esm({
72
73
  this.region = options.region;
73
74
  this.endpoint = options.endpoint;
74
75
  this.forcePathStyle = options.forcePathStyle ?? false;
76
+ this.metrics = options.metrics ?? new import_observability2.NoopMetricsRegistry();
77
+ }
78
+ /**
79
+ * Wrap a storage operation with metrics instrumentation.
80
+ * Records ok/error counters, a duration histogram, and an error counter
81
+ * keyed by error class on failure. Never swallows the underlying error.
82
+ */
83
+ async track(op, fn) {
84
+ const started = Date.now();
85
+ const baseLabels = { adapter: "s3", op };
86
+ try {
87
+ const out = await fn();
88
+ try {
89
+ this.metrics.counter(import_observability2.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "ok" });
90
+ this.metrics.histogram(import_observability2.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
91
+ } catch {
92
+ }
93
+ return out;
94
+ } catch (err) {
95
+ try {
96
+ this.metrics.counter(import_observability2.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "error" });
97
+ this.metrics.histogram(import_observability2.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
98
+ const errorClass = err?.name || err?.constructor?.name || "Error";
99
+ this.metrics.counter(import_observability2.SEMCONV.storageErrorsTotal, { ...baseLabels, errorClass });
100
+ } catch {
101
+ }
102
+ throw err;
103
+ }
75
104
  }
76
105
  /**
77
106
  * Lazily resolve the AWS S3 client to avoid crashing at import time when
@@ -121,67 +150,79 @@ var init_s3_storage_adapter = __esm({
121
150
  // Basic operations
122
151
  // ---------------------------------------------------------------------------
123
152
  async upload(key, data, options) {
124
- const client = await this.getClient();
125
- const s3 = await this.s3Mod();
126
- const body = data instanceof Buffer ? data : await streamToBuffer(data);
127
- const cmd = new s3.PutObjectCommand({
128
- Bucket: this.bucket,
129
- Key: key,
130
- Body: body,
131
- ContentType: options?.contentType,
132
- Metadata: options?.metadata,
133
- ACL: options?.acl === "public-read" ? "public-read" : void 0
153
+ return this.track("put", async () => {
154
+ const client = await this.getClient();
155
+ const s3 = await this.s3Mod();
156
+ const body = data instanceof Buffer ? data : await streamToBuffer(data);
157
+ const cmd = new s3.PutObjectCommand({
158
+ Bucket: this.bucket,
159
+ Key: key,
160
+ Body: body,
161
+ ContentType: options?.contentType,
162
+ Metadata: options?.metadata,
163
+ ACL: options?.acl === "public-read" ? "public-read" : void 0
164
+ });
165
+ await client.send(cmd);
134
166
  });
135
- await client.send(cmd);
136
167
  }
137
168
  async download(key) {
138
- const client = await this.getClient();
139
- const s3 = await this.s3Mod();
140
- const cmd = new s3.GetObjectCommand({ Bucket: this.bucket, Key: key });
141
- const res = await client.send(cmd);
142
- return streamToBuffer(res.Body);
169
+ return this.track("get", async () => {
170
+ const client = await this.getClient();
171
+ const s3 = await this.s3Mod();
172
+ const cmd = new s3.GetObjectCommand({ Bucket: this.bucket, Key: key });
173
+ const res = await client.send(cmd);
174
+ return streamToBuffer(res.Body);
175
+ });
143
176
  }
144
177
  async delete(key) {
145
- const client = await this.getClient();
146
- const s3 = await this.s3Mod();
147
- const cmd = new s3.DeleteObjectCommand({ Bucket: this.bucket, Key: key });
148
- await client.send(cmd);
178
+ return this.track("delete", async () => {
179
+ const client = await this.getClient();
180
+ const s3 = await this.s3Mod();
181
+ const cmd = new s3.DeleteObjectCommand({ Bucket: this.bucket, Key: key });
182
+ await client.send(cmd);
183
+ });
149
184
  }
150
185
  async exists(key) {
151
- const client = await this.getClient();
152
- const s3 = await this.s3Mod();
153
- try {
154
- const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
155
- await client.send(cmd);
156
- return true;
157
- } catch (err) {
158
- if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) return false;
159
- throw err;
160
- }
186
+ return this.track("head", async () => {
187
+ const client = await this.getClient();
188
+ const s3 = await this.s3Mod();
189
+ try {
190
+ const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
191
+ await client.send(cmd);
192
+ return true;
193
+ } catch (err) {
194
+ if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) return false;
195
+ throw err;
196
+ }
197
+ });
161
198
  }
162
199
  async getInfo(key) {
163
- const client = await this.getClient();
164
- const s3 = await this.s3Mod();
165
- const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
166
- const res = await client.send(cmd);
167
- return {
168
- key,
169
- size: res.ContentLength ?? 0,
170
- contentType: res.ContentType,
171
- lastModified: res.LastModified ?? /* @__PURE__ */ new Date(),
172
- metadata: res.Metadata
173
- };
200
+ return this.track("head", async () => {
201
+ const client = await this.getClient();
202
+ const s3 = await this.s3Mod();
203
+ const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
204
+ const res = await client.send(cmd);
205
+ return {
206
+ key,
207
+ size: res.ContentLength ?? 0,
208
+ contentType: res.ContentType,
209
+ lastModified: res.LastModified ?? /* @__PURE__ */ new Date(),
210
+ metadata: res.Metadata
211
+ };
212
+ });
174
213
  }
175
214
  async list(prefix) {
176
- const client = await this.getClient();
177
- const s3 = await this.s3Mod();
178
- const cmd = new s3.ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix });
179
- const res = await client.send(cmd);
180
- return (res.Contents ?? []).map((item) => ({
181
- key: item.Key,
182
- size: item.Size ?? 0,
183
- lastModified: item.LastModified ?? /* @__PURE__ */ new Date()
184
- }));
215
+ return this.track("list", async () => {
216
+ const client = await this.getClient();
217
+ const s3 = await this.s3Mod();
218
+ const cmd = new s3.ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix });
219
+ const res = await client.send(cmd);
220
+ return (res.Contents ?? []).map((item) => ({
221
+ key: item.Key,
222
+ size: item.Size ?? 0,
223
+ lastModified: item.LastModified ?? /* @__PURE__ */ new Date()
224
+ }));
225
+ });
185
226
  }
186
227
  // ---------------------------------------------------------------------------
187
228
  // Presigned URLs
@@ -308,10 +349,14 @@ __export(index_exports, {
308
349
  });
309
350
  module.exports = __toCommonJS(index_exports);
310
351
 
352
+ // src/storage-service-plugin.ts
353
+ var import_observability3 = require("@objectstack/observability");
354
+
311
355
  // src/local-storage-adapter.ts
312
356
  var import_node_fs = require("fs");
313
357
  var import_node_path = require("path");
314
358
  var import_node_crypto = require("crypto");
359
+ var import_observability = require("@objectstack/observability");
315
360
  var LocalStorageAdapter = class {
316
361
  constructor(options) {
317
362
  this.rootDir = options.rootDir;
@@ -319,6 +364,33 @@ var LocalStorageAdapter = class {
319
364
  this.baseUrl = options.baseUrl ?? "";
320
365
  this.basePath = options.basePath ?? "/api/v1/storage";
321
366
  this.signingSecret = options.signingSecret ?? (0, import_node_crypto.randomUUID)();
367
+ this.metrics = options.metrics ?? new import_observability.NoopMetricsRegistry();
368
+ }
369
+ /**
370
+ * Wrap a storage operation with metrics instrumentation. Never swallows
371
+ * the underlying error; instrumentation failures are silently ignored.
372
+ */
373
+ async track(op, fn) {
374
+ const started = Date.now();
375
+ const baseLabels = { adapter: "local", op };
376
+ try {
377
+ const out = await fn();
378
+ try {
379
+ this.metrics.counter(import_observability.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "ok" });
380
+ this.metrics.histogram(import_observability.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
381
+ } catch {
382
+ }
383
+ return out;
384
+ } catch (err) {
385
+ try {
386
+ this.metrics.counter(import_observability.SEMCONV.storageOperationsTotal, { ...baseLabels, result: "error" });
387
+ this.metrics.histogram(import_observability.SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
388
+ const errorClass = err?.name || err?.constructor?.name || "Error";
389
+ this.metrics.counter(import_observability.SEMCONV.storageErrorsTotal, { ...baseLabels, errorClass });
390
+ } catch {
391
+ }
392
+ throw err;
393
+ }
322
394
  }
323
395
  // ---------------------------------------------------------------------------
324
396
  // Path helpers
@@ -339,61 +411,72 @@ var LocalStorageAdapter = class {
339
411
  // Basic file operations
340
412
  // ---------------------------------------------------------------------------
341
413
  async upload(key, data, _options) {
342
- const filePath = this.resolvePath(key);
343
- await import_node_fs.promises.mkdir((0, import_node_path.dirname)(filePath), { recursive: true });
344
- if (data instanceof Buffer) {
345
- await import_node_fs.promises.writeFile(filePath, data);
346
- return;
347
- }
348
- const chunks = [];
349
- const reader = data.getReader();
350
- let done = false;
351
- while (!done) {
352
- const result = await reader.read();
353
- done = result.done;
354
- if (result.value) chunks.push(result.value);
355
- }
356
- await import_node_fs.promises.writeFile(filePath, Buffer.concat(chunks));
414
+ return this.track("put", async () => {
415
+ const filePath = this.resolvePath(key);
416
+ await import_node_fs.promises.mkdir((0, import_node_path.dirname)(filePath), { recursive: true });
417
+ if (data instanceof Buffer) {
418
+ await import_node_fs.promises.writeFile(filePath, data);
419
+ return;
420
+ }
421
+ const chunks = [];
422
+ const reader = data.getReader();
423
+ let done = false;
424
+ while (!done) {
425
+ const result = await reader.read();
426
+ done = result.done;
427
+ if (result.value) chunks.push(result.value);
428
+ }
429
+ await import_node_fs.promises.writeFile(filePath, Buffer.concat(chunks));
430
+ });
357
431
  }
358
432
  async download(key) {
359
- return import_node_fs.promises.readFile(this.resolvePath(key));
433
+ return this.track("get", async () => import_node_fs.promises.readFile(this.resolvePath(key)));
360
434
  }
361
435
  async delete(key) {
362
- await import_node_fs.promises.unlink(this.resolvePath(key)).catch((err) => {
363
- if (err && err.code === "ENOENT") return;
364
- throw err;
436
+ return this.track("delete", async () => {
437
+ await import_node_fs.promises.unlink(this.resolvePath(key)).catch((err) => {
438
+ if (err && err.code === "ENOENT") return;
439
+ throw err;
440
+ });
365
441
  });
366
442
  }
367
443
  async exists(key) {
368
- try {
369
- await import_node_fs.promises.access(this.resolvePath(key));
370
- return true;
371
- } catch {
372
- return false;
373
- }
444
+ return this.track("head", async () => {
445
+ try {
446
+ await import_node_fs.promises.access(this.resolvePath(key));
447
+ return true;
448
+ } catch {
449
+ return false;
450
+ }
451
+ });
374
452
  }
375
453
  async getInfo(key) {
376
- const filePath = this.resolvePath(key);
377
- const stat = await import_node_fs.promises.stat(filePath);
378
- return { key, size: stat.size, lastModified: stat.mtime };
454
+ return this.track("head", async () => {
455
+ const filePath = this.resolvePath(key);
456
+ const stat = await import_node_fs.promises.stat(filePath);
457
+ return { key, size: stat.size, lastModified: stat.mtime };
458
+ });
379
459
  }
380
460
  async list(prefix) {
381
- const dirPath = this.resolvePath(prefix);
382
- try {
383
- const entries = await import_node_fs.promises.readdir(dirPath);
384
- const results = [];
385
- for (const entry of entries) {
386
- if (entry.startsWith(".")) continue;
387
- const fullKey = prefix ? `${prefix}/${entry}` : entry;
388
- try {
389
- results.push(await this.getInfo(fullKey));
390
- } catch {
461
+ return this.track("list", async () => {
462
+ const dirPath = this.resolvePath(prefix);
463
+ try {
464
+ const entries = await import_node_fs.promises.readdir(dirPath);
465
+ const results = [];
466
+ for (const entry of entries) {
467
+ if (entry.startsWith(".")) continue;
468
+ const fullKey = prefix ? `${prefix}/${entry}` : entry;
469
+ try {
470
+ const stat = await import_node_fs.promises.stat(this.resolvePath(fullKey));
471
+ results.push({ key: fullKey, size: stat.size, lastModified: stat.mtime });
472
+ } catch {
473
+ }
391
474
  }
475
+ return results;
476
+ } catch {
477
+ return [];
392
478
  }
393
- return results;
394
- } catch {
395
- return [];
396
- }
479
+ });
397
480
  }
398
481
  // ---------------------------------------------------------------------------
399
482
  // Presigned URL helpers
@@ -1265,6 +1348,7 @@ var StorageServicePlugin = class {
1265
1348
  this.type = "standard";
1266
1349
  this.storage = null;
1267
1350
  this.store = null;
1351
+ this.metrics = new import_observability3.NoopMetricsRegistry();
1268
1352
  this.options = { adapter: "local", ...options };
1269
1353
  }
1270
1354
  /** Build a concrete adapter from a values map (settings-derived). */
@@ -1282,7 +1366,8 @@ var StorageServicePlugin = class {
1282
1366
  endpoint: values.s3_endpoint || void 0,
1283
1367
  accessKeyId: values.s3_access_key_id || void 0,
1284
1368
  secretAccessKey: values.s3_secret_access_key || void 0,
1285
- forcePathStyle: !!values.s3_force_path_style
1369
+ forcePathStyle: !!values.s3_force_path_style,
1370
+ metrics: this.metrics
1286
1371
  };
1287
1372
  return new S3StorageAdapter(opts);
1288
1373
  }
@@ -1291,10 +1376,12 @@ var StorageServicePlugin = class {
1291
1376
  basePath: this.options.basePath ?? "/api/v1/storage",
1292
1377
  ...this.options.local ?? {},
1293
1378
  // settings value wins over any constructor-provided local.rootDir
1294
- rootDir
1379
+ rootDir,
1380
+ metrics: this.metrics
1295
1381
  });
1296
1382
  }
1297
1383
  async init(ctx) {
1384
+ this.metrics = resolveMetrics(ctx, this.options.metrics);
1298
1385
  const adapter = this.options.adapter;
1299
1386
  let initial;
1300
1387
  if (adapter === "s3") {
@@ -1303,11 +1390,11 @@ var StorageServicePlugin = class {
1303
1390
  if (!s3Opts) {
1304
1391
  throw new Error('StorageServicePlugin: s3 options are required when adapter is "s3"');
1305
1392
  }
1306
- initial = new S3Ctor(s3Opts);
1393
+ initial = new S3Ctor({ ...s3Opts, metrics: this.metrics });
1307
1394
  } else {
1308
1395
  const rootDir = this.options.local?.rootDir ?? "./storage";
1309
1396
  const basePath = this.options.basePath ?? "/api/v1/storage";
1310
- initial = new LocalStorageAdapter({ rootDir, basePath, ...this.options.local });
1397
+ initial = new LocalStorageAdapter({ rootDir, basePath, ...this.options.local, metrics: this.metrics });
1311
1398
  }
1312
1399
  this.storage = new SwappableStorageService(initial, (prev, next) => {
1313
1400
  const prevName = prev?.constructor?.name ?? "unknown";
@@ -1317,7 +1404,9 @@ var StorageServicePlugin = class {
1317
1404
  );
1318
1405
  });
1319
1406
  ctx.registerService("file-storage", this.storage);
1320
- ctx.logger.info(`StorageServicePlugin: registered ${adapter} storage adapter (swappable)`);
1407
+ ctx.logger.info(
1408
+ `StorageServicePlugin: registered ${adapter} storage adapter (swappable, metrics=${this.metrics.constructor?.name ?? "unknown"})`
1409
+ );
1321
1410
  try {
1322
1411
  ctx.getService("manifest").register({
1323
1412
  id: "com.objectstack.service.storage",
@@ -1429,6 +1518,15 @@ var StorageServicePlugin = class {
1429
1518
  });
1430
1519
  }
1431
1520
  };
1521
+ function resolveMetrics(ctx, override) {
1522
+ if (override) return override;
1523
+ try {
1524
+ const m = ctx.getService(import_observability3.OBSERVABILITY_METRICS_SERVICE);
1525
+ if (m) return m;
1526
+ } catch {
1527
+ }
1528
+ return new import_observability3.NoopMetricsRegistry();
1529
+ }
1432
1530
 
1433
1531
  // src/index.ts
1434
1532
  init_s3_storage_adapter();