@mushinjs/plugin-stats 0.0.0-stage → 0.2.0-next.20261113103537

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Setobox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,52 @@
1
+ /** Counts for one hour. Keys hold the bot id where it matters, e.g. `${botId}|${userId}`. */
2
+ export interface Counters {
3
+ /** Bot id → messages received. */
4
+ in: Record<string, number>;
5
+ /** Bot id → messages sent. */
6
+ out: Record<string, number>;
7
+ /** `${botId}|${type}:${id}` → messages received in that chat. */
8
+ channels: Record<string, number>;
9
+ /** `${botId}|${userId}` → messages received from that person. */
10
+ users: Record<string, number>;
11
+ /** `${botId}|${command}` → times the command ran. */
12
+ commands: Record<string, number>;
13
+ /** Flow id → live runs. */
14
+ flows: Record<string, number>;
15
+ /** Flow id → live runs that failed. */
16
+ flowErrors: Record<string, number>;
17
+ /** `${provider name}|${model}` → model requests. */
18
+ llm: Record<string, {
19
+ calls: number;
20
+ errors: number;
21
+ tokens: number;
22
+ }>;
23
+ }
24
+ /**
25
+ * One local day: counters per hour (`00`–`23`). Days older than the hourly retention keep one
26
+ * bucket, `*`, for the whole day. `names` remembers display names of chats, people and flows.
27
+ */
28
+ export interface DayDoc {
29
+ day: string;
30
+ hours: Record<string, Counters>;
31
+ names: Record<string, string>;
32
+ }
33
+ export declare const emptyCounters: () => Counters;
34
+ /** Local date and hour of a time, e.g. `['2026-10-25', '09']`. */
35
+ export declare function dayHour(time: number | Date): [string, string];
36
+ /** Midnight of a local day key. */
37
+ export declare function dayStart(day: string): number;
38
+ export declare function bump(map: Record<string, number>, key: string, by?: number): void;
39
+ /** Adds `from` into `into`. */
40
+ export declare function merge(into: Counters, from: Counters): Counters;
41
+ /** A day with its hours folded into one bucket, for days kept only as daily totals. */
42
+ export declare function compact(doc: DayDoc): DayDoc;
43
+ export declare const sum: (map: Record<string, number>) => number;
44
+ export type Metric = 'messages' | 'received' | 'sent' | 'commands' | 'flows' | 'llmCalls' | 'llmTokens';
45
+ /**
46
+ * The value of a metric in one bucket, split into series: messages by bot, model usage by
47
+ * provider and model; the rest as one `total` series.
48
+ */
49
+ export declare function metricValues(counters: Counters, metric: Metric): Record<string, number>;
50
+ export type Dimension = 'channels' | 'users' | 'commands' | 'flows' | 'llm';
51
+ /** Counts of one dimension, highest first. */
52
+ export declare function top(counters: Counters, dimension: Dimension, limit: number): (readonly [string, number])[];
@@ -0,0 +1,84 @@
1
+ // src/counters.ts
2
+ const emptyCounters = () => ({
3
+ in: {},
4
+ out: {},
5
+ channels: {},
6
+ users: {},
7
+ commands: {},
8
+ flows: {},
9
+ flowErrors: {},
10
+ llm: {}
11
+ });
12
+ const pad = (value) => String(value).padStart(2, "0");
13
+ /** Local date and hour of a time, e.g. `['2026-10-25', '09']`. */
14
+ function dayHour(time) {
15
+ const date = new Date(time);
16
+ return [`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`, pad(date.getHours())];
17
+ }
18
+ /** Midnight of a local day key. */
19
+ function dayStart(day) {
20
+ const [year, month, date] = day.split("-").map(Number);
21
+ return new Date(year, month - 1, date).getTime();
22
+ }
23
+ function bump(map, key, by = 1) {
24
+ map[key] = (map[key] ?? 0) + by;
25
+ }
26
+ /** Adds `from` into `into`. */
27
+ function merge(into, from) {
28
+ for (const key of [
29
+ "in",
30
+ "out",
31
+ "channels",
32
+ "users",
33
+ "commands",
34
+ "flows",
35
+ "flowErrors"
36
+ ]) for (const [name, count] of Object.entries(from[key] ?? {})) bump(into[key], name, count);
37
+ for (const [name, usage] of Object.entries(from.llm ?? {})) {
38
+ const entry = into.llm[name] ??= {
39
+ calls: 0,
40
+ errors: 0,
41
+ tokens: 0
42
+ };
43
+ entry.calls += usage.calls;
44
+ entry.errors += usage.errors;
45
+ entry.tokens += usage.tokens;
46
+ }
47
+ return into;
48
+ }
49
+ /** A day with its hours folded into one bucket, for days kept only as daily totals. */
50
+ function compact(doc) {
51
+ const total = emptyCounters();
52
+ for (const counters of Object.values(doc.hours)) merge(total, counters);
53
+ return {
54
+ ...doc,
55
+ hours: { "*": total }
56
+ };
57
+ }
58
+ const sum = (map) => Object.values(map).reduce((total, value) => total + value, 0);
59
+ /**
60
+ * The value of a metric in one bucket, split into series: messages by bot, model usage by
61
+ * provider and model; the rest as one `total` series.
62
+ */
63
+ function metricValues(counters, metric) {
64
+ switch (metric) {
65
+ case "messages": {
66
+ const values = {};
67
+ for (const key of ["in", "out"]) for (const [bot, count] of Object.entries(counters[key])) bump(values, bot, count);
68
+ return values;
69
+ }
70
+ case "received": return { ...counters.in };
71
+ case "sent": return { ...counters.out };
72
+ case "commands": return { total: sum(counters.commands) };
73
+ case "flows": return { total: sum(counters.flows) };
74
+ case "llmCalls": return Object.fromEntries(Object.entries(counters.llm).map(([key, usage]) => [key, usage.calls]));
75
+ case "llmTokens": return Object.fromEntries(Object.entries(counters.llm).map(([key, usage]) => [key, usage.tokens]));
76
+ }
77
+ }
78
+ /** Counts of one dimension, highest first. */
79
+ function top(counters, dimension, limit) {
80
+ return (dimension === "llm" ? Object.entries(counters.llm).map(([key, usage]) => [key, usage.calls]) : Object.entries(counters[dimension])).sort((a, b) => b[1] - a[1]).slice(0, limit);
81
+ }
82
+ export { bump, compact, dayHour, dayStart, emptyCounters, merge, metricValues, sum, top };
83
+
84
+ //# sourceMappingURL=counters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"counters.js","names":[],"sources":["../src/counters.ts"],"sourcesContent":["/** Counts for one hour. Keys hold the bot id where it matters, e.g. `${botId}|${userId}`. */\nexport interface Counters {\n /** Bot id → messages received. */\n in: Record<string, number>\n /** Bot id → messages sent. */\n out: Record<string, number>\n /** `${botId}|${type}:${id}` → messages received in that chat. */\n channels: Record<string, number>\n /** `${botId}|${userId}` → messages received from that person. */\n users: Record<string, number>\n /** `${botId}|${command}` → times the command ran. */\n commands: Record<string, number>\n /** Flow id → live runs. */\n flows: Record<string, number>\n /** Flow id → live runs that failed. */\n flowErrors: Record<string, number>\n /** `${provider name}|${model}` → model requests. */\n llm: Record<string, { calls: number; errors: number; tokens: number }>\n}\n\n/**\n * One local day: counters per hour (`00`–`23`). Days older than the hourly retention keep one\n * bucket, `*`, for the whole day. `names` remembers display names of chats, people and flows.\n */\nexport interface DayDoc {\n day: string\n hours: Record<string, Counters>\n names: Record<string, string>\n}\n\nexport const emptyCounters = (): Counters => ({ in: {}, out: {}, channels: {}, users: {}, commands: {}, flows: {}, flowErrors: {}, llm: {} })\n\nconst pad = (value: number) => String(value).padStart(2, '0')\n\n/** Local date and hour of a time, e.g. `['2026-10-25', '09']`. */\nexport function dayHour(time: number | Date): [string, string] {\n const date = new Date(time)\n return [`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`, pad(date.getHours())]\n}\n\n/** Midnight of a local day key. */\nexport function dayStart(day: string) {\n const [year, month, date] = day.split('-').map(Number)\n return new Date(year, month - 1, date).getTime()\n}\n\nexport function bump(map: Record<string, number>, key: string, by = 1) {\n map[key] = (map[key] ?? 0) + by\n}\n\n/** Adds `from` into `into`. */\nexport function merge(into: Counters, from: Counters) {\n for (const key of ['in', 'out', 'channels', 'users', 'commands', 'flows', 'flowErrors'] as const) {\n for (const [name, count] of Object.entries(from[key] ?? {})) bump(into[key], name, count)\n }\n for (const [name, usage] of Object.entries(from.llm ?? {})) {\n const entry = into.llm[name] ??= { calls: 0, errors: 0, tokens: 0 }\n entry.calls += usage.calls\n entry.errors += usage.errors\n entry.tokens += usage.tokens\n }\n return into\n}\n\n/** A day with its hours folded into one bucket, for days kept only as daily totals. */\nexport function compact(doc: DayDoc): DayDoc {\n const total = emptyCounters()\n for (const counters of Object.values(doc.hours)) merge(total, counters)\n return { ...doc, hours: { '*': total } }\n}\n\nexport const sum = (map: Record<string, number>) => Object.values(map).reduce((total, value) => total + value, 0)\n\nexport type Metric = 'messages' | 'received' | 'sent' | 'commands' | 'flows' | 'llmCalls' | 'llmTokens'\n\n/**\n * The value of a metric in one bucket, split into series: messages by bot, model usage by\n * provider and model; the rest as one `total` series.\n */\nexport function metricValues(counters: Counters, metric: Metric): Record<string, number> {\n switch (metric) {\n case 'messages': {\n const values: Record<string, number> = {}\n for (const key of ['in', 'out'] as const) for (const [bot, count] of Object.entries(counters[key])) bump(values, bot, count)\n return values\n }\n case 'received': return { ...counters.in }\n case 'sent': return { ...counters.out }\n case 'commands': return { total: sum(counters.commands) }\n case 'flows': return { total: sum(counters.flows) }\n case 'llmCalls': return Object.fromEntries(Object.entries(counters.llm).map(([key, usage]) => [key, usage.calls]))\n case 'llmTokens': return Object.fromEntries(Object.entries(counters.llm).map(([key, usage]) => [key, usage.tokens]))\n }\n}\n\nexport type Dimension = 'channels' | 'users' | 'commands' | 'flows' | 'llm'\n\n/** Counts of one dimension, highest first. */\nexport function top(counters: Counters, dimension: Dimension, limit: number) {\n const entries = dimension === 'llm'\n ? Object.entries(counters.llm).map(([key, usage]) => [key, usage.calls] as const)\n : Object.entries(counters[dimension])\n return entries.sort((a, b) => b[1] - a[1]).slice(0, limit)\n}\n"],"mappings":";AA8BA,MAAa,uBAAiC;CAAE,IAAI,CAAC;CAAG,KAAK,CAAC;CAAG,UAAU,CAAC;CAAG,OAAO,CAAC;CAAG,UAAU,CAAC;CAAG,OAAO,CAAC;CAAG,YAAY,CAAC;CAAG,KAAK,CAAC;AAAE;AAE3I,MAAM,OAAO,UAAkB,OAAO,KAAK,CAAC,CAAC,SAAS,GAAG,GAAG;;AAG5D,SAAgB,QAAQ,MAAuC;CAC7D,MAAM,OAAO,IAAI,KAAK,IAAI;CAC1B,OAAO,CAAC,GAAG,KAAK,YAAY,EAAE,GAAG,IAAI,KAAK,SAAS,IAAI,CAAC,EAAE,GAAG,IAAI,KAAK,QAAQ,CAAC,KAAK,IAAI,KAAK,SAAS,CAAC,CAAC;AAC1G;;AAGA,SAAgB,SAAS,KAAa;CACpC,MAAM,CAAC,MAAM,OAAO,QAAQ,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM;CACrD,OAAO,IAAI,KAAK,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,QAAQ;AACjD;AAEA,SAAgB,KAAK,KAA6B,KAAa,KAAK,GAAG;CACrE,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAC/B;;AAGA,SAAgB,MAAM,MAAgB,MAAgB;CACpD,KAAK,MAAM,OAAO;EAAC;EAAM;EAAO;EAAY;EAAS;EAAY;EAAS;CAAY,GACpF,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,GAAG,KAAK,KAAK,MAAM,MAAM,KAAK;CAE1F,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,OAAO,CAAC,CAAC,GAAG;EAC1D,MAAM,QAAQ,KAAK,IAAI,UAAU;GAAE,OAAO;GAAG,QAAQ;GAAG,QAAQ;EAAE;EAClE,MAAM,SAAS,MAAM;EACrB,MAAM,UAAU,MAAM;EACtB,MAAM,UAAU,MAAM;CACxB;CACA,OAAO;AACT;;AAGA,SAAgB,QAAQ,KAAqB;CAC3C,MAAM,QAAQ,cAAc;CAC5B,KAAK,MAAM,YAAY,OAAO,OAAO,IAAI,KAAK,GAAG,MAAM,OAAO,QAAQ;CACtE,OAAO;EAAE,GAAG;EAAK,OAAO,EAAE,KAAK,MAAM;CAAE;AACzC;AAEA,MAAa,OAAO,QAAgC,OAAO,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,UAAU,QAAQ,OAAO,CAAC;;;;;AAQhH,SAAgB,aAAa,UAAoB,QAAwC;CACvF,QAAQ,QAAR;EACE,KAAK,YAAY;GACf,MAAM,SAAiC,CAAC;GACxC,KAAK,MAAM,OAAO,CAAC,MAAM,KAAK,GAAY,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,IAAI,GAAG,KAAK,QAAQ,KAAK,KAAK;GAC3H,OAAO;EACT;EACA,KAAK,YAAY,OAAO,EAAE,GAAG,SAAS,GAAG;EACzC,KAAK,QAAQ,OAAO,EAAE,GAAG,SAAS,IAAI;EACtC,KAAK,YAAY,OAAO,EAAE,OAAO,IAAI,SAAS,QAAQ,EAAE;EACxD,KAAK,SAAS,OAAO,EAAE,OAAO,IAAI,SAAS,KAAK,EAAE;EAClD,KAAK,YAAY,OAAO,OAAO,YAAY,OAAO,QAAQ,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC;EACjH,KAAK,aAAa,OAAO,OAAO,YAAY,OAAO,QAAQ,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CAAC,KAAK,MAAM,MAAM,CAAC,CAAC;CACrH;AACF;;AAKA,SAAgB,IAAI,UAAoB,WAAsB,OAAe;CAI3E,QAHgB,cAAc,QAC1B,OAAO,QAAQ,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CAAC,KAAK,MAAM,KAAK,CAAU,IAC9E,OAAO,QAAQ,SAAS,UAAU,EAAA,CACvB,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,MAAM,GAAG,KAAK;AAC3D"}
@@ -0,0 +1,162 @@
1
+ import { Counters, DayDoc, Dimension, Metric, bump, compact, dayHour, dayStart, emptyCounters, merge, metricValues, sum, top } from "./counters.js";
2
+ import { Context, Service } from "@mushinjs/core";
3
+ declare module '@mushinjs/core' {
4
+ interface Context {
5
+ stats: StatsService;
6
+ }
7
+ interface Events {
8
+ 'stats/sample'(sample: PerfSample): void;
9
+ }
10
+ }
11
+ /** Process load at one moment: CPU in percent of one core, memory in bytes, event loop delay in ms. */
12
+ export interface PerfSample {
13
+ t: number;
14
+ cpu: number;
15
+ rss: number;
16
+ heapUsed: number;
17
+ heapTotal: number;
18
+ loopMean: number;
19
+ loopMax: number;
20
+ }
21
+ /**
22
+ * Counts messages, commands, flow runs and model requests per hour, stored as one document per
23
+ * day under `stats/`, and samples the process's CPU, memory and event loop for the overview page.
24
+ */
25
+ export declare class StatsService extends Service {
26
+ static inject: string[];
27
+ private home;
28
+ private days;
29
+ private dirty;
30
+ private samples;
31
+ private dataSize?;
32
+ private startedAt;
33
+ constructor(ctx: Context);
34
+ [Service.init](): AsyncGenerator<(() => boolean) | import("cordis").Disposable<Promise<void>>, void, unknown>;
35
+ private load;
36
+ /** Adds to the bucket of the hour `time` falls in. */
37
+ private count;
38
+ /** Writes changed days; days other than today leave memory once written. */
39
+ flush(): Promise<void>;
40
+ /** Folds days past the hourly retention into daily totals and deletes days past the daily one. */
41
+ private prune;
42
+ /** Days covering the last `hours` hours, oldest first. */
43
+ private daysFor;
44
+ /** Counters for whole hours in [from, to], one bucket each, plus the doc they came from. */
45
+ private buckets;
46
+ /** Totals since local midnight. */
47
+ today(): Promise<{
48
+ received: number;
49
+ sent: number;
50
+ commands: number;
51
+ flowRuns: number;
52
+ flowErrors: number;
53
+ llmCalls: number;
54
+ llmTokens: number;
55
+ }>;
56
+ /**
57
+ * A metric over the last `hours` hours, one point per hour or per day, split into series (by bot
58
+ * for messages, by provider and model for model usage).
59
+ */
60
+ series(input: {
61
+ metric: Metric;
62
+ hours: number;
63
+ granularity?: 'hour' | 'day';
64
+ }): Promise<{
65
+ granularity: "hour" | "day";
66
+ series: {
67
+ key: string;
68
+ name: string;
69
+ }[];
70
+ points: {
71
+ t: number;
72
+ values: Record<string, number>;
73
+ }[];
74
+ }>;
75
+ /** The most active chats, people, commands, flows or models over the last `days` days. */
76
+ top(input: {
77
+ dimension: Dimension;
78
+ days: number;
79
+ limit?: number;
80
+ }): Promise<({
81
+ key: string;
82
+ name: string;
83
+ count: number;
84
+ errors: number;
85
+ tokens?: undefined;
86
+ id?: undefined;
87
+ botId?: undefined;
88
+ bot?: undefined;
89
+ } | {
90
+ key: string;
91
+ name: string;
92
+ count: number;
93
+ tokens: number;
94
+ errors: number;
95
+ id?: undefined;
96
+ botId?: undefined;
97
+ bot?: undefined;
98
+ } | {
99
+ key: string;
100
+ name: string;
101
+ id: string;
102
+ botId: string;
103
+ bot: string | undefined;
104
+ count: number;
105
+ errors?: undefined;
106
+ tokens?: undefined;
107
+ })[]>;
108
+ /** The last hour of process samples, oldest first. */
109
+ performance(): PerfSample[];
110
+ private sampler;
111
+ private dataDirSize;
112
+ /** Everything the overview's number cards and version panel show. */
113
+ overview(): Promise<{
114
+ bots: {
115
+ online: number;
116
+ total: number;
117
+ };
118
+ appPlugins: {
119
+ enabled: number;
120
+ total: number;
121
+ };
122
+ botPlugins: {
123
+ enabled: number;
124
+ };
125
+ flows: {
126
+ total: number;
127
+ enabled: number;
128
+ withErrors: number;
129
+ };
130
+ commands: number;
131
+ providers: {
132
+ total: number;
133
+ passed: number;
134
+ };
135
+ today: {
136
+ received: number;
137
+ sent: number;
138
+ commands: number;
139
+ flowRuns: number;
140
+ flowErrors: number;
141
+ llmCalls: number;
142
+ llmTokens: number;
143
+ };
144
+ versions: {
145
+ mushin: string | undefined;
146
+ node: string;
147
+ cordis: string | undefined;
148
+ plugins: {
149
+ package: string;
150
+ enabled: boolean;
151
+ version: string | undefined;
152
+ }[];
153
+ };
154
+ platform: string;
155
+ startedAt: number;
156
+ uptime: number;
157
+ dataBytes: number;
158
+ sample: PerfSample | undefined;
159
+ }>;
160
+ private defineActions;
161
+ }
162
+ export { Counters, DayDoc, Dimension, Metric, StatsService as default, bump, compact, dayHour, dayStart, emptyCounters, merge, metricValues, sum, top };
package/dist/index.js ADDED
@@ -0,0 +1,418 @@
1
+ import { bump, compact, dayHour, dayStart, emptyCounters, merge, metricValues, sum, top } from "./counters.js";
2
+ import { createRequire } from "node:module";
3
+ import { readdir, stat } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { monitorEventLoopDelay } from "node:perf_hooks";
6
+ import { Service, z } from "@mushinjs/core";
7
+ import { isSandbox } from "@mushinjs/plugin-bots";
8
+ // src/index.ts
9
+ const HOUR = 36e5;
10
+ const DAY = 24 * HOUR;
11
+ /** One sample every 5 seconds, an hour of them kept. */
12
+ const SAMPLE_EVERY = 5e3;
13
+ const SAMPLE_LIMIT = 720;
14
+ const FLUSH_EVERY = 3e4;
15
+ /** Hourly buckets are kept this many days, then folded into one bucket per day. */
16
+ const HOURLY_DAYS = 30;
17
+ /** Daily totals are kept this many days. */
18
+ const DAILY_DAYS = 365;
19
+ const DATA_SIZE_TTL = 6e4;
20
+ /** The event loop histogram measures timer ticks of this many ms; the delay is what goes beyond it. */
21
+ const LOOP_RESOLUTION = 10;
22
+ const lag = (ns) => Math.max(0, Math.round((ns / 1e6 - LOOP_RESOLUTION) * 100) / 100);
23
+ const require = createRequire(import.meta.url);
24
+ const versionOf = (name) => {
25
+ try {
26
+ return require(`${name}/package.json`).version;
27
+ } catch {}
28
+ };
29
+ const METRICS = [
30
+ "messages",
31
+ "received",
32
+ "sent",
33
+ "commands",
34
+ "flows",
35
+ "llmCalls",
36
+ "llmTokens"
37
+ ];
38
+ const DIMENSIONS = [
39
+ "channels",
40
+ "users",
41
+ "commands",
42
+ "flows",
43
+ "llm"
44
+ ];
45
+ async function directorySize(dir) {
46
+ let total = 0;
47
+ for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
48
+ const path = join(dir, entry.name);
49
+ if (entry.isDirectory()) total += await directorySize(path);
50
+ else if (entry.isFile()) total += (await stat(path).catch(() => void 0))?.size ?? 0;
51
+ }
52
+ return total;
53
+ }
54
+ /**
55
+ * Counts messages, commands, flow runs and model requests per hour, stored as one document per
56
+ * day under `stats/`, and samples the process's CPU, memory and event loop for the overview page.
57
+ */
58
+ var StatsService = class extends Service {
59
+ static inject = ["storage", "actions"];
60
+ home;
61
+ days = /* @__PURE__ */ new Map();
62
+ dirty = /* @__PURE__ */ new Set();
63
+ samples = [];
64
+ dataSize;
65
+ startedAt = Date.now();
66
+ constructor(ctx) {
67
+ super(ctx, "stats");
68
+ this.home = ctx;
69
+ }
70
+ async *[Service.init]() {
71
+ const [today] = dayHour(Date.now());
72
+ await this.prune(today);
73
+ yield this.home.on("bots/message", (record) => !isSandbox(record.channel) && this.count(record.timestamp, (counters, doc) => {
74
+ if (record.direction === "out") return bump(counters.out, record.botId);
75
+ bump(counters.in, record.botId);
76
+ const channel = `${record.botId}|${record.channel.type}:${record.channel.id}`;
77
+ const user = `${record.botId}|${record.user.id}`;
78
+ bump(counters.channels, channel);
79
+ bump(counters.users, user);
80
+ if (record.channel.name) doc.names[channel] = record.channel.name;
81
+ if (record.user.name) doc.names[user] = record.user.name;
82
+ }));
83
+ yield this.home.on("bots/command", (call) => this.count(call.at, (counters) => bump(counters.commands, `${call.botId}|${call.name}`)));
84
+ yield this.home.on("flows/run", (trace) => {
85
+ if (trace.test) return;
86
+ this.count(trace.at, (counters, doc) => {
87
+ bump(counters.flows, trace.flowId);
88
+ if (trace.error) bump(counters.flowErrors, trace.flowId);
89
+ const name = this.home.get("flows")?.list().find((flow) => flow.id === trace.flowId)?.name;
90
+ if (name) doc.names[`flow:${trace.flowId}`] = name;
91
+ });
92
+ });
93
+ yield this.home.on("llm/called", (call) => this.count(Date.now(), (counters) => {
94
+ const usage = counters.llm[`${call.providerName}|${call.model}`] ??= {
95
+ calls: 0,
96
+ errors: 0,
97
+ tokens: 0
98
+ };
99
+ usage.calls++;
100
+ if (!call.ok) usage.errors++;
101
+ usage.tokens += (call.inputTokens ?? 0) + (call.outputTokens ?? 0);
102
+ }));
103
+ yield this.home.effect(() => {
104
+ const timer = setInterval(() => {
105
+ const [day] = dayHour(Date.now());
106
+ this.flush().then(() => this.prune(day)).catch((error) => this.home.logger("stats").warn("saving statistics failed: %s", error));
107
+ }, FLUSH_EVERY);
108
+ return () => {
109
+ clearInterval(timer);
110
+ return this.flush();
111
+ };
112
+ }, "stats flush");
113
+ yield this.home.effect(() => this.sampler(), "stats sampler");
114
+ yield* this.defineActions();
115
+ }
116
+ async load(day) {
117
+ let doc = this.days.get(day);
118
+ if (!doc) {
119
+ const stored = await this.home.storage.get(`stats/${day}`);
120
+ doc = this.days.get(day) ?? {
121
+ day,
122
+ hours: stored?.hours ?? {},
123
+ names: stored?.names ?? {}
124
+ };
125
+ this.days.set(day, doc);
126
+ }
127
+ return doc;
128
+ }
129
+ /** Adds to the bucket of the hour `time` falls in. */
130
+ count(time, update) {
131
+ const [day, hour] = dayHour(time);
132
+ const apply = (doc) => {
133
+ update(doc.hours[hour] ??= emptyCounters(), doc);
134
+ this.dirty.add(day);
135
+ };
136
+ const cached = this.days.get(day);
137
+ if (cached) apply(cached);
138
+ else this.load(day).then(apply).catch(() => {});
139
+ }
140
+ /** Writes changed days; days other than today leave memory once written. */
141
+ async flush() {
142
+ const [today] = dayHour(Date.now());
143
+ for (const day of [...this.dirty]) {
144
+ this.dirty.delete(day);
145
+ const doc = this.days.get(day);
146
+ if (doc) await this.home.storage.set(`stats/${day}`, doc);
147
+ }
148
+ for (const day of this.days.keys()) if (day !== today && !this.dirty.has(day)) this.days.delete(day);
149
+ }
150
+ /** Folds days past the hourly retention into daily totals and deletes days past the daily one. */
151
+ async prune(today) {
152
+ const now = dayStart(today);
153
+ for (const key of await this.home.storage.list("stats")) {
154
+ const day = key.slice(6);
155
+ const age = (now - dayStart(day)) / DAY;
156
+ if (!Number.isFinite(age) || age < HOURLY_DAYS) continue;
157
+ if (age >= DAILY_DAYS) {
158
+ await this.home.storage.delete(key);
159
+ continue;
160
+ }
161
+ const doc = await this.home.storage.get(key);
162
+ if (doc && !doc.hours["*"]) await this.home.storage.set(key, compact(doc));
163
+ }
164
+ }
165
+ /** Days covering the last `hours` hours, oldest first. */
166
+ async daysFor(from, to) {
167
+ const docs = [];
168
+ for (let time = dayStart(dayHour(from)[0]); time <= to; time += DAY) docs.push(await this.load(dayHour(time + 2 * HOUR)[0]));
169
+ return docs;
170
+ }
171
+ /** Counters for whole hours in [from, to], one bucket each, plus the doc they came from. */
172
+ async buckets(from, to) {
173
+ const result = [];
174
+ for (const doc of await this.daysFor(from, to)) {
175
+ const start = dayStart(doc.day);
176
+ for (const [hour, counters] of Object.entries(doc.hours)) {
177
+ const time = hour === "*" ? start : start + Number(hour) * HOUR;
178
+ if (time + (hour === "*" ? DAY : HOUR) <= from || time > to) continue;
179
+ result.push({
180
+ time,
181
+ counters,
182
+ doc
183
+ });
184
+ }
185
+ }
186
+ return result;
187
+ }
188
+ /** Totals since local midnight. */
189
+ async today() {
190
+ const now = Date.now();
191
+ const total = emptyCounters();
192
+ for (const { counters } of await this.buckets(dayStart(dayHour(now)[0]), now)) merge(total, counters);
193
+ const llm = Object.values(total.llm);
194
+ return {
195
+ received: sum(total.in),
196
+ sent: sum(total.out),
197
+ commands: sum(total.commands),
198
+ flowRuns: sum(total.flows),
199
+ flowErrors: sum(total.flowErrors),
200
+ llmCalls: llm.reduce((count, usage) => count + usage.calls, 0),
201
+ llmTokens: llm.reduce((count, usage) => count + usage.tokens, 0)
202
+ };
203
+ }
204
+ /**
205
+ * A metric over the last `hours` hours, one point per hour or per day, split into series (by bot
206
+ * for messages, by provider and model for model usage).
207
+ */
208
+ async series(input) {
209
+ const granularity = input.granularity ?? (input.hours > 72 ? "day" : "hour");
210
+ const now = Date.now();
211
+ const step = granularity === "day" ? DAY : HOUR;
212
+ const first = granularity === "day" ? dayStart(dayHour(now - (Math.ceil(input.hours / 24) - 1) * DAY)[0]) : Math.floor(now / HOUR) * HOUR - (input.hours - 1) * HOUR;
213
+ const slotOf = (time) => granularity === "day" ? dayStart(dayHour(time)[0]) : Math.floor(time / HOUR) * HOUR;
214
+ const points = /* @__PURE__ */ new Map();
215
+ for (let time = first; time <= now; time += step) points.set(slotOf(time + (granularity === "day" ? 2 * HOUR : 0)), {});
216
+ const keys = /* @__PURE__ */ new Set();
217
+ for (const { time, counters } of await this.buckets(first, now)) {
218
+ const values = points.get(slotOf(time));
219
+ if (!values) continue;
220
+ for (const [key, value] of Object.entries(metricValues(counters, input.metric))) {
221
+ keys.add(key);
222
+ bump(values, key, value);
223
+ }
224
+ }
225
+ const bots = this.home.get("bots");
226
+ const nameOf = (key) => {
227
+ if (input.metric === "messages" || input.metric === "received" || input.metric === "sent") return bots?.list().find((bot) => bot.id === key)?.name ?? key;
228
+ return key === "total" ? key : key.replace("|", " · ");
229
+ };
230
+ return {
231
+ granularity,
232
+ series: [...keys].sort().map((key) => ({
233
+ key,
234
+ name: nameOf(key)
235
+ })),
236
+ points: [...points].map(([t, values]) => ({
237
+ t,
238
+ values
239
+ }))
240
+ };
241
+ }
242
+ /** The most active chats, people, commands, flows or models over the last `days` days. */
243
+ async top(input) {
244
+ const now = Date.now();
245
+ const from = dayStart(dayHour(now - (input.days - 1) * DAY)[0]);
246
+ const total = emptyCounters();
247
+ const names = {};
248
+ for (const { counters, doc } of await this.buckets(from, now)) {
249
+ merge(total, counters);
250
+ Object.assign(names, doc.names);
251
+ }
252
+ const bots = this.home.get("bots");
253
+ const flows = this.home.get("flows");
254
+ const botName = (id) => bots?.list().find((bot) => bot.id === id)?.name;
255
+ return top(total, input.dimension, input.limit ?? 10).map(([key, count]) => {
256
+ if (input.dimension === "flows") return {
257
+ key,
258
+ name: flows?.list().find((flow) => flow.id === key)?.name ?? names[`flow:${key}`] ?? key,
259
+ count,
260
+ errors: total.flowErrors[key] ?? 0
261
+ };
262
+ if (input.dimension === "llm") {
263
+ const [provider, model] = key.split("|");
264
+ return {
265
+ key,
266
+ name: `${provider} · ${model}`,
267
+ count,
268
+ tokens: total.llm[key].tokens,
269
+ errors: total.llm[key].errors
270
+ };
271
+ }
272
+ const [botId, rest] = [key.slice(0, key.indexOf("|")), key.slice(key.indexOf("|") + 1)];
273
+ return {
274
+ key,
275
+ name: input.dimension === "commands" ? `/${rest}` : names[key] ?? rest.replace(/^(group|private):/, ""),
276
+ id: rest,
277
+ botId,
278
+ bot: botName(botId),
279
+ count
280
+ };
281
+ });
282
+ }
283
+ /** The last hour of process samples, oldest first. */
284
+ performance() {
285
+ return this.samples;
286
+ }
287
+ sampler() {
288
+ const histogram = monitorEventLoopDelay({ resolution: LOOP_RESOLUTION });
289
+ histogram.enable();
290
+ let cpu = process.cpuUsage();
291
+ let at = performance.now();
292
+ const timer = setInterval(() => {
293
+ const nextCpu = process.cpuUsage();
294
+ const nextAt = performance.now();
295
+ const used = (nextCpu.user - cpu.user + nextCpu.system - cpu.system) / 1e3;
296
+ const memory = process.memoryUsage();
297
+ const sample = {
298
+ t: Date.now(),
299
+ cpu: Math.round(used / Math.max(1, nextAt - at) * 1e3) / 10,
300
+ rss: memory.rss,
301
+ heapUsed: memory.heapUsed,
302
+ heapTotal: memory.heapTotal,
303
+ loopMean: Number.isFinite(histogram.mean) ? lag(histogram.mean) : 0,
304
+ loopMax: lag(histogram.max)
305
+ };
306
+ cpu = nextCpu;
307
+ at = nextAt;
308
+ histogram.reset();
309
+ this.samples.push(sample);
310
+ if (this.samples.length > SAMPLE_LIMIT) this.samples.splice(0, this.samples.length - SAMPLE_LIMIT);
311
+ this.home.emit("stats/sample", sample);
312
+ }, SAMPLE_EVERY);
313
+ timer.unref?.();
314
+ return () => {
315
+ clearInterval(timer);
316
+ histogram.disable();
317
+ };
318
+ }
319
+ async dataDirSize() {
320
+ if (!this.dataSize || Date.now() - this.dataSize.at > DATA_SIZE_TTL) this.dataSize = {
321
+ at: Date.now(),
322
+ bytes: await directorySize(this.home.storage.dir)
323
+ };
324
+ return this.dataSize.bytes;
325
+ }
326
+ /** Everything the overview's number cards and version panel show. */
327
+ async overview() {
328
+ const bots = this.home.get("bots");
329
+ const flows = this.home.get("flows");
330
+ const llm = this.home.get("llm");
331
+ const manager = this.home.manager;
332
+ const botList = bots?.list() ?? [];
333
+ const flowList = flows?.list() ?? [];
334
+ const providers = llm?.list() ?? [];
335
+ const commands = new Set((bots?.commandList().commands ?? []).map((entry) => `${entry.botId}|${entry.name.toLowerCase()}`));
336
+ const catalog = await manager.catalog().catch(() => []);
337
+ const versions = new Map(catalog.map((entry) => [entry.package, entry.version]));
338
+ return {
339
+ bots: {
340
+ online: botList.filter((bot) => bot.status === "online").length,
341
+ total: botList.length
342
+ },
343
+ appPlugins: {
344
+ enabled: manager.entries.filter((entry) => entry.enabled !== false).length,
345
+ total: manager.entries.length
346
+ },
347
+ botPlugins: { enabled: botList.reduce((count, bot) => count + bot.plugins.filter((plugin) => plugin.enabled).length, 0) },
348
+ flows: {
349
+ total: flowList.length,
350
+ enabled: flowList.filter((flow) => flow.enabled).length,
351
+ withErrors: flowList.filter((flow) => flow.errors > 0).length
352
+ },
353
+ commands: commands.size,
354
+ providers: {
355
+ total: providers.length,
356
+ passed: providers.filter((provider) => provider.lastTest?.ok).length
357
+ },
358
+ today: await this.today(),
359
+ versions: {
360
+ mushin: versionOf("@mushinjs/core"),
361
+ node: process.version,
362
+ cordis: versionOf("cordis"),
363
+ plugins: manager.entries.map((entry) => ({
364
+ package: entry.package,
365
+ enabled: entry.enabled !== false,
366
+ version: versions.get(entry.package)
367
+ }))
368
+ },
369
+ platform: `${process.platform} ${process.arch}`,
370
+ startedAt: this.startedAt,
371
+ uptime: Math.round(process.uptime()),
372
+ dataBytes: await this.dataDirSize(),
373
+ sample: this.samples.at(-1)
374
+ };
375
+ }
376
+ *defineActions() {
377
+ const actions = this.home.actions;
378
+ yield actions.define({
379
+ name: "stats.overview",
380
+ description: "Instance at a glance: bots online, plugins, flows (and how many have errors), commands, providers, today's messages, commands, flow runs and model tokens, versions, uptime and data directory size.",
381
+ risk: "read",
382
+ input: z.object({}),
383
+ handler: () => this.overview()
384
+ });
385
+ yield actions.define({
386
+ name: "stats.series",
387
+ description: "A metric over time, one point per hour or day: messages (split by bot), received, sent, commands, flows (runs), llmCalls or llmTokens (split by provider and model).",
388
+ risk: "read",
389
+ input: z.object({
390
+ metric: z.enum(METRICS),
391
+ hours: z.number().int().min(1).max(8784).default(24).describe("How far back, e.g. 24 or 168 (7 days)"),
392
+ granularity: z.enum(["hour", "day"]).optional().describe("Defaults to hour up to 3 days, day beyond")
393
+ }),
394
+ handler: (input) => this.series(input)
395
+ });
396
+ yield actions.define({
397
+ name: "stats.top",
398
+ description: "The busiest chats, users, commands, flows or models (llm) over the last days, highest first.",
399
+ risk: "read",
400
+ input: z.object({
401
+ dimension: z.enum(DIMENSIONS),
402
+ days: z.number().int().min(1).max(366).default(7),
403
+ limit: z.number().int().min(1).max(50).optional()
404
+ }),
405
+ handler: (input) => this.top(input)
406
+ });
407
+ yield actions.define({
408
+ name: "stats.performance",
409
+ description: "Process CPU (percent of one core), memory (bytes) and event loop delay (ms), one sample every 5 seconds for the last hour.",
410
+ risk: "read",
411
+ input: z.object({}),
412
+ handler: () => this.performance()
413
+ });
414
+ }
415
+ };
416
+ export { StatsService, StatsService as default, bump, compact, dayHour, dayStart, emptyCounters, merge, metricValues, sum, top };
417
+
418
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { readdir, stat } from 'node:fs/promises'\nimport { createRequire } from 'node:module'\nimport { join } from 'node:path'\nimport { monitorEventLoopDelay } from 'node:perf_hooks'\nimport { Context, Service, z } from '@mushinjs/core'\nimport { isSandbox } from '@mushinjs/plugin-bots'\nimport type {} from '@mushinjs/plugin-flows'\nimport type {} from '@mushinjs/plugin-providers'\nimport type {} from '@mushinjs/plugin-storage'\nimport {\n bump, compact, type Counters, type DayDoc, dayHour, dayStart, type Dimension, emptyCounters, merge, type Metric, metricValues, sum, top,\n} from './counters'\n\nexport * from './counters'\n\ndeclare module '@mushinjs/core' {\n interface Context {\n stats: StatsService\n }\n\n interface Events {\n 'stats/sample'(sample: PerfSample): void\n }\n}\n\n/** Process load at one moment: CPU in percent of one core, memory in bytes, event loop delay in ms. */\nexport interface PerfSample {\n t: number\n cpu: number\n rss: number\n heapUsed: number\n heapTotal: number\n loopMean: number\n loopMax: number\n}\n\nconst HOUR = 3_600_000\nconst DAY = 24 * HOUR\n/** One sample every 5 seconds, an hour of them kept. */\nconst SAMPLE_EVERY = 5_000\nconst SAMPLE_LIMIT = 720\nconst FLUSH_EVERY = 30_000\n/** Hourly buckets are kept this many days, then folded into one bucket per day. */\nconst HOURLY_DAYS = 30\n/** Daily totals are kept this many days. */\nconst DAILY_DAYS = 365\nconst DATA_SIZE_TTL = 60_000\n/** The event loop histogram measures timer ticks of this many ms; the delay is what goes beyond it. */\nconst LOOP_RESOLUTION = 10\nconst lag = (ns: number) => Math.max(0, Math.round((ns / 1e6 - LOOP_RESOLUTION) * 100) / 100)\n\nconst require = createRequire(import.meta.url)\nconst versionOf = (name: string) => {\n try {\n return (require(`${name}/package.json`) as { version: string }).version\n } catch {}\n}\n\nconst METRICS = ['messages', 'received', 'sent', 'commands', 'flows', 'llmCalls', 'llmTokens'] as const satisfies Metric[]\nconst DIMENSIONS = ['channels', 'users', 'commands', 'flows', 'llm'] as const satisfies Dimension[]\n\nasync function directorySize(dir: string): Promise<number> {\n let total = 0\n for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {\n const path = join(dir, entry.name)\n if (entry.isDirectory()) total += await directorySize(path)\n else if (entry.isFile()) total += (await stat(path).catch(() => undefined))?.size ?? 0\n }\n return total\n}\n\n/**\n * Counts messages, commands, flow runs and model requests per hour, stored as one document per\n * day under `stats/`, and samples the process's CPU, memory and event loop for the overview page.\n */\nexport class StatsService extends Service {\n static inject = ['storage', 'actions']\n\n private home: Context\n private days = new Map<string, DayDoc>()\n private dirty = new Set<string>()\n private samples: PerfSample[] = []\n private dataSize?: { at: number; bytes: number }\n private startedAt = Date.now()\n\n constructor(ctx: Context) {\n super(ctx, 'stats')\n this.home = ctx\n }\n\n async *[Service.init]() {\n const [today] = dayHour(Date.now())\n await this.prune(today)\n yield this.home.on('bots/message', (record) => !isSandbox(record.channel) && this.count(record.timestamp, (counters, doc) => {\n if (record.direction === 'out') return bump(counters.out, record.botId)\n bump(counters.in, record.botId)\n const channel = `${record.botId}|${record.channel.type}:${record.channel.id}`\n const user = `${record.botId}|${record.user.id}`\n bump(counters.channels, channel)\n bump(counters.users, user)\n if (record.channel.name) doc.names[channel] = record.channel.name\n if (record.user.name) doc.names[user] = record.user.name\n }))\n yield this.home.on('bots/command', (call) => this.count(call.at, (counters) => bump(counters.commands, `${call.botId}|${call.name}`)))\n yield this.home.on('flows/run', (trace) => {\n if (trace.test) return\n this.count(trace.at, (counters, doc) => {\n bump(counters.flows, trace.flowId)\n if (trace.error) bump(counters.flowErrors, trace.flowId)\n const name = this.home.get('flows')?.list().find((flow) => flow.id === trace.flowId)?.name\n if (name) doc.names[`flow:${trace.flowId}`] = name\n })\n })\n yield this.home.on('llm/called', (call) => this.count(Date.now(), (counters) => {\n const usage = counters.llm[`${call.providerName}|${call.model}`] ??= { calls: 0, errors: 0, tokens: 0 }\n usage.calls++\n if (!call.ok) usage.errors++\n usage.tokens += (call.inputTokens ?? 0) + (call.outputTokens ?? 0)\n }))\n yield this.home.effect(() => {\n const timer = setInterval(() => {\n const [day] = dayHour(Date.now())\n this.flush().then(() => this.prune(day)).catch((error) => this.home.logger('stats').warn('saving statistics failed: %s', error))\n }, FLUSH_EVERY)\n return () => {\n clearInterval(timer)\n return this.flush()\n }\n }, 'stats flush')\n yield this.home.effect(() => this.sampler(), 'stats sampler')\n yield* this.defineActions()\n }\n\n // ---- counting ----\n\n private async load(day: string) {\n let doc = this.days.get(day)\n if (!doc) {\n const stored = await this.home.storage.get<DayDoc>(`stats/${day}`)\n // Counting may have started on this day while it was being read.\n doc = this.days.get(day) ?? { day, hours: stored?.hours ?? {}, names: stored?.names ?? {} }\n this.days.set(day, doc)\n }\n return doc\n }\n\n /** Adds to the bucket of the hour `time` falls in. */\n private count(time: number, update: (counters: Counters, doc: DayDoc) => void) {\n const [day, hour] = dayHour(time)\n const apply = (doc: DayDoc) => {\n update(doc.hours[hour] ??= emptyCounters(), doc)\n this.dirty.add(day)\n }\n const cached = this.days.get(day)\n if (cached) apply(cached)\n else this.load(day).then(apply).catch(() => {})\n }\n\n /** Writes changed days; days other than today leave memory once written. */\n async flush() {\n const [today] = dayHour(Date.now())\n for (const day of [...this.dirty]) {\n this.dirty.delete(day)\n const doc = this.days.get(day)\n if (doc) await this.home.storage.set(`stats/${day}`, doc)\n }\n for (const day of this.days.keys()) if (day !== today && !this.dirty.has(day)) this.days.delete(day)\n }\n\n /** Folds days past the hourly retention into daily totals and deletes days past the daily one. */\n private async prune(today: string) {\n const now = dayStart(today)\n for (const key of await this.home.storage.list('stats')) {\n const day = key.slice('stats/'.length)\n const age = (now - dayStart(day)) / DAY\n if (!Number.isFinite(age) || age < HOURLY_DAYS) continue\n if (age >= DAILY_DAYS) {\n await this.home.storage.delete(key)\n continue\n }\n const doc = await this.home.storage.get<DayDoc>(key)\n if (doc && !doc.hours['*']) await this.home.storage.set(key, compact(doc))\n }\n }\n\n /** Days covering the last `hours` hours, oldest first. */\n private async daysFor(from: number, to: number) {\n const docs: DayDoc[] = []\n for (let time = dayStart(dayHour(from)[0]); time <= to; time += DAY) {\n // Daylight saving can make a day 23 or 25 hours long; take the day the time falls in.\n docs.push(await this.load(dayHour(time + 2 * HOUR)[0]))\n }\n return docs\n }\n\n /** Counters for whole hours in [from, to], one bucket each, plus the doc they came from. */\n private async buckets(from: number, to: number) {\n const result: { time: number; counters: Counters; doc: DayDoc }[] = []\n for (const doc of await this.daysFor(from, to)) {\n const start = dayStart(doc.day)\n for (const [hour, counters] of Object.entries(doc.hours)) {\n const time = hour === '*' ? start : start + Number(hour) * HOUR\n if (time + (hour === '*' ? DAY : HOUR) <= from || time > to) continue\n result.push({ time, counters, doc })\n }\n }\n return result\n }\n\n // ---- queries ----\n\n /** Totals since local midnight. */\n async today() {\n const now = Date.now()\n const total = emptyCounters()\n for (const { counters } of await this.buckets(dayStart(dayHour(now)[0]), now)) merge(total, counters)\n const llm = Object.values(total.llm)\n return {\n received: sum(total.in),\n sent: sum(total.out),\n commands: sum(total.commands),\n flowRuns: sum(total.flows),\n flowErrors: sum(total.flowErrors),\n llmCalls: llm.reduce((count, usage) => count + usage.calls, 0),\n llmTokens: llm.reduce((count, usage) => count + usage.tokens, 0),\n }\n }\n\n /**\n * A metric over the last `hours` hours, one point per hour or per day, split into series (by bot\n * for messages, by provider and model for model usage).\n */\n async series(input: { metric: Metric; hours: number; granularity?: 'hour' | 'day' }) {\n const granularity = input.granularity ?? (input.hours > 72 ? 'day' : 'hour')\n const now = Date.now()\n const step = granularity === 'day' ? DAY : HOUR\n const first = granularity === 'day'\n ? dayStart(dayHour(now - (Math.ceil(input.hours / 24) - 1) * DAY)[0])\n : Math.floor(now / HOUR) * HOUR - (input.hours - 1) * HOUR\n const slotOf = (time: number) => (granularity === 'day' ? dayStart(dayHour(time)[0]) : Math.floor(time / HOUR) * HOUR)\n const points = new Map<number, Record<string, number>>()\n for (let time = first; time <= now; time += step) points.set(slotOf(time + (granularity === 'day' ? 2 * HOUR : 0)), {})\n const keys = new Set<string>()\n for (const { time, counters } of await this.buckets(first, now)) {\n const values = points.get(slotOf(time))\n if (!values) continue\n for (const [key, value] of Object.entries(metricValues(counters, input.metric))) {\n keys.add(key)\n bump(values, key, value)\n }\n }\n const bots = this.home.get('bots')\n const nameOf = (key: string) => {\n if (input.metric === 'messages' || input.metric === 'received' || input.metric === 'sent') {\n return bots?.list().find((bot) => bot.id === key)?.name ?? key\n }\n return key === 'total' ? key : key.replace('|', ' · ')\n }\n return {\n granularity,\n series: [...keys].sort().map((key) => ({ key, name: nameOf(key) })),\n points: [...points].map(([t, values]) => ({ t, values })),\n }\n }\n\n /** The most active chats, people, commands, flows or models over the last `days` days. */\n async top(input: { dimension: Dimension; days: number; limit?: number }) {\n const now = Date.now()\n const from = dayStart(dayHour(now - (input.days - 1) * DAY)[0])\n const total = emptyCounters()\n const names: Record<string, string> = {}\n for (const { counters, doc } of await this.buckets(from, now)) {\n merge(total, counters)\n Object.assign(names, doc.names)\n }\n const bots = this.home.get('bots')\n const flows = this.home.get('flows')\n const botName = (id: string) => bots?.list().find((bot) => bot.id === id)?.name\n return top(total, input.dimension, input.limit ?? 10).map(([key, count]) => {\n if (input.dimension === 'flows') {\n return { key, name: flows?.list().find((flow) => flow.id === key)?.name ?? names[`flow:${key}`] ?? key, count, errors: total.flowErrors[key] ?? 0 }\n }\n if (input.dimension === 'llm') {\n const [provider, model] = key.split('|')\n return { key, name: `${provider} · ${model}`, count, tokens: total.llm[key].tokens, errors: total.llm[key].errors }\n }\n const [botId, rest] = [key.slice(0, key.indexOf('|')), key.slice(key.indexOf('|') + 1)]\n const name = input.dimension === 'commands' ? `/${rest}` : names[key] ?? rest.replace(/^(group|private):/, '')\n return { key, name, id: rest, botId, bot: botName(botId), count }\n })\n }\n\n /** The last hour of process samples, oldest first. */\n performance() {\n return this.samples\n }\n\n private sampler() {\n const histogram = monitorEventLoopDelay({ resolution: LOOP_RESOLUTION })\n histogram.enable()\n let cpu = process.cpuUsage()\n let at = performance.now()\n const timer = setInterval(() => {\n const nextCpu = process.cpuUsage()\n const nextAt = performance.now()\n const used = (nextCpu.user - cpu.user + nextCpu.system - cpu.system) / 1000\n const memory = process.memoryUsage()\n const sample: PerfSample = {\n t: Date.now(),\n cpu: Math.round((used / Math.max(1, nextAt - at)) * 1000) / 10,\n rss: memory.rss,\n heapUsed: memory.heapUsed,\n heapTotal: memory.heapTotal,\n loopMean: Number.isFinite(histogram.mean) ? lag(histogram.mean) : 0,\n loopMax: lag(histogram.max),\n }\n cpu = nextCpu\n at = nextAt\n histogram.reset()\n this.samples.push(sample)\n if (this.samples.length > SAMPLE_LIMIT) this.samples.splice(0, this.samples.length - SAMPLE_LIMIT)\n this.home.emit('stats/sample', sample)\n }, SAMPLE_EVERY)\n timer.unref?.()\n return () => {\n clearInterval(timer)\n histogram.disable()\n }\n }\n\n private async dataDirSize() {\n if (!this.dataSize || Date.now() - this.dataSize.at > DATA_SIZE_TTL) {\n this.dataSize = { at: Date.now(), bytes: await directorySize(this.home.storage.dir) }\n }\n return this.dataSize.bytes\n }\n\n /** Everything the overview's number cards and version panel show. */\n async overview() {\n const bots = this.home.get('bots')\n const flows = this.home.get('flows')\n const llm = this.home.get('llm')\n const manager = this.home.manager\n const botList = bots?.list() ?? []\n const flowList = flows?.list() ?? []\n const providers = llm?.list() ?? []\n const commands = new Set((bots?.commandList().commands ?? []).map((entry) => `${entry.botId}|${entry.name.toLowerCase()}`))\n const catalog = await manager.catalog().catch(() => [])\n const versions = new Map(catalog.map((entry) => [entry.package, entry.version]))\n return {\n bots: { online: botList.filter((bot) => bot.status === 'online').length, total: botList.length },\n appPlugins: { enabled: manager.entries.filter((entry) => entry.enabled !== false).length, total: manager.entries.length },\n botPlugins: { enabled: botList.reduce((count, bot) => count + bot.plugins.filter((plugin) => plugin.enabled).length, 0) },\n flows: { total: flowList.length, enabled: flowList.filter((flow) => flow.enabled).length, withErrors: flowList.filter((flow) => flow.errors > 0).length },\n commands: commands.size,\n providers: { total: providers.length, passed: providers.filter((provider) => provider.lastTest?.ok).length },\n today: await this.today(),\n versions: {\n mushin: versionOf('@mushinjs/core'),\n node: process.version,\n cordis: versionOf('cordis'),\n plugins: manager.entries.map((entry) => ({ package: entry.package, enabled: entry.enabled !== false, version: versions.get(entry.package) })),\n },\n platform: `${process.platform} ${process.arch}`,\n startedAt: this.startedAt,\n uptime: Math.round(process.uptime()),\n dataBytes: await this.dataDirSize(),\n sample: this.samples.at(-1),\n }\n }\n\n // ---- actions ----\n\n private *defineActions() {\n const actions = this.home.actions\n\n yield actions.define({\n name: 'stats.overview',\n description: 'Instance at a glance: bots online, plugins, flows (and how many have errors), commands, providers, today\\'s messages, commands, flow runs and model tokens, versions, uptime and data directory size.',\n risk: 'read',\n input: z.object({}),\n handler: () => this.overview(),\n })\n\n yield actions.define({\n name: 'stats.series',\n description: 'A metric over time, one point per hour or day: messages (split by bot), received, sent, commands, flows (runs), llmCalls or llmTokens (split by provider and model).',\n risk: 'read',\n input: z.object({\n metric: z.enum(METRICS),\n hours: z.number().int().min(1).max(24 * 366).default(24).describe('How far back, e.g. 24 or 168 (7 days)'),\n granularity: z.enum(['hour', 'day']).optional().describe('Defaults to hour up to 3 days, day beyond'),\n }),\n handler: (input) => this.series(input),\n })\n\n yield actions.define({\n name: 'stats.top',\n description: 'The busiest chats, users, commands, flows or models (llm) over the last days, highest first.',\n risk: 'read',\n input: z.object({\n dimension: z.enum(DIMENSIONS),\n days: z.number().int().min(1).max(366).default(7),\n limit: z.number().int().min(1).max(50).optional(),\n }),\n handler: (input) => this.top(input),\n })\n\n yield actions.define({\n name: 'stats.performance',\n description: 'Process CPU (percent of one core), memory (bytes) and event loop delay (ms), one sample every 5 seconds for the last hour.',\n risk: 'read',\n input: z.object({}),\n handler: () => this.performance(),\n })\n }\n}\n\nexport default StatsService\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@mushinjs/plugin-stats",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0-next.20261113103537",
4
+ "description": "Usage statistics and runtime performance for the overview page",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mushinjs/mushin.git",
10
+ "directory": "packages/plugin-stats"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "keywords": [
26
+ "mushin-plugin"
27
+ ],
28
+ "mushin": {
29
+ "kind": "app",
30
+ "displayName": {
31
+ "en": "Statistics",
32
+ "zh-CN": "统计"
33
+ },
34
+ "description": {
35
+ "en": "Counts messages, commands, flow runs and model usage by hour, and samples CPU and memory for the overview page",
36
+ "zh-CN": "按小时统计消息、指令、流程运行和模型用量,并采样 CPU 与内存,供总览页使用"
37
+ },
38
+ "provides": [
39
+ "stats"
40
+ ],
41
+ "requires": {
42
+ "plugins": {
43
+ "@mushinjs/plugin-storage": "*"
44
+ }
45
+ },
46
+ "optional": {
47
+ "plugins": {
48
+ "@mushinjs/plugin-bots": "*",
49
+ "@mushinjs/plugin-flows": "*",
50
+ "@mushinjs/plugin-providers": "*"
51
+ }
52
+ }
53
+ },
54
+ "dependencies": {
55
+ "@mushinjs/core": "0.2.0-next.20261113103537",
56
+ "@mushinjs/plugin-bots": "0.2.0-next.20261113103537",
57
+ "@mushinjs/plugin-flows": "0.2.0-next.20261113103537",
58
+ "@mushinjs/plugin-providers": "0.2.0-next.20261113103537",
59
+ "@mushinjs/plugin-storage": "0.2.0-next.20261113103537",
60
+ "cordis": "4.0.0-rc.11"
61
+ }
6
62
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.