encore.dev 1.52.5 → 1.53.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/metrics/mod.ts ADDED
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Custom metrics for Encore applications.
3
+ *
4
+ * This module provides counters and gauges that can be statically analyzed
5
+ * by the Encore compiler and automatically exported to observability backends.
6
+ *
7
+ * @example Simple counter
8
+ * ```typescript
9
+ * import { Counter } from 'encore.dev/metrics';
10
+ *
11
+ * export const ordersProcessed = new Counter("orders_processed");
12
+ *
13
+ * ordersProcessed.increment();
14
+ * ```
15
+ *
16
+ * @example Counter with labels
17
+ * ```typescript
18
+ * import { CounterGroup } from 'encore.dev/metrics';
19
+ *
20
+ * interface Labels {
21
+ * success: boolean;
22
+ * }
23
+ *
24
+ * export const ordersProcessed = new CounterGroup<Labels>("orders_processed");
25
+ *
26
+ * ordersProcessed.with({ success: true }).increment();
27
+ * ```
28
+ */
29
+
30
+ import * as runtime from "../internal/runtime/mod";
31
+ import { MetricType } from "../internal/runtime/mod";
32
+ import { getRegistry, getBuffer } from "../internal/metrics/registry";
33
+ import {
34
+ AtomicCounter,
35
+ AtomicGauge,
36
+ processLabelsToPairs,
37
+ serializeLabels
38
+ } from "../internal/metrics/mod";
39
+ import { currentRequest } from "../req_meta";
40
+
41
+ export interface MetricConfig {}
42
+
43
+ /**
44
+ * Resolves the service name for a metric by checking:
45
+ * 1. If there's only one service using this metric in the runtime config
46
+ * 2. Otherwise, looks at the current request context
47
+ */
48
+ function resolveServiceName(metricName: string): string | undefined {
49
+ const rtConfig = runtime.runtimeConfig();
50
+ const rtSvcs = rtConfig.metrics[metricName]?.services ?? [];
51
+ if (rtSvcs.length === 1) {
52
+ return rtSvcs[0];
53
+ }
54
+
55
+ const currReq = currentRequest();
56
+ if (currReq) {
57
+ if (currReq.type === "api-call") {
58
+ return currReq.api.service;
59
+ } else {
60
+ return currReq.service;
61
+ }
62
+ }
63
+
64
+ return undefined;
65
+ }
66
+
67
+ /**
68
+ * A Counter tracks cumulative values that only increase.
69
+ * Use counters for metrics like request counts, errors, etc.
70
+ */
71
+ export class Counter {
72
+ private name: string;
73
+ private cache: Map<string, AtomicCounter>;
74
+ private labelPairs: [string, string][];
75
+ private cfg: MetricConfig;
76
+
77
+ constructor(name: string, cfg?: MetricConfig) {
78
+ this.name = name;
79
+ this.cfg = cfg ?? {};
80
+ this.labelPairs = [];
81
+ this.cache = new Map();
82
+ }
83
+
84
+ /**
85
+ * Increment the counter by the given value (default 1).
86
+ */
87
+ increment(value: number = 1): void {
88
+ const serviceName = resolveServiceName(this.name);
89
+ if (!serviceName) {
90
+ return;
91
+ }
92
+
93
+ let metric = this.cache.get(serviceName);
94
+ if (!metric) {
95
+ const registry = getRegistry();
96
+ const buffer = getBuffer();
97
+
98
+ // If registry or buffer are not initialized, silently skip
99
+ if (!registry || !buffer) {
100
+ return;
101
+ }
102
+
103
+ const slot = registry.allocateSlot(
104
+ this.name,
105
+ this.labelPairs,
106
+ serviceName,
107
+ MetricType.Counter
108
+ );
109
+ metric = new AtomicCounter(buffer, slot);
110
+ this.cache.set(serviceName, metric);
111
+ }
112
+
113
+ metric.increment(value);
114
+ }
115
+
116
+ ref(): Counter {
117
+ return this;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * A CounterGroup tracks counters with labels.
123
+ * Each unique combination of label values creates a separate counter time series.
124
+ *
125
+ * @typeParam L - The label interface (must have string/number/boolean fields)
126
+ * Note: Number values in labels are converted to integers using Math.floor().
127
+ */
128
+ export class CounterGroup<
129
+ L extends Record<keyof L, string | number | boolean>
130
+ > {
131
+ private name: string;
132
+ private labelCache: Map<string, Counter>;
133
+ private cfg: MetricConfig;
134
+
135
+ constructor(name: string, cfg?: MetricConfig) {
136
+ this.name = name;
137
+ this.labelCache = new Map();
138
+ this.cfg = cfg ?? {};
139
+ }
140
+
141
+ /**
142
+ * Get a counter for the given label values.
143
+ *
144
+ * Note: Number values in labels are converted to integers using Math.floor().
145
+ */
146
+ with(labels: L): Counter {
147
+ const labelKey = serializeLabels(labels);
148
+
149
+ let cached = this.labelCache.get(labelKey);
150
+ if (!cached) {
151
+ // Create counter instance
152
+ cached = new Counter(this.name, this.cfg);
153
+
154
+ const labelPairs = processLabelsToPairs(labels);
155
+ (cached as any).labelPairs = labelPairs;
156
+
157
+ this.labelCache.set(labelKey, cached);
158
+ }
159
+
160
+ return cached;
161
+ }
162
+
163
+ ref(): CounterGroup<L> {
164
+ return this;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * A Gauge tracks values that can go up or down.
170
+ * Use gauges for metrics like memory usage, active connections, temperature, etc.
171
+ */
172
+ export class Gauge {
173
+ private name: string;
174
+ private cache: Map<string, AtomicGauge>;
175
+ private labelPairs: [string, string][];
176
+ private cfg: MetricConfig;
177
+
178
+ constructor(name: string, cfg?: MetricConfig) {
179
+ this.name = name;
180
+ this.cfg = cfg ?? {};
181
+ this.labelPairs = [];
182
+ this.cache = new Map();
183
+ }
184
+
185
+ /**
186
+ * Set the gauge to the given value.
187
+ */
188
+ set(value: number): void {
189
+ const serviceName = resolveServiceName(this.name);
190
+ if (!serviceName) {
191
+ return;
192
+ }
193
+
194
+ let metric = this.cache.get(serviceName);
195
+ if (!metric) {
196
+ const registry = getRegistry();
197
+ const buffer = getBuffer();
198
+
199
+ // If registry or buffer are not initialized, silently skip
200
+ if (!registry || !buffer) {
201
+ return;
202
+ }
203
+
204
+ const slot = registry.allocateSlot(
205
+ this.name,
206
+ this.labelPairs,
207
+ serviceName,
208
+ MetricType.Gauge
209
+ );
210
+ metric = new AtomicGauge(buffer, slot);
211
+ this.cache.set(serviceName, metric);
212
+ }
213
+
214
+ metric.set(value);
215
+ }
216
+
217
+ ref(): Gauge {
218
+ return this;
219
+ }
220
+ }
221
+
222
+ export class GaugeGroup<L extends Record<keyof L, string | number | boolean>> {
223
+ private name: string;
224
+ private labelCache: Map<string, Gauge>;
225
+ private cfg: MetricConfig;
226
+
227
+ constructor(name: string, cfg?: MetricConfig) {
228
+ this.name = name;
229
+ this.labelCache = new Map();
230
+ this.cfg = cfg ?? {};
231
+ }
232
+
233
+ /**
234
+ * Get a gauge for the given label values.
235
+ *
236
+ * Note: Number values in labels are converted to integers using Math.floor().
237
+ */
238
+ with(labels: L): Gauge {
239
+ const labelKey = serializeLabels(labels);
240
+
241
+ let cached = this.labelCache.get(labelKey);
242
+ if (!cached) {
243
+ // Create gauge instance
244
+ cached = new Gauge(this.name, this.cfg);
245
+
246
+ const labelPairs = processLabelsToPairs(labels);
247
+ (cached as any).labelPairs = labelPairs;
248
+
249
+ this.labelCache.set(labelKey, cached);
250
+ }
251
+
252
+ return cached;
253
+ }
254
+
255
+ ref(): GaugeGroup<L> {
256
+ return this;
257
+ }
258
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "encore.dev",
3
3
  "description": "Encore's JavaScript/TypeScript SDK",
4
- "version": "1.52.5",
4
+ "version": "1.53.1",
5
5
  "license": "MPL-2.0",
6
6
  "bugs": {
7
7
  "url": "https://github.com/encoredev/encore/issues"
@@ -79,6 +79,11 @@
79
79
  "bun": "./types/mod.ts",
80
80
  "default": "./dist/types/mod.js"
81
81
  },
82
+ "./metrics": {
83
+ "types": "./metrics/mod.ts",
84
+ "bun": "./metrics/mod.ts",
85
+ "default": "./dist/metrics/mod.js"
86
+ },
82
87
  "./internal/codegen/*": {
83
88
  "types": "./internal/codegen/*.ts",
84
89
  "bun": "./internal/codegen/*.ts",