@oxy.so/telemetry 2.1.0 → 2.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/README.md +57 -23
- package/dist/cjs/browser.js +11 -0
- package/dist/cjs/collector.js +41 -15
- package/dist/cjs/index.js +22 -1
- package/dist/cjs/metrics.js +10 -0
- package/dist/cjs/server.js +25 -2
- package/dist/cjs/socket.js +2 -6
- package/dist/esm/browser.js +11 -0
- package/dist/esm/collector.js +40 -16
- package/dist/esm/index.js +21 -0
- package/dist/esm/metrics.js +9 -0
- package/dist/esm/server.js +24 -2
- package/dist/esm/socket.js +3 -7
- package/dist/types/browser.d.ts +11 -0
- package/dist/types/collector.d.ts +21 -20
- package/dist/types/index.d.ts +57 -4
- package/dist/types/metrics.d.ts +5 -0
- package/dist/types/server.d.ts +26 -1
- package/package.json +12 -11
- package/src/__tests__/collector.test.ts +17 -0
- package/src/__tests__/emfGolden.test.ts +53 -0
- package/src/__tests__/singleSources.test.ts +184 -0
- package/src/browser.ts +11 -0
- package/src/collector.ts +67 -42
- package/src/index.ts +65 -4
- package/src/metrics.ts +16 -0
- package/src/server.ts +36 -3
- package/src/socket.ts +3 -7
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ Redis, React, React Native or Expo.
|
|
|
6
6
|
|
|
7
7
|
## Entry points
|
|
8
8
|
|
|
9
|
-
- `@oxy.so/telemetry` — shared header names and
|
|
9
|
+
- `@oxy.so/telemetry` — shared header names, the CORS header lists and the wire
|
|
10
|
+
types (`TrafficAggregate`, `ActivityAggregate`, `InfrastructureSnapshot`).
|
|
10
11
|
- `@oxy.so/telemetry/browser` — ephemeral browser metadata.
|
|
11
12
|
- `@oxy.so/telemetry/collector` — bounded HTTP/custom-transport flow aggregation.
|
|
12
13
|
- `@oxy.so/telemetry/server` — pure validation, route bucketing and cardinality
|
|
@@ -20,12 +21,19 @@ side effects.
|
|
|
20
21
|
## Browser metadata
|
|
21
22
|
|
|
22
23
|
```ts
|
|
23
|
-
import {
|
|
24
|
+
import { getBrowserActivityIdHeader, peekBrowserEdgeRegionHeader } from '@oxy.so/telemetry/browser';
|
|
24
25
|
|
|
25
|
-
const headers =
|
|
26
|
+
const headers = { ...peekBrowserEdgeRegionHeader(), ...getBrowserActivityIdHeader() };
|
|
26
27
|
await fetch('https://api.oxy.so/session/status', { headers });
|
|
27
28
|
```
|
|
28
29
|
|
|
30
|
+
This is what `@oxy.so/core` sends on every request: the peek never waits on the
|
|
31
|
+
trace. `getBrowserEdgeRegionHeader()` is the awaiting variant.
|
|
32
|
+
|
|
33
|
+
A CORS allow-list for these headers is `OXY_TELEMETRY_REQUEST_HEADERS` (from the
|
|
34
|
+
root entry), and `OXY_TELEMETRY_EXPOSED_HEADERS` for `Access-Control-Expose-Headers`;
|
|
35
|
+
import them instead of repeating the header names.
|
|
36
|
+
|
|
29
37
|
`X-Oxy-Activity-Id` is generated in memory for the current page runtime and
|
|
30
38
|
rotates every five minutes. It is not placed in a cookie, local storage,
|
|
31
39
|
session storage, IndexedDB or a URL. Closing or reloading the page loses it.
|
|
@@ -35,34 +43,32 @@ from the same-origin `/cdn-cgi/trace` endpoint. The helper reads no IP field and
|
|
|
35
43
|
returns, logs and persists none of the rest of the trace response. Discovery is
|
|
36
44
|
cached for 15 seconds per runtime (including failed discovery), has a one-second timeout and is disabled on loopback.
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
crypto, location and trace-fetch dependencies
|
|
46
|
+
`createBrowserTelemetry()` builds an isolated runtime with injected clock,
|
|
47
|
+
crypto, location and trace-fetch dependencies; it is the test seam.
|
|
40
48
|
|
|
41
49
|
## Server primitives
|
|
42
50
|
|
|
43
51
|
```ts
|
|
44
52
|
import {
|
|
45
|
-
activityFlow,
|
|
46
53
|
metadataFromHeaders,
|
|
47
54
|
presenceKeys,
|
|
48
|
-
|
|
49
|
-
type ActivityCardinalityAdapter,
|
|
55
|
+
sourceRegionFromEdgePop,
|
|
50
56
|
} from '@oxy.so/telemetry/server';
|
|
51
57
|
|
|
52
58
|
const metadata = metadataFromHeaders(request.headers);
|
|
53
|
-
const sourceRegion = metadata.edgePop
|
|
54
|
-
const flow = activityFlow(sourceRegion, serviceFromPath(request.path));
|
|
59
|
+
const sourceRegion = sourceRegionFromEdgePop(metadata.edgePop); // 'edge-mad' | undefined
|
|
55
60
|
const keys = sourceRegion ? presenceKeys(sourceRegion, Date.now()) : [];
|
|
56
61
|
```
|
|
57
62
|
|
|
58
63
|
Validation accepts a three-letter PoP and a 16–64 character opaque activity ID.
|
|
59
|
-
It ignores IP and forwarding headers.
|
|
60
|
-
static first route segment, so IDs and full paths cannot enter aggregates.
|
|
61
|
-
|
|
62
|
-
`ActivityCardinalityAdapter` describes the minimal storage boundary for an
|
|
63
|
-
implementation such as Redis HyperLogLog. This package does not instantiate a
|
|
64
|
+
It ignores IP and forwarding headers. This package does not instantiate a
|
|
64
65
|
store, keep server-global maps, start timers or send realtime events.
|
|
65
66
|
|
|
67
|
+
`serviceFromPath`, `activityFlow`, `presenceBucket`, `ActivityCardinalityAdapter`,
|
|
68
|
+
`ActivityBucket`, `DEFAULT_ACTIVE_CLIENT_WINDOW_MS`, `getBrowserTelemetryHeaders`
|
|
69
|
+
and `createBrowserTelemetry` are deprecated: nothing in Oxy uses them and they go in
|
|
70
|
+
the next major.
|
|
71
|
+
|
|
66
72
|
## Active-client definition
|
|
67
73
|
|
|
68
74
|
An active client is one distinct ephemeral activity ID observed for an edge PoP
|
|
@@ -106,8 +112,10 @@ follow the actual movement, even when two services share one region.
|
|
|
106
112
|
identity after route authentication. `instrumentTrafficFetch` observes actual
|
|
107
113
|
outbound calls; internal destination regions come from the peer's `X-Oxy-Region`
|
|
108
114
|
response, not a guessed IP location. Unlocated peers remain unlocated. Health,
|
|
109
|
-
telemetry collection, infrastructure snapshots
|
|
110
|
-
excluded from observation
|
|
115
|
+
telemetry collection, infrastructure snapshots, service-token minting and
|
|
116
|
+
`/cdn-cgi/` are excluded from observation to prevent control traffic feedback;
|
|
117
|
+
`isObservedPath(path, direction)` (reading the `UNOBSERVED_PATHS` table) is the one predicate, so an adapter that does its own
|
|
118
|
+
interception imports it instead of copying a pattern.
|
|
111
119
|
Custom transports can call the collector's `record` method explicitly.
|
|
112
120
|
|
|
113
121
|
Each collector retains at most 512 distinct pending flows and publishes chunks
|
|
@@ -116,7 +124,8 @@ durable retry queue. These are live operational observations, not billing or
|
|
|
116
124
|
lossless historical request totals. No dashboard viewer is needed to collect.
|
|
117
125
|
|
|
118
126
|
Realtime transport publishes aggregate buckets, not individual metadata. A
|
|
119
|
-
socket event
|
|
127
|
+
socket event carries `ActivityAggregate` (a `TrafficAggregate` plus
|
|
128
|
+
`activeClients`; the infrastructure snapshot is `InfrastructureSnapshot`): request count, approximate active
|
|
120
129
|
clients, source/target regions, service and window timestamps. Transport and
|
|
121
130
|
fan-out are application concerns and intentionally absent from this package.
|
|
122
131
|
|
|
@@ -131,11 +140,15 @@ installed in each service before this can represent the whole ecosystem.
|
|
|
131
140
|
The authenticated publisher is `createEcosystemTraffic` from `@oxy.so/core/server`.
|
|
132
141
|
It exposes HTTP middleware, `wrapFetch`, `record` for explicit transport events,
|
|
133
142
|
and `stop` for graceful removal. It requires a known infrastructure region (or
|
|
134
|
-
explicit infrastructure coordinates) and
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
explicit infrastructure coordinates) and registered temporary workload identity
|
|
144
|
+
for own service publication ([ADR 0049](../../docs/adr/0049-first-party-platform-authority.md)).
|
|
145
|
+
The shared OxyServer performs attestation and live binding checks. A process can
|
|
146
|
+
supply `workloadCredentials` from `createAwsWorkloadCredentialProvider()`
|
|
147
|
+
(`@oxy.so/core/server/aws`) for the supported temporary AWS provider chain.
|
|
148
|
+
There is no activity-key or service-key fallback for own publication. External
|
|
149
|
+
customer API credentials remain separate and do not become first-party authority.
|
|
150
|
+
Credentials, user metadata and paths never enter public snapshots. Activity's
|
|
151
|
+
internal/external `scope` field classifies traffic; it is not an OAuth permission.
|
|
139
152
|
|
|
140
153
|
## Operational metrics (CloudWatch EMF)
|
|
141
154
|
|
|
@@ -166,6 +179,27 @@ Route, method, table and every other label stay plain properties on the line:
|
|
|
166
179
|
`maxSeries` (default 2000) series are held per interval; past that, new series
|
|
167
180
|
fold into one whose labels read `other`.
|
|
168
181
|
|
|
182
|
+
On ECS only, `createEmfPublisherIfDeployed(options)` returns the same publisher
|
|
183
|
+
and `null` elsewhere (it checks `ECS_CONTAINER_METADATA_URI_V4`), so a service
|
|
184
|
+
does not repeat that gate.
|
|
185
|
+
|
|
186
|
+
### Conformance fixture
|
|
187
|
+
|
|
188
|
+
`fixtures/emf-golden.json` is the cross-language contract for the EMF line format.
|
|
189
|
+
Each case holds a metric `definition`, `service`, `namespace`, a fixed `now`
|
|
190
|
+
(the line's `Timestamp`), optional `maxSeries`, the `observations`
|
|
191
|
+
(`{labels, value}`; the strings `"NaN"`, `"Infinity"` and `"-Infinity"` stand for
|
|
192
|
+
the non-finite numbers JSON cannot carry) and the `expectedLines` the publisher
|
|
193
|
+
must write on flush, in series first-seen order. Lines are compared as parsed
|
|
194
|
+
JSON, not as bytes. `src/__tests__/emfGolden.test.ts` runs every case through the
|
|
195
|
+
real publisher.
|
|
196
|
+
|
|
197
|
+
Kaana's Go `internal/emf` must consume the same file (a vendored copy that a
|
|
198
|
+
Kaana test keeps byte-identical to this one) and pass every case; change the
|
|
199
|
+
fixture here first. Label values are truncated by UTF-16 code units, so a Go
|
|
200
|
+
implementation counts UTF-16 units, not bytes or runes. The fixture avoids a cut
|
|
201
|
+
inside a surrogate pair, whose result is a lone surrogate that UTF-8 cannot carry.
|
|
202
|
+
|
|
169
203
|
## Development
|
|
170
204
|
|
|
171
205
|
```bash
|
package/dist/cjs/browser.js
CHANGED
|
@@ -30,6 +30,12 @@ function eligibleLocation(location) {
|
|
|
30
30
|
location.hostname !== '127.0.0.1' &&
|
|
31
31
|
location.hostname !== '[::1]');
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* @deprecated No Oxy repo constructs its own runtime: use the module-level
|
|
35
|
+
* `getBrowserActivityIdHeader`, `getBrowserEdgeRegionHeader` and
|
|
36
|
+
* `peekBrowserEdgeRegionHeader`. The factory remains the test seam. Removal from
|
|
37
|
+
* the public surface is planned for the next major.
|
|
38
|
+
*/
|
|
33
39
|
function createBrowserTelemetry(options = {}) {
|
|
34
40
|
let activityId = null;
|
|
35
41
|
let activityIdCreatedAt = 0;
|
|
@@ -125,5 +131,10 @@ const getBrowserEdgeRegionHeader = () => defaultBrowserTelemetry.getEdgeRegionHe
|
|
|
125
131
|
exports.getBrowserEdgeRegionHeader = getBrowserEdgeRegionHeader;
|
|
126
132
|
const peekBrowserEdgeRegionHeader = () => defaultBrowserTelemetry.peekEdgeRegionHeader();
|
|
127
133
|
exports.peekBrowserEdgeRegionHeader = peekBrowserEdgeRegionHeader;
|
|
134
|
+
/**
|
|
135
|
+
* @deprecated Unused by any Oxy repo. `@oxy.so/core` sends
|
|
136
|
+
* `peekBrowserEdgeRegionHeader()` and `getBrowserActivityIdHeader()` per request.
|
|
137
|
+
* Removal is planned for the next major.
|
|
138
|
+
*/
|
|
128
139
|
const getBrowserTelemetryHeaders = () => defaultBrowserTelemetry.getHeaders();
|
|
129
140
|
exports.getBrowserTelemetryHeaders = getBrowserTelemetryHeaders;
|
package/dist/cjs/collector.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UNOBSERVED_PATHS = void 0;
|
|
3
4
|
exports.trafficType = trafficType;
|
|
5
|
+
exports.isObservedPath = isObservedPath;
|
|
4
6
|
exports.createTrafficCollector = createTrafficCollector;
|
|
5
7
|
exports.trafficMiddleware = trafficMiddleware;
|
|
6
8
|
exports.instrumentTrafficFetch = instrumentTrafficFetch;
|
|
7
9
|
exports.resolveOxyServiceEndpoint = resolveOxyServiceEndpoint;
|
|
8
10
|
exports.infrastructureLocation = infrastructureLocation;
|
|
9
11
|
exports.normalizeInfrastructureRegion = normalizeInfrastructureRegion;
|
|
12
|
+
const index_js_1 = require("./index.js");
|
|
10
13
|
const server_js_1 = require("./server.js");
|
|
11
14
|
const TYPES = {
|
|
12
15
|
auth: 'identity',
|
|
@@ -48,6 +51,35 @@ function trafficType(path) {
|
|
|
48
51
|
parts.shift();
|
|
49
52
|
return TYPES[parts[0] ?? ''] ?? 'platform';
|
|
50
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Every path a reporter does not observe, in ONE table. Each entry says which
|
|
56
|
+
* directions it applies to: the inbound and outbound sets differ today (an
|
|
57
|
+
* outbound call to a probe or to `/platform-stats` IS recorded), and that is
|
|
58
|
+
* preserved exactly. Unifying them is a separate, deliberate decision.
|
|
59
|
+
*/
|
|
60
|
+
exports.UNOBSERVED_PATHS = [
|
|
61
|
+
{
|
|
62
|
+
directions: 'inbound',
|
|
63
|
+
matches: (path) => /^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|infra-status)(?:\/|$)/.test(path),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
directions: 'inbound',
|
|
67
|
+
matches: (path) => /^\/(?:api\/)?platform-infrastructure(?:\/|$)/.test(path),
|
|
68
|
+
},
|
|
69
|
+
{ directions: 'outbound', matches: (path) => path === '/platform-infrastructure' },
|
|
70
|
+
{ directions: 'both', matches: (path) => path.startsWith('/internal/activity') },
|
|
71
|
+
{ directions: 'both', matches: (path) => path.startsWith('/auth/service-token') },
|
|
72
|
+
{ directions: 'inbound', matches: (path) => path.startsWith('/cdn-cgi/') },
|
|
73
|
+
];
|
|
74
|
+
/**
|
|
75
|
+
* Is this path part of what a reporter observes in this direction? Collection
|
|
76
|
+
* itself, service-token minting and (inbound) probes and the dashboard
|
|
77
|
+
* transport are control traffic: observing them would feed back into the very
|
|
78
|
+
* activity they carry.
|
|
79
|
+
*/
|
|
80
|
+
function isObservedPath(path, direction) {
|
|
81
|
+
return !exports.UNOBSERVED_PATHS.some((entry) => (entry.directions === 'both' || entry.directions === direction) && entry.matches(path));
|
|
82
|
+
}
|
|
51
83
|
function createTrafficCollector(publish, now = Date.now) {
|
|
52
84
|
const pending = new Map();
|
|
53
85
|
let flushing = null;
|
|
@@ -88,13 +120,10 @@ function createTrafficCollector(publish, now = Date.now) {
|
|
|
88
120
|
}
|
|
89
121
|
function trafficMiddleware(collector, service, region) {
|
|
90
122
|
return (request, response, next) => {
|
|
91
|
-
response.setHeader?.(
|
|
123
|
+
response.setHeader?.(index_js_1.OXY_REGION_HEADER, region);
|
|
92
124
|
const path = request.path ?? request.url ?? '/';
|
|
93
125
|
// Never report collection itself, probes or the dashboard transport.
|
|
94
|
-
if (
|
|
95
|
-
path.startsWith('/internal/activity') ||
|
|
96
|
-
path.startsWith('/auth/service-token') ||
|
|
97
|
-
path.startsWith('/cdn-cgi/')) {
|
|
126
|
+
if (!isObservedPath(path, 'inbound')) {
|
|
98
127
|
next();
|
|
99
128
|
return;
|
|
100
129
|
}
|
|
@@ -108,12 +137,10 @@ function trafficMiddleware(collector, service, region) {
|
|
|
108
137
|
.replace(/[^a-z0-9-]/g, '-')
|
|
109
138
|
.slice(0, 40);
|
|
110
139
|
const internal = Boolean(name);
|
|
111
|
-
const forwardedRegion = request.headers[
|
|
140
|
+
const forwardedRegion = request.headers[index_js_1.OXY_SOURCE_REGION_HEADER.toLowerCase()];
|
|
112
141
|
const peerRegion = internal
|
|
113
142
|
? normalizeInfrastructureRegion(forwardedRegion)
|
|
114
|
-
: edgePop
|
|
115
|
-
? `edge-${edgePop}`
|
|
116
|
-
: undefined;
|
|
143
|
+
: (0, server_js_1.sourceRegionFromEdgePop)(edgePop);
|
|
117
144
|
return {
|
|
118
145
|
region,
|
|
119
146
|
service,
|
|
@@ -157,9 +184,7 @@ function instrumentTrafficFetch(fetcher, collector, service, region, resolveEndp
|
|
|
157
184
|
return Object.assign(async (input, init) => {
|
|
158
185
|
const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);
|
|
159
186
|
// Collection/auth control traffic cannot generate a feedback loop.
|
|
160
|
-
if (url.pathname
|
|
161
|
-
url.pathname.startsWith('/auth/service-token') ||
|
|
162
|
-
url.pathname === '/platform-infrastructure')
|
|
187
|
+
if (!isObservedPath(url.pathname, 'outbound'))
|
|
163
188
|
return fetcher(input, init);
|
|
164
189
|
const peer = resolveEndpoint(url);
|
|
165
190
|
const flow = {
|
|
@@ -175,7 +200,7 @@ function instrumentTrafficFetch(fetcher, collector, service, region, resolveEndp
|
|
|
175
200
|
};
|
|
176
201
|
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
177
202
|
if (peer)
|
|
178
|
-
headers.set(
|
|
203
|
+
headers.set(index_js_1.OXY_SOURCE_REGION_HEADER, region);
|
|
179
204
|
let response;
|
|
180
205
|
try {
|
|
181
206
|
response = await fetcher(input, { ...init, headers });
|
|
@@ -185,7 +210,7 @@ function instrumentTrafficFetch(fetcher, collector, service, region, resolveEndp
|
|
|
185
210
|
throw error;
|
|
186
211
|
}
|
|
187
212
|
const targetRegion = peer
|
|
188
|
-
? normalizeInfrastructureRegion(response.headers.get(
|
|
213
|
+
? normalizeInfrastructureRegion(response.headers.get(index_js_1.OXY_REGION_HEADER) ?? undefined)
|
|
189
214
|
: undefined;
|
|
190
215
|
collector.record({ ...flow, targetRegion });
|
|
191
216
|
collector.record({
|
|
@@ -203,11 +228,12 @@ function instrumentTrafficFetch(fetcher, collector, service, region, resolveEndp
|
|
|
203
228
|
// to be an internal service. Region is infrastructure metadata, never IP geo.
|
|
204
229
|
const OXY_API_SERVICES = {
|
|
205
230
|
'api.oxy.so': 'oxy-api',
|
|
206
|
-
'api.website.oxy.so': 'website',
|
|
207
231
|
'website-api.oxy.so': 'website',
|
|
208
232
|
'api.mention.earth': 'mention',
|
|
209
233
|
'api.mercaria.co': 'mercaria',
|
|
234
|
+
// Old Alia app builds still call it; goes when api.alia.onl is retired (Gemies ADR 0034).
|
|
210
235
|
'api.alia.onl': 'alia',
|
|
236
|
+
'api.gemies.ai': 'gemies',
|
|
211
237
|
'kaana.ai': 'kaana',
|
|
212
238
|
'api.homiio.com': 'homiio',
|
|
213
239
|
'api.syra.fm': 'syra',
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.OXY_EDGE_REGION_HEADER = exports.OXY_ACTIVITY_ID_HEADER = void 0;
|
|
3
|
+
exports.OXY_TELEMETRY_EXPOSED_HEADERS = exports.OXY_TELEMETRY_REQUEST_HEADERS = exports.OXY_SOURCE_REGION_HEADER = exports.OXY_REGION_HEADER = exports.OXY_EDGE_REGION_HEADER = exports.OXY_ACTIVITY_ID_HEADER = void 0;
|
|
4
|
+
/** Request header a browser sends: the ephemeral anonymous activity ID. */
|
|
4
5
|
exports.OXY_ACTIVITY_ID_HEADER = 'X-Oxy-Activity-Id';
|
|
6
|
+
/** Request header a browser sends: the three-letter Cloudflare PoP code. */
|
|
5
7
|
exports.OXY_EDGE_REGION_HEADER = 'X-Oxy-Edge-Region';
|
|
8
|
+
/** Response header a reporting service sets: the region that processed the request. */
|
|
9
|
+
exports.OXY_REGION_HEADER = 'X-Oxy-Region';
|
|
10
|
+
/** Request header one Oxy service sends another: the caller's infrastructure region. */
|
|
11
|
+
exports.OXY_SOURCE_REGION_HEADER = 'X-Oxy-Source-Region';
|
|
12
|
+
/**
|
|
13
|
+
* Request headers a browser may send and a CORS allow-list must therefore
|
|
14
|
+
* permit (`Access-Control-Allow-Headers`). `X-Oxy-Source-Region` is service to
|
|
15
|
+
* service only and is deliberately absent. The list is derived from the
|
|
16
|
+
* constants above, so a CORS list built from it cannot drift from them.
|
|
17
|
+
*/
|
|
18
|
+
exports.OXY_TELEMETRY_REQUEST_HEADERS = Object.freeze([
|
|
19
|
+
exports.OXY_ACTIVITY_ID_HEADER,
|
|
20
|
+
exports.OXY_EDGE_REGION_HEADER,
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Response headers a browser client may need to read
|
|
24
|
+
* (`Access-Control-Expose-Headers`).
|
|
25
|
+
*/
|
|
26
|
+
exports.OXY_TELEMETRY_EXPOSED_HEADERS = Object.freeze([exports.OXY_REGION_HEADER]);
|
package/dist/cjs/metrics.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
28
|
exports.HTTP_RATE_LIMITED = exports.HTTP_REQUEST_LATENCY = void 0;
|
|
29
29
|
exports.statusClass = statusClass;
|
|
30
|
+
exports.createEmfPublisherIfDeployed = createEmfPublisherIfDeployed;
|
|
30
31
|
exports.createEmfPublisher = createEmfPublisher;
|
|
31
32
|
/** One HTTP request: SampleCount is the request count, percentiles the latency. */
|
|
32
33
|
exports.HTTP_REQUEST_LATENCY = {
|
|
@@ -102,6 +103,15 @@ function seriesKey(metric, labels) {
|
|
|
102
103
|
function defaultWrite(line) {
|
|
103
104
|
globalThis.process?.stdout?.write?.(`${line}\n`);
|
|
104
105
|
}
|
|
106
|
+
/** Set by ECS on every task: the one reliable signal that `awslogs` is capturing stdout. */
|
|
107
|
+
const ECS_METADATA_ENV = 'ECS_CONTAINER_METADATA_URI_V4';
|
|
108
|
+
/**
|
|
109
|
+
* `createEmfPublisher` on ECS, `null` everywhere else (a laptop, CI, a test):
|
|
110
|
+
* the gate oxy-api and Mention each repeated. `env` is injectable for tests.
|
|
111
|
+
*/
|
|
112
|
+
function createEmfPublisherIfDeployed(options, env = globalThis.process?.env ?? {}) {
|
|
113
|
+
return env[ECS_METADATA_ENV] ? createEmfPublisher(options) : null;
|
|
114
|
+
}
|
|
105
115
|
function createEmfPublisher(options) {
|
|
106
116
|
const flushIntervalMs = options.flushIntervalMs ?? 60000;
|
|
107
117
|
const maxSeries = options.maxSeries ?? 2000;
|
package/dist/cjs/server.js
CHANGED
|
@@ -3,12 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.DEFAULT_ACTIVE_CLIENT_WINDOW_MS = exports.DEFAULT_PRESENCE_BUCKET_MS = void 0;
|
|
4
4
|
exports.normalizeEdgePop = normalizeEdgePop;
|
|
5
5
|
exports.normalizeActivityId = normalizeActivityId;
|
|
6
|
+
exports.sourceRegionFromEdgePop = sourceRegionFromEdgePop;
|
|
6
7
|
exports.metadataFromHeaders = metadataFromHeaders;
|
|
7
8
|
exports.serviceFromPath = serviceFromPath;
|
|
8
9
|
exports.activityFlow = activityFlow;
|
|
9
10
|
exports.presenceBucket = presenceBucket;
|
|
10
11
|
exports.presenceKeys = presenceKeys;
|
|
12
|
+
const index_js_1 = require("./index.js");
|
|
11
13
|
exports.DEFAULT_PRESENCE_BUCKET_MS = 30000;
|
|
14
|
+
/** @deprecated Unused by any Oxy repo; `presenceKeys` already encodes the window. Removal is planned for the next major. */
|
|
12
15
|
exports.DEFAULT_ACTIVE_CLIENT_WINDOW_MS = 60000;
|
|
13
16
|
function firstHeaderValue(value) {
|
|
14
17
|
if (typeof value === 'string')
|
|
@@ -25,23 +28,43 @@ function normalizeActivityId(value) {
|
|
|
25
28
|
const candidate = firstHeaderValue(value);
|
|
26
29
|
return candidate && /^[a-z0-9_-]{16,64}$/i.test(candidate) ? candidate : undefined;
|
|
27
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The collector's source region for a normalised edge PoP (`edge-<pop>`), the
|
|
33
|
+
* one derivation every reporter shares. `undefined` when there is no PoP.
|
|
34
|
+
*/
|
|
35
|
+
function sourceRegionFromEdgePop(edgePop) {
|
|
36
|
+
return edgePop ? `edge-${edgePop}` : undefined;
|
|
37
|
+
}
|
|
28
38
|
function metadataFromHeaders(headers) {
|
|
29
|
-
const forwardedPop = normalizeEdgePop(headers[
|
|
39
|
+
const forwardedPop = normalizeEdgePop(headers[index_js_1.OXY_EDGE_REGION_HEADER.toLowerCase()]);
|
|
30
40
|
const ray = firstHeaderValue(headers['cf-ray']);
|
|
31
41
|
const rayPop = ray?.match(/-([a-z]{3})$/i)?.[1]?.toLowerCase();
|
|
32
|
-
const activityId = normalizeActivityId(headers[
|
|
42
|
+
const activityId = normalizeActivityId(headers[index_js_1.OXY_ACTIVITY_ID_HEADER.toLowerCase()]);
|
|
33
43
|
return {
|
|
34
44
|
...(activityId ? { activityId } : {}),
|
|
35
45
|
...(forwardedPop || rayPop ? { edgePop: forwardedPop ?? rayPop } : {}),
|
|
36
46
|
};
|
|
37
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* @deprecated Unused by any Oxy repo. For the bounded activity category of a
|
|
50
|
+
* path use `trafficType` from `@oxy.so/telemetry/collector`. Removal is planned
|
|
51
|
+
* for the next major.
|
|
52
|
+
*/
|
|
38
53
|
function serviceFromPath(path) {
|
|
39
54
|
const segment = path.split('/').filter(Boolean)[0];
|
|
40
55
|
return segment && /^[a-z][a-z0-9-]{0,31}$/i.test(segment) ? segment.toLowerCase() : 'platform';
|
|
41
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* @deprecated Unused by any Oxy repo; `createTrafficCollector` keys its own
|
|
59
|
+
* flows. Removal is planned for the next major.
|
|
60
|
+
*/
|
|
42
61
|
function activityFlow(sourceRegion, service) {
|
|
43
62
|
return { sourceRegion, service, key: `${sourceRegion ?? ''}|${service}` };
|
|
44
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* @deprecated Unused outside `presenceKeys`; call `presenceKeys` for the
|
|
66
|
+
* current and previous bucket keys. Removal is planned for the next major.
|
|
67
|
+
*/
|
|
45
68
|
function presenceBucket(observedAt, bucketMs = exports.DEFAULT_PRESENCE_BUCKET_MS) {
|
|
46
69
|
if (!Number.isFinite(observedAt) || !Number.isFinite(bucketMs) || bucketMs <= 0) {
|
|
47
70
|
throw new RangeError('observedAt and bucketMs must define a finite positive bucket');
|
package/dist/cjs/socket.js
CHANGED
|
@@ -49,9 +49,7 @@ function observeTrafficSocket(socket, collector, service, region, options = {})
|
|
|
49
49
|
const edgePop = (0, server_js_1.metadataFromHeaders)(socket.handshake.headers).edgePop;
|
|
50
50
|
const peerRegion = peerService
|
|
51
51
|
? (0, collector_js_1.normalizeInfrastructureRegion)(peer?.region)
|
|
52
|
-
: edgePop
|
|
53
|
-
? `edge-${edgePop}`
|
|
54
|
-
: undefined;
|
|
52
|
+
: (0, server_js_1.sourceRegionFromEdgePop)(edgePop);
|
|
55
53
|
collector.record({
|
|
56
54
|
region,
|
|
57
55
|
service,
|
|
@@ -90,9 +88,7 @@ function observeTrafficWebSocket(socket, collector, service, region, options = {
|
|
|
90
88
|
const edgePop = (0, server_js_1.metadataFromHeaders)(options.headers ?? {}).edgePop;
|
|
91
89
|
const peerRegion = peerService
|
|
92
90
|
? (0, collector_js_1.normalizeInfrastructureRegion)(peer?.region)
|
|
93
|
-
: edgePop
|
|
94
|
-
? `edge-${edgePop}`
|
|
95
|
-
: undefined;
|
|
91
|
+
: (0, server_js_1.sourceRegionFromEdgePop)(edgePop);
|
|
96
92
|
collector.record({
|
|
97
93
|
service,
|
|
98
94
|
region,
|
package/dist/esm/browser.js
CHANGED
|
@@ -26,6 +26,12 @@ function eligibleLocation(location) {
|
|
|
26
26
|
location.hostname !== '127.0.0.1' &&
|
|
27
27
|
location.hostname !== '[::1]');
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated No Oxy repo constructs its own runtime: use the module-level
|
|
31
|
+
* `getBrowserActivityIdHeader`, `getBrowserEdgeRegionHeader` and
|
|
32
|
+
* `peekBrowserEdgeRegionHeader`. The factory remains the test seam. Removal from
|
|
33
|
+
* the public surface is planned for the next major.
|
|
34
|
+
*/
|
|
29
35
|
export function createBrowserTelemetry(options = {}) {
|
|
30
36
|
let activityId = null;
|
|
31
37
|
let activityIdCreatedAt = 0;
|
|
@@ -118,4 +124,9 @@ const defaultBrowserTelemetry = createBrowserTelemetry();
|
|
|
118
124
|
export const getBrowserActivityIdHeader = () => defaultBrowserTelemetry.getActivityIdHeader();
|
|
119
125
|
export const getBrowserEdgeRegionHeader = () => defaultBrowserTelemetry.getEdgeRegionHeader();
|
|
120
126
|
export const peekBrowserEdgeRegionHeader = () => defaultBrowserTelemetry.peekEdgeRegionHeader();
|
|
127
|
+
/**
|
|
128
|
+
* @deprecated Unused by any Oxy repo. `@oxy.so/core` sends
|
|
129
|
+
* `peekBrowserEdgeRegionHeader()` and `getBrowserActivityIdHeader()` per request.
|
|
130
|
+
* Removal is planned for the next major.
|
|
131
|
+
*/
|
|
121
132
|
export const getBrowserTelemetryHeaders = () => defaultBrowserTelemetry.getHeaders();
|
package/dist/esm/collector.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OXY_REGION_HEADER, OXY_SOURCE_REGION_HEADER, } from './index.js';
|
|
2
|
+
import { metadataFromHeaders, sourceRegionFromEdgePop } from './server.js';
|
|
2
3
|
const TYPES = {
|
|
3
4
|
auth: 'identity',
|
|
4
5
|
session: 'identity',
|
|
@@ -39,6 +40,35 @@ export function trafficType(path) {
|
|
|
39
40
|
parts.shift();
|
|
40
41
|
return TYPES[parts[0] ?? ''] ?? 'platform';
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Every path a reporter does not observe, in ONE table. Each entry says which
|
|
45
|
+
* directions it applies to: the inbound and outbound sets differ today (an
|
|
46
|
+
* outbound call to a probe or to `/platform-stats` IS recorded), and that is
|
|
47
|
+
* preserved exactly. Unifying them is a separate, deliberate decision.
|
|
48
|
+
*/
|
|
49
|
+
export const UNOBSERVED_PATHS = [
|
|
50
|
+
{
|
|
51
|
+
directions: 'inbound',
|
|
52
|
+
matches: (path) => /^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|infra-status)(?:\/|$)/.test(path),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
directions: 'inbound',
|
|
56
|
+
matches: (path) => /^\/(?:api\/)?platform-infrastructure(?:\/|$)/.test(path),
|
|
57
|
+
},
|
|
58
|
+
{ directions: 'outbound', matches: (path) => path === '/platform-infrastructure' },
|
|
59
|
+
{ directions: 'both', matches: (path) => path.startsWith('/internal/activity') },
|
|
60
|
+
{ directions: 'both', matches: (path) => path.startsWith('/auth/service-token') },
|
|
61
|
+
{ directions: 'inbound', matches: (path) => path.startsWith('/cdn-cgi/') },
|
|
62
|
+
];
|
|
63
|
+
/**
|
|
64
|
+
* Is this path part of what a reporter observes in this direction? Collection
|
|
65
|
+
* itself, service-token minting and (inbound) probes and the dashboard
|
|
66
|
+
* transport are control traffic: observing them would feed back into the very
|
|
67
|
+
* activity they carry.
|
|
68
|
+
*/
|
|
69
|
+
export function isObservedPath(path, direction) {
|
|
70
|
+
return !UNOBSERVED_PATHS.some((entry) => (entry.directions === 'both' || entry.directions === direction) && entry.matches(path));
|
|
71
|
+
}
|
|
42
72
|
export function createTrafficCollector(publish, now = Date.now) {
|
|
43
73
|
const pending = new Map();
|
|
44
74
|
let flushing = null;
|
|
@@ -79,13 +109,10 @@ export function createTrafficCollector(publish, now = Date.now) {
|
|
|
79
109
|
}
|
|
80
110
|
export function trafficMiddleware(collector, service, region) {
|
|
81
111
|
return (request, response, next) => {
|
|
82
|
-
response.setHeader?.(
|
|
112
|
+
response.setHeader?.(OXY_REGION_HEADER, region);
|
|
83
113
|
const path = request.path ?? request.url ?? '/';
|
|
84
114
|
// Never report collection itself, probes or the dashboard transport.
|
|
85
|
-
if (
|
|
86
|
-
path.startsWith('/internal/activity') ||
|
|
87
|
-
path.startsWith('/auth/service-token') ||
|
|
88
|
-
path.startsWith('/cdn-cgi/')) {
|
|
115
|
+
if (!isObservedPath(path, 'inbound')) {
|
|
89
116
|
next();
|
|
90
117
|
return;
|
|
91
118
|
}
|
|
@@ -99,12 +126,10 @@ export function trafficMiddleware(collector, service, region) {
|
|
|
99
126
|
.replace(/[^a-z0-9-]/g, '-')
|
|
100
127
|
.slice(0, 40);
|
|
101
128
|
const internal = Boolean(name);
|
|
102
|
-
const forwardedRegion = request.headers[
|
|
129
|
+
const forwardedRegion = request.headers[OXY_SOURCE_REGION_HEADER.toLowerCase()];
|
|
103
130
|
const peerRegion = internal
|
|
104
131
|
? normalizeInfrastructureRegion(forwardedRegion)
|
|
105
|
-
: edgePop
|
|
106
|
-
? `edge-${edgePop}`
|
|
107
|
-
: undefined;
|
|
132
|
+
: sourceRegionFromEdgePop(edgePop);
|
|
108
133
|
return {
|
|
109
134
|
region,
|
|
110
135
|
service,
|
|
@@ -148,9 +173,7 @@ export function instrumentTrafficFetch(fetcher, collector, service, region, reso
|
|
|
148
173
|
return Object.assign(async (input, init) => {
|
|
149
174
|
const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);
|
|
150
175
|
// Collection/auth control traffic cannot generate a feedback loop.
|
|
151
|
-
if (url.pathname
|
|
152
|
-
url.pathname.startsWith('/auth/service-token') ||
|
|
153
|
-
url.pathname === '/platform-infrastructure')
|
|
176
|
+
if (!isObservedPath(url.pathname, 'outbound'))
|
|
154
177
|
return fetcher(input, init);
|
|
155
178
|
const peer = resolveEndpoint(url);
|
|
156
179
|
const flow = {
|
|
@@ -166,7 +189,7 @@ export function instrumentTrafficFetch(fetcher, collector, service, region, reso
|
|
|
166
189
|
};
|
|
167
190
|
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
168
191
|
if (peer)
|
|
169
|
-
headers.set(
|
|
192
|
+
headers.set(OXY_SOURCE_REGION_HEADER, region);
|
|
170
193
|
let response;
|
|
171
194
|
try {
|
|
172
195
|
response = await fetcher(input, { ...init, headers });
|
|
@@ -176,7 +199,7 @@ export function instrumentTrafficFetch(fetcher, collector, service, region, reso
|
|
|
176
199
|
throw error;
|
|
177
200
|
}
|
|
178
201
|
const targetRegion = peer
|
|
179
|
-
? normalizeInfrastructureRegion(response.headers.get(
|
|
202
|
+
? normalizeInfrastructureRegion(response.headers.get(OXY_REGION_HEADER) ?? undefined)
|
|
180
203
|
: undefined;
|
|
181
204
|
collector.record({ ...flow, targetRegion });
|
|
182
205
|
collector.record({
|
|
@@ -194,11 +217,12 @@ export function instrumentTrafficFetch(fetcher, collector, service, region, reso
|
|
|
194
217
|
// to be an internal service. Region is infrastructure metadata, never IP geo.
|
|
195
218
|
const OXY_API_SERVICES = {
|
|
196
219
|
'api.oxy.so': 'oxy-api',
|
|
197
|
-
'api.website.oxy.so': 'website',
|
|
198
220
|
'website-api.oxy.so': 'website',
|
|
199
221
|
'api.mention.earth': 'mention',
|
|
200
222
|
'api.mercaria.co': 'mercaria',
|
|
223
|
+
// Old Alia app builds still call it; goes when api.alia.onl is retired (Gemies ADR 0034).
|
|
201
224
|
'api.alia.onl': 'alia',
|
|
225
|
+
'api.gemies.ai': 'gemies',
|
|
202
226
|
'kaana.ai': 'kaana',
|
|
203
227
|
'api.homiio.com': 'homiio',
|
|
204
228
|
'api.syra.fm': 'syra',
|
package/dist/esm/index.js
CHANGED
|
@@ -1,2 +1,23 @@
|
|
|
1
|
+
/** Request header a browser sends: the ephemeral anonymous activity ID. */
|
|
1
2
|
export const OXY_ACTIVITY_ID_HEADER = 'X-Oxy-Activity-Id';
|
|
3
|
+
/** Request header a browser sends: the three-letter Cloudflare PoP code. */
|
|
2
4
|
export const OXY_EDGE_REGION_HEADER = 'X-Oxy-Edge-Region';
|
|
5
|
+
/** Response header a reporting service sets: the region that processed the request. */
|
|
6
|
+
export const OXY_REGION_HEADER = 'X-Oxy-Region';
|
|
7
|
+
/** Request header one Oxy service sends another: the caller's infrastructure region. */
|
|
8
|
+
export const OXY_SOURCE_REGION_HEADER = 'X-Oxy-Source-Region';
|
|
9
|
+
/**
|
|
10
|
+
* Request headers a browser may send and a CORS allow-list must therefore
|
|
11
|
+
* permit (`Access-Control-Allow-Headers`). `X-Oxy-Source-Region` is service to
|
|
12
|
+
* service only and is deliberately absent. The list is derived from the
|
|
13
|
+
* constants above, so a CORS list built from it cannot drift from them.
|
|
14
|
+
*/
|
|
15
|
+
export const OXY_TELEMETRY_REQUEST_HEADERS = Object.freeze([
|
|
16
|
+
OXY_ACTIVITY_ID_HEADER,
|
|
17
|
+
OXY_EDGE_REGION_HEADER,
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Response headers a browser client may need to read
|
|
21
|
+
* (`Access-Control-Expose-Headers`).
|
|
22
|
+
*/
|
|
23
|
+
export const OXY_TELEMETRY_EXPOSED_HEADERS = Object.freeze([OXY_REGION_HEADER]);
|
package/dist/esm/metrics.js
CHANGED
|
@@ -97,6 +97,15 @@ function seriesKey(metric, labels) {
|
|
|
97
97
|
function defaultWrite(line) {
|
|
98
98
|
globalThis.process?.stdout?.write?.(`${line}\n`);
|
|
99
99
|
}
|
|
100
|
+
/** Set by ECS on every task: the one reliable signal that `awslogs` is capturing stdout. */
|
|
101
|
+
const ECS_METADATA_ENV = 'ECS_CONTAINER_METADATA_URI_V4';
|
|
102
|
+
/**
|
|
103
|
+
* `createEmfPublisher` on ECS, `null` everywhere else (a laptop, CI, a test):
|
|
104
|
+
* the gate oxy-api and Mention each repeated. `env` is injectable for tests.
|
|
105
|
+
*/
|
|
106
|
+
export function createEmfPublisherIfDeployed(options, env = globalThis.process?.env ?? {}) {
|
|
107
|
+
return env[ECS_METADATA_ENV] ? createEmfPublisher(options) : null;
|
|
108
|
+
}
|
|
100
109
|
export function createEmfPublisher(options) {
|
|
101
110
|
const flushIntervalMs = options.flushIntervalMs ?? 60000;
|
|
102
111
|
const maxSeries = options.maxSeries ?? 2000;
|