@stage-labs/metro 0.1.0-beta.224 → 0.1.0-beta.225
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.225",
|
|
4
4
|
"description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile, statfs } from 'node:fs/promises';
|
|
2
|
+
import { uptime } from 'node:os';
|
|
2
3
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
4
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
4
5
|
import { signV4 } from '@metro-labs/http/sigv4';
|
|
@@ -10,12 +11,19 @@ const IMDS_TIMEOUT_MS = 2_000;
|
|
|
10
11
|
const SEND_TIMEOUT_MS = 15_000;
|
|
11
12
|
const NAMESPACE = 'CWAgent';
|
|
12
13
|
const CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8';
|
|
14
|
+
const BOOT_WINDOW_S = 300;
|
|
15
|
+
const SENT = 'sending memory and disk to CloudWatch every minute';
|
|
13
16
|
|
|
14
17
|
export interface Readings {
|
|
15
18
|
memory: number;
|
|
16
19
|
disk: number;
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
export interface Started {
|
|
23
|
+
name: 'server_booted' | 'metro_started';
|
|
24
|
+
at: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
interface RoleKeys {
|
|
20
28
|
accessKeyId: string;
|
|
21
29
|
secretAccessKey: string;
|
|
@@ -95,10 +103,15 @@ export async function thisInstance(deps: PublisherDeps): Promise<Instance | null
|
|
|
95
103
|
return { instanceId, region, keys: keysOf(await imdsGet(deps, token, `meta-data/iam/security-credentials/${role}`)) };
|
|
96
104
|
}
|
|
97
105
|
|
|
98
|
-
export function
|
|
99
|
-
const
|
|
106
|
+
export function startedOf(nowMs: number, processSeconds: number, bootSeconds: number): Started {
|
|
107
|
+
const at = (secondsAgo: number): number => Math.round(nowMs / 1000 - secondsAgo);
|
|
108
|
+
return bootSeconds < BOOT_WINDOW_S ? { name: 'server_booted', at: at(bootSeconds) } : { name: 'metro_started', at: at(processSeconds) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function readingParams(instanceId: string, readings: Readings, started: Started | null = null): Record<string, string> {
|
|
112
|
+
const datum = (at: number, name: string, value: number, dims: [string, string][], unit = 'Percent'): [string, string][] => [
|
|
100
113
|
[`MetricData.member.${String(at)}.MetricName`, name],
|
|
101
|
-
[`MetricData.member.${String(at)}.Unit`,
|
|
114
|
+
[`MetricData.member.${String(at)}.Unit`, unit],
|
|
102
115
|
[`MetricData.member.${String(at)}.Value`, value.toFixed(2)],
|
|
103
116
|
...dims.flatMap(([key, v], i): [string, string][] => [
|
|
104
117
|
[`MetricData.member.${String(at)}.Dimensions.member.${String(i + 1)}.Name`, key],
|
|
@@ -114,12 +127,13 @@ export function readingParams(instanceId: string, readings: Readings): Record<st
|
|
|
114
127
|
['InstanceId', instanceId],
|
|
115
128
|
['path', '/'],
|
|
116
129
|
]),
|
|
130
|
+
...(started === null ? [] : datum(3, started.name, started.at, [['InstanceId', instanceId]], 'Seconds')),
|
|
117
131
|
]);
|
|
118
132
|
}
|
|
119
133
|
|
|
120
|
-
async function send(deps: PublisherDeps, instance: Instance, keys: RoleKeys, readings: Readings): Promise<void> {
|
|
134
|
+
async function send(deps: PublisherDeps, instance: Instance, keys: RoleKeys, readings: Readings, started: Started | null): Promise<void> {
|
|
121
135
|
const url = `https://monitoring.${instance.region}.amazonaws.com/`;
|
|
122
|
-
const body = new URLSearchParams(readingParams(instance.instanceId, readings)).toString();
|
|
136
|
+
const body = new URLSearchParams(readingParams(instance.instanceId, readings, started)).toString();
|
|
123
137
|
const signed = await signV4({ method: 'POST', url, headers: { 'content-type': CONTENT_TYPE }, body, region: instance.region, service: 'monitoring', ...keys });
|
|
124
138
|
const res = await deps.fetch(url, { method: 'POST', headers: signed.headers, body, signal: AbortSignal.timeout(SEND_TIMEOUT_MS) });
|
|
125
139
|
if (res.ok) return;
|
|
@@ -129,18 +143,19 @@ async function send(deps: PublisherDeps, instance: Instance, keys: RoleKeys, rea
|
|
|
129
143
|
throw new Error(`CloudWatch refused the readings: ${code}${message === undefined ? '' : ` (${message})`}`);
|
|
130
144
|
}
|
|
131
145
|
|
|
132
|
-
export async function publishOnce(deps: PublisherDeps = LIVE): Promise<string> {
|
|
146
|
+
export async function publishOnce(deps: PublisherDeps = LIVE, started: Started | null = null): Promise<string> {
|
|
133
147
|
const instance = await thisInstance(deps);
|
|
134
148
|
if (instance === null) return 'off: this server is not an EC2 instance';
|
|
135
149
|
if (instance.keys === null) return `waiting: instance ${instance.instanceId} has no IAM role, so memory and disk are not sent`;
|
|
136
150
|
const readings = await deps.readings();
|
|
137
151
|
if (readings === null) return 'skipped: could not read memory or disk';
|
|
138
|
-
await send(deps, instance, instance.keys, readings);
|
|
139
|
-
return
|
|
152
|
+
await send(deps, instance, instance.keys, readings, started);
|
|
153
|
+
return SENT;
|
|
140
154
|
}
|
|
141
155
|
|
|
142
156
|
export function startCloudWatchPublisher(): void {
|
|
143
157
|
if (process.platform !== 'linux') return;
|
|
158
|
+
let started: Started | null = startedOf(Date.now(), process.uptime(), uptime());
|
|
144
159
|
let last = '';
|
|
145
160
|
const note = (status: string, warn: boolean): void => {
|
|
146
161
|
if (status === last) return;
|
|
@@ -149,8 +164,9 @@ export function startCloudWatchPublisher(): void {
|
|
|
149
164
|
else log.info({ status }, 'cloudwatch');
|
|
150
165
|
};
|
|
151
166
|
const run = (): void => {
|
|
152
|
-
publishOnce()
|
|
167
|
+
publishOnce(LIVE, started)
|
|
153
168
|
.then((status) => {
|
|
169
|
+
if (status === SENT) started = null;
|
|
154
170
|
note(status, false);
|
|
155
171
|
})
|
|
156
172
|
.catch((err: unknown) => {
|
package/runtime/runtime.json
CHANGED