@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/src/collector.ts CHANGED
@@ -1,24 +1,19 @@
1
- import { metadataFromHeaders, type TelemetryHeaders } from './server.js';
1
+ import {
2
+ OXY_REGION_HEADER,
3
+ OXY_SOURCE_REGION_HEADER,
4
+ type TrafficAggregate,
5
+ type TrafficFlow,
6
+ type TrafficType,
7
+ } from './index.js';
8
+ import { metadataFromHeaders, sourceRegionFromEdgePop, type TelemetryHeaders } from './server.js';
2
9
 
3
- export type TrafficScope = 'internal' | 'external';
4
- export type TrafficDirection = 'inbound' | 'outbound';
5
- export type TrafficType = 'identity' | 'ai' | 'communication' | 'media' | 'platform';
6
- export interface TrafficFlow {
7
- region: string;
8
- sourceRegion?: string;
9
- targetRegion?: string;
10
- sourceService?: string;
11
- targetService?: string;
12
- service: string;
13
- scope: TrafficScope;
14
- direction: TrafficDirection;
15
- activityType: TrafficType;
16
- }
17
- export interface TrafficAggregate extends TrafficFlow {
18
- requests: number;
19
- windowStartedAt: string;
20
- emittedAt: string;
21
- }
10
+ export type {
11
+ TrafficAggregate,
12
+ TrafficDirection,
13
+ TrafficFlow,
14
+ TrafficScope,
15
+ TrafficType,
16
+ } from './index.js';
22
17
 
23
18
  const TYPES: Record<string, TrafficType> = {
24
19
  auth: 'identity',
@@ -58,6 +53,49 @@ export function trafficType(path: string): TrafficType {
58
53
  return TYPES[parts[0] ?? ''] ?? 'platform';
59
54
  }
60
55
 
56
+ /** Which side of a request a reporter is observing. */
57
+ export type ObservationDirection = 'inbound' | 'outbound';
58
+
59
+ /**
60
+ * Every path a reporter does not observe, in ONE table. Each entry says which
61
+ * directions it applies to: the inbound and outbound sets differ today (an
62
+ * outbound call to a probe or to `/platform-stats` IS recorded), and that is
63
+ * preserved exactly. Unifying them is a separate, deliberate decision.
64
+ */
65
+ export const UNOBSERVED_PATHS: readonly {
66
+ readonly directions: ObservationDirection | 'both';
67
+ readonly matches: (path: string) => boolean;
68
+ }[] = [
69
+ {
70
+ directions: 'inbound',
71
+ matches: (path) =>
72
+ /^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|infra-status)(?:\/|$)/.test(
73
+ path,
74
+ ),
75
+ },
76
+ {
77
+ directions: 'inbound',
78
+ matches: (path) => /^\/(?:api\/)?platform-infrastructure(?:\/|$)/.test(path),
79
+ },
80
+ { directions: 'outbound', matches: (path) => path === '/platform-infrastructure' },
81
+ { directions: 'both', matches: (path) => path.startsWith('/internal/activity') },
82
+ { directions: 'both', matches: (path) => path.startsWith('/auth/service-token') },
83
+ { directions: 'inbound', matches: (path) => path.startsWith('/cdn-cgi/') },
84
+ ];
85
+
86
+ /**
87
+ * Is this path part of what a reporter observes in this direction? Collection
88
+ * itself, service-token minting and (inbound) probes and the dashboard
89
+ * transport are control traffic: observing them would feed back into the very
90
+ * activity they carry.
91
+ */
92
+ export function isObservedPath(path: string, direction: ObservationDirection): boolean {
93
+ return !UNOBSERVED_PATHS.some(
94
+ (entry) =>
95
+ (entry.directions === 'both' || entry.directions === direction) && entry.matches(path),
96
+ );
97
+ }
98
+
61
99
  export function createTrafficCollector(
62
100
  publish: (events: TrafficAggregate[]) => Promise<unknown>,
63
101
  now = Date.now,
@@ -112,17 +150,10 @@ export function trafficMiddleware(
112
150
  region: string,
113
151
  ) {
114
152
  return (request: HttpRequest, response: HttpResponse, next: () => void): void => {
115
- response.setHeader?.('X-Oxy-Region', region);
153
+ response.setHeader?.(OXY_REGION_HEADER, region);
116
154
  const path = request.path ?? request.url ?? '/';
117
155
  // Never report collection itself, probes or the dashboard transport.
118
- if (
119
- /^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|platform-infrastructure|infra-status)(?:\/|$)/.test(
120
- path,
121
- ) ||
122
- path.startsWith('/internal/activity') ||
123
- path.startsWith('/auth/service-token') ||
124
- path.startsWith('/cdn-cgi/')
125
- ) {
156
+ if (!isObservedPath(path, 'inbound')) {
126
157
  next();
127
158
  return;
128
159
  }
@@ -136,12 +167,10 @@ export function trafficMiddleware(
136
167
  .replace(/[^a-z0-9-]/g, '-')
137
168
  .slice(0, 40);
138
169
  const internal = Boolean(name);
139
- const forwardedRegion = request.headers['x-oxy-source-region'];
170
+ const forwardedRegion = request.headers[OXY_SOURCE_REGION_HEADER.toLowerCase()];
140
171
  const peerRegion = internal
141
172
  ? normalizeInfrastructureRegion(forwardedRegion)
142
- : edgePop
143
- ? `edge-${edgePop}`
144
- : undefined;
173
+ : sourceRegionFromEdgePop(edgePop);
145
174
  return {
146
175
  region,
147
176
  service,
@@ -196,12 +225,7 @@ export function instrumentTrafficFetch(
196
225
  typeof input === 'string' ? input : input instanceof URL ? input.href : input.url,
197
226
  );
198
227
  // Collection/auth control traffic cannot generate a feedback loop.
199
- if (
200
- url.pathname.startsWith('/internal/activity') ||
201
- url.pathname.startsWith('/auth/service-token') ||
202
- url.pathname === '/platform-infrastructure'
203
- )
204
- return fetcher(input, init);
228
+ if (!isObservedPath(url.pathname, 'outbound')) return fetcher(input, init);
205
229
  const peer = resolveEndpoint(url);
206
230
  const flow: TrafficFlow = {
207
231
  service,
@@ -217,7 +241,7 @@ export function instrumentTrafficFetch(
217
241
  const headers = new Headers(
218
242
  init?.headers ?? (input instanceof Request ? input.headers : undefined),
219
243
  );
220
- if (peer) headers.set('X-Oxy-Source-Region', region);
244
+ if (peer) headers.set(OXY_SOURCE_REGION_HEADER, region);
221
245
  let response: Response;
222
246
  try {
223
247
  response = await fetcher(input, { ...init, headers });
@@ -226,7 +250,7 @@ export function instrumentTrafficFetch(
226
250
  throw error;
227
251
  }
228
252
  const targetRegion = peer
229
- ? normalizeInfrastructureRegion(response.headers.get('X-Oxy-Region') ?? undefined)
253
+ ? normalizeInfrastructureRegion(response.headers.get(OXY_REGION_HEADER) ?? undefined)
230
254
  : undefined;
231
255
  collector.record({ ...flow, targetRegion });
232
256
  collector.record({
@@ -245,11 +269,12 @@ export function instrumentTrafficFetch(
245
269
  // to be an internal service. Region is infrastructure metadata, never IP geo.
246
270
  const OXY_API_SERVICES: Record<string, string> = {
247
271
  'api.oxy.so': 'oxy-api',
248
- 'api.website.oxy.so': 'website',
249
272
  'website-api.oxy.so': 'website',
250
273
  'api.mention.earth': 'mention',
251
274
  'api.mercaria.co': 'mercaria',
275
+ // Old Alia app builds still call it; goes when api.alia.onl is retired (Gemies ADR 0034).
252
276
  'api.alia.onl': 'alia',
277
+ 'api.gemies.ai': 'gemies',
253
278
  'kaana.ai': 'kaana',
254
279
  'api.homiio.com': 'homiio',
255
280
  'api.syra.fm': 'syra',
package/src/index.ts CHANGED
@@ -1,18 +1,79 @@
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
+ /**
11
+ * Request headers a browser may send and a CORS allow-list must therefore
12
+ * permit (`Access-Control-Allow-Headers`). `X-Oxy-Source-Region` is service to
13
+ * service only and is deliberately absent. The list is derived from the
14
+ * constants above, so a CORS list built from it cannot drift from them.
15
+ */
16
+ export const OXY_TELEMETRY_REQUEST_HEADERS: readonly string[] = Object.freeze([
17
+ OXY_ACTIVITY_ID_HEADER,
18
+ OXY_EDGE_REGION_HEADER,
19
+ ]);
20
+
21
+ /**
22
+ * Response headers a browser client may need to read
23
+ * (`Access-Control-Expose-Headers`).
24
+ */
25
+ export const OXY_TELEMETRY_EXPOSED_HEADERS: readonly string[] = Object.freeze([OXY_REGION_HEADER]);
3
26
 
4
27
  export interface AnonymousActivityMetadata {
5
28
  activityId?: string;
6
29
  edgePop?: string;
7
30
  }
8
31
 
9
- export interface ActivityAggregate {
32
+ export type TrafficScope = 'internal' | 'external';
33
+ export type TrafficDirection = 'inbound' | 'outbound';
34
+ export type TrafficType = 'identity' | 'ai' | 'communication' | 'media' | 'platform';
35
+
36
+ /** One observed flow, as a reporter records it. */
37
+ export interface TrafficFlow {
38
+ region: string;
10
39
  sourceRegion?: string;
11
- targetRegion: string;
40
+ targetRegion?: string;
41
+ sourceService?: string;
42
+ targetService?: string;
12
43
  service: string;
44
+ scope: TrafficScope;
45
+ direction: TrafficDirection;
46
+ activityType: TrafficType;
47
+ }
48
+
49
+ /**
50
+ * A flow with its request count and window: the exact shape the API's strict
51
+ * `platformActivityBatchSchema` accepts on `POST /internal/activity`.
52
+ */
53
+ export interface TrafficAggregate extends TrafficFlow {
13
54
  requests: number;
14
- activeClients: number;
15
55
  windowStartedAt: string;
16
56
  emittedAt: string;
17
- direction: 'inbound';
57
+ }
58
+
59
+ /**
60
+ * The `platform_activity` socket payload: an aggregate plus the approximate
61
+ * active-client count for its origin.
62
+ */
63
+ export type ActivityAggregate = TrafficAggregate & { activeClients: number };
64
+
65
+ /** One region in the `platform_infrastructure` snapshot. */
66
+ export interface InfrastructureNode {
67
+ region: string;
68
+ label: string;
69
+ coordinates: [number, number];
70
+ services: string[];
71
+ instances: number;
72
+ status: 'online' | 'degraded' | 'offline' | 'unknown';
73
+ }
74
+
75
+ /** The `platform_infrastructure` socket payload. */
76
+ export interface InfrastructureSnapshot {
77
+ emittedAt: string;
78
+ nodes: InfrastructureNode[];
18
79
  }
package/src/metrics.ts CHANGED
@@ -182,6 +182,22 @@ function defaultWrite(line: string): void {
182
182
  (globalThis as StdoutHost).process?.stdout?.write?.(`${line}\n`);
183
183
  }
184
184
 
185
+ /** Set by ECS on every task: the one reliable signal that `awslogs` is capturing stdout. */
186
+ const ECS_METADATA_ENV = 'ECS_CONTAINER_METADATA_URI_V4';
187
+
188
+ /**
189
+ * `createEmfPublisher` on ECS, `null` everywhere else (a laptop, CI, a test):
190
+ * the gate oxy-api and Mention each repeated. `env` is injectable for tests.
191
+ */
192
+ export function createEmfPublisherIfDeployed(
193
+ options: EmfPublisherOptions,
194
+ env: Readonly<Record<string, string | undefined>> = (
195
+ globalThis as { process?: { env?: Record<string, string | undefined> } }
196
+ ).process?.env ?? {},
197
+ ): EmfPublisher | null {
198
+ return env[ECS_METADATA_ENV] ? createEmfPublisher(options) : null;
199
+ }
200
+
185
201
  export function createEmfPublisher(options: EmfPublisherOptions): EmfPublisher {
186
202
  const flushIntervalMs = options.flushIntervalMs ?? 60_000;
187
203
  const maxSeries = options.maxSeries ?? 2_000;
package/src/server.ts CHANGED
@@ -1,16 +1,27 @@
1
- import type { ActivityAggregate, AnonymousActivityMetadata } from './index.js';
1
+ import {
2
+ type ActivityAggregate,
3
+ type AnonymousActivityMetadata,
4
+ OXY_ACTIVITY_ID_HEADER,
5
+ OXY_EDGE_REGION_HEADER,
6
+ } from './index.js';
2
7
 
3
8
  export const DEFAULT_PRESENCE_BUCKET_MS = 30_000;
9
+ /** @deprecated Unused by any Oxy repo; `presenceKeys` already encodes the window. Removal is planned for the next major. */
4
10
  export const DEFAULT_ACTIVE_CLIENT_WINDOW_MS = 60_000;
5
11
 
6
12
  export type TelemetryHeaderValue = string | readonly string[] | undefined;
7
13
  export type TelemetryHeaders = Readonly<Record<string, TelemetryHeaderValue>>;
8
14
 
15
+ /**
16
+ * @deprecated Unused by any Oxy repo (presence counting lives in the API). No
17
+ * replacement; removal is planned for the next major.
18
+ */
9
19
  export interface ActivityCardinalityAdapter {
10
20
  add(key: string, activityId: string, ttlSeconds: number): Promise<void>;
11
21
  count(keys: readonly string[]): Promise<number>;
12
22
  }
13
23
 
24
+ /** @deprecated Unused by any Oxy repo; see `activityFlow`. */
14
25
  export interface ActivityFlow {
15
26
  sourceRegion?: string;
16
27
  service: string;
@@ -34,26 +45,47 @@ export function normalizeActivityId(value: TelemetryHeaderValue): string | undef
34
45
  return candidate && /^[a-z0-9_-]{16,64}$/i.test(candidate) ? candidate : undefined;
35
46
  }
36
47
 
48
+ /**
49
+ * The collector's source region for a normalised edge PoP (`edge-<pop>`), the
50
+ * one derivation every reporter shares. `undefined` when there is no PoP.
51
+ */
52
+ export function sourceRegionFromEdgePop(edgePop: string | undefined): string | undefined {
53
+ return edgePop ? `edge-${edgePop}` : undefined;
54
+ }
55
+
37
56
  export function metadataFromHeaders(headers: TelemetryHeaders): AnonymousActivityMetadata {
38
- const forwardedPop = normalizeEdgePop(headers['x-oxy-edge-region']);
57
+ const forwardedPop = normalizeEdgePop(headers[OXY_EDGE_REGION_HEADER.toLowerCase()]);
39
58
  const ray = firstHeaderValue(headers['cf-ray']);
40
59
  const rayPop = ray?.match(/-([a-z]{3})$/i)?.[1]?.toLowerCase();
41
- const activityId = normalizeActivityId(headers['x-oxy-activity-id']);
60
+ const activityId = normalizeActivityId(headers[OXY_ACTIVITY_ID_HEADER.toLowerCase()]);
42
61
  return {
43
62
  ...(activityId ? { activityId } : {}),
44
63
  ...(forwardedPop || rayPop ? { edgePop: forwardedPop ?? rayPop } : {}),
45
64
  };
46
65
  }
47
66
 
67
+ /**
68
+ * @deprecated Unused by any Oxy repo. For the bounded activity category of a
69
+ * path use `trafficType` from `@oxy.so/telemetry/collector`. Removal is planned
70
+ * for the next major.
71
+ */
48
72
  export function serviceFromPath(path: string): string {
49
73
  const segment = path.split('/').filter(Boolean)[0];
50
74
  return segment && /^[a-z][a-z0-9-]{0,31}$/i.test(segment) ? segment.toLowerCase() : 'platform';
51
75
  }
52
76
 
77
+ /**
78
+ * @deprecated Unused by any Oxy repo; `createTrafficCollector` keys its own
79
+ * flows. Removal is planned for the next major.
80
+ */
53
81
  export function activityFlow(sourceRegion: string | undefined, service: string): ActivityFlow {
54
82
  return { sourceRegion, service, key: `${sourceRegion ?? ''}|${service}` };
55
83
  }
56
84
 
85
+ /**
86
+ * @deprecated Unused outside `presenceKeys`; call `presenceKeys` for the
87
+ * current and previous bucket keys. Removal is planned for the next major.
88
+ */
57
89
  export function presenceBucket(observedAt: number, bucketMs = DEFAULT_PRESENCE_BUCKET_MS): number {
58
90
  if (!Number.isFinite(observedAt) || !Number.isFinite(bucketMs) || bucketMs <= 0) {
59
91
  throw new RangeError('observedAt and bucketMs must define a finite positive bucket');
@@ -73,4 +105,5 @@ export function presenceKeys(
73
105
  ];
74
106
  }
75
107
 
108
+ /** @deprecated Unused by any Oxy repo; use `ActivityAggregate` from `@oxy.so/telemetry`. */
76
109
  export type ActivityBucket = ActivityAggregate;
package/src/socket.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { normalizeInfrastructureRegion, type TrafficFlow, type TrafficType } from './collector.js';
2
- import { metadataFromHeaders, type TelemetryHeaders } from './server.js';
2
+ import { metadataFromHeaders, sourceRegionFromEdgePop, type TelemetryHeaders } from './server.js';
3
3
 
4
4
  export interface TrafficSocket {
5
5
  handshake: { headers: TelemetryHeaders };
@@ -63,9 +63,7 @@ export function observeTrafficSocket(
63
63
  const edgePop = metadataFromHeaders(socket.handshake.headers).edgePop;
64
64
  const peerRegion = peerService
65
65
  ? normalizeInfrastructureRegion(peer?.region)
66
- : edgePop
67
- ? `edge-${edgePop}`
68
- : undefined;
66
+ : sourceRegionFromEdgePop(edgePop);
69
67
  collector.record({
70
68
  region,
71
69
  service,
@@ -125,9 +123,7 @@ export function observeTrafficWebSocket(
125
123
  const edgePop = metadataFromHeaders(options.headers ?? {}).edgePop;
126
124
  const peerRegion = peerService
127
125
  ? normalizeInfrastructureRegion(peer?.region)
128
- : edgePop
129
- ? `edge-${edgePop}`
130
- : undefined;
126
+ : sourceRegionFromEdgePop(edgePop);
131
127
  collector.record({
132
128
  service,
133
129
  region,