@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.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Plugin, PluginContext } from '@objectstack/core';
2
+ import { MetricsRegistry } from '@objectstack/observability';
2
3
  import { IStorageService, StorageUploadOptions, StorageFileInfo, PresignedUploadDescriptor, PresignedDownloadDescriptor, IDataEngine, IHttpServer } from '@objectstack/spec/contracts';
3
4
  import { z } from 'zod';
4
5
  import * as _objectstack_spec_data from '@objectstack/spec/data';
@@ -28,6 +29,8 @@ interface LocalStorageAdapterOptions {
28
29
  * Auto-generated if omitted (suitable for single-process dev usage).
29
30
  */
30
31
  signingSecret?: string;
32
+ /** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */
33
+ metrics?: MetricsRegistry;
31
34
  }
32
35
  interface PresignTokenPayload {
33
36
  k: string;
@@ -52,7 +55,13 @@ declare class LocalStorageAdapter implements IStorageService {
52
55
  private readonly baseUrl;
53
56
  private readonly basePath;
54
57
  private readonly signingSecret;
58
+ private readonly metrics;
55
59
  constructor(options: LocalStorageAdapterOptions);
60
+ /**
61
+ * Wrap a storage operation with metrics instrumentation. Never swallows
62
+ * the underlying error; instrumentation failures are silently ignored.
63
+ */
64
+ private track;
56
65
  private resolvePath;
57
66
  private resolvePartPath;
58
67
  upload(key: string, data: Buffer | ReadableStream, _options?: StorageUploadOptions): Promise<void>;
@@ -123,6 +132,13 @@ interface StorageServicePluginOptions {
123
132
  * adapter constructor-driven (useful in tests). Default: true.
124
133
  */
125
134
  bindToSettings?: boolean;
135
+ /**
136
+ * Optional explicit metrics backend. Wins over the service-registry
137
+ * lookup. Mostly an escape hatch for tests; production hosts should
138
+ * register `ObservabilityServicePlugin` (from `@objectstack/runtime`)
139
+ * once and let every service pick the host's backend up automatically.
140
+ */
141
+ metrics?: MetricsRegistry;
126
142
  }
127
143
  /**
128
144
  * StorageServicePlugin — Production IStorageService implementation.
@@ -156,6 +172,7 @@ declare class StorageServicePlugin implements Plugin {
156
172
  private readonly options;
157
173
  private storage;
158
174
  private store;
175
+ private metrics;
159
176
  constructor(options?: StorageServicePluginOptions);
160
177
  /** Build a concrete adapter from a values map (settings-derived). */
161
178
  private buildAdapterFromValues;
@@ -238,6 +255,8 @@ interface S3StorageAdapterOptions {
238
255
  secretAccessKey?: string;
239
256
  /** Force path-style URLs (needed for MinIO / self-hosted) */
240
257
  forcePathStyle?: boolean;
258
+ /** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */
259
+ metrics?: MetricsRegistry;
241
260
  }
242
261
  /**
243
262
  * S3 storage adapter implementing IStorageService.
@@ -261,8 +280,15 @@ declare class S3StorageAdapter implements IStorageService {
261
280
  private readonly region;
262
281
  private readonly endpoint?;
263
282
  private readonly forcePathStyle;
283
+ private readonly metrics;
264
284
  private clientPromise;
265
285
  constructor(options: S3StorageAdapterOptions);
286
+ /**
287
+ * Wrap a storage operation with metrics instrumentation.
288
+ * Records ok/error counters, a duration histogram, and an error counter
289
+ * keyed by error class on failure. Never swallows the underlying error.
290
+ */
291
+ private track;
266
292
  /**
267
293
  * Lazily resolve the AWS S3 client to avoid crashing at import time when
268
294
  * `@aws-sdk/client-s3` isn't installed.
@@ -711,7 +737,7 @@ declare const SystemFile: Omit<{
711
737
  } | undefined;
712
738
  partitioning?: {
713
739
  enabled: boolean;
714
- strategy: "hash" | "list" | "range";
740
+ strategy: "list" | "hash" | "range";
715
741
  key: string;
716
742
  interval?: string | undefined;
717
743
  } | undefined;
@@ -1025,7 +1051,7 @@ declare const SystemFile: Omit<{
1025
1051
  trash: boolean;
1026
1052
  mru: boolean;
1027
1053
  clone: boolean;
1028
- apiMethods?: ("get" | "search" | "upsert" | "create" | "import" | "delete" | "list" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
1054
+ apiMethods?: ("get" | "delete" | "list" | "search" | "upsert" | "create" | "import" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
1029
1055
  } | undefined;
1030
1056
  recordTypes?: string[] | undefined;
1031
1057
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
@@ -3847,7 +3873,7 @@ declare const SystemUploadSession: Omit<{
3847
3873
  } | undefined;
3848
3874
  partitioning?: {
3849
3875
  enabled: boolean;
3850
- strategy: "hash" | "list" | "range";
3876
+ strategy: "list" | "hash" | "range";
3851
3877
  key: string;
3852
3878
  interval?: string | undefined;
3853
3879
  } | undefined;
@@ -4161,7 +4187,7 @@ declare const SystemUploadSession: Omit<{
4161
4187
  trash: boolean;
4162
4188
  mru: boolean;
4163
4189
  clone: boolean;
4164
- apiMethods?: ("get" | "search" | "upsert" | "create" | "import" | "delete" | "list" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
4190
+ apiMethods?: ("get" | "delete" | "list" | "search" | "upsert" | "create" | "import" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
4165
4191
  } | undefined;
4166
4192
  recordTypes?: string[] | undefined;
4167
4193
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Plugin, PluginContext } from '@objectstack/core';
2
+ import { MetricsRegistry } from '@objectstack/observability';
2
3
  import { IStorageService, StorageUploadOptions, StorageFileInfo, PresignedUploadDescriptor, PresignedDownloadDescriptor, IDataEngine, IHttpServer } from '@objectstack/spec/contracts';
3
4
  import { z } from 'zod';
4
5
  import * as _objectstack_spec_data from '@objectstack/spec/data';
@@ -28,6 +29,8 @@ interface LocalStorageAdapterOptions {
28
29
  * Auto-generated if omitted (suitable for single-process dev usage).
29
30
  */
30
31
  signingSecret?: string;
32
+ /** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */
33
+ metrics?: MetricsRegistry;
31
34
  }
32
35
  interface PresignTokenPayload {
33
36
  k: string;
@@ -52,7 +55,13 @@ declare class LocalStorageAdapter implements IStorageService {
52
55
  private readonly baseUrl;
53
56
  private readonly basePath;
54
57
  private readonly signingSecret;
58
+ private readonly metrics;
55
59
  constructor(options: LocalStorageAdapterOptions);
60
+ /**
61
+ * Wrap a storage operation with metrics instrumentation. Never swallows
62
+ * the underlying error; instrumentation failures are silently ignored.
63
+ */
64
+ private track;
56
65
  private resolvePath;
57
66
  private resolvePartPath;
58
67
  upload(key: string, data: Buffer | ReadableStream, _options?: StorageUploadOptions): Promise<void>;
@@ -123,6 +132,13 @@ interface StorageServicePluginOptions {
123
132
  * adapter constructor-driven (useful in tests). Default: true.
124
133
  */
125
134
  bindToSettings?: boolean;
135
+ /**
136
+ * Optional explicit metrics backend. Wins over the service-registry
137
+ * lookup. Mostly an escape hatch for tests; production hosts should
138
+ * register `ObservabilityServicePlugin` (from `@objectstack/runtime`)
139
+ * once and let every service pick the host's backend up automatically.
140
+ */
141
+ metrics?: MetricsRegistry;
126
142
  }
127
143
  /**
128
144
  * StorageServicePlugin — Production IStorageService implementation.
@@ -156,6 +172,7 @@ declare class StorageServicePlugin implements Plugin {
156
172
  private readonly options;
157
173
  private storage;
158
174
  private store;
175
+ private metrics;
159
176
  constructor(options?: StorageServicePluginOptions);
160
177
  /** Build a concrete adapter from a values map (settings-derived). */
161
178
  private buildAdapterFromValues;
@@ -238,6 +255,8 @@ interface S3StorageAdapterOptions {
238
255
  secretAccessKey?: string;
239
256
  /** Force path-style URLs (needed for MinIO / self-hosted) */
240
257
  forcePathStyle?: boolean;
258
+ /** Optional MetricsRegistry for instrumentation. Defaults to NoopMetricsRegistry. */
259
+ metrics?: MetricsRegistry;
241
260
  }
242
261
  /**
243
262
  * S3 storage adapter implementing IStorageService.
@@ -261,8 +280,15 @@ declare class S3StorageAdapter implements IStorageService {
261
280
  private readonly region;
262
281
  private readonly endpoint?;
263
282
  private readonly forcePathStyle;
283
+ private readonly metrics;
264
284
  private clientPromise;
265
285
  constructor(options: S3StorageAdapterOptions);
286
+ /**
287
+ * Wrap a storage operation with metrics instrumentation.
288
+ * Records ok/error counters, a duration histogram, and an error counter
289
+ * keyed by error class on failure. Never swallows the underlying error.
290
+ */
291
+ private track;
266
292
  /**
267
293
  * Lazily resolve the AWS S3 client to avoid crashing at import time when
268
294
  * `@aws-sdk/client-s3` isn't installed.
@@ -711,7 +737,7 @@ declare const SystemFile: Omit<{
711
737
  } | undefined;
712
738
  partitioning?: {
713
739
  enabled: boolean;
714
- strategy: "hash" | "list" | "range";
740
+ strategy: "list" | "hash" | "range";
715
741
  key: string;
716
742
  interval?: string | undefined;
717
743
  } | undefined;
@@ -1025,7 +1051,7 @@ declare const SystemFile: Omit<{
1025
1051
  trash: boolean;
1026
1052
  mru: boolean;
1027
1053
  clone: boolean;
1028
- apiMethods?: ("get" | "search" | "upsert" | "create" | "import" | "delete" | "list" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
1054
+ apiMethods?: ("get" | "delete" | "list" | "search" | "upsert" | "create" | "import" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
1029
1055
  } | undefined;
1030
1056
  recordTypes?: string[] | undefined;
1031
1057
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
@@ -3847,7 +3873,7 @@ declare const SystemUploadSession: Omit<{
3847
3873
  } | undefined;
3848
3874
  partitioning?: {
3849
3875
  enabled: boolean;
3850
- strategy: "hash" | "list" | "range";
3876
+ strategy: "list" | "hash" | "range";
3851
3877
  key: string;
3852
3878
  interval?: string | undefined;
3853
3879
  } | undefined;
@@ -4161,7 +4187,7 @@ declare const SystemUploadSession: Omit<{
4161
4187
  trash: boolean;
4162
4188
  mru: boolean;
4163
4189
  clone: boolean;
4164
- apiMethods?: ("get" | "search" | "upsert" | "create" | "import" | "delete" | "list" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
4190
+ apiMethods?: ("get" | "delete" | "list" | "search" | "upsert" | "create" | "import" | "update" | "history" | "bulk" | "aggregate" | "restore" | "purge" | "export")[] | undefined;
4165
4191
  } | undefined;
4166
4192
  recordTypes?: string[] | undefined;
4167
4193
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
package/dist/index.js CHANGED
@@ -13,6 +13,10 @@ var s3_storage_adapter_exports = {};
13
13
  __export(s3_storage_adapter_exports, {
14
14
  S3StorageAdapter: () => S3StorageAdapter
15
15
  });
16
+ import {
17
+ NoopMetricsRegistry as NoopMetricsRegistry2,
18
+ SEMCONV as SEMCONV2
19
+ } from "@objectstack/observability";
16
20
  async function streamToBuffer(stream) {
17
21
  if (Buffer.isBuffer(stream)) return stream;
18
22
  if (stream instanceof Uint8Array) return Buffer.from(stream);
@@ -50,6 +54,34 @@ var init_s3_storage_adapter = __esm({
50
54
  this.region = options.region;
51
55
  this.endpoint = options.endpoint;
52
56
  this.forcePathStyle = options.forcePathStyle ?? false;
57
+ this.metrics = options.metrics ?? new NoopMetricsRegistry2();
58
+ }
59
+ /**
60
+ * Wrap a storage operation with metrics instrumentation.
61
+ * Records ok/error counters, a duration histogram, and an error counter
62
+ * keyed by error class on failure. Never swallows the underlying error.
63
+ */
64
+ async track(op, fn) {
65
+ const started = Date.now();
66
+ const baseLabels = { adapter: "s3", op };
67
+ try {
68
+ const out = await fn();
69
+ try {
70
+ this.metrics.counter(SEMCONV2.storageOperationsTotal, { ...baseLabels, result: "ok" });
71
+ this.metrics.histogram(SEMCONV2.storageOperationDurationMs, Date.now() - started, baseLabels);
72
+ } catch {
73
+ }
74
+ return out;
75
+ } catch (err) {
76
+ try {
77
+ this.metrics.counter(SEMCONV2.storageOperationsTotal, { ...baseLabels, result: "error" });
78
+ this.metrics.histogram(SEMCONV2.storageOperationDurationMs, Date.now() - started, baseLabels);
79
+ const errorClass = err?.name || err?.constructor?.name || "Error";
80
+ this.metrics.counter(SEMCONV2.storageErrorsTotal, { ...baseLabels, errorClass });
81
+ } catch {
82
+ }
83
+ throw err;
84
+ }
53
85
  }
54
86
  /**
55
87
  * Lazily resolve the AWS S3 client to avoid crashing at import time when
@@ -99,67 +131,79 @@ var init_s3_storage_adapter = __esm({
99
131
  // Basic operations
100
132
  // ---------------------------------------------------------------------------
101
133
  async upload(key, data, options) {
102
- const client = await this.getClient();
103
- const s3 = await this.s3Mod();
104
- const body = data instanceof Buffer ? data : await streamToBuffer(data);
105
- const cmd = new s3.PutObjectCommand({
106
- Bucket: this.bucket,
107
- Key: key,
108
- Body: body,
109
- ContentType: options?.contentType,
110
- Metadata: options?.metadata,
111
- ACL: options?.acl === "public-read" ? "public-read" : void 0
134
+ return this.track("put", async () => {
135
+ const client = await this.getClient();
136
+ const s3 = await this.s3Mod();
137
+ const body = data instanceof Buffer ? data : await streamToBuffer(data);
138
+ const cmd = new s3.PutObjectCommand({
139
+ Bucket: this.bucket,
140
+ Key: key,
141
+ Body: body,
142
+ ContentType: options?.contentType,
143
+ Metadata: options?.metadata,
144
+ ACL: options?.acl === "public-read" ? "public-read" : void 0
145
+ });
146
+ await client.send(cmd);
112
147
  });
113
- await client.send(cmd);
114
148
  }
115
149
  async download(key) {
116
- const client = await this.getClient();
117
- const s3 = await this.s3Mod();
118
- const cmd = new s3.GetObjectCommand({ Bucket: this.bucket, Key: key });
119
- const res = await client.send(cmd);
120
- return streamToBuffer(res.Body);
150
+ return this.track("get", async () => {
151
+ const client = await this.getClient();
152
+ const s3 = await this.s3Mod();
153
+ const cmd = new s3.GetObjectCommand({ Bucket: this.bucket, Key: key });
154
+ const res = await client.send(cmd);
155
+ return streamToBuffer(res.Body);
156
+ });
121
157
  }
122
158
  async delete(key) {
123
- const client = await this.getClient();
124
- const s3 = await this.s3Mod();
125
- const cmd = new s3.DeleteObjectCommand({ Bucket: this.bucket, Key: key });
126
- await client.send(cmd);
159
+ return this.track("delete", async () => {
160
+ const client = await this.getClient();
161
+ const s3 = await this.s3Mod();
162
+ const cmd = new s3.DeleteObjectCommand({ Bucket: this.bucket, Key: key });
163
+ await client.send(cmd);
164
+ });
127
165
  }
128
166
  async exists(key) {
129
- const client = await this.getClient();
130
- const s3 = await this.s3Mod();
131
- try {
132
- const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
133
- await client.send(cmd);
134
- return true;
135
- } catch (err) {
136
- if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) return false;
137
- throw err;
138
- }
167
+ return this.track("head", async () => {
168
+ const client = await this.getClient();
169
+ const s3 = await this.s3Mod();
170
+ try {
171
+ const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
172
+ await client.send(cmd);
173
+ return true;
174
+ } catch (err) {
175
+ if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) return false;
176
+ throw err;
177
+ }
178
+ });
139
179
  }
140
180
  async getInfo(key) {
141
- const client = await this.getClient();
142
- const s3 = await this.s3Mod();
143
- const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
144
- const res = await client.send(cmd);
145
- return {
146
- key,
147
- size: res.ContentLength ?? 0,
148
- contentType: res.ContentType,
149
- lastModified: res.LastModified ?? /* @__PURE__ */ new Date(),
150
- metadata: res.Metadata
151
- };
181
+ return this.track("head", async () => {
182
+ const client = await this.getClient();
183
+ const s3 = await this.s3Mod();
184
+ const cmd = new s3.HeadObjectCommand({ Bucket: this.bucket, Key: key });
185
+ const res = await client.send(cmd);
186
+ return {
187
+ key,
188
+ size: res.ContentLength ?? 0,
189
+ contentType: res.ContentType,
190
+ lastModified: res.LastModified ?? /* @__PURE__ */ new Date(),
191
+ metadata: res.Metadata
192
+ };
193
+ });
152
194
  }
153
195
  async list(prefix) {
154
- const client = await this.getClient();
155
- const s3 = await this.s3Mod();
156
- const cmd = new s3.ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix });
157
- const res = await client.send(cmd);
158
- return (res.Contents ?? []).map((item) => ({
159
- key: item.Key,
160
- size: item.Size ?? 0,
161
- lastModified: item.LastModified ?? /* @__PURE__ */ new Date()
162
- }));
196
+ return this.track("list", async () => {
197
+ const client = await this.getClient();
198
+ const s3 = await this.s3Mod();
199
+ const cmd = new s3.ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix });
200
+ const res = await client.send(cmd);
201
+ return (res.Contents ?? []).map((item) => ({
202
+ key: item.Key,
203
+ size: item.Size ?? 0,
204
+ lastModified: item.LastModified ?? /* @__PURE__ */ new Date()
205
+ }));
206
+ });
163
207
  }
164
208
  // ---------------------------------------------------------------------------
165
209
  // Presigned URLs
@@ -272,10 +316,20 @@ var init_s3_storage_adapter = __esm({
272
316
  }
273
317
  });
274
318
 
319
+ // src/storage-service-plugin.ts
320
+ import {
321
+ OBSERVABILITY_METRICS_SERVICE,
322
+ NoopMetricsRegistry as NoopMetricsRegistry3
323
+ } from "@objectstack/observability";
324
+
275
325
  // src/local-storage-adapter.ts
276
326
  import { promises as fs, createReadStream, createWriteStream } from "fs";
277
327
  import { join, dirname } from "path";
278
328
  import { createHmac, randomUUID } from "crypto";
329
+ import {
330
+ NoopMetricsRegistry,
331
+ SEMCONV
332
+ } from "@objectstack/observability";
279
333
  var LocalStorageAdapter = class {
280
334
  constructor(options) {
281
335
  this.rootDir = options.rootDir;
@@ -283,6 +337,33 @@ var LocalStorageAdapter = class {
283
337
  this.baseUrl = options.baseUrl ?? "";
284
338
  this.basePath = options.basePath ?? "/api/v1/storage";
285
339
  this.signingSecret = options.signingSecret ?? randomUUID();
340
+ this.metrics = options.metrics ?? new NoopMetricsRegistry();
341
+ }
342
+ /**
343
+ * Wrap a storage operation with metrics instrumentation. Never swallows
344
+ * the underlying error; instrumentation failures are silently ignored.
345
+ */
346
+ async track(op, fn) {
347
+ const started = Date.now();
348
+ const baseLabels = { adapter: "local", op };
349
+ try {
350
+ const out = await fn();
351
+ try {
352
+ this.metrics.counter(SEMCONV.storageOperationsTotal, { ...baseLabels, result: "ok" });
353
+ this.metrics.histogram(SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
354
+ } catch {
355
+ }
356
+ return out;
357
+ } catch (err) {
358
+ try {
359
+ this.metrics.counter(SEMCONV.storageOperationsTotal, { ...baseLabels, result: "error" });
360
+ this.metrics.histogram(SEMCONV.storageOperationDurationMs, Date.now() - started, baseLabels);
361
+ const errorClass = err?.name || err?.constructor?.name || "Error";
362
+ this.metrics.counter(SEMCONV.storageErrorsTotal, { ...baseLabels, errorClass });
363
+ } catch {
364
+ }
365
+ throw err;
366
+ }
286
367
  }
287
368
  // ---------------------------------------------------------------------------
288
369
  // Path helpers
@@ -303,61 +384,72 @@ var LocalStorageAdapter = class {
303
384
  // Basic file operations
304
385
  // ---------------------------------------------------------------------------
305
386
  async upload(key, data, _options) {
306
- const filePath = this.resolvePath(key);
307
- await fs.mkdir(dirname(filePath), { recursive: true });
308
- if (data instanceof Buffer) {
309
- await fs.writeFile(filePath, data);
310
- return;
311
- }
312
- const chunks = [];
313
- const reader = data.getReader();
314
- let done = false;
315
- while (!done) {
316
- const result = await reader.read();
317
- done = result.done;
318
- if (result.value) chunks.push(result.value);
319
- }
320
- await fs.writeFile(filePath, Buffer.concat(chunks));
387
+ return this.track("put", async () => {
388
+ const filePath = this.resolvePath(key);
389
+ await fs.mkdir(dirname(filePath), { recursive: true });
390
+ if (data instanceof Buffer) {
391
+ await fs.writeFile(filePath, data);
392
+ return;
393
+ }
394
+ const chunks = [];
395
+ const reader = data.getReader();
396
+ let done = false;
397
+ while (!done) {
398
+ const result = await reader.read();
399
+ done = result.done;
400
+ if (result.value) chunks.push(result.value);
401
+ }
402
+ await fs.writeFile(filePath, Buffer.concat(chunks));
403
+ });
321
404
  }
322
405
  async download(key) {
323
- return fs.readFile(this.resolvePath(key));
406
+ return this.track("get", async () => fs.readFile(this.resolvePath(key)));
324
407
  }
325
408
  async delete(key) {
326
- await fs.unlink(this.resolvePath(key)).catch((err) => {
327
- if (err && err.code === "ENOENT") return;
328
- throw err;
409
+ return this.track("delete", async () => {
410
+ await fs.unlink(this.resolvePath(key)).catch((err) => {
411
+ if (err && err.code === "ENOENT") return;
412
+ throw err;
413
+ });
329
414
  });
330
415
  }
331
416
  async exists(key) {
332
- try {
333
- await fs.access(this.resolvePath(key));
334
- return true;
335
- } catch {
336
- return false;
337
- }
417
+ return this.track("head", async () => {
418
+ try {
419
+ await fs.access(this.resolvePath(key));
420
+ return true;
421
+ } catch {
422
+ return false;
423
+ }
424
+ });
338
425
  }
339
426
  async getInfo(key) {
340
- const filePath = this.resolvePath(key);
341
- const stat = await fs.stat(filePath);
342
- return { key, size: stat.size, lastModified: stat.mtime };
427
+ return this.track("head", async () => {
428
+ const filePath = this.resolvePath(key);
429
+ const stat = await fs.stat(filePath);
430
+ return { key, size: stat.size, lastModified: stat.mtime };
431
+ });
343
432
  }
344
433
  async list(prefix) {
345
- const dirPath = this.resolvePath(prefix);
346
- try {
347
- const entries = await fs.readdir(dirPath);
348
- const results = [];
349
- for (const entry of entries) {
350
- if (entry.startsWith(".")) continue;
351
- const fullKey = prefix ? `${prefix}/${entry}` : entry;
352
- try {
353
- results.push(await this.getInfo(fullKey));
354
- } catch {
434
+ return this.track("list", async () => {
435
+ const dirPath = this.resolvePath(prefix);
436
+ try {
437
+ const entries = await fs.readdir(dirPath);
438
+ const results = [];
439
+ for (const entry of entries) {
440
+ if (entry.startsWith(".")) continue;
441
+ const fullKey = prefix ? `${prefix}/${entry}` : entry;
442
+ try {
443
+ const stat = await fs.stat(this.resolvePath(fullKey));
444
+ results.push({ key: fullKey, size: stat.size, lastModified: stat.mtime });
445
+ } catch {
446
+ }
355
447
  }
448
+ return results;
449
+ } catch {
450
+ return [];
356
451
  }
357
- return results;
358
- } catch {
359
- return [];
360
- }
452
+ });
361
453
  }
362
454
  // ---------------------------------------------------------------------------
363
455
  // Presigned URL helpers
@@ -1229,6 +1321,7 @@ var StorageServicePlugin = class {
1229
1321
  this.type = "standard";
1230
1322
  this.storage = null;
1231
1323
  this.store = null;
1324
+ this.metrics = new NoopMetricsRegistry3();
1232
1325
  this.options = { adapter: "local", ...options };
1233
1326
  }
1234
1327
  /** Build a concrete adapter from a values map (settings-derived). */
@@ -1246,7 +1339,8 @@ var StorageServicePlugin = class {
1246
1339
  endpoint: values.s3_endpoint || void 0,
1247
1340
  accessKeyId: values.s3_access_key_id || void 0,
1248
1341
  secretAccessKey: values.s3_secret_access_key || void 0,
1249
- forcePathStyle: !!values.s3_force_path_style
1342
+ forcePathStyle: !!values.s3_force_path_style,
1343
+ metrics: this.metrics
1250
1344
  };
1251
1345
  return new S3StorageAdapter(opts);
1252
1346
  }
@@ -1255,10 +1349,12 @@ var StorageServicePlugin = class {
1255
1349
  basePath: this.options.basePath ?? "/api/v1/storage",
1256
1350
  ...this.options.local ?? {},
1257
1351
  // settings value wins over any constructor-provided local.rootDir
1258
- rootDir
1352
+ rootDir,
1353
+ metrics: this.metrics
1259
1354
  });
1260
1355
  }
1261
1356
  async init(ctx) {
1357
+ this.metrics = resolveMetrics(ctx, this.options.metrics);
1262
1358
  const adapter = this.options.adapter;
1263
1359
  let initial;
1264
1360
  if (adapter === "s3") {
@@ -1267,11 +1363,11 @@ var StorageServicePlugin = class {
1267
1363
  if (!s3Opts) {
1268
1364
  throw new Error('StorageServicePlugin: s3 options are required when adapter is "s3"');
1269
1365
  }
1270
- initial = new S3Ctor(s3Opts);
1366
+ initial = new S3Ctor({ ...s3Opts, metrics: this.metrics });
1271
1367
  } else {
1272
1368
  const rootDir = this.options.local?.rootDir ?? "./storage";
1273
1369
  const basePath = this.options.basePath ?? "/api/v1/storage";
1274
- initial = new LocalStorageAdapter({ rootDir, basePath, ...this.options.local });
1370
+ initial = new LocalStorageAdapter({ rootDir, basePath, ...this.options.local, metrics: this.metrics });
1275
1371
  }
1276
1372
  this.storage = new SwappableStorageService(initial, (prev, next) => {
1277
1373
  const prevName = prev?.constructor?.name ?? "unknown";
@@ -1281,7 +1377,9 @@ var StorageServicePlugin = class {
1281
1377
  );
1282
1378
  });
1283
1379
  ctx.registerService("file-storage", this.storage);
1284
- ctx.logger.info(`StorageServicePlugin: registered ${adapter} storage adapter (swappable)`);
1380
+ ctx.logger.info(
1381
+ `StorageServicePlugin: registered ${adapter} storage adapter (swappable, metrics=${this.metrics.constructor?.name ?? "unknown"})`
1382
+ );
1285
1383
  try {
1286
1384
  ctx.getService("manifest").register({
1287
1385
  id: "com.objectstack.service.storage",
@@ -1393,6 +1491,15 @@ var StorageServicePlugin = class {
1393
1491
  });
1394
1492
  }
1395
1493
  };
1494
+ function resolveMetrics(ctx, override) {
1495
+ if (override) return override;
1496
+ try {
1497
+ const m = ctx.getService(OBSERVABILITY_METRICS_SERVICE);
1498
+ if (m) return m;
1499
+ } catch {
1500
+ }
1501
+ return new NoopMetricsRegistry3();
1502
+ }
1396
1503
 
1397
1504
  // src/index.ts
1398
1505
  init_s3_storage_adapter();