@hasna/logs 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/README.md +73 -0
- package/dashboard/bun.lock +526 -0
- package/dashboard/eslint.config.js +23 -0
- package/dashboard/index.html +13 -0
- package/dashboard/package.json +32 -0
- package/dashboard/public/favicon.svg +1 -0
- package/dashboard/public/icons.svg +24 -0
- package/dashboard/src/App.css +184 -0
- package/dashboard/src/App.tsx +49 -0
- package/dashboard/src/api.ts +33 -0
- package/dashboard/src/assets/hero.png +0 -0
- package/dashboard/src/assets/react.svg +1 -0
- package/dashboard/src/assets/vite.svg +1 -0
- package/dashboard/src/index.css +111 -0
- package/dashboard/src/main.tsx +10 -0
- package/dashboard/src/pages/Alerts.tsx +69 -0
- package/dashboard/src/pages/Issues.tsx +50 -0
- package/dashboard/src/pages/Perf.tsx +75 -0
- package/dashboard/src/pages/Projects.tsx +67 -0
- package/dashboard/src/pages/Summary.tsx +67 -0
- package/dashboard/src/pages/Tail.tsx +65 -0
- package/dashboard/tsconfig.app.json +28 -0
- package/dashboard/tsconfig.json +7 -0
- package/dashboard/tsconfig.node.json +26 -0
- package/dashboard/vite.config.ts +14 -0
- package/dist/cli/index.js +80 -9
- package/dist/mcp/index.js +217 -96
- package/dist/server/index.js +307 -7
- package/package.json +3 -1
- package/sdk/package.json +3 -2
- package/sdk/src/index.ts +1 -1
- package/sdk/src/types.ts +56 -0
- package/src/cli/index.ts +70 -4
- package/src/lib/count.test.ts +44 -0
- package/src/lib/count.ts +45 -0
- package/src/lib/diagnose.ts +26 -11
- package/src/lib/parse-time.test.ts +37 -0
- package/src/lib/parse-time.ts +14 -0
- package/src/lib/projects.ts +10 -0
- package/src/lib/query.ts +10 -2
- package/src/lib/session-context.ts +28 -0
- package/src/lib/summarize.ts +2 -1
- package/src/mcp/index.ts +138 -59
- package/src/server/index.ts +4 -1
- package/src/server/routes/logs.ts +28 -1
package/dist/server/index.js
CHANGED
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
setPageAuth,
|
|
7
7
|
setRetentionPolicy,
|
|
8
8
|
startScheduler
|
|
9
|
-
} from "../index-
|
|
9
|
+
} from "../index-c1rwjhff.js";
|
|
10
|
+
import {
|
|
11
|
+
countLogs
|
|
12
|
+
} from "../index-n8qd55mt.js";
|
|
10
13
|
import {
|
|
11
14
|
createAlertRule,
|
|
12
15
|
createPage,
|
|
@@ -15,7 +18,6 @@ import {
|
|
|
15
18
|
getDb,
|
|
16
19
|
getIssue,
|
|
17
20
|
getLatestSnapshot,
|
|
18
|
-
getLogContext,
|
|
19
21
|
getPerfTrend,
|
|
20
22
|
getProject,
|
|
21
23
|
ingestBatch,
|
|
@@ -24,19 +26,24 @@ import {
|
|
|
24
26
|
listIssues,
|
|
25
27
|
listPages,
|
|
26
28
|
listProjects,
|
|
27
|
-
|
|
29
|
+
resolveProjectId,
|
|
28
30
|
summarizeLogs,
|
|
29
|
-
tailLogs,
|
|
30
31
|
updateAlertRule,
|
|
31
32
|
updateIssueStatus,
|
|
32
33
|
updateProject
|
|
33
|
-
} from "../index-
|
|
34
|
+
} from "../index-34xx795x.js";
|
|
34
35
|
import {
|
|
35
36
|
createJob,
|
|
36
37
|
deleteJob,
|
|
37
38
|
listJobs,
|
|
38
39
|
updateJob
|
|
39
40
|
} from "../jobs-124e878j.js";
|
|
41
|
+
import {
|
|
42
|
+
getLogContext,
|
|
43
|
+
parseTime,
|
|
44
|
+
searchLogs,
|
|
45
|
+
tailLogs
|
|
46
|
+
} from "../query-d5b0chp4.js";
|
|
40
47
|
import {
|
|
41
48
|
exportToCsv,
|
|
42
49
|
exportToJson
|
|
@@ -1669,6 +1676,277 @@ var cors = (options) => {
|
|
|
1669
1676
|
};
|
|
1670
1677
|
};
|
|
1671
1678
|
|
|
1679
|
+
// node_modules/hono/dist/adapter/bun/serve-static.js
|
|
1680
|
+
import { stat } from "fs/promises";
|
|
1681
|
+
import { join } from "path";
|
|
1682
|
+
|
|
1683
|
+
// node_modules/hono/dist/utils/compress.js
|
|
1684
|
+
var COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/(?!event-stream(?:[;\s]|$))[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
|
|
1685
|
+
|
|
1686
|
+
// node_modules/hono/dist/utils/mime.js
|
|
1687
|
+
var getMimeType = (filename, mimes = baseMimes) => {
|
|
1688
|
+
const regexp = /\.([a-zA-Z0-9]+?)$/;
|
|
1689
|
+
const match2 = filename.match(regexp);
|
|
1690
|
+
if (!match2) {
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
let mimeType = mimes[match2[1].toLowerCase()];
|
|
1694
|
+
if (mimeType && mimeType.startsWith("text")) {
|
|
1695
|
+
mimeType += "; charset=utf-8";
|
|
1696
|
+
}
|
|
1697
|
+
return mimeType;
|
|
1698
|
+
};
|
|
1699
|
+
var _baseMimes = {
|
|
1700
|
+
aac: "audio/aac",
|
|
1701
|
+
avi: "video/x-msvideo",
|
|
1702
|
+
avif: "image/avif",
|
|
1703
|
+
av1: "video/av1",
|
|
1704
|
+
bin: "application/octet-stream",
|
|
1705
|
+
bmp: "image/bmp",
|
|
1706
|
+
css: "text/css",
|
|
1707
|
+
csv: "text/csv",
|
|
1708
|
+
eot: "application/vnd.ms-fontobject",
|
|
1709
|
+
epub: "application/epub+zip",
|
|
1710
|
+
gif: "image/gif",
|
|
1711
|
+
gz: "application/gzip",
|
|
1712
|
+
htm: "text/html",
|
|
1713
|
+
html: "text/html",
|
|
1714
|
+
ico: "image/x-icon",
|
|
1715
|
+
ics: "text/calendar",
|
|
1716
|
+
jpeg: "image/jpeg",
|
|
1717
|
+
jpg: "image/jpeg",
|
|
1718
|
+
js: "text/javascript",
|
|
1719
|
+
json: "application/json",
|
|
1720
|
+
jsonld: "application/ld+json",
|
|
1721
|
+
map: "application/json",
|
|
1722
|
+
mid: "audio/x-midi",
|
|
1723
|
+
midi: "audio/x-midi",
|
|
1724
|
+
mjs: "text/javascript",
|
|
1725
|
+
mp3: "audio/mpeg",
|
|
1726
|
+
mp4: "video/mp4",
|
|
1727
|
+
mpeg: "video/mpeg",
|
|
1728
|
+
oga: "audio/ogg",
|
|
1729
|
+
ogv: "video/ogg",
|
|
1730
|
+
ogx: "application/ogg",
|
|
1731
|
+
opus: "audio/opus",
|
|
1732
|
+
otf: "font/otf",
|
|
1733
|
+
pdf: "application/pdf",
|
|
1734
|
+
png: "image/png",
|
|
1735
|
+
rtf: "application/rtf",
|
|
1736
|
+
svg: "image/svg+xml",
|
|
1737
|
+
tif: "image/tiff",
|
|
1738
|
+
tiff: "image/tiff",
|
|
1739
|
+
ts: "video/mp2t",
|
|
1740
|
+
ttf: "font/ttf",
|
|
1741
|
+
txt: "text/plain",
|
|
1742
|
+
wasm: "application/wasm",
|
|
1743
|
+
webm: "video/webm",
|
|
1744
|
+
weba: "audio/webm",
|
|
1745
|
+
webmanifest: "application/manifest+json",
|
|
1746
|
+
webp: "image/webp",
|
|
1747
|
+
woff: "font/woff",
|
|
1748
|
+
woff2: "font/woff2",
|
|
1749
|
+
xhtml: "application/xhtml+xml",
|
|
1750
|
+
xml: "application/xml",
|
|
1751
|
+
zip: "application/zip",
|
|
1752
|
+
"3gp": "video/3gpp",
|
|
1753
|
+
"3g2": "video/3gpp2",
|
|
1754
|
+
gltf: "model/gltf+json",
|
|
1755
|
+
glb: "model/gltf-binary"
|
|
1756
|
+
};
|
|
1757
|
+
var baseMimes = _baseMimes;
|
|
1758
|
+
|
|
1759
|
+
// node_modules/hono/dist/middleware/serve-static/path.js
|
|
1760
|
+
var defaultJoin = (...paths) => {
|
|
1761
|
+
let result = paths.filter((p) => p !== "").join("/");
|
|
1762
|
+
result = result.replace(/(?<=\/)\/+/g, "");
|
|
1763
|
+
const segments = result.split("/");
|
|
1764
|
+
const resolved = [];
|
|
1765
|
+
for (const segment of segments) {
|
|
1766
|
+
if (segment === ".." && resolved.length > 0 && resolved.at(-1) !== "..") {
|
|
1767
|
+
resolved.pop();
|
|
1768
|
+
} else if (segment !== ".") {
|
|
1769
|
+
resolved.push(segment);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
return resolved.join("/") || ".";
|
|
1773
|
+
};
|
|
1774
|
+
|
|
1775
|
+
// node_modules/hono/dist/middleware/serve-static/index.js
|
|
1776
|
+
var ENCODINGS = {
|
|
1777
|
+
br: ".br",
|
|
1778
|
+
zstd: ".zst",
|
|
1779
|
+
gzip: ".gz"
|
|
1780
|
+
};
|
|
1781
|
+
var ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS);
|
|
1782
|
+
var DEFAULT_DOCUMENT = "index.html";
|
|
1783
|
+
var serveStatic = (options) => {
|
|
1784
|
+
const root = options.root ?? "./";
|
|
1785
|
+
const optionPath = options.path;
|
|
1786
|
+
const join = options.join ?? defaultJoin;
|
|
1787
|
+
return async (c, next) => {
|
|
1788
|
+
if (c.finalized) {
|
|
1789
|
+
return next();
|
|
1790
|
+
}
|
|
1791
|
+
let filename;
|
|
1792
|
+
if (options.path) {
|
|
1793
|
+
filename = options.path;
|
|
1794
|
+
} else {
|
|
1795
|
+
try {
|
|
1796
|
+
filename = tryDecodeURI(c.req.path);
|
|
1797
|
+
if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(filename)) {
|
|
1798
|
+
throw new Error;
|
|
1799
|
+
}
|
|
1800
|
+
} catch {
|
|
1801
|
+
await options.onNotFound?.(c.req.path, c);
|
|
1802
|
+
return next();
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
let path = join(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename) : filename);
|
|
1806
|
+
if (options.isDir && await options.isDir(path)) {
|
|
1807
|
+
path = join(path, DEFAULT_DOCUMENT);
|
|
1808
|
+
}
|
|
1809
|
+
const getContent = options.getContent;
|
|
1810
|
+
let content = await getContent(path, c);
|
|
1811
|
+
if (content instanceof Response) {
|
|
1812
|
+
return c.newResponse(content.body, content);
|
|
1813
|
+
}
|
|
1814
|
+
if (content) {
|
|
1815
|
+
const mimeType = options.mimes && getMimeType(path, options.mimes) || getMimeType(path);
|
|
1816
|
+
c.header("Content-Type", mimeType || "application/octet-stream");
|
|
1817
|
+
if (options.precompressed && (!mimeType || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) {
|
|
1818
|
+
const acceptEncodingSet = new Set(c.req.header("Accept-Encoding")?.split(",").map((encoding) => encoding.trim()));
|
|
1819
|
+
for (const encoding of ENCODINGS_ORDERED_KEYS) {
|
|
1820
|
+
if (!acceptEncodingSet.has(encoding)) {
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
const compressedContent = await getContent(path + ENCODINGS[encoding], c);
|
|
1824
|
+
if (compressedContent) {
|
|
1825
|
+
content = compressedContent;
|
|
1826
|
+
c.header("Content-Encoding", encoding);
|
|
1827
|
+
c.header("Vary", "Accept-Encoding", { append: true });
|
|
1828
|
+
break;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
await options.onFound?.(path, c);
|
|
1833
|
+
return c.body(content);
|
|
1834
|
+
}
|
|
1835
|
+
await options.onNotFound?.(path, c);
|
|
1836
|
+
await next();
|
|
1837
|
+
return;
|
|
1838
|
+
};
|
|
1839
|
+
};
|
|
1840
|
+
|
|
1841
|
+
// node_modules/hono/dist/adapter/bun/serve-static.js
|
|
1842
|
+
var serveStatic2 = (options) => {
|
|
1843
|
+
return async function serveStatic2(c, next) {
|
|
1844
|
+
const getContent = async (path) => {
|
|
1845
|
+
const file = Bun.file(path);
|
|
1846
|
+
return await file.exists() ? file : null;
|
|
1847
|
+
};
|
|
1848
|
+
const isDir = async (path) => {
|
|
1849
|
+
let isDir2;
|
|
1850
|
+
try {
|
|
1851
|
+
const stats = await stat(path);
|
|
1852
|
+
isDir2 = stats.isDirectory();
|
|
1853
|
+
} catch {}
|
|
1854
|
+
return isDir2;
|
|
1855
|
+
};
|
|
1856
|
+
return serveStatic({
|
|
1857
|
+
...options,
|
|
1858
|
+
getContent,
|
|
1859
|
+
join,
|
|
1860
|
+
isDir
|
|
1861
|
+
})(c, next);
|
|
1862
|
+
};
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
// node_modules/hono/dist/helper/ssg/middleware.js
|
|
1866
|
+
var X_HONO_DISABLE_SSG_HEADER_KEY = "x-hono-disable-ssg";
|
|
1867
|
+
var SSG_DISABLED_RESPONSE = (() => {
|
|
1868
|
+
try {
|
|
1869
|
+
return new Response("SSG is disabled", {
|
|
1870
|
+
status: 404,
|
|
1871
|
+
headers: { [X_HONO_DISABLE_SSG_HEADER_KEY]: "true" }
|
|
1872
|
+
});
|
|
1873
|
+
} catch {
|
|
1874
|
+
return null;
|
|
1875
|
+
}
|
|
1876
|
+
})();
|
|
1877
|
+
// node_modules/hono/dist/adapter/bun/ssg.js
|
|
1878
|
+
var { write } = Bun;
|
|
1879
|
+
|
|
1880
|
+
// node_modules/hono/dist/helper/websocket/index.js
|
|
1881
|
+
var WSContext = class {
|
|
1882
|
+
#init;
|
|
1883
|
+
constructor(init) {
|
|
1884
|
+
this.#init = init;
|
|
1885
|
+
this.raw = init.raw;
|
|
1886
|
+
this.url = init.url ? new URL(init.url) : null;
|
|
1887
|
+
this.protocol = init.protocol ?? null;
|
|
1888
|
+
}
|
|
1889
|
+
send(source, options) {
|
|
1890
|
+
this.#init.send(source, options ?? {});
|
|
1891
|
+
}
|
|
1892
|
+
raw;
|
|
1893
|
+
binaryType = "arraybuffer";
|
|
1894
|
+
get readyState() {
|
|
1895
|
+
return this.#init.readyState;
|
|
1896
|
+
}
|
|
1897
|
+
url;
|
|
1898
|
+
protocol;
|
|
1899
|
+
close(code, reason) {
|
|
1900
|
+
this.#init.close(code, reason);
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
var defineWebSocketHelper = (handler) => {
|
|
1904
|
+
return (...args) => {
|
|
1905
|
+
if (typeof args[0] === "function") {
|
|
1906
|
+
const [createEvents, options] = args;
|
|
1907
|
+
return async function upgradeWebSocket(c, next) {
|
|
1908
|
+
const events = await createEvents(c);
|
|
1909
|
+
const result = await handler(c, events, options);
|
|
1910
|
+
if (result) {
|
|
1911
|
+
return result;
|
|
1912
|
+
}
|
|
1913
|
+
await next();
|
|
1914
|
+
};
|
|
1915
|
+
} else {
|
|
1916
|
+
const [c, events, options] = args;
|
|
1917
|
+
return (async () => {
|
|
1918
|
+
const upgraded = await handler(c, events, options);
|
|
1919
|
+
if (!upgraded) {
|
|
1920
|
+
throw new Error("Failed to upgrade WebSocket");
|
|
1921
|
+
}
|
|
1922
|
+
return upgraded;
|
|
1923
|
+
})();
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
};
|
|
1927
|
+
|
|
1928
|
+
// node_modules/hono/dist/adapter/bun/server.js
|
|
1929
|
+
var getBunServer = (c) => ("server" in c.env) ? c.env.server : c.env;
|
|
1930
|
+
|
|
1931
|
+
// node_modules/hono/dist/adapter/bun/websocket.js
|
|
1932
|
+
var upgradeWebSocket = defineWebSocketHelper((c, events) => {
|
|
1933
|
+
const server = getBunServer(c);
|
|
1934
|
+
if (!server) {
|
|
1935
|
+
throw new TypeError("env has to include the 2nd argument of fetch.");
|
|
1936
|
+
}
|
|
1937
|
+
const upgradeResult = server.upgrade(c.req.raw, {
|
|
1938
|
+
data: {
|
|
1939
|
+
events,
|
|
1940
|
+
url: new URL(c.req.url),
|
|
1941
|
+
protocol: c.req.url
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
if (upgradeResult) {
|
|
1945
|
+
return new Response(null);
|
|
1946
|
+
}
|
|
1947
|
+
return;
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1672
1950
|
// src/lib/browser-script.ts
|
|
1673
1951
|
function getBrowserScript(serverUrl) {
|
|
1674
1952
|
return `(function(){
|
|
@@ -1824,9 +2102,29 @@ function logsRoutes(db) {
|
|
|
1824
2102
|
});
|
|
1825
2103
|
app.get("/summary", (c) => {
|
|
1826
2104
|
const { project_id, since } = c.req.query();
|
|
1827
|
-
const summary = summarizeLogs(db, project_id || undefined, since || undefined);
|
|
2105
|
+
const summary = summarizeLogs(db, resolveProjectId(db, project_id) || undefined, parseTime(since) || since || undefined);
|
|
1828
2106
|
return c.json(summary);
|
|
1829
2107
|
});
|
|
2108
|
+
app.get("/count", (c) => {
|
|
2109
|
+
const { project_id, service, level, since, until } = c.req.query();
|
|
2110
|
+
return c.json(countLogs(db, {
|
|
2111
|
+
project_id: resolveProjectId(db, project_id) || undefined,
|
|
2112
|
+
service: service || undefined,
|
|
2113
|
+
level: level || undefined,
|
|
2114
|
+
since: since || undefined,
|
|
2115
|
+
until: until || undefined
|
|
2116
|
+
}));
|
|
2117
|
+
});
|
|
2118
|
+
app.get("/recent-errors", (c) => {
|
|
2119
|
+
const { project_id, since, limit } = c.req.query();
|
|
2120
|
+
const rows = searchLogs(db, {
|
|
2121
|
+
project_id: resolveProjectId(db, project_id) || undefined,
|
|
2122
|
+
level: ["error", "fatal"],
|
|
2123
|
+
since: parseTime(since || "1h"),
|
|
2124
|
+
limit: limit ? Number(limit) : 20
|
|
2125
|
+
});
|
|
2126
|
+
return c.json(rows.map((r) => ({ id: r.id, timestamp: r.timestamp, level: r.level, message: r.message, service: r.service, age_seconds: Math.floor((Date.now() - new Date(r.timestamp).getTime()) / 1000) })));
|
|
2127
|
+
});
|
|
1830
2128
|
app.get("/:trace_id/context", (c) => {
|
|
1831
2129
|
const rows = getLogContext(db, c.req.param("trace_id"));
|
|
1832
2130
|
return c.json(rows);
|
|
@@ -2160,7 +2458,9 @@ app.route("/api/alerts", alertsRoutes(db));
|
|
|
2160
2458
|
app.route("/api/issues", issuesRoutes(db));
|
|
2161
2459
|
app.route("/api/perf", perfRoutes(db));
|
|
2162
2460
|
app.get("/health", (c) => c.json(getHealth(db)));
|
|
2163
|
-
app.get("/", (c) => c.
|
|
2461
|
+
app.get("/dashboard", (c) => c.redirect("/dashboard/"));
|
|
2462
|
+
app.use("/dashboard/*", serveStatic2({ root: "./dashboard/dist", rewriteRequestPath: (p) => p.replace(/^\/dashboard/, "") }));
|
|
2463
|
+
app.get("/", (c) => c.json({ service: "@hasna/logs", port: PORT, status: "ok", dashboard: `http://localhost:${PORT}/dashboard/` }));
|
|
2164
2464
|
startScheduler(db);
|
|
2165
2465
|
console.log(`@hasna/logs server running on http://localhost:${PORT}`);
|
|
2166
2466
|
var server_default = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/logs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Log aggregation + browser script + headless page scanner + performance monitoring for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "bun build src/cli/index.ts src/mcp/index.ts src/server/index.ts --outdir dist --target bun --splitting --external playwright --external playwright-core --external electron --external chromium-bidi --external lighthouse",
|
|
15
|
+
"build:dashboard": "cd dashboard && bun run build",
|
|
16
|
+
"build:all": "bun run build:dashboard && bun run build",
|
|
15
17
|
"dev": "bun run src/server/index.ts",
|
|
16
18
|
"test": "bun test",
|
|
17
19
|
"test:coverage": "bun test --coverage",
|
package/sdk/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/logs-sdk",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Zero-dependency fetch client for @hasna/logs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-dependency fetch client for @hasna/logs — push logs, query, browse issues, perf snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "bun build src/index.ts --outdir dist --target browser"
|
|
18
18
|
},
|
|
19
|
+
"keywords": ["logs", "monitoring", "sdk", "ai-agents"],
|
|
19
20
|
"author": "Andrei Hasna <andrei@hasna.com>",
|
|
20
21
|
"license": "MIT"
|
|
21
22
|
}
|
package/sdk/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LogEntry, LogLevel, LogQuery, LogRow, LogSummary, Page, PerformanceSnapshot, Project, ScanJob } from "
|
|
1
|
+
import type { LogEntry, LogLevel, LogQuery, LogRow, LogSummary, Page, PerformanceSnapshot, Project, ScanJob } from "./types.ts"
|
|
2
2
|
|
|
3
3
|
export type { LogEntry, LogLevel, LogQuery, LogRow, LogSummary, Page, PerformanceSnapshot, Project, ScanJob }
|
|
4
4
|
|
package/sdk/src/types.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error" | "fatal"
|
|
2
|
+
export type LogSource = "sdk" | "script" | "scanner"
|
|
3
|
+
|
|
4
|
+
export interface LogEntry {
|
|
5
|
+
level: LogLevel
|
|
6
|
+
message: string
|
|
7
|
+
project_id?: string
|
|
8
|
+
page_id?: string
|
|
9
|
+
source?: LogSource
|
|
10
|
+
service?: string
|
|
11
|
+
trace_id?: string
|
|
12
|
+
session_id?: string
|
|
13
|
+
agent?: string
|
|
14
|
+
url?: string
|
|
15
|
+
stack_trace?: string
|
|
16
|
+
metadata?: Record<string, unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LogRow {
|
|
20
|
+
id: string; timestamp: string; project_id: string | null; page_id: string | null
|
|
21
|
+
level: LogLevel; source: LogSource; service: string | null; message: string
|
|
22
|
+
trace_id: string | null; session_id: string | null; agent: string | null
|
|
23
|
+
url: string | null; stack_trace: string | null; metadata: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Project {
|
|
27
|
+
id: string; name: string; github_repo: string | null; base_url: string | null
|
|
28
|
+
description: string | null; created_at: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Page {
|
|
32
|
+
id: string; project_id: string; url: string; path: string
|
|
33
|
+
name: string | null; last_scanned_at: string | null; created_at: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ScanJob {
|
|
37
|
+
id: string; project_id: string; page_id: string | null
|
|
38
|
+
schedule: string; enabled: number; last_run_at: string | null; created_at: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PerformanceSnapshot {
|
|
42
|
+
id: string; timestamp: string; project_id: string; page_id: string | null
|
|
43
|
+
url: string; lcp: number | null; fcp: number | null; cls: number | null
|
|
44
|
+
tti: number | null; ttfb: number | null; score: number | null; raw_audit: string | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LogQuery {
|
|
48
|
+
project_id?: string; page_id?: string; level?: LogLevel | LogLevel[]
|
|
49
|
+
service?: string; since?: string; until?: string; text?: string
|
|
50
|
+
trace_id?: string; limit?: number; offset?: number; fields?: string[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LogSummary {
|
|
54
|
+
project_id: string | null; service: string | null; page_id: string | null
|
|
55
|
+
level: LogLevel; count: number; latest: string
|
|
56
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -151,6 +151,58 @@ program.command("scan")
|
|
|
151
151
|
console.log("Scan complete.")
|
|
152
152
|
})
|
|
153
153
|
|
|
154
|
+
// ── logs watch ────────────────────────────────────────────
|
|
155
|
+
program.command("watch")
|
|
156
|
+
.description("Stream new logs in real time with color coding")
|
|
157
|
+
.option("--project <id>")
|
|
158
|
+
.option("--level <levels>", "Comma-separated levels")
|
|
159
|
+
.option("--service <name>")
|
|
160
|
+
.action(async (opts) => {
|
|
161
|
+
const db = getDb()
|
|
162
|
+
const { searchLogs } = await import("../lib/query.ts")
|
|
163
|
+
|
|
164
|
+
const COLORS: Record<string, string> = {
|
|
165
|
+
debug: "\x1b[90m", info: "\x1b[36m", warn: "\x1b[33m", error: "\x1b[31m", fatal: "\x1b[35m",
|
|
166
|
+
}
|
|
167
|
+
const RESET = "\x1b[0m"
|
|
168
|
+
const BOLD = "\x1b[1m"
|
|
169
|
+
|
|
170
|
+
let lastTimestamp = new Date().toISOString()
|
|
171
|
+
let errorCount = 0
|
|
172
|
+
let warnCount = 0
|
|
173
|
+
|
|
174
|
+
process.stdout.write(`\x1b[2J\x1b[H`) // clear screen
|
|
175
|
+
console.log(`${BOLD}@hasna/logs watch${RESET} — Ctrl+C to exit\n`)
|
|
176
|
+
|
|
177
|
+
const poll = () => {
|
|
178
|
+
const rows = searchLogs(db, {
|
|
179
|
+
project_id: opts.project,
|
|
180
|
+
level: opts.level ? (opts.level.split(",") as LogLevel[]) : undefined,
|
|
181
|
+
service: opts.service,
|
|
182
|
+
since: lastTimestamp,
|
|
183
|
+
limit: 100,
|
|
184
|
+
}).reverse()
|
|
185
|
+
|
|
186
|
+
for (const row of rows) {
|
|
187
|
+
if (row.timestamp <= lastTimestamp) continue
|
|
188
|
+
lastTimestamp = row.timestamp
|
|
189
|
+
if (row.level === "error" || row.level === "fatal") errorCount++
|
|
190
|
+
if (row.level === "warn") warnCount++
|
|
191
|
+
const color = COLORS[row.level] ?? ""
|
|
192
|
+
const ts = row.timestamp.slice(11, 19)
|
|
193
|
+
const svc = (row.service ?? "-").padEnd(12)
|
|
194
|
+
const lvl = row.level.toUpperCase().padEnd(5)
|
|
195
|
+
console.log(`${color}${ts} ${BOLD}${lvl}${RESET}${color} ${svc} ${row.message}${RESET}`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Update terminal title with counts
|
|
199
|
+
process.stdout.write(`\x1b]2;logs: ${errorCount}E ${warnCount}W\x07`)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const interval = setInterval(poll, 500)
|
|
203
|
+
process.on("SIGINT", () => { clearInterval(interval); console.log(`\n\nErrors: ${errorCount} Warnings: ${warnCount}`); process.exit(0) })
|
|
204
|
+
})
|
|
205
|
+
|
|
154
206
|
// ── logs export ───────────────────────────────────────────
|
|
155
207
|
program.command("export")
|
|
156
208
|
.description("Export logs to JSON or CSV")
|
|
@@ -203,11 +255,25 @@ program.command("mcp")
|
|
|
203
255
|
.option("--gemini", "Install into Gemini")
|
|
204
256
|
.action(async (opts) => {
|
|
205
257
|
if (opts.claude || opts.codex || opts.gemini) {
|
|
206
|
-
const
|
|
207
|
-
|
|
258
|
+
const { execSync } = await import("node:child_process")
|
|
259
|
+
// Resolve the MCP binary path — works from both source and dist
|
|
260
|
+
const selfPath = process.argv[1] ?? new URL(import.meta.url).pathname
|
|
261
|
+
const mcpBin = selfPath.replace(/cli\/index\.(ts|js)$/, "mcp/index.$1")
|
|
262
|
+
const runtime = process.execPath // bun or node
|
|
263
|
+
|
|
208
264
|
if (opts.claude) {
|
|
209
|
-
const
|
|
210
|
-
|
|
265
|
+
const cmd = `claude mcp add --transport stdio --scope user logs -- ${runtime} ${mcpBin}`
|
|
266
|
+
console.log(`Running: ${cmd}`)
|
|
267
|
+
execSync(cmd, { stdio: "inherit" })
|
|
268
|
+
console.log("✓ Installed logs-mcp into Claude Code")
|
|
269
|
+
}
|
|
270
|
+
if (opts.codex) {
|
|
271
|
+
const config = `[mcp_servers.logs]\ncommand = "${runtime}"\nargs = ["${mcpBin}"]`
|
|
272
|
+
console.log("Add to ~/.codex/config.toml:\n\n" + config)
|
|
273
|
+
}
|
|
274
|
+
if (opts.gemini) {
|
|
275
|
+
const config = JSON.stringify({ mcpServers: { logs: { command: runtime, args: [mcpBin] } } }, null, 2)
|
|
276
|
+
console.log("Add to ~/.gemini/settings.json mcpServers:\n\n" + config)
|
|
211
277
|
}
|
|
212
278
|
return
|
|
213
279
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test"
|
|
2
|
+
import { createTestDb } from "../db/index.ts"
|
|
3
|
+
import { ingestBatch } from "./ingest.ts"
|
|
4
|
+
import { countLogs } from "./count.ts"
|
|
5
|
+
|
|
6
|
+
describe("countLogs", () => {
|
|
7
|
+
it("counts all logs", () => {
|
|
8
|
+
const db = createTestDb()
|
|
9
|
+
ingestBatch(db, [{ level: "error", message: "e" }, { level: "warn", message: "w" }, { level: "info", message: "i" }])
|
|
10
|
+
const c = countLogs(db, {})
|
|
11
|
+
expect(c.total).toBe(3)
|
|
12
|
+
expect(c.errors).toBe(1)
|
|
13
|
+
expect(c.warns).toBe(1)
|
|
14
|
+
expect(c.fatals).toBe(0)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("filters by project", () => {
|
|
18
|
+
const db = createTestDb()
|
|
19
|
+
const p = db.prepare("INSERT INTO projects (name) VALUES ('app') RETURNING id").get() as { id: string }
|
|
20
|
+
ingestBatch(db, [{ level: "error", message: "e", project_id: p.id }, { level: "error", message: "e2" }])
|
|
21
|
+
const c = countLogs(db, { project_id: p.id })
|
|
22
|
+
expect(c.total).toBe(1)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("filters by service", () => {
|
|
26
|
+
const db = createTestDb()
|
|
27
|
+
ingestBatch(db, [{ level: "error", message: "e", service: "api" }, { level: "error", message: "e2", service: "db" }])
|
|
28
|
+
expect(countLogs(db, { service: "api" }).total).toBe(1)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("returns zero counts for empty db", () => {
|
|
32
|
+
const c = countLogs(createTestDb(), {})
|
|
33
|
+
expect(c.total).toBe(0)
|
|
34
|
+
expect(c.errors).toBe(0)
|
|
35
|
+
expect(c.by_level).toEqual({})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("accepts relative since", () => {
|
|
39
|
+
const db = createTestDb()
|
|
40
|
+
ingestBatch(db, [{ level: "error", message: "recent" }])
|
|
41
|
+
const c = countLogs(db, { since: "1h" })
|
|
42
|
+
expect(c.total).toBe(1)
|
|
43
|
+
})
|
|
44
|
+
})
|
package/src/lib/count.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import { parseTime } from "./parse-time.ts"
|
|
3
|
+
|
|
4
|
+
export interface LogCount {
|
|
5
|
+
total: number
|
|
6
|
+
errors: number
|
|
7
|
+
warns: number
|
|
8
|
+
fatals: number
|
|
9
|
+
by_level: Record<string, number>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function countLogs(db: Database, opts: {
|
|
13
|
+
project_id?: string
|
|
14
|
+
service?: string
|
|
15
|
+
level?: string
|
|
16
|
+
since?: string
|
|
17
|
+
until?: string
|
|
18
|
+
}): LogCount {
|
|
19
|
+
const conditions: string[] = []
|
|
20
|
+
const params: Record<string, unknown> = {}
|
|
21
|
+
|
|
22
|
+
if (opts.project_id) { conditions.push("project_id = $p"); params.$p = opts.project_id }
|
|
23
|
+
if (opts.service) { conditions.push("service = $service"); params.$service = opts.service }
|
|
24
|
+
if (opts.level) { conditions.push("level = $level"); params.$level = opts.level }
|
|
25
|
+
const since = parseTime(opts.since)
|
|
26
|
+
const until = parseTime(opts.until)
|
|
27
|
+
if (since) { conditions.push("timestamp >= $since"); params.$since = since }
|
|
28
|
+
if (until) { conditions.push("timestamp <= $until"); params.$until = until }
|
|
29
|
+
|
|
30
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""
|
|
31
|
+
|
|
32
|
+
const byLevel = db.prepare(`SELECT level, COUNT(*) as c FROM logs ${where} GROUP BY level`)
|
|
33
|
+
.all(params) as { level: string; c: number }[]
|
|
34
|
+
|
|
35
|
+
const by_level = Object.fromEntries(byLevel.map(r => [r.level, r.c]))
|
|
36
|
+
const total = byLevel.reduce((s, r) => s + r.c, 0)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
total,
|
|
40
|
+
errors: by_level["error"] ?? 0,
|
|
41
|
+
warns: by_level["warn"] ?? 0,
|
|
42
|
+
fatals: by_level["fatal"] ?? 0,
|
|
43
|
+
by_level,
|
|
44
|
+
}
|
|
45
|
+
}
|