@metamask-previews/tooling-insight 1.0.1-preview-898fae5
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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/daily-anonymizer.cjs +8 -0
- package/dist/daily-anonymizer.cjs.map +1 -0
- package/dist/daily-anonymizer.d.cts +3 -0
- package/dist/daily-anonymizer.d.cts.map +1 -0
- package/dist/daily-anonymizer.d.mts +3 -0
- package/dist/daily-anonymizer.d.mts.map +1 -0
- package/dist/daily-anonymizer.mjs +6 -0
- package/dist/daily-anonymizer.mjs.map +1 -0
- package/dist/index.cjs +6 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/allowlist.cjs +159 -0
- package/dist/lib/allowlist.cjs.map +1 -0
- package/dist/lib/allowlist.d.cts +31 -0
- package/dist/lib/allowlist.d.cts.map +1 -0
- package/dist/lib/allowlist.d.mts +31 -0
- package/dist/lib/allowlist.d.mts.map +1 -0
- package/dist/lib/allowlist.mjs +155 -0
- package/dist/lib/allowlist.mjs.map +1 -0
- package/dist/lib/csv.cjs +152 -0
- package/dist/lib/csv.cjs.map +1 -0
- package/dist/lib/csv.d.cts +16 -0
- package/dist/lib/csv.d.cts.map +1 -0
- package/dist/lib/csv.d.mts +16 -0
- package/dist/lib/csv.d.mts.map +1 -0
- package/dist/lib/csv.mjs +149 -0
- package/dist/lib/csv.mjs.map +1 -0
- package/dist/lib/exposition.cjs +102 -0
- package/dist/lib/exposition.cjs.map +1 -0
- package/dist/lib/exposition.d.cts +9 -0
- package/dist/lib/exposition.d.cts.map +1 -0
- package/dist/lib/exposition.d.mts +9 -0
- package/dist/lib/exposition.d.mts.map +1 -0
- package/dist/lib/exposition.mjs +99 -0
- package/dist/lib/exposition.mjs.map +1 -0
- package/dist/lib/fold.cjs +294 -0
- package/dist/lib/fold.cjs.map +1 -0
- package/dist/lib/fold.d.cts +32 -0
- package/dist/lib/fold.d.cts.map +1 -0
- package/dist/lib/fold.d.mts +32 -0
- package/dist/lib/fold.d.mts.map +1 -0
- package/dist/lib/fold.mjs +288 -0
- package/dist/lib/fold.mjs.map +1 -0
- package/dist/lib/log.cjs +116 -0
- package/dist/lib/log.cjs.map +1 -0
- package/dist/lib/log.d.cts +32 -0
- package/dist/lib/log.d.cts.map +1 -0
- package/dist/lib/log.d.mts +32 -0
- package/dist/lib/log.d.mts.map +1 -0
- package/dist/lib/log.mjs +113 -0
- package/dist/lib/log.mjs.map +1 -0
- package/dist/lib/paths.cjs +91 -0
- package/dist/lib/paths.cjs.map +1 -0
- package/dist/lib/paths.d.cts +45 -0
- package/dist/lib/paths.d.cts.map +1 -0
- package/dist/lib/paths.d.mts +45 -0
- package/dist/lib/paths.d.mts.map +1 -0
- package/dist/lib/paths.mjs +82 -0
- package/dist/lib/paths.mjs.map +1 -0
- package/dist/lib/push.cjs +122 -0
- package/dist/lib/push.cjs.map +1 -0
- package/dist/lib/push.d.cts +58 -0
- package/dist/lib/push.d.cts.map +1 -0
- package/dist/lib/push.d.mts +58 -0
- package/dist/lib/push.d.mts.map +1 -0
- package/dist/lib/push.mjs +116 -0
- package/dist/lib/push.mjs.map +1 -0
- package/dist/lib/remoteWrite.cjs +177 -0
- package/dist/lib/remoteWrite.cjs.map +1 -0
- package/dist/lib/remoteWrite.d.cts +24 -0
- package/dist/lib/remoteWrite.d.cts.map +1 -0
- package/dist/lib/remoteWrite.d.mts +24 -0
- package/dist/lib/remoteWrite.d.mts.map +1 -0
- package/dist/lib/remoteWrite.mjs +172 -0
- package/dist/lib/remoteWrite.mjs.map +1 -0
- package/dist/lib/state.cjs +100 -0
- package/dist/lib/state.cjs.map +1 -0
- package/dist/lib/state.d.cts +28 -0
- package/dist/lib/state.d.cts.map +1 -0
- package/dist/lib/state.d.mts +28 -0
- package/dist/lib/state.d.mts.map +1 -0
- package/dist/lib/state.mjs +95 -0
- package/dist/lib/state.mjs.map +1 -0
- package/dist/lib/types.cjs +3 -0
- package/dist/lib/types.cjs.map +1 -0
- package/dist/lib/types.d.cts +82 -0
- package/dist/lib/types.d.cts.map +1 -0
- package/dist/lib/types.d.mts +82 -0
- package/dist/lib/types.d.mts.map +1 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/lib/types.mjs.map +1 -0
- package/dist/run.cjs +137 -0
- package/dist/run.cjs.map +1 -0
- package/dist/run.d.cts +20 -0
- package/dist/run.d.cts.map +1 -0
- package/dist/run.d.mts +20 -0
- package/dist/run.d.mts.map +1 -0
- package/dist/run.mjs +134 -0
- package/dist/run.mjs.map +1 -0
- package/package.json +100 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Allowlist, SamplePoint, ToolEvent } from "./types.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* Classic histogram bucket upper bounds in milliseconds, covering sub-second
|
|
4
|
+
* scripts up to ~10 minutes. A synthetic `+Inf` bucket is always appended.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DURATION_BUCKETS_MS: readonly number[];
|
|
7
|
+
/**
|
|
8
|
+
* Fold raw CSV events into anonymized Prometheus sample points for pending days.
|
|
9
|
+
*
|
|
10
|
+
* @param events - All parsed tool events.
|
|
11
|
+
* @param allowlist - Skill and script allowlists used to filter events.
|
|
12
|
+
* @param instanceUuid - Per-install UUID used as the Prometheus `instance` label.
|
|
13
|
+
* @param lastPushedDay - The last UTC day that was successfully pushed, or null.
|
|
14
|
+
* @param todayUtc - The current UTC day (events on this day are not yet pushed).
|
|
15
|
+
* @returns An array of sample points ready for Prometheus exposition.
|
|
16
|
+
*/
|
|
17
|
+
export declare function fold(events: ToolEvent[], allowlist: Allowlist, instanceUuid: string, lastPushedDay: string | null, todayUtc: string): SamplePoint[];
|
|
18
|
+
/**
|
|
19
|
+
* Returns the current UTC calendar day as `YYYY-MM-DD`.
|
|
20
|
+
*
|
|
21
|
+
* @param now - The current date; defaults to `new Date()`.
|
|
22
|
+
* @returns The UTC day string.
|
|
23
|
+
*/
|
|
24
|
+
export declare function todayUtcDay(now?: Date): string;
|
|
25
|
+
/**
|
|
26
|
+
* Returns the latest `day` value across all sample points, or null for an empty array.
|
|
27
|
+
*
|
|
28
|
+
* @param samples - The sample points to inspect.
|
|
29
|
+
* @returns The maximum UTC day string, or null.
|
|
30
|
+
*/
|
|
31
|
+
export declare function maxDay(samples: SamplePoint[]): string | null;
|
|
32
|
+
//# sourceMappingURL=fold.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fold.d.mts","sourceRoot":"","sources":["../../src/lib/fold.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EAIT,WAAW,EACX,SAAS,EACV,oBAAgB;AAQjB;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAEhD,CAAC;AA8JF;;;;;;;;;GASG;AACH,wBAAgB,IAAI,CAClB,MAAM,EAAE,SAAS,EAAE,EACnB,SAAS,EAAE,SAAS,EACpB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,QAAQ,EAAE,MAAM,GACf,WAAW,EAAE,CA6If;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,OAAa,GAAG,MAAM,CAEpD;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,GAAG,IAAI,CAS5D"}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
const TERMINAL_EVENT_TYPES = new Set([
|
|
2
|
+
'success',
|
|
3
|
+
'failure',
|
|
4
|
+
'interrupted',
|
|
5
|
+
]);
|
|
6
|
+
/**
|
|
7
|
+
* Classic histogram bucket upper bounds in milliseconds, covering sub-second
|
|
8
|
+
* scripts up to ~10 minutes. A synthetic `+Inf` bucket is always appended.
|
|
9
|
+
*/
|
|
10
|
+
export const DURATION_BUCKETS_MS = [
|
|
11
|
+
100, 250, 500, 1000, 2000, 5000, 10000, 30000, 60000, 120000, 300000, 600000,
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Extract the UTC calendar day from an ISO-8601 timestamp string.
|
|
15
|
+
*
|
|
16
|
+
* @param iso - An ISO-8601 timestamp string.
|
|
17
|
+
* @returns The `YYYY-MM-DD` portion of the timestamp.
|
|
18
|
+
*/
|
|
19
|
+
function utcDayFromIso(iso) {
|
|
20
|
+
return iso.slice(0, 10);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Convert a `YYYY-MM-DD` UTC day string to a Unix timestamp at midnight UTC.
|
|
24
|
+
*
|
|
25
|
+
* @param day - A UTC day string in `YYYY-MM-DD` format.
|
|
26
|
+
* @returns Milliseconds since epoch for midnight UTC on that day.
|
|
27
|
+
*/
|
|
28
|
+
function utcMidnightMs(day) {
|
|
29
|
+
const parts = day.split('-');
|
|
30
|
+
const year = Number(parts[0]);
|
|
31
|
+
const month = Number(parts[1]);
|
|
32
|
+
const dayOfMonth = Number(parts[2]);
|
|
33
|
+
return Date.UTC(year, month - 1, dayOfMonth);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Returns true when a given day has pending (unpushed) events.
|
|
37
|
+
* Today's events are excluded so the day is complete before pushing.
|
|
38
|
+
*
|
|
39
|
+
* @param day - UTC calendar day of the event.
|
|
40
|
+
* @param lastPushedDay - The most recently pushed day, or `null` if never pushed.
|
|
41
|
+
* @param todayUtc - The current UTC calendar day.
|
|
42
|
+
* @returns `true` when the day is after `lastPushedDay` and before today.
|
|
43
|
+
*/
|
|
44
|
+
function isDayPending(day, lastPushedDay, todayUtc) {
|
|
45
|
+
if (day >= todayUtc) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (lastPushedDay === null) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return day > lastPushedDay;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Strip the well-known tool-type prefix (`skill:` or `yarn:`) from a tool name.
|
|
55
|
+
*
|
|
56
|
+
* @param toolName - Raw tool name from the CSV event.
|
|
57
|
+
* @param toolType - The parsed tool type for the event.
|
|
58
|
+
* @returns The tool name without its prefix, or unchanged when no prefix matches.
|
|
59
|
+
*/
|
|
60
|
+
function stripToolName(toolName, toolType) {
|
|
61
|
+
if (toolType === 'skill' && toolName.startsWith('skill:')) {
|
|
62
|
+
return toolName.slice('skill:'.length);
|
|
63
|
+
}
|
|
64
|
+
if (toolType === 'yarn_script' && toolName.startsWith('yarn:')) {
|
|
65
|
+
return toolName.slice('yarn:'.length);
|
|
66
|
+
}
|
|
67
|
+
return toolName;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Map a raw CSV event to its Prometheus `event_type` label value.
|
|
71
|
+
* Returns `null` for `end` events whose `success` field is not a boolean.
|
|
72
|
+
*
|
|
73
|
+
* @param event - The raw tool event to classify.
|
|
74
|
+
* @returns The corresponding `EventTypeLabel`, or `null` to drop the event.
|
|
75
|
+
*/
|
|
76
|
+
function mapEventType(event) {
|
|
77
|
+
// Switch over the exhaustive CsvEventType union.
|
|
78
|
+
switch (event.event_type) {
|
|
79
|
+
case 'start':
|
|
80
|
+
return 'start';
|
|
81
|
+
case 'interrupted':
|
|
82
|
+
return 'interrupted';
|
|
83
|
+
case 'end':
|
|
84
|
+
if (event.success === true) {
|
|
85
|
+
return 'success';
|
|
86
|
+
}
|
|
87
|
+
if (event.success === false) {
|
|
88
|
+
return 'failure';
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
default:
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Returns true when the stripped tool name appears on the allowlist for its type.
|
|
97
|
+
*
|
|
98
|
+
* @param strippedName - Tool name after prefix stripping.
|
|
99
|
+
* @param toolType - The tool type (skill or yarn_script).
|
|
100
|
+
* @param allowlist - The combined skill and script allowlist.
|
|
101
|
+
* @returns `true` when the tool is allowlisted.
|
|
102
|
+
*/
|
|
103
|
+
function isAllowlisted(strippedName, toolType, allowlist) {
|
|
104
|
+
if (toolType === 'skill') {
|
|
105
|
+
// The allowlist is built from the MetaMask/ConsenSys skills repo cache:
|
|
106
|
+
// extractSkillNames reads directory names (e.g. `create-pr`) and stores
|
|
107
|
+
// them with the `mms-` prefix (e.g. `mms-create-pr`). A skill event passes
|
|
108
|
+
// only when its logged name — after stripping the `skill:` type prefix —
|
|
109
|
+
// is found verbatim in that Set. Skills not present in the repo cache
|
|
110
|
+
// (e.g. personal Cursor skills logged as `skill:pr-review-queue`) never
|
|
111
|
+
// match because the Set has `mms-pr-review-queue`, not `pr-review-queue`.
|
|
112
|
+
return allowlist.skills.has(strippedName);
|
|
113
|
+
}
|
|
114
|
+
return allowlist.scripts.has(strippedName);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Build a stable string key that uniquely identifies a (day, labels, metric) combination.
|
|
118
|
+
*
|
|
119
|
+
* @param day - UTC calendar day string.
|
|
120
|
+
* @param labels - Prometheus label set for the sample.
|
|
121
|
+
* @param metric - Prometheus metric name.
|
|
122
|
+
* @returns A JSON-serialised key.
|
|
123
|
+
*/
|
|
124
|
+
function groupKey(day, labels, metric) {
|
|
125
|
+
return JSON.stringify({ day, metric, labels });
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Fold raw CSV events into anonymized Prometheus sample points for pending days.
|
|
129
|
+
*
|
|
130
|
+
* @param events - All parsed tool events.
|
|
131
|
+
* @param allowlist - Skill and script allowlists used to filter events.
|
|
132
|
+
* @param instanceUuid - Per-install UUID used as the Prometheus `instance` label.
|
|
133
|
+
* @param lastPushedDay - The last UTC day that was successfully pushed, or null.
|
|
134
|
+
* @param todayUtc - The current UTC day (events on this day are not yet pushed).
|
|
135
|
+
* @returns An array of sample points ready for Prometheus exposition.
|
|
136
|
+
*/
|
|
137
|
+
export function fold(events, allowlist, instanceUuid, lastPushedDay, todayUtc) {
|
|
138
|
+
const usageCounts = new Map();
|
|
139
|
+
const durationSums = new Map();
|
|
140
|
+
// Keyed by the same group key as durationSums (terminal events with a duration).
|
|
141
|
+
const durationHistograms = new Map();
|
|
142
|
+
for (const event of events) {
|
|
143
|
+
const day = utcDayFromIso(event.created_at);
|
|
144
|
+
if (!isDayPending(day, lastPushedDay, todayUtc)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const strippedName = stripToolName(event.tool_name, event.tool_type);
|
|
148
|
+
if (!isAllowlisted(strippedName, event.tool_type, allowlist)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const eventTypeLabel = mapEventType(event);
|
|
152
|
+
if (!eventTypeLabel) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const labels = {
|
|
156
|
+
repo: event.repo,
|
|
157
|
+
tool_name: strippedName,
|
|
158
|
+
tool_type: event.tool_type,
|
|
159
|
+
event_type: eventTypeLabel,
|
|
160
|
+
agent_vendor: event.agent_vendor,
|
|
161
|
+
instance: instanceUuid,
|
|
162
|
+
};
|
|
163
|
+
const usageMetric = 'metamask_devtools_usage_count';
|
|
164
|
+
const usageKey = groupKey(day, labels, usageMetric);
|
|
165
|
+
const existingUsage = usageCounts.get(usageKey);
|
|
166
|
+
if (existingUsage) {
|
|
167
|
+
existingUsage.value += 1;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
usageCounts.set(usageKey, {
|
|
171
|
+
metric: usageMetric,
|
|
172
|
+
labels,
|
|
173
|
+
value: 1,
|
|
174
|
+
day,
|
|
175
|
+
timestampMs: utcMidnightMs(day),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (TERMINAL_EVENT_TYPES.has(eventTypeLabel) &&
|
|
179
|
+
event.duration_ms !== null) {
|
|
180
|
+
const durationMetric = 'metamask_devtools_duration_sum_ms';
|
|
181
|
+
const durationKey = groupKey(day, labels, durationMetric);
|
|
182
|
+
const existingDuration = durationSums.get(durationKey);
|
|
183
|
+
if (existingDuration) {
|
|
184
|
+
existingDuration.value += event.duration_ms;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
durationSums.set(durationKey, {
|
|
188
|
+
metric: durationMetric,
|
|
189
|
+
labels,
|
|
190
|
+
value: event.duration_ms,
|
|
191
|
+
day,
|
|
192
|
+
timestampMs: utcMidnightMs(day),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// Accumulate the histogram for the same group key (reuse durationKey
|
|
196
|
+
// since both are keyed on the same (day, labels) combination).
|
|
197
|
+
const existing = durationHistograms.get(durationKey);
|
|
198
|
+
if (existing) {
|
|
199
|
+
existing.sum += event.duration_ms;
|
|
200
|
+
existing.count += 1;
|
|
201
|
+
// Cumulative: increment every bucket whose upper bound >= duration.
|
|
202
|
+
existing.bucketCounts = existing.bucketCounts.map((count, i) => event.duration_ms !== null &&
|
|
203
|
+
event.duration_ms <= DURATION_BUCKETS_MS[i]
|
|
204
|
+
? count + 1
|
|
205
|
+
: count);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
const bucketCounts = DURATION_BUCKETS_MS.map((bound) => event.duration_ms !== null && event.duration_ms <= bound ? 1 : 0);
|
|
209
|
+
durationHistograms.set(durationKey, {
|
|
210
|
+
day,
|
|
211
|
+
labels,
|
|
212
|
+
sum: event.duration_ms,
|
|
213
|
+
count: 1,
|
|
214
|
+
bucketCounts,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const histogramSamples = [];
|
|
220
|
+
for (const acc of durationHistograms.values()) {
|
|
221
|
+
const { day, labels } = acc;
|
|
222
|
+
const timestampMs = utcMidnightMs(day);
|
|
223
|
+
histogramSamples.push({
|
|
224
|
+
metric: 'metamask_devtools_duration_ms_sum',
|
|
225
|
+
labels,
|
|
226
|
+
value: acc.sum,
|
|
227
|
+
day,
|
|
228
|
+
timestampMs,
|
|
229
|
+
});
|
|
230
|
+
histogramSamples.push({
|
|
231
|
+
metric: 'metamask_devtools_duration_ms_count',
|
|
232
|
+
labels,
|
|
233
|
+
value: acc.count,
|
|
234
|
+
day,
|
|
235
|
+
timestampMs,
|
|
236
|
+
});
|
|
237
|
+
acc.bucketCounts.forEach((count, i) => {
|
|
238
|
+
histogramSamples.push({
|
|
239
|
+
metric: 'metamask_devtools_duration_ms_bucket',
|
|
240
|
+
labels,
|
|
241
|
+
value: count,
|
|
242
|
+
day,
|
|
243
|
+
timestampMs,
|
|
244
|
+
le: String(DURATION_BUCKETS_MS[i]),
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
// The +Inf bucket equals total count (every observation falls below +Inf).
|
|
248
|
+
histogramSamples.push({
|
|
249
|
+
metric: 'metamask_devtools_duration_ms_bucket',
|
|
250
|
+
labels,
|
|
251
|
+
value: acc.count,
|
|
252
|
+
day,
|
|
253
|
+
timestampMs,
|
|
254
|
+
le: '+Inf',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return [
|
|
258
|
+
...usageCounts.values(),
|
|
259
|
+
...durationSums.values(),
|
|
260
|
+
...histogramSamples,
|
|
261
|
+
];
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Returns the current UTC calendar day as `YYYY-MM-DD`.
|
|
265
|
+
*
|
|
266
|
+
* @param now - The current date; defaults to `new Date()`.
|
|
267
|
+
* @returns The UTC day string.
|
|
268
|
+
*/
|
|
269
|
+
export function todayUtcDay(now = new Date()) {
|
|
270
|
+
return now.toISOString().slice(0, 10);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Returns the latest `day` value across all sample points, or null for an empty array.
|
|
274
|
+
*
|
|
275
|
+
* @param samples - The sample points to inspect.
|
|
276
|
+
* @returns The maximum UTC day string, or null.
|
|
277
|
+
*/
|
|
278
|
+
export function maxDay(samples) {
|
|
279
|
+
if (samples.length === 0) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
// Reduce over string[] so there is no undefined initial value issue — reduce without
|
|
283
|
+
// initialValue on a non-empty array is always safe and avoids a dead branch.
|
|
284
|
+
return samples
|
|
285
|
+
.map((sample) => sample.day)
|
|
286
|
+
.reduce((max, day) => (day > max ? day : max));
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=fold.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fold.mjs","sourceRoot":"","sources":["../../src/lib/fold.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n Allowlist,\n EventTypeLabel,\n MetricName,\n SampleLabels,\n SamplePoint,\n ToolEvent,\n} from './types';\n\nconst TERMINAL_EVENT_TYPES: ReadonlySet<EventTypeLabel> = new Set([\n 'success',\n 'failure',\n 'interrupted',\n]);\n\n/**\n * Classic histogram bucket upper bounds in milliseconds, covering sub-second\n * scripts up to ~10 minutes. A synthetic `+Inf` bucket is always appended.\n */\nexport const DURATION_BUCKETS_MS: readonly number[] = [\n 100, 250, 500, 1000, 2000, 5000, 10000, 30000, 60000, 120000, 300000, 600000,\n];\n\n/** Accumulated histogram state for a single group/day. */\ntype HistogramAccumulator = {\n /** UTC calendar day this group belongs to. */\n day: string;\n /** Prometheus label set shared by every sample in this group. */\n labels: SampleLabels;\n /** Sum of all observed durations for this group. */\n sum: number;\n /** Count of all observed durations for this group. */\n count: number;\n /**\n * Cumulative bucket counts, one entry per `DURATION_BUCKETS_MS` boundary.\n * `bucketCounts[i]` is the number of observations where `duration_ms <= DURATION_BUCKETS_MS[i]`.\n */\n bucketCounts: number[];\n};\n\n/**\n * Extract the UTC calendar day from an ISO-8601 timestamp string.\n *\n * @param iso - An ISO-8601 timestamp string.\n * @returns The `YYYY-MM-DD` portion of the timestamp.\n */\nfunction utcDayFromIso(iso: string): string {\n return iso.slice(0, 10);\n}\n\n/**\n * Convert a `YYYY-MM-DD` UTC day string to a Unix timestamp at midnight UTC.\n *\n * @param day - A UTC day string in `YYYY-MM-DD` format.\n * @returns Milliseconds since epoch for midnight UTC on that day.\n */\nfunction utcMidnightMs(day: string): number {\n const parts = day.split('-');\n const year = Number(parts[0]);\n const month = Number(parts[1]);\n const dayOfMonth = Number(parts[2]);\n return Date.UTC(year, month - 1, dayOfMonth);\n}\n\n/**\n * Returns true when a given day has pending (unpushed) events.\n * Today's events are excluded so the day is complete before pushing.\n *\n * @param day - UTC calendar day of the event.\n * @param lastPushedDay - The most recently pushed day, or `null` if never pushed.\n * @param todayUtc - The current UTC calendar day.\n * @returns `true` when the day is after `lastPushedDay` and before today.\n */\nfunction isDayPending(\n day: string,\n lastPushedDay: string | null,\n todayUtc: string,\n): boolean {\n if (day >= todayUtc) {\n return false;\n }\n if (lastPushedDay === null) {\n return true;\n }\n return day > lastPushedDay;\n}\n\n/**\n * Strip the well-known tool-type prefix (`skill:` or `yarn:`) from a tool name.\n *\n * @param toolName - Raw tool name from the CSV event.\n * @param toolType - The parsed tool type for the event.\n * @returns The tool name without its prefix, or unchanged when no prefix matches.\n */\nfunction stripToolName(\n toolName: string,\n toolType: ToolEvent['tool_type'],\n): string {\n if (toolType === 'skill' && toolName.startsWith('skill:')) {\n return toolName.slice('skill:'.length);\n }\n if (toolType === 'yarn_script' && toolName.startsWith('yarn:')) {\n return toolName.slice('yarn:'.length);\n }\n return toolName;\n}\n\n/**\n * Map a raw CSV event to its Prometheus `event_type` label value.\n * Returns `null` for `end` events whose `success` field is not a boolean.\n *\n * @param event - The raw tool event to classify.\n * @returns The corresponding `EventTypeLabel`, or `null` to drop the event.\n */\nfunction mapEventType(event: ToolEvent): EventTypeLabel | null {\n // Switch over the exhaustive CsvEventType union.\n switch (event.event_type) {\n case 'start':\n return 'start';\n case 'interrupted':\n return 'interrupted';\n case 'end':\n if (event.success === true) {\n return 'success';\n }\n if (event.success === false) {\n return 'failure';\n }\n return null;\n default:\n return null;\n }\n}\n\n/**\n * Returns true when the stripped tool name appears on the allowlist for its type.\n *\n * @param strippedName - Tool name after prefix stripping.\n * @param toolType - The tool type (skill or yarn_script).\n * @param allowlist - The combined skill and script allowlist.\n * @returns `true` when the tool is allowlisted.\n */\nfunction isAllowlisted(\n strippedName: string,\n toolType: ToolEvent['tool_type'],\n allowlist: Allowlist,\n): boolean {\n if (toolType === 'skill') {\n // The allowlist is built from the MetaMask/ConsenSys skills repo cache:\n // extractSkillNames reads directory names (e.g. `create-pr`) and stores\n // them with the `mms-` prefix (e.g. `mms-create-pr`). A skill event passes\n // only when its logged name — after stripping the `skill:` type prefix —\n // is found verbatim in that Set. Skills not present in the repo cache\n // (e.g. personal Cursor skills logged as `skill:pr-review-queue`) never\n // match because the Set has `mms-pr-review-queue`, not `pr-review-queue`.\n return allowlist.skills.has(strippedName);\n }\n return allowlist.scripts.has(strippedName);\n}\n\n/** Opaque key used to group events into a single `SamplePoint`. */\ntype GroupKey = string;\n\n/**\n * Build a stable string key that uniquely identifies a (day, labels, metric) combination.\n *\n * @param day - UTC calendar day string.\n * @param labels - Prometheus label set for the sample.\n * @param metric - Prometheus metric name.\n * @returns A JSON-serialised key.\n */\nfunction groupKey(\n day: string,\n labels: SampleLabels,\n metric: MetricName,\n): GroupKey {\n return JSON.stringify({ day, metric, labels });\n}\n\n/**\n * Fold raw CSV events into anonymized Prometheus sample points for pending days.\n *\n * @param events - All parsed tool events.\n * @param allowlist - Skill and script allowlists used to filter events.\n * @param instanceUuid - Per-install UUID used as the Prometheus `instance` label.\n * @param lastPushedDay - The last UTC day that was successfully pushed, or null.\n * @param todayUtc - The current UTC day (events on this day are not yet pushed).\n * @returns An array of sample points ready for Prometheus exposition.\n */\nexport function fold(\n events: ToolEvent[],\n allowlist: Allowlist,\n instanceUuid: string,\n lastPushedDay: string | null,\n todayUtc: string,\n): SamplePoint[] {\n const usageCounts = new Map<GroupKey, SamplePoint>();\n const durationSums = new Map<GroupKey, SamplePoint>();\n // Keyed by the same group key as durationSums (terminal events with a duration).\n const durationHistograms = new Map<GroupKey, HistogramAccumulator>();\n\n for (const event of events) {\n const day = utcDayFromIso(event.created_at);\n if (!isDayPending(day, lastPushedDay, todayUtc)) {\n continue;\n }\n\n const strippedName = stripToolName(event.tool_name, event.tool_type);\n if (!isAllowlisted(strippedName, event.tool_type, allowlist)) {\n continue;\n }\n\n const eventTypeLabel = mapEventType(event);\n if (!eventTypeLabel) {\n continue;\n }\n\n const labels: SampleLabels = {\n repo: event.repo,\n tool_name: strippedName,\n tool_type: event.tool_type,\n event_type: eventTypeLabel,\n agent_vendor: event.agent_vendor,\n instance: instanceUuid,\n };\n\n const usageMetric: MetricName = 'metamask_devtools_usage_count';\n const usageKey = groupKey(day, labels, usageMetric);\n const existingUsage = usageCounts.get(usageKey);\n if (existingUsage) {\n existingUsage.value += 1;\n } else {\n usageCounts.set(usageKey, {\n metric: usageMetric,\n labels,\n value: 1,\n day,\n timestampMs: utcMidnightMs(day),\n });\n }\n\n if (\n TERMINAL_EVENT_TYPES.has(eventTypeLabel) &&\n event.duration_ms !== null\n ) {\n const durationMetric: MetricName = 'metamask_devtools_duration_sum_ms';\n const durationKey = groupKey(day, labels, durationMetric);\n const existingDuration = durationSums.get(durationKey);\n if (existingDuration) {\n existingDuration.value += event.duration_ms;\n } else {\n durationSums.set(durationKey, {\n metric: durationMetric,\n labels,\n value: event.duration_ms,\n day,\n timestampMs: utcMidnightMs(day),\n });\n }\n\n // Accumulate the histogram for the same group key (reuse durationKey\n // since both are keyed on the same (day, labels) combination).\n const existing = durationHistograms.get(durationKey);\n if (existing) {\n existing.sum += event.duration_ms;\n existing.count += 1;\n // Cumulative: increment every bucket whose upper bound >= duration.\n existing.bucketCounts = existing.bucketCounts.map((count, i) =>\n event.duration_ms !== null &&\n event.duration_ms <= (DURATION_BUCKETS_MS[i] as number)\n ? count + 1\n : count,\n );\n } else {\n const bucketCounts = DURATION_BUCKETS_MS.map((bound) =>\n event.duration_ms !== null && event.duration_ms <= bound ? 1 : 0,\n );\n durationHistograms.set(durationKey, {\n day,\n labels,\n sum: event.duration_ms,\n count: 1,\n bucketCounts,\n });\n }\n }\n }\n\n const histogramSamples: SamplePoint[] = [];\n for (const acc of durationHistograms.values()) {\n const { day, labels } = acc;\n const timestampMs = utcMidnightMs(day);\n\n histogramSamples.push({\n metric: 'metamask_devtools_duration_ms_sum',\n labels,\n value: acc.sum,\n day,\n timestampMs,\n });\n\n histogramSamples.push({\n metric: 'metamask_devtools_duration_ms_count',\n labels,\n value: acc.count,\n day,\n timestampMs,\n });\n\n acc.bucketCounts.forEach((count, i) => {\n histogramSamples.push({\n metric: 'metamask_devtools_duration_ms_bucket',\n labels,\n value: count,\n day,\n timestampMs,\n le: String(DURATION_BUCKETS_MS[i]),\n });\n });\n\n // The +Inf bucket equals total count (every observation falls below +Inf).\n histogramSamples.push({\n metric: 'metamask_devtools_duration_ms_bucket',\n labels,\n value: acc.count,\n day,\n timestampMs,\n le: '+Inf',\n });\n }\n\n return [\n ...usageCounts.values(),\n ...durationSums.values(),\n ...histogramSamples,\n ];\n}\n\n/**\n * Returns the current UTC calendar day as `YYYY-MM-DD`.\n *\n * @param now - The current date; defaults to `new Date()`.\n * @returns The UTC day string.\n */\nexport function todayUtcDay(now = new Date()): string {\n return now.toISOString().slice(0, 10);\n}\n\n/**\n * Returns the latest `day` value across all sample points, or null for an empty array.\n *\n * @param samples - The sample points to inspect.\n * @returns The maximum UTC day string, or null.\n */\nexport function maxDay(samples: SamplePoint[]): string | null {\n if (samples.length === 0) {\n return null;\n }\n // Reduce over string[] so there is no undefined initial value issue — reduce without\n // initialValue on a non-empty array is always safe and avoids a dead branch.\n return samples\n .map((sample) => sample.day)\n .reduce((max, day) => (day > max ? day : max));\n}\n"]}
|
package/dist/lib/log.cjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLogger = createLogger;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const paths_1 = require("./paths.cjs");
|
|
7
|
+
const MAX_LOG_BYTES = 1024 * 1024;
|
|
8
|
+
const MAX_ROTATED = 3;
|
|
9
|
+
/**
|
|
10
|
+
* Create the log directory if it does not yet exist.
|
|
11
|
+
*
|
|
12
|
+
* @param path - Absolute path to the log file whose parent directory to create.
|
|
13
|
+
*/
|
|
14
|
+
function ensureLogDir(path) {
|
|
15
|
+
const dir = (0, node_path_1.dirname)(path);
|
|
16
|
+
if (!(0, node_fs_1.existsSync)(dir)) {
|
|
17
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Rotate the log file when it exceeds 1 MiB.
|
|
22
|
+
*
|
|
23
|
+
* @param path - Absolute path to the log file to rotate.
|
|
24
|
+
*/
|
|
25
|
+
function rotateIfNeeded(path) {
|
|
26
|
+
if (!(0, node_fs_1.existsSync)(path)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const { size } = (0, node_fs_1.statSync)(path);
|
|
30
|
+
if (size <= MAX_LOG_BYTES) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
for (let index = MAX_ROTATED; index >= 1; index -= 1) {
|
|
34
|
+
const from = index === 1 ? path : `${path}.${index - 1}`;
|
|
35
|
+
const to = `${path}.${index}`;
|
|
36
|
+
if ((0, node_fs_1.existsSync)(from)) {
|
|
37
|
+
if ((0, node_fs_1.existsSync)(to)) {
|
|
38
|
+
(0, node_fs_1.unlinkSync)(to);
|
|
39
|
+
}
|
|
40
|
+
(0, node_fs_1.renameSync)(from, to);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create a structured logger that appends JSON lines to the anonymizer log file,
|
|
46
|
+
* rotating when the file exceeds 1 MiB.
|
|
47
|
+
*
|
|
48
|
+
* @param cli - The CLI name to embed in every log entry.
|
|
49
|
+
* @param logPath - Path to the log file; defaults to the configured anonymizer log path.
|
|
50
|
+
* @returns An object with `info`, `error`, and `readLog` methods.
|
|
51
|
+
*/
|
|
52
|
+
function createLogger(cli, logPath = (0, paths_1.getAnonymizerLogPath)()) {
|
|
53
|
+
/**
|
|
54
|
+
* Append an entry to the log file, rotating first when needed.
|
|
55
|
+
*
|
|
56
|
+
* @param entry - The log entry to serialise.
|
|
57
|
+
* @param verbose - When true, also writes to stdout.
|
|
58
|
+
*/
|
|
59
|
+
const write = (entry, verbose) => {
|
|
60
|
+
ensureLogDir(logPath);
|
|
61
|
+
rotateIfNeeded(logPath);
|
|
62
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
63
|
+
(0, node_fs_1.writeFileSync)(logPath, line, { flag: 'a' });
|
|
64
|
+
if (verbose) {
|
|
65
|
+
process.stdout.write(line);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
/**
|
|
70
|
+
* Log an informational message.
|
|
71
|
+
*
|
|
72
|
+
* @param message - The message to log.
|
|
73
|
+
* @param extra - Optional extra data to attach to the log entry.
|
|
74
|
+
* @param verbose - When true, also writes to stdout.
|
|
75
|
+
*/
|
|
76
|
+
info(message, extra, verbose = false) {
|
|
77
|
+
write({
|
|
78
|
+
ts: new Date().toISOString(),
|
|
79
|
+
cli,
|
|
80
|
+
level: 'info',
|
|
81
|
+
message,
|
|
82
|
+
// exactOptionalPropertyTypes: omit the key when undefined rather than
|
|
83
|
+
// assigning undefined to an optional property.
|
|
84
|
+
...(extra === undefined ? {} : { extra }),
|
|
85
|
+
}, verbose);
|
|
86
|
+
},
|
|
87
|
+
/**
|
|
88
|
+
* Log an error message.
|
|
89
|
+
*
|
|
90
|
+
* @param message - The message to log.
|
|
91
|
+
* @param extra - Optional extra data to attach to the log entry.
|
|
92
|
+
* @param verbose - When true, also writes to stdout.
|
|
93
|
+
*/
|
|
94
|
+
error(message, extra, verbose = false) {
|
|
95
|
+
write({
|
|
96
|
+
ts: new Date().toISOString(),
|
|
97
|
+
cli,
|
|
98
|
+
level: 'error',
|
|
99
|
+
message,
|
|
100
|
+
...(extra === undefined ? {} : { extra }),
|
|
101
|
+
}, verbose);
|
|
102
|
+
},
|
|
103
|
+
/**
|
|
104
|
+
* Read the full contents of the log file.
|
|
105
|
+
*
|
|
106
|
+
* @returns The log file contents, or an empty string if the file does not exist.
|
|
107
|
+
*/
|
|
108
|
+
readLog() {
|
|
109
|
+
if (!(0, node_fs_1.existsSync)(logPath)) {
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
return (0, node_fs_1.readFileSync)(logPath, 'utf8');
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=log.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.cjs","sourceRoot":"","sources":["../../src/lib/log.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n statSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname } from 'node:path';\n\nimport { getAnonymizerLogPath } from './paths';\n\n/** One structured log entry written to the anonymizer log file. */\nexport type LogEntry = {\n /** ISO-8601 timestamp of when the entry was written. */\n ts: string;\n /** The CLI name that produced the entry. */\n cli: string;\n /** Severity level. */\n level: 'info' | 'error';\n /** Human-readable message. */\n message: string;\n /** Optional extra structured data to attach to the entry. */\n extra?: Record<string, unknown>;\n};\n\n/** The type returned by `createLogger`. */\nexport type LoggerInstance = {\n /** Log an informational message. */\n info: (\n message: string,\n extra?: Record<string, unknown>,\n verbose?: boolean,\n ) => void;\n /** Log an error message. */\n error: (\n message: string,\n extra?: Record<string, unknown>,\n verbose?: boolean,\n ) => void;\n /** Read the full contents of the log file. */\n readLog: () => string;\n};\n\nconst MAX_LOG_BYTES = 1024 * 1024;\nconst MAX_ROTATED = 3;\n\n/**\n * Create the log directory if it does not yet exist.\n *\n * @param path - Absolute path to the log file whose parent directory to create.\n */\nfunction ensureLogDir(path: string): void {\n const dir = dirname(path);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n}\n\n/**\n * Rotate the log file when it exceeds 1 MiB.\n *\n * @param path - Absolute path to the log file to rotate.\n */\nfunction rotateIfNeeded(path: string): void {\n if (!existsSync(path)) {\n return;\n }\n const { size } = statSync(path);\n if (size <= MAX_LOG_BYTES) {\n return;\n }\n for (let index = MAX_ROTATED; index >= 1; index -= 1) {\n const from = index === 1 ? path : `${path}.${index - 1}`;\n const to = `${path}.${index}`;\n if (existsSync(from)) {\n if (existsSync(to)) {\n unlinkSync(to);\n }\n renameSync(from, to);\n }\n }\n}\n\n/**\n * Create a structured logger that appends JSON lines to the anonymizer log file,\n * rotating when the file exceeds 1 MiB.\n *\n * @param cli - The CLI name to embed in every log entry.\n * @param logPath - Path to the log file; defaults to the configured anonymizer log path.\n * @returns An object with `info`, `error`, and `readLog` methods.\n */\nexport function createLogger(\n cli: string,\n logPath = getAnonymizerLogPath(),\n): LoggerInstance {\n /**\n * Append an entry to the log file, rotating first when needed.\n *\n * @param entry - The log entry to serialise.\n * @param verbose - When true, also writes to stdout.\n */\n const write = (entry: LogEntry, verbose: boolean): void => {\n ensureLogDir(logPath);\n rotateIfNeeded(logPath);\n const line = `${JSON.stringify(entry)}\\n`;\n writeFileSync(logPath, line, { flag: 'a' });\n if (verbose) {\n process.stdout.write(line);\n }\n };\n\n return {\n /**\n * Log an informational message.\n *\n * @param message - The message to log.\n * @param extra - Optional extra data to attach to the log entry.\n * @param verbose - When true, also writes to stdout.\n */\n info(\n message: string,\n extra?: Record<string, unknown>,\n verbose = false,\n ): void {\n write(\n {\n ts: new Date().toISOString(),\n cli,\n level: 'info',\n message,\n // exactOptionalPropertyTypes: omit the key when undefined rather than\n // assigning undefined to an optional property.\n ...(extra === undefined ? {} : { extra }),\n },\n verbose,\n );\n },\n /**\n * Log an error message.\n *\n * @param message - The message to log.\n * @param extra - Optional extra data to attach to the log entry.\n * @param verbose - When true, also writes to stdout.\n */\n error(\n message: string,\n extra?: Record<string, unknown>,\n verbose = false,\n ): void {\n write(\n {\n ts: new Date().toISOString(),\n cli,\n level: 'error',\n message,\n ...(extra === undefined ? {} : { extra }),\n },\n verbose,\n );\n },\n /**\n * Read the full contents of the log file.\n *\n * @returns The log file contents, or an empty string if the file does not exist.\n */\n readLog(): string {\n if (!existsSync(logPath)) {\n return '';\n }\n return readFileSync(logPath, 'utf8');\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** One structured log entry written to the anonymizer log file. */
|
|
2
|
+
export type LogEntry = {
|
|
3
|
+
/** ISO-8601 timestamp of when the entry was written. */
|
|
4
|
+
ts: string;
|
|
5
|
+
/** The CLI name that produced the entry. */
|
|
6
|
+
cli: string;
|
|
7
|
+
/** Severity level. */
|
|
8
|
+
level: 'info' | 'error';
|
|
9
|
+
/** Human-readable message. */
|
|
10
|
+
message: string;
|
|
11
|
+
/** Optional extra structured data to attach to the entry. */
|
|
12
|
+
extra?: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
/** The type returned by `createLogger`. */
|
|
15
|
+
export type LoggerInstance = {
|
|
16
|
+
/** Log an informational message. */
|
|
17
|
+
info: (message: string, extra?: Record<string, unknown>, verbose?: boolean) => void;
|
|
18
|
+
/** Log an error message. */
|
|
19
|
+
error: (message: string, extra?: Record<string, unknown>, verbose?: boolean) => void;
|
|
20
|
+
/** Read the full contents of the log file. */
|
|
21
|
+
readLog: () => string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Create a structured logger that appends JSON lines to the anonymizer log file,
|
|
25
|
+
* rotating when the file exceeds 1 MiB.
|
|
26
|
+
*
|
|
27
|
+
* @param cli - The CLI name to embed in every log entry.
|
|
28
|
+
* @param logPath - Path to the log file; defaults to the configured anonymizer log path.
|
|
29
|
+
* @returns An object with `info`, `error`, and `readLog` methods.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createLogger(cli: string, logPath?: string): LoggerInstance;
|
|
32
|
+
//# sourceMappingURL=log.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.cts","sourceRoot":"","sources":["../../src/lib/log.ts"],"names":[],"mappings":"AAaA,mEAAmE;AACnE,MAAM,MAAM,QAAQ,GAAG;IACrB,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,sBAAsB;IACtB,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF,2CAA2C;AAC3C,MAAM,MAAM,cAAc,GAAG;IAC3B,oCAAoC;IACpC,IAAI,EAAE,CACJ,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,CAAC,EAAE,OAAO,KACd,IAAI,CAAC;IACV,4BAA4B;IAC5B,KAAK,EAAE,CACL,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,CAAC,EAAE,OAAO,KACd,IAAI,CAAC;IACV,8CAA8C;IAC9C,OAAO,EAAE,MAAM,MAAM,CAAC;CACvB,CAAC;AA0CF;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,OAAO,SAAyB,GAC/B,cAAc,CA8EhB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** One structured log entry written to the anonymizer log file. */
|
|
2
|
+
export type LogEntry = {
|
|
3
|
+
/** ISO-8601 timestamp of when the entry was written. */
|
|
4
|
+
ts: string;
|
|
5
|
+
/** The CLI name that produced the entry. */
|
|
6
|
+
cli: string;
|
|
7
|
+
/** Severity level. */
|
|
8
|
+
level: 'info' | 'error';
|
|
9
|
+
/** Human-readable message. */
|
|
10
|
+
message: string;
|
|
11
|
+
/** Optional extra structured data to attach to the entry. */
|
|
12
|
+
extra?: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
/** The type returned by `createLogger`. */
|
|
15
|
+
export type LoggerInstance = {
|
|
16
|
+
/** Log an informational message. */
|
|
17
|
+
info: (message: string, extra?: Record<string, unknown>, verbose?: boolean) => void;
|
|
18
|
+
/** Log an error message. */
|
|
19
|
+
error: (message: string, extra?: Record<string, unknown>, verbose?: boolean) => void;
|
|
20
|
+
/** Read the full contents of the log file. */
|
|
21
|
+
readLog: () => string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Create a structured logger that appends JSON lines to the anonymizer log file,
|
|
25
|
+
* rotating when the file exceeds 1 MiB.
|
|
26
|
+
*
|
|
27
|
+
* @param cli - The CLI name to embed in every log entry.
|
|
28
|
+
* @param logPath - Path to the log file; defaults to the configured anonymizer log path.
|
|
29
|
+
* @returns An object with `info`, `error`, and `readLog` methods.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createLogger(cli: string, logPath?: string): LoggerInstance;
|
|
32
|
+
//# sourceMappingURL=log.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.mts","sourceRoot":"","sources":["../../src/lib/log.ts"],"names":[],"mappings":"AAaA,mEAAmE;AACnE,MAAM,MAAM,QAAQ,GAAG;IACrB,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,sBAAsB;IACtB,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF,2CAA2C;AAC3C,MAAM,MAAM,cAAc,GAAG;IAC3B,oCAAoC;IACpC,IAAI,EAAE,CACJ,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,CAAC,EAAE,OAAO,KACd,IAAI,CAAC;IACV,4BAA4B;IAC5B,KAAK,EAAE,CACL,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,CAAC,EAAE,OAAO,KACd,IAAI,CAAC;IACV,8CAA8C;IAC9C,OAAO,EAAE,MAAM,MAAM,CAAC;CACvB,CAAC;AA0CF;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,OAAO,SAAyB,GAC/B,cAAc,CA8EhB"}
|