@tryghost/metrics 3.5.4 → 3.6.1

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/README.md CHANGED
@@ -65,6 +65,75 @@ An invalid configured rate throws when the instance is constructed. An invalid
65
65
  per-call rate is ignored in favour of the configured rate, so a bad call site
66
66
  cannot break the code path it is measuring.
67
67
 
68
+ ### Batching
69
+
70
+ Sampling reduces how many metrics are shipped; batching reduces how many
71
+ requests they take. With batching on, the Elasticsearch transport buffers
72
+ documents and ships them with the bulk API, so a hot code path costs one
73
+ request per batch instead of one per metric. It is off by default:
74
+
75
+ ```js
76
+ const metrics = require('@tryghost/metrics');
77
+
78
+ const batched = new metrics.GhostMetrics({
79
+ metrics: {
80
+ transports: ['elasticsearch'],
81
+ batch: true,
82
+ },
83
+ });
84
+ ```
85
+
86
+ `batch: true` uses the defaults; pass an object to override them:
87
+
88
+ | Option | Default | Meaning |
89
+ | --------------- | ----------- | --------------------------------------------------------------- |
90
+ | `enabled` | `true` | Set to `false` to turn batching off without dropping the config |
91
+ | `size` | `100` | Buffered documents that trigger a bulk request |
92
+ | `maxWaitMs` | `5000` | How long a document may sit in the buffer before it is shipped |
93
+ | `maxBufferSize` | `size * 10` | Hard cap on buffered documents; further metrics are dropped |
94
+
95
+ ```js
96
+ const batched = new metrics.GhostMetrics({
97
+ metrics: {
98
+ transports: ['elasticsearch'],
99
+ batch: {
100
+ size: 500,
101
+ maxWaitMs: 2000,
102
+ },
103
+ },
104
+ });
105
+ ```
106
+
107
+ Documents for different metric names can share a batch, because each bulk
108
+ operation carries its own index. Sampling still applies first, so only metrics
109
+ that survive their sample rate are buffered.
110
+
111
+ Batching changes two things worth knowing about:
112
+
113
+ - `metric()` resolves once the metric is **buffered**, not once it has been
114
+ shipped. It was already fire-and-forget for most callers, but a caller that
115
+ awaited delivery no longer gets it.
116
+ - Buffered metrics are lost if the process exits without draining them. Call
117
+ `flush()` from your shutdown handler; it is a no-op when batching is off, so
118
+ it is always safe to call.
119
+
120
+ ```js
121
+ process.on('SIGTERM', async () => {
122
+ await batched.flush();
123
+ process.exit(0);
124
+ });
125
+ ```
126
+
127
+ The buffer is bounded: once it holds `maxBufferSize` documents, further metrics
128
+ are dropped rather than growing the heap while Elasticsearch is slow or down.
129
+ Only one bulk request is ever in flight, and a failed batch is dropped without
130
+ rejecting, so a metric call can never break the code path it is measuring.
131
+
132
+ Invalid batch config throws when the instance is constructed.
133
+
134
+ The stdout transport is unaffected - it writes locally, so there is nothing to
135
+ batch.
136
+
68
137
  ### Types
69
138
 
70
139
  Types ship with the package. `GhostMetrics` is exported as both a value (the
@@ -77,6 +146,7 @@ import type {
77
146
  GhostMetricsOptions,
78
147
  MetricsOptions,
79
148
  MetricOptions,
149
+ BatchOptions,
80
150
  ElasticsearchOptions,
81
151
  MetricShipper,
82
152
  } from '@tryghost/metrics';
@@ -1,3 +1,5 @@
1
+ import MetricsBatch = require('./MetricsBatch');
2
+
1
3
  /**
2
4
  * Elasticsearch transport configuration.
3
5
  */
@@ -12,6 +14,29 @@ interface ElasticsearchOptions {
12
14
  proxy?: string;
13
15
  }
14
16
 
17
+ /**
18
+ * Batch shipping configuration for network transports.
19
+ */
20
+ interface BatchOptions {
21
+ /** Set to `false` to turn batching off while keeping the rest of the config. @default true */
22
+ enabled?: boolean;
23
+ /** Buffered documents that trigger a bulk request. @default 100 */
24
+ size?: number;
25
+ /** How long a document may sit in the buffer before it is shipped, in milliseconds. @default 5000 */
26
+ maxWaitMs?: number;
27
+ /** Hard cap on buffered documents; further metrics are dropped. @default `size * 10` */
28
+ maxBufferSize?: number;
29
+ }
30
+
31
+ /**
32
+ * Resolved batch configuration, with every value filled in.
33
+ */
34
+ interface ResolvedBatchOptions {
35
+ size: number;
36
+ maxWaitMs: number;
37
+ maxBufferSize: number;
38
+ }
39
+
15
40
  /**
16
41
  * Metrics-specific configuration, piggy-backed on the logging config.
17
42
  */
@@ -27,6 +52,12 @@ interface MetricsOptions {
27
52
  sampleRate?: number;
28
53
  /** Per-metric sample rate overrides, keyed by metric name. Same validation as `sampleRate`. */
29
54
  sampleRates?: Record<string, number>;
55
+ /**
56
+ * Ship metrics in batches rather than one request per metric. `true` uses the defaults,
57
+ * an object overrides them, absent or `false` keeps one request per metric. Throws at
58
+ * construction time for values that aren't positive whole numbers.
59
+ */
60
+ batch?: boolean | BatchOptions;
30
61
  }
31
62
 
32
63
  /**
@@ -71,7 +102,11 @@ declare class GhostMetrics {
71
102
  metadata: Record<string, unknown>;
72
103
  sampleRate: number;
73
104
  sampleRates: Record<string, number>;
105
+ /** Resolved batch config, or `null` when batching is off. */
106
+ batch: ResolvedBatchOptions | null;
74
107
  shippers: Record<string, MetricShipper>;
108
+ /** Buffers belonging to batching transports, drained by {@link GhostMetrics.flush}. */
109
+ batches: MetricsBatch[];
75
110
 
76
111
  constructor(options?: GhostMetricsOptions);
77
112
 
@@ -82,10 +117,16 @@ declare class GhostMetrics {
82
117
 
83
118
  /**
84
119
  * Setup ElasticSearch metric shipper. Metrics are shipped to a per-metric
85
- * index named `metrics-<name>`; the metric name should be sluggified.
120
+ * index named `metrics-<name>`; the metric name should be sluggified. With
121
+ * batching on, documents are buffered and shipped with the bulk API.
86
122
  */
87
123
  setupElasticsearchShipper(): void;
88
124
 
125
+ /**
126
+ * Ship anything buffered by batching transports. A no-op when batching is off.
127
+ */
128
+ flush(): Promise<void>;
129
+
89
130
  /**
90
131
  * Resolve the sample rate for a metric: per-call override, then per-metric config,
91
132
  * then the instance default.
@@ -94,6 +135,8 @@ declare class GhostMetrics {
94
135
 
95
136
  /**
96
137
  * Ship a metric through every configured transport, unless it is dropped by sampling.
138
+ * With batching on, the returned promise resolves once the metric is buffered rather
139
+ * than once it has been shipped.
97
140
  * @param name Metric name, should be slugified for back-end compatibility (e.g. `"memory-usage"`).
98
141
  * @param value Metric value; coerced to an object before being shipped.
99
142
  * @param options Per-call options, e.g. a `sampleRate` override.
@@ -106,6 +149,8 @@ declare namespace GhostMetrics {
106
149
  ElasticsearchOptions,
107
150
  MetricsOptions,
108
151
  MetricOptions,
152
+ BatchOptions,
153
+ ResolvedBatchOptions,
109
154
  GhostMetricsOptions,
110
155
  MetricShipper,
111
156
  };
@@ -1,4 +1,10 @@
1
1
  const jsonStringifySafe = require('json-stringify-safe');
2
+ const MetricsBatch = require('./MetricsBatch');
3
+
4
+ const DEFAULT_BATCH_SIZE = 100;
5
+ const DEFAULT_BATCH_MAX_WAIT_MS = 5000;
6
+ // Ten batches' worth of headroom before the buffer starts shedding load
7
+ const DEFAULT_BATCH_BUFFER_FACTOR = 10;
2
8
 
3
9
  /**
4
10
  * @description Check a value is usable as a sample rate: a number between 0 and 1 inclusive
@@ -27,6 +33,156 @@ function normalizeSampleRate(rate, key) {
27
33
  return rate;
28
34
  }
29
35
 
36
+ /**
37
+ * @description Validate a batching config value that must be a positive whole number
38
+ * @param {any} value Candidate value
39
+ * @param {number} fallback Value to use when unset
40
+ * @param {string} key Config key used in the error message
41
+ * @returns {number}
42
+ */
43
+ function normalizePositiveInteger(value, fallback, key) {
44
+ if (value === undefined || value === null) {
45
+ return fallback;
46
+ }
47
+
48
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
49
+ throw new Error(`${key} must be a positive whole number, got ${jsonStringifySafe(value)}`);
50
+ }
51
+
52
+ return value;
53
+ }
54
+
55
+ /**
56
+ * @description Validate the batching config, returning null when batching is off
57
+ * @param {any} batch Candidate batch config: absent/false for off, true or a property bag for on
58
+ * @returns {{size: number, maxWaitMs: number, maxBufferSize: number} | null}
59
+ */
60
+ function normalizeBatch(batch) {
61
+ if (batch === undefined || batch === null || batch === false) {
62
+ return null;
63
+ }
64
+
65
+ if (batch === true) {
66
+ batch = {};
67
+ }
68
+
69
+ if (typeof batch !== 'object' || Array.isArray(batch)) {
70
+ throw new Error(
71
+ `metrics.batch must be a boolean or an object, got ${jsonStringifySafe(batch)}`,
72
+ );
73
+ }
74
+
75
+ // `enabled` lets a deployment turn batching off without discarding the rest
76
+ // of the config, which matters when it is layered from config files
77
+ if ('enabled' in batch && typeof batch.enabled !== 'boolean') {
78
+ throw new Error(
79
+ `metrics.batch.enabled must be a boolean, got ${jsonStringifySafe(batch.enabled)}`,
80
+ );
81
+ }
82
+
83
+ if (batch.enabled === false) {
84
+ return null;
85
+ }
86
+
87
+ const size = normalizePositiveInteger(batch.size, DEFAULT_BATCH_SIZE, 'metrics.batch.size');
88
+ const maxWaitMs = normalizePositiveInteger(
89
+ batch.maxWaitMs,
90
+ DEFAULT_BATCH_MAX_WAIT_MS,
91
+ 'metrics.batch.maxWaitMs',
92
+ );
93
+ const maxBufferSize = normalizePositiveInteger(
94
+ batch.maxBufferSize,
95
+ size * DEFAULT_BATCH_BUFFER_FACTOR,
96
+ 'metrics.batch.maxBufferSize',
97
+ );
98
+
99
+ if (maxBufferSize < size) {
100
+ throw new Error(
101
+ `metrics.batch.maxBufferSize must be at least metrics.batch.size (${size}), got ${maxBufferSize}`,
102
+ );
103
+ }
104
+
105
+ return { size, maxWaitMs, maxBufferSize };
106
+ }
107
+
108
+ /**
109
+ * @description Clone structured metric data without allowing unusual values
110
+ * to make instrumentation throw.
111
+ * @param {any} value Value to clone
112
+ * @returns {any}
113
+ */
114
+ function cloneMetricValueFallback(value, seen = new WeakMap()) {
115
+ if (typeof value !== 'object' || value === null) {
116
+ return value;
117
+ }
118
+
119
+ if (seen.has(value)) {
120
+ return seen.get(value);
121
+ }
122
+
123
+ const prototype = Object.getPrototypeOf(value);
124
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
125
+ return value;
126
+ }
127
+
128
+ const clone = Array.isArray(value) ? [] : Object.create(prototype);
129
+ seen.set(value, clone);
130
+
131
+ for (const [key, nestedValue] of Object.entries(value)) {
132
+ clone[key] = cloneMetricValueFallback(nestedValue, seen);
133
+ }
134
+
135
+ return clone;
136
+ }
137
+
138
+ function cloneMetricValue(value) {
139
+ if (typeof value !== 'object' || value === null) {
140
+ return value;
141
+ }
142
+
143
+ try {
144
+ return structuredClone(value);
145
+ } catch {
146
+ // Keep instrumentation from throwing for unusual non-cloneable values.
147
+ return cloneMetricValueFallback(value);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * @description Build the document shipped for a metric value
153
+ * The value is copied rather than mutated, so a batched document can't be
154
+ * changed by the caller after it has been buffered.
155
+ * @param {any} value Value of the metric
156
+ * @param {object} metadata Metadata to ship alongside the value
157
+ * @param {number} sampleRate Rate the metric survived
158
+ * @returns {object}
159
+ */
160
+ function buildDocument(value, metadata, sampleRate) {
161
+ // Metrics are expected to be structured data. Clone them synchronously so
162
+ // callers can safely reuse or mutate nested values after `metric()` returns.
163
+ const clonedValue = cloneMetricValue(value);
164
+ const document =
165
+ typeof clonedValue === 'object' && clonedValue !== null && !Array.isArray(clonedValue)
166
+ ? clonedValue
167
+ : { value: clonedValue };
168
+
169
+ if (!('@timestamp' in document)) {
170
+ document['@timestamp'] = Date.now();
171
+ }
172
+
173
+ if (metadata) {
174
+ document.metadata = cloneMetricValue(metadata);
175
+ }
176
+
177
+ // Sampled documents carry their rate so consumers can scale counts back up.
178
+ // An absent sampleRate means the metric was not sampled (i.e. a rate of 1).
179
+ if (sampleRate < 1) {
180
+ document.sampleRate = sampleRate;
181
+ }
182
+
183
+ return document;
184
+ }
185
+
30
186
  /**
31
187
  * @description Metric shipper class built on the loggingrc config used in Ghost projects
32
188
  */
@@ -40,6 +196,7 @@ class GhostMetrics {
40
196
  * metrics.metadata: A property bag of metadata values to be shipped alongside the metric value
41
197
  * metrics.sampleRate: Default proportion of metrics to ship, between 0 and 1 (defaults to 1, ship everything)
42
198
  * metrics.sampleRates: Per-metric sample rate overrides, keyed by metric name
199
+ * metrics.batch: Batch shipping config for network transports; absent or false to ship one metric per request
43
200
  * elasticsearch: Elasticsearch transport configuration
44
201
  * @param {object} options Bag of options
45
202
  */
@@ -56,6 +213,7 @@ class GhostMetrics {
56
213
  this.transports = options.metrics.transports || [];
57
214
  this.metadata = options.metrics.metadata || {};
58
215
  this.sampleRate = normalizeSampleRate(options.metrics.sampleRate, 'metrics.sampleRate');
216
+ this.batch = normalizeBatch(options.metrics.batch);
59
217
 
60
218
  for (const [name, rate] of Object.entries(options.metrics.sampleRates || {})) {
61
219
  this.sampleRates[name] = normalizeSampleRate(rate, `metrics.sampleRates.${name}`);
@@ -64,6 +222,7 @@ class GhostMetrics {
64
222
  this.transports = [];
65
223
  this.metadata = {};
66
224
  this.sampleRate = 1;
225
+ this.batch = null;
67
226
  }
68
227
 
69
228
  // CASE: special env variable to enable long mode and level info
@@ -72,6 +231,8 @@ class GhostMetrics {
72
231
  }
73
232
 
74
233
  this.shippers = {};
234
+ // Populated by transports that buffer, so `flush()` can drain them
235
+ this.batches = [];
75
236
 
76
237
  this.transports.forEach((transport) => {
77
238
  let transportFn = `setup${transport[0].toUpperCase()}${transport.substr(1)}Shipper`;
@@ -126,29 +287,49 @@ class GhostMetrics {
126
287
  proxy: 'proxy' in this.elasticsearch ? this.elasticsearch.proxy : null,
127
288
  });
128
289
 
129
- this.shippers.elasticsearch = (name, value, sampleRate) => {
130
- if (typeof value !== 'object') {
131
- value = { value };
132
- }
290
+ if (!this.batch) {
291
+ this.shippers.elasticsearch = (name, value, sampleRate) => {
292
+ return elasticSearch.index(
293
+ buildDocument(value, this.metadata, sampleRate),
294
+ `metrics-${name}`,
295
+ );
296
+ };
133
297
 
134
- if (!('@timestamp' in value)) {
135
- value['@timestamp'] = Date.now();
136
- }
298
+ return;
299
+ }
137
300
 
138
- if (this.metadata) {
139
- value.metadata = this.metadata;
140
- }
301
+ // Batched metrics are shipped with the bulk API, so a hot code path
302
+ // costs one request per batch instead of one per metric. Documents for
303
+ // different metric names can share a batch because each operation
304
+ // carries its own index.
305
+ const batch = new MetricsBatch({
306
+ ...this.batch,
307
+ ship: (operations) => elasticSearch.bulk(operations),
308
+ });
141
309
 
142
- // Sampled documents carry their rate so consumers can scale counts back up.
143
- // An absent sampleRate means the metric was not sampled (i.e. a rate of 1).
144
- if (sampleRate < 1) {
145
- value.sampleRate = sampleRate;
146
- }
310
+ this.batches.push(batch);
147
311
 
148
- return elasticSearch.index(value, `metrics-${name}`);
312
+ this.shippers.elasticsearch = (name, value, sampleRate) => {
313
+ batch.add({
314
+ index: `metrics-${name}`,
315
+ document: buildDocument(value, this.metadata, sampleRate),
316
+ });
317
+
318
+ // Resolves once the metric is buffered, not once it is shipped
319
+ return Promise.resolve();
149
320
  };
150
321
  }
151
322
 
323
+ /**
324
+ * @description Ship anything buffered by batching transports
325
+ * A no-op when batching is off, so callers (e.g. a shutdown handler) can
326
+ * always call it without knowing how the instance is configured.
327
+ * @returns {Promise<void>}
328
+ */
329
+ async flush() {
330
+ await Promise.allSettled(this.batches.map((batch) => batch.flush()));
331
+ }
332
+
152
333
  /**
153
334
  * @description Resolve the sample rate for a metric, most specific config first
154
335
  * Invalid per-call overrides are ignored rather than thrown, so a bad call site cannot
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Buffers shipping operations and hands them off in batches.
3
+ *
4
+ * Batching trades delivery latency for a much smaller number of requests.
5
+ * Buffered operations are handed off when the buffer reaches `size`, when
6
+ * `maxWaitMs` has elapsed since the first buffered operation, or when
7
+ * {@link MetricsBatch.flush} is called.
8
+ */
9
+ declare class MetricsBatch<T = unknown> {
10
+ /** Number of buffered operations that triggers a hand-off. */
11
+ size: number;
12
+ /** How long an operation may sit in the buffer before it is handed off, in milliseconds. */
13
+ maxWaitMs: number;
14
+ /** Hard cap on buffered operations; further operations are dropped. */
15
+ maxBufferSize: number;
16
+ /** Hands a batch of operations to the transport. */
17
+ ship: (operations: T[]) => Promise<unknown>;
18
+ /** Operations waiting to be shipped. */
19
+ buffer: T[];
20
+ /** Count of operations dropped because the buffer was full. */
21
+ dropped: number;
22
+
23
+ constructor(options: {
24
+ size: number;
25
+ maxWaitMs: number;
26
+ maxBufferSize: number;
27
+ ship: (operations: T[]) => Promise<unknown>;
28
+ });
29
+
30
+ /**
31
+ * Buffer an operation, handing the buffer off if it is now full. Operations
32
+ * arriving once the buffer is at `maxBufferSize` are dropped.
33
+ */
34
+ add(operation: T): void;
35
+
36
+ /** Start the max-wait timer, unless one is already running. */
37
+ startTimer(): void;
38
+
39
+ /**
40
+ * Ship everything buffered right now, waiting behind any in-flight batch.
41
+ * Never rejects.
42
+ */
43
+ flush(): Promise<void>;
44
+
45
+ /** Hand the whole buffer to the transport. Never rejects. */
46
+ drain(): Promise<void>;
47
+ }
48
+
49
+ export = MetricsBatch;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @description Buffers shipping operations and hands them off in batches
3
+ *
4
+ * Batching trades delivery latency for a much smaller number of requests: the
5
+ * caller gets one round-trip per batch instead of one per metric. Buffered
6
+ * operations are handed off when the buffer reaches `size`, when `maxWaitMs`
7
+ * has elapsed since the first buffered operation, or when `flush()` is called.
8
+ */
9
+ class MetricsBatch {
10
+ /**
11
+ * @param {object} options Bag of options
12
+ * @param {number} options.size Number of buffered operations that triggers a hand-off
13
+ * @param {number} options.maxWaitMs How long an operation may sit in the buffer before it is handed off
14
+ * @param {number} options.maxBufferSize Hard cap on buffered operations; further operations are dropped
15
+ * @param {(operations: any[]) => Promise<any>} options.ship Hands a batch of operations to the transport
16
+ */
17
+ constructor({ size, maxWaitMs, maxBufferSize, ship }) {
18
+ this.size = size;
19
+ this.maxWaitMs = maxWaitMs;
20
+ this.maxBufferSize = maxBufferSize;
21
+ this.ship = ship;
22
+
23
+ this.buffer = [];
24
+ this.timer = null;
25
+ // Serialises hand-offs, so only one batch is ever in flight
26
+ this.pending = Promise.resolve();
27
+ // A non-zero count means the transport couldn't keep up and metrics
28
+ // were lost; read it off the instance when metrics go missing
29
+ this.dropped = 0;
30
+ }
31
+
32
+ /**
33
+ * @description Buffer an operation, handing the buffer off if it is now full
34
+ * @param {any} operation Operation to ship
35
+ */
36
+ add(operation) {
37
+ if (this.buffer.length >= this.maxBufferSize) {
38
+ // Shed load rather than growing without bound when the transport is
39
+ // slow or down: losing metrics beats exhausting the heap
40
+ this.dropped += 1;
41
+ return;
42
+ }
43
+
44
+ this.buffer.push(operation);
45
+
46
+ // Queue exactly once when the buffer crosses the threshold. `drain()`
47
+ // resets the buffer before awaiting the transport, so the next buffer
48
+ // can independently cross the threshold and line up behind it.
49
+ if (this.buffer.length === this.size) {
50
+ this.flush();
51
+ return;
52
+ }
53
+
54
+ this.startTimer();
55
+ }
56
+
57
+ /**
58
+ * @description Start the max-wait timer, unless one is already running
59
+ * The timer is unreferenced so a partially full buffer can never hold the
60
+ * event loop - and therefore the process - open.
61
+ */
62
+ startTimer() {
63
+ if (this.timer) {
64
+ return;
65
+ }
66
+
67
+ this.timer = setTimeout(() => {
68
+ this.timer = null;
69
+ this.flush();
70
+ }, this.maxWaitMs);
71
+
72
+ this.timer.unref?.();
73
+ }
74
+
75
+ /**
76
+ * @description Ship everything buffered right now
77
+ * Waits behind any in-flight batch, so batches reach the transport in
78
+ * order and only one request is open at a time.
79
+ * @returns {Promise<void>}
80
+ */
81
+ flush() {
82
+ if (this.timer) {
83
+ clearTimeout(this.timer);
84
+ this.timer = null;
85
+ }
86
+
87
+ const previous = this.pending;
88
+
89
+ this.pending = previous.then(() => this.drain());
90
+
91
+ return this.pending;
92
+ }
93
+
94
+ /**
95
+ * @description Hand the whole buffer to the transport
96
+ * Never rejects, so a failed batch can't poison the hand-off chain or the
97
+ * code path being measured. Transports report their own failures - the
98
+ * Elasticsearch one logs them under the `logging:elasticsearch` debug namespace.
99
+ * @returns {Promise<void>}
100
+ */
101
+ async drain() {
102
+ if (this.buffer.length === 0) {
103
+ return;
104
+ }
105
+
106
+ const operations = this.buffer;
107
+ this.buffer = [];
108
+
109
+ try {
110
+ await this.ship(operations);
111
+ } catch {
112
+ // Swallowed: a metric call can never break the path it measures
113
+ }
114
+ }
115
+ }
116
+
117
+ module.exports = MetricsBatch;
package/lib/metrics.d.ts CHANGED
@@ -17,6 +17,8 @@ declare namespace metrics {
17
17
  export type ElasticsearchOptions = GhostMetricsClass.ElasticsearchOptions;
18
18
  export type MetricsOptions = GhostMetricsClass.MetricsOptions;
19
19
  export type MetricOptions = GhostMetricsClass.MetricOptions;
20
+ export type BatchOptions = GhostMetricsClass.BatchOptions;
21
+ export type ResolvedBatchOptions = GhostMetricsClass.ResolvedBatchOptions;
20
22
  export type GhostMetricsOptions = GhostMetricsClass.GhostMetricsOptions;
21
23
  export type MetricShipper = GhostMetricsClass.MetricShipper;
22
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryghost/metrics",
3
- "version": "3.5.4",
3
+ "version": "3.6.1",
4
4
  "license": "MIT",
5
5
  "author": "Ghost Foundation",
6
6
  "repository": {
@@ -19,9 +19,9 @@
19
19
  "access": "public"
20
20
  },
21
21
  "dependencies": {
22
- "@tryghost/elasticsearch": "^5.4.10",
23
- "@tryghost/pretty-stream": "^2.3.13",
24
- "@tryghost/root-utils": "^2.3.13",
22
+ "@tryghost/elasticsearch": "^5.4.12",
23
+ "@tryghost/pretty-stream": "^2.3.15",
24
+ "@tryghost/root-utils": "^2.3.15",
25
25
  "json-stringify-safe": "^5.0.1"
26
26
  },
27
27
  "devDependencies": {