@hasna/logs 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +116 -12
- package/dist/mcp/index.js +306 -100
- package/dist/server/index.js +592 -7
- package/package.json +12 -2
- 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 +114 -4
- package/src/db/index.ts +10 -0
- package/src/db/migrations/001_alert_rules.ts +21 -0
- package/src/db/migrations/002_issues.ts +21 -0
- package/src/db/migrations/003_retention.ts +15 -0
- package/src/db/migrations/004_page_auth.ts +13 -0
- package/src/lib/alerts.test.ts +67 -0
- package/src/lib/alerts.ts +117 -0
- package/src/lib/compare.test.ts +52 -0
- package/src/lib/compare.ts +85 -0
- package/src/lib/diagnose.test.ts +55 -0
- package/src/lib/diagnose.ts +76 -0
- package/src/lib/export.test.ts +66 -0
- package/src/lib/export.ts +65 -0
- package/src/lib/health.test.ts +48 -0
- package/src/lib/health.ts +51 -0
- package/src/lib/ingest.ts +25 -2
- package/src/lib/issues.test.ts +79 -0
- package/src/lib/issues.ts +70 -0
- package/src/lib/page-auth.test.ts +54 -0
- package/src/lib/page-auth.ts +48 -0
- package/src/lib/retention.test.ts +42 -0
- package/src/lib/retention.ts +62 -0
- package/src/lib/scanner.ts +21 -2
- package/src/lib/scheduler.ts +6 -0
- package/src/lib/session-context.ts +28 -0
- package/src/mcp/index.ts +133 -89
- package/src/server/index.ts +12 -1
- package/src/server/routes/alerts.ts +32 -0
- package/src/server/routes/issues.ts +43 -0
- package/src/server/routes/logs.ts +21 -0
- package/src/server/routes/projects.ts +25 -0
- package/src/server/routes/stream.ts +43 -0
package/dist/server/index.js
CHANGED
|
@@ -1,31 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
import {
|
|
4
|
+
deletePageAuth,
|
|
5
|
+
runRetentionForProject,
|
|
6
|
+
setPageAuth,
|
|
7
|
+
setRetentionPolicy,
|
|
4
8
|
startScheduler
|
|
5
|
-
} from "../index-
|
|
9
|
+
} from "../index-2p6ynjet.js";
|
|
6
10
|
import {
|
|
11
|
+
createAlertRule,
|
|
7
12
|
createPage,
|
|
8
13
|
createProject,
|
|
14
|
+
deleteAlertRule,
|
|
9
15
|
getDb,
|
|
16
|
+
getIssue,
|
|
10
17
|
getLatestSnapshot,
|
|
11
|
-
getLogContext,
|
|
12
18
|
getPerfTrend,
|
|
13
19
|
getProject,
|
|
14
20
|
ingestBatch,
|
|
15
21
|
ingestLog,
|
|
22
|
+
listAlertRules,
|
|
23
|
+
listIssues,
|
|
16
24
|
listPages,
|
|
17
25
|
listProjects,
|
|
18
|
-
searchLogs,
|
|
19
26
|
summarizeLogs,
|
|
20
|
-
|
|
27
|
+
updateAlertRule,
|
|
28
|
+
updateIssueStatus,
|
|
21
29
|
updateProject
|
|
22
|
-
} from "../index-
|
|
30
|
+
} from "../index-77ss2sf4.js";
|
|
23
31
|
import {
|
|
24
32
|
createJob,
|
|
25
33
|
deleteJob,
|
|
26
34
|
listJobs,
|
|
27
35
|
updateJob
|
|
28
|
-
} from "../
|
|
36
|
+
} from "../jobs-124e878j.js";
|
|
37
|
+
import {
|
|
38
|
+
getLogContext,
|
|
39
|
+
searchLogs,
|
|
40
|
+
tailLogs
|
|
41
|
+
} from "../query-0qv7fvzt.js";
|
|
42
|
+
import {
|
|
43
|
+
exportToCsv,
|
|
44
|
+
exportToJson
|
|
45
|
+
} from "../export-yjaw2sr3.js";
|
|
46
|
+
import {
|
|
47
|
+
getHealth
|
|
48
|
+
} from "../health-f2qrebqc.js";
|
|
49
|
+
import"../index-g8dczzvv.js";
|
|
29
50
|
|
|
30
51
|
// node_modules/hono/dist/compose.js
|
|
31
52
|
var compose = (middleware, onError, onNotFound) => {
|
|
@@ -1650,6 +1671,277 @@ var cors = (options) => {
|
|
|
1650
1671
|
};
|
|
1651
1672
|
};
|
|
1652
1673
|
|
|
1674
|
+
// node_modules/hono/dist/adapter/bun/serve-static.js
|
|
1675
|
+
import { stat } from "fs/promises";
|
|
1676
|
+
import { join } from "path";
|
|
1677
|
+
|
|
1678
|
+
// node_modules/hono/dist/utils/compress.js
|
|
1679
|
+
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;
|
|
1680
|
+
|
|
1681
|
+
// node_modules/hono/dist/utils/mime.js
|
|
1682
|
+
var getMimeType = (filename, mimes = baseMimes) => {
|
|
1683
|
+
const regexp = /\.([a-zA-Z0-9]+?)$/;
|
|
1684
|
+
const match2 = filename.match(regexp);
|
|
1685
|
+
if (!match2) {
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
let mimeType = mimes[match2[1].toLowerCase()];
|
|
1689
|
+
if (mimeType && mimeType.startsWith("text")) {
|
|
1690
|
+
mimeType += "; charset=utf-8";
|
|
1691
|
+
}
|
|
1692
|
+
return mimeType;
|
|
1693
|
+
};
|
|
1694
|
+
var _baseMimes = {
|
|
1695
|
+
aac: "audio/aac",
|
|
1696
|
+
avi: "video/x-msvideo",
|
|
1697
|
+
avif: "image/avif",
|
|
1698
|
+
av1: "video/av1",
|
|
1699
|
+
bin: "application/octet-stream",
|
|
1700
|
+
bmp: "image/bmp",
|
|
1701
|
+
css: "text/css",
|
|
1702
|
+
csv: "text/csv",
|
|
1703
|
+
eot: "application/vnd.ms-fontobject",
|
|
1704
|
+
epub: "application/epub+zip",
|
|
1705
|
+
gif: "image/gif",
|
|
1706
|
+
gz: "application/gzip",
|
|
1707
|
+
htm: "text/html",
|
|
1708
|
+
html: "text/html",
|
|
1709
|
+
ico: "image/x-icon",
|
|
1710
|
+
ics: "text/calendar",
|
|
1711
|
+
jpeg: "image/jpeg",
|
|
1712
|
+
jpg: "image/jpeg",
|
|
1713
|
+
js: "text/javascript",
|
|
1714
|
+
json: "application/json",
|
|
1715
|
+
jsonld: "application/ld+json",
|
|
1716
|
+
map: "application/json",
|
|
1717
|
+
mid: "audio/x-midi",
|
|
1718
|
+
midi: "audio/x-midi",
|
|
1719
|
+
mjs: "text/javascript",
|
|
1720
|
+
mp3: "audio/mpeg",
|
|
1721
|
+
mp4: "video/mp4",
|
|
1722
|
+
mpeg: "video/mpeg",
|
|
1723
|
+
oga: "audio/ogg",
|
|
1724
|
+
ogv: "video/ogg",
|
|
1725
|
+
ogx: "application/ogg",
|
|
1726
|
+
opus: "audio/opus",
|
|
1727
|
+
otf: "font/otf",
|
|
1728
|
+
pdf: "application/pdf",
|
|
1729
|
+
png: "image/png",
|
|
1730
|
+
rtf: "application/rtf",
|
|
1731
|
+
svg: "image/svg+xml",
|
|
1732
|
+
tif: "image/tiff",
|
|
1733
|
+
tiff: "image/tiff",
|
|
1734
|
+
ts: "video/mp2t",
|
|
1735
|
+
ttf: "font/ttf",
|
|
1736
|
+
txt: "text/plain",
|
|
1737
|
+
wasm: "application/wasm",
|
|
1738
|
+
webm: "video/webm",
|
|
1739
|
+
weba: "audio/webm",
|
|
1740
|
+
webmanifest: "application/manifest+json",
|
|
1741
|
+
webp: "image/webp",
|
|
1742
|
+
woff: "font/woff",
|
|
1743
|
+
woff2: "font/woff2",
|
|
1744
|
+
xhtml: "application/xhtml+xml",
|
|
1745
|
+
xml: "application/xml",
|
|
1746
|
+
zip: "application/zip",
|
|
1747
|
+
"3gp": "video/3gpp",
|
|
1748
|
+
"3g2": "video/3gpp2",
|
|
1749
|
+
gltf: "model/gltf+json",
|
|
1750
|
+
glb: "model/gltf-binary"
|
|
1751
|
+
};
|
|
1752
|
+
var baseMimes = _baseMimes;
|
|
1753
|
+
|
|
1754
|
+
// node_modules/hono/dist/middleware/serve-static/path.js
|
|
1755
|
+
var defaultJoin = (...paths) => {
|
|
1756
|
+
let result = paths.filter((p) => p !== "").join("/");
|
|
1757
|
+
result = result.replace(/(?<=\/)\/+/g, "");
|
|
1758
|
+
const segments = result.split("/");
|
|
1759
|
+
const resolved = [];
|
|
1760
|
+
for (const segment of segments) {
|
|
1761
|
+
if (segment === ".." && resolved.length > 0 && resolved.at(-1) !== "..") {
|
|
1762
|
+
resolved.pop();
|
|
1763
|
+
} else if (segment !== ".") {
|
|
1764
|
+
resolved.push(segment);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
return resolved.join("/") || ".";
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1770
|
+
// node_modules/hono/dist/middleware/serve-static/index.js
|
|
1771
|
+
var ENCODINGS = {
|
|
1772
|
+
br: ".br",
|
|
1773
|
+
zstd: ".zst",
|
|
1774
|
+
gzip: ".gz"
|
|
1775
|
+
};
|
|
1776
|
+
var ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS);
|
|
1777
|
+
var DEFAULT_DOCUMENT = "index.html";
|
|
1778
|
+
var serveStatic = (options) => {
|
|
1779
|
+
const root = options.root ?? "./";
|
|
1780
|
+
const optionPath = options.path;
|
|
1781
|
+
const join = options.join ?? defaultJoin;
|
|
1782
|
+
return async (c, next) => {
|
|
1783
|
+
if (c.finalized) {
|
|
1784
|
+
return next();
|
|
1785
|
+
}
|
|
1786
|
+
let filename;
|
|
1787
|
+
if (options.path) {
|
|
1788
|
+
filename = options.path;
|
|
1789
|
+
} else {
|
|
1790
|
+
try {
|
|
1791
|
+
filename = tryDecodeURI(c.req.path);
|
|
1792
|
+
if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(filename)) {
|
|
1793
|
+
throw new Error;
|
|
1794
|
+
}
|
|
1795
|
+
} catch {
|
|
1796
|
+
await options.onNotFound?.(c.req.path, c);
|
|
1797
|
+
return next();
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
let path = join(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename) : filename);
|
|
1801
|
+
if (options.isDir && await options.isDir(path)) {
|
|
1802
|
+
path = join(path, DEFAULT_DOCUMENT);
|
|
1803
|
+
}
|
|
1804
|
+
const getContent = options.getContent;
|
|
1805
|
+
let content = await getContent(path, c);
|
|
1806
|
+
if (content instanceof Response) {
|
|
1807
|
+
return c.newResponse(content.body, content);
|
|
1808
|
+
}
|
|
1809
|
+
if (content) {
|
|
1810
|
+
const mimeType = options.mimes && getMimeType(path, options.mimes) || getMimeType(path);
|
|
1811
|
+
c.header("Content-Type", mimeType || "application/octet-stream");
|
|
1812
|
+
if (options.precompressed && (!mimeType || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) {
|
|
1813
|
+
const acceptEncodingSet = new Set(c.req.header("Accept-Encoding")?.split(",").map((encoding) => encoding.trim()));
|
|
1814
|
+
for (const encoding of ENCODINGS_ORDERED_KEYS) {
|
|
1815
|
+
if (!acceptEncodingSet.has(encoding)) {
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
const compressedContent = await getContent(path + ENCODINGS[encoding], c);
|
|
1819
|
+
if (compressedContent) {
|
|
1820
|
+
content = compressedContent;
|
|
1821
|
+
c.header("Content-Encoding", encoding);
|
|
1822
|
+
c.header("Vary", "Accept-Encoding", { append: true });
|
|
1823
|
+
break;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
await options.onFound?.(path, c);
|
|
1828
|
+
return c.body(content);
|
|
1829
|
+
}
|
|
1830
|
+
await options.onNotFound?.(path, c);
|
|
1831
|
+
await next();
|
|
1832
|
+
return;
|
|
1833
|
+
};
|
|
1834
|
+
};
|
|
1835
|
+
|
|
1836
|
+
// node_modules/hono/dist/adapter/bun/serve-static.js
|
|
1837
|
+
var serveStatic2 = (options) => {
|
|
1838
|
+
return async function serveStatic2(c, next) {
|
|
1839
|
+
const getContent = async (path) => {
|
|
1840
|
+
const file = Bun.file(path);
|
|
1841
|
+
return await file.exists() ? file : null;
|
|
1842
|
+
};
|
|
1843
|
+
const isDir = async (path) => {
|
|
1844
|
+
let isDir2;
|
|
1845
|
+
try {
|
|
1846
|
+
const stats = await stat(path);
|
|
1847
|
+
isDir2 = stats.isDirectory();
|
|
1848
|
+
} catch {}
|
|
1849
|
+
return isDir2;
|
|
1850
|
+
};
|
|
1851
|
+
return serveStatic({
|
|
1852
|
+
...options,
|
|
1853
|
+
getContent,
|
|
1854
|
+
join,
|
|
1855
|
+
isDir
|
|
1856
|
+
})(c, next);
|
|
1857
|
+
};
|
|
1858
|
+
};
|
|
1859
|
+
|
|
1860
|
+
// node_modules/hono/dist/helper/ssg/middleware.js
|
|
1861
|
+
var X_HONO_DISABLE_SSG_HEADER_KEY = "x-hono-disable-ssg";
|
|
1862
|
+
var SSG_DISABLED_RESPONSE = (() => {
|
|
1863
|
+
try {
|
|
1864
|
+
return new Response("SSG is disabled", {
|
|
1865
|
+
status: 404,
|
|
1866
|
+
headers: { [X_HONO_DISABLE_SSG_HEADER_KEY]: "true" }
|
|
1867
|
+
});
|
|
1868
|
+
} catch {
|
|
1869
|
+
return null;
|
|
1870
|
+
}
|
|
1871
|
+
})();
|
|
1872
|
+
// node_modules/hono/dist/adapter/bun/ssg.js
|
|
1873
|
+
var { write } = Bun;
|
|
1874
|
+
|
|
1875
|
+
// node_modules/hono/dist/helper/websocket/index.js
|
|
1876
|
+
var WSContext = class {
|
|
1877
|
+
#init;
|
|
1878
|
+
constructor(init) {
|
|
1879
|
+
this.#init = init;
|
|
1880
|
+
this.raw = init.raw;
|
|
1881
|
+
this.url = init.url ? new URL(init.url) : null;
|
|
1882
|
+
this.protocol = init.protocol ?? null;
|
|
1883
|
+
}
|
|
1884
|
+
send(source, options) {
|
|
1885
|
+
this.#init.send(source, options ?? {});
|
|
1886
|
+
}
|
|
1887
|
+
raw;
|
|
1888
|
+
binaryType = "arraybuffer";
|
|
1889
|
+
get readyState() {
|
|
1890
|
+
return this.#init.readyState;
|
|
1891
|
+
}
|
|
1892
|
+
url;
|
|
1893
|
+
protocol;
|
|
1894
|
+
close(code, reason) {
|
|
1895
|
+
this.#init.close(code, reason);
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
var defineWebSocketHelper = (handler) => {
|
|
1899
|
+
return (...args) => {
|
|
1900
|
+
if (typeof args[0] === "function") {
|
|
1901
|
+
const [createEvents, options] = args;
|
|
1902
|
+
return async function upgradeWebSocket(c, next) {
|
|
1903
|
+
const events = await createEvents(c);
|
|
1904
|
+
const result = await handler(c, events, options);
|
|
1905
|
+
if (result) {
|
|
1906
|
+
return result;
|
|
1907
|
+
}
|
|
1908
|
+
await next();
|
|
1909
|
+
};
|
|
1910
|
+
} else {
|
|
1911
|
+
const [c, events, options] = args;
|
|
1912
|
+
return (async () => {
|
|
1913
|
+
const upgraded = await handler(c, events, options);
|
|
1914
|
+
if (!upgraded) {
|
|
1915
|
+
throw new Error("Failed to upgrade WebSocket");
|
|
1916
|
+
}
|
|
1917
|
+
return upgraded;
|
|
1918
|
+
})();
|
|
1919
|
+
}
|
|
1920
|
+
};
|
|
1921
|
+
};
|
|
1922
|
+
|
|
1923
|
+
// node_modules/hono/dist/adapter/bun/server.js
|
|
1924
|
+
var getBunServer = (c) => ("server" in c.env) ? c.env.server : c.env;
|
|
1925
|
+
|
|
1926
|
+
// node_modules/hono/dist/adapter/bun/websocket.js
|
|
1927
|
+
var upgradeWebSocket = defineWebSocketHelper((c, events) => {
|
|
1928
|
+
const server = getBunServer(c);
|
|
1929
|
+
if (!server) {
|
|
1930
|
+
throw new TypeError("env has to include the 2nd argument of fetch.");
|
|
1931
|
+
}
|
|
1932
|
+
const upgradeResult = server.upgrade(c.req.raw, {
|
|
1933
|
+
data: {
|
|
1934
|
+
events,
|
|
1935
|
+
url: new URL(c.req.url),
|
|
1936
|
+
protocol: c.req.url
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
if (upgradeResult) {
|
|
1940
|
+
return new Response(null);
|
|
1941
|
+
}
|
|
1942
|
+
return;
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1653
1945
|
// src/lib/browser-script.ts
|
|
1654
1946
|
function getBrowserScript(serverUrl) {
|
|
1655
1947
|
return `(function(){
|
|
@@ -1674,6 +1966,71 @@ window.__logs={push:push,flush:flush,config:cfg};
|
|
|
1674
1966
|
})();`;
|
|
1675
1967
|
}
|
|
1676
1968
|
|
|
1969
|
+
// src/server/routes/alerts.ts
|
|
1970
|
+
function alertsRoutes(db) {
|
|
1971
|
+
const app = new Hono2;
|
|
1972
|
+
app.post("/", async (c) => {
|
|
1973
|
+
const body = await c.req.json();
|
|
1974
|
+
if (!body.project_id || !body.name)
|
|
1975
|
+
return c.json({ error: "project_id and name required" }, 422);
|
|
1976
|
+
return c.json(createAlertRule(db, body), 201);
|
|
1977
|
+
});
|
|
1978
|
+
app.get("/", (c) => {
|
|
1979
|
+
const { project_id } = c.req.query();
|
|
1980
|
+
return c.json(listAlertRules(db, project_id || undefined));
|
|
1981
|
+
});
|
|
1982
|
+
app.put("/:id", async (c) => {
|
|
1983
|
+
const body = await c.req.json();
|
|
1984
|
+
const updated = updateAlertRule(db, c.req.param("id"), body);
|
|
1985
|
+
if (!updated)
|
|
1986
|
+
return c.json({ error: "not found" }, 404);
|
|
1987
|
+
return c.json(updated);
|
|
1988
|
+
});
|
|
1989
|
+
app.delete("/:id", (c) => {
|
|
1990
|
+
deleteAlertRule(db, c.req.param("id"));
|
|
1991
|
+
return c.json({ deleted: true });
|
|
1992
|
+
});
|
|
1993
|
+
return app;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// src/server/routes/issues.ts
|
|
1997
|
+
function issuesRoutes(db) {
|
|
1998
|
+
const app = new Hono2;
|
|
1999
|
+
app.get("/", (c) => {
|
|
2000
|
+
const { project_id, status, limit } = c.req.query();
|
|
2001
|
+
return c.json(listIssues(db, project_id || undefined, status || undefined, limit ? Number(limit) : 50));
|
|
2002
|
+
});
|
|
2003
|
+
app.get("/:id", (c) => {
|
|
2004
|
+
const issue = getIssue(db, c.req.param("id"));
|
|
2005
|
+
if (!issue)
|
|
2006
|
+
return c.json({ error: "not found" }, 404);
|
|
2007
|
+
return c.json(issue);
|
|
2008
|
+
});
|
|
2009
|
+
app.get("/:id/logs", (c) => {
|
|
2010
|
+
const issue = getIssue(db, c.req.param("id"));
|
|
2011
|
+
if (!issue)
|
|
2012
|
+
return c.json({ error: "not found" }, 404);
|
|
2013
|
+
const rows = searchLogs(db, {
|
|
2014
|
+
project_id: issue.project_id ?? undefined,
|
|
2015
|
+
level: issue.level,
|
|
2016
|
+
service: issue.service ?? undefined,
|
|
2017
|
+
text: issue.message_template.slice(0, 50),
|
|
2018
|
+
limit: 50
|
|
2019
|
+
});
|
|
2020
|
+
return c.json(rows);
|
|
2021
|
+
});
|
|
2022
|
+
app.put("/:id", async (c) => {
|
|
2023
|
+
const { status } = await c.req.json();
|
|
2024
|
+
if (!["open", "resolved", "ignored"].includes(status))
|
|
2025
|
+
return c.json({ error: "invalid status" }, 422);
|
|
2026
|
+
const updated = updateIssueStatus(db, c.req.param("id"), status);
|
|
2027
|
+
if (!updated)
|
|
2028
|
+
return c.json({ error: "not found" }, 404);
|
|
2029
|
+
return c.json(updated);
|
|
2030
|
+
});
|
|
2031
|
+
return app;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
1677
2034
|
// src/server/routes/jobs.ts
|
|
1678
2035
|
function jobsRoutes(db) {
|
|
1679
2036
|
const app = new Hono2;
|
|
@@ -1747,6 +2104,23 @@ function logsRoutes(db) {
|
|
|
1747
2104
|
const rows = getLogContext(db, c.req.param("trace_id"));
|
|
1748
2105
|
return c.json(rows);
|
|
1749
2106
|
});
|
|
2107
|
+
app.get("/export", (c) => {
|
|
2108
|
+
const { project_id, since, until, level, service, format, limit } = c.req.query();
|
|
2109
|
+
const opts = { project_id: project_id || undefined, since: since || undefined, until: until || undefined, level: level || undefined, service: service || undefined, limit: limit ? Number(limit) : undefined };
|
|
2110
|
+
if (format === "csv") {
|
|
2111
|
+
c.header("Content-Type", "text/csv");
|
|
2112
|
+
c.header("Content-Disposition", "attachment; filename=logs.csv");
|
|
2113
|
+
const chunks2 = [];
|
|
2114
|
+
exportToCsv(db, opts, (s) => chunks2.push(s));
|
|
2115
|
+
return c.text(chunks2.join(""));
|
|
2116
|
+
}
|
|
2117
|
+
c.header("Content-Type", "application/json");
|
|
2118
|
+
c.header("Content-Disposition", "attachment; filename=logs.json");
|
|
2119
|
+
const chunks = [];
|
|
2120
|
+
exportToJson(db, opts, (s) => chunks.push(s));
|
|
2121
|
+
return c.text(chunks.join(`
|
|
2122
|
+
`));
|
|
2123
|
+
});
|
|
1750
2124
|
return app;
|
|
1751
2125
|
}
|
|
1752
2126
|
|
|
@@ -1823,6 +2197,26 @@ function projectsRoutes(db) {
|
|
|
1823
2197
|
return c.json(page, 201);
|
|
1824
2198
|
});
|
|
1825
2199
|
app.get("/:id/pages", (c) => c.json(listPages(db, c.req.param("id"))));
|
|
2200
|
+
app.put("/:id/retention", async (c) => {
|
|
2201
|
+
const body = await c.req.json();
|
|
2202
|
+
setRetentionPolicy(db, c.req.param("id"), body);
|
|
2203
|
+
return c.json({ updated: true });
|
|
2204
|
+
});
|
|
2205
|
+
app.post("/:id/retention/run", (c) => {
|
|
2206
|
+
const result = runRetentionForProject(db, c.req.param("id"));
|
|
2207
|
+
return c.json(result);
|
|
2208
|
+
});
|
|
2209
|
+
app.post("/:id/pages/:page_id/auth", async (c) => {
|
|
2210
|
+
const { type, credentials } = await c.req.json();
|
|
2211
|
+
if (!type || !credentials)
|
|
2212
|
+
return c.json({ error: "type and credentials required" }, 422);
|
|
2213
|
+
const result = setPageAuth(db, c.req.param("page_id"), type, credentials);
|
|
2214
|
+
return c.json({ id: result.id, type: result.type, created_at: result.created_at }, 201);
|
|
2215
|
+
});
|
|
2216
|
+
app.delete("/:id/pages/:page_id/auth", (c) => {
|
|
2217
|
+
deletePageAuth(db, c.req.param("page_id"));
|
|
2218
|
+
return c.json({ deleted: true });
|
|
2219
|
+
});
|
|
1826
2220
|
app.post("/:id/sync-repo", async (c) => {
|
|
1827
2221
|
const project = getProject(db, c.req.param("id"));
|
|
1828
2222
|
if (!project)
|
|
@@ -1835,6 +2229,191 @@ function projectsRoutes(db) {
|
|
|
1835
2229
|
return app;
|
|
1836
2230
|
}
|
|
1837
2231
|
|
|
2232
|
+
// node_modules/hono/dist/utils/stream.js
|
|
2233
|
+
var StreamingApi = class {
|
|
2234
|
+
writer;
|
|
2235
|
+
encoder;
|
|
2236
|
+
writable;
|
|
2237
|
+
abortSubscribers = [];
|
|
2238
|
+
responseReadable;
|
|
2239
|
+
aborted = false;
|
|
2240
|
+
closed = false;
|
|
2241
|
+
constructor(writable, _readable) {
|
|
2242
|
+
this.writable = writable;
|
|
2243
|
+
this.writer = writable.getWriter();
|
|
2244
|
+
this.encoder = new TextEncoder;
|
|
2245
|
+
const reader = _readable.getReader();
|
|
2246
|
+
this.abortSubscribers.push(async () => {
|
|
2247
|
+
await reader.cancel();
|
|
2248
|
+
});
|
|
2249
|
+
this.responseReadable = new ReadableStream({
|
|
2250
|
+
async pull(controller) {
|
|
2251
|
+
const { done, value } = await reader.read();
|
|
2252
|
+
done ? controller.close() : controller.enqueue(value);
|
|
2253
|
+
},
|
|
2254
|
+
cancel: () => {
|
|
2255
|
+
this.abort();
|
|
2256
|
+
}
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
async write(input) {
|
|
2260
|
+
try {
|
|
2261
|
+
if (typeof input === "string") {
|
|
2262
|
+
input = this.encoder.encode(input);
|
|
2263
|
+
}
|
|
2264
|
+
await this.writer.write(input);
|
|
2265
|
+
} catch {}
|
|
2266
|
+
return this;
|
|
2267
|
+
}
|
|
2268
|
+
async writeln(input) {
|
|
2269
|
+
await this.write(input + `
|
|
2270
|
+
`);
|
|
2271
|
+
return this;
|
|
2272
|
+
}
|
|
2273
|
+
sleep(ms) {
|
|
2274
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
2275
|
+
}
|
|
2276
|
+
async close() {
|
|
2277
|
+
try {
|
|
2278
|
+
await this.writer.close();
|
|
2279
|
+
} catch {}
|
|
2280
|
+
this.closed = true;
|
|
2281
|
+
}
|
|
2282
|
+
async pipe(body) {
|
|
2283
|
+
this.writer.releaseLock();
|
|
2284
|
+
await body.pipeTo(this.writable, { preventClose: true });
|
|
2285
|
+
this.writer = this.writable.getWriter();
|
|
2286
|
+
}
|
|
2287
|
+
onAbort(listener) {
|
|
2288
|
+
this.abortSubscribers.push(listener);
|
|
2289
|
+
}
|
|
2290
|
+
abort() {
|
|
2291
|
+
if (!this.aborted) {
|
|
2292
|
+
this.aborted = true;
|
|
2293
|
+
this.abortSubscribers.forEach((subscriber) => subscriber());
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
|
|
2298
|
+
// node_modules/hono/dist/helper/streaming/utils.js
|
|
2299
|
+
var isOldBunVersion = () => {
|
|
2300
|
+
const version = typeof Bun !== "undefined" ? Bun.version : undefined;
|
|
2301
|
+
if (version === undefined) {
|
|
2302
|
+
return false;
|
|
2303
|
+
}
|
|
2304
|
+
const result = version.startsWith("1.1") || version.startsWith("1.0") || version.startsWith("0.");
|
|
2305
|
+
isOldBunVersion = () => result;
|
|
2306
|
+
return result;
|
|
2307
|
+
};
|
|
2308
|
+
|
|
2309
|
+
// node_modules/hono/dist/helper/streaming/sse.js
|
|
2310
|
+
var SSEStreamingApi = class extends StreamingApi {
|
|
2311
|
+
constructor(writable, readable) {
|
|
2312
|
+
super(writable, readable);
|
|
2313
|
+
}
|
|
2314
|
+
async writeSSE(message) {
|
|
2315
|
+
const data = await resolveCallback(message.data, HtmlEscapedCallbackPhase.Stringify, false, {});
|
|
2316
|
+
const dataLines = data.split(/\r\n|\r|\n/).map((line) => {
|
|
2317
|
+
return `data: ${line}`;
|
|
2318
|
+
}).join(`
|
|
2319
|
+
`);
|
|
2320
|
+
for (const key of ["event", "id", "retry"]) {
|
|
2321
|
+
if (message[key] && /[\r\n]/.test(message[key])) {
|
|
2322
|
+
throw new Error(`${key} must not contain "\\r" or "\\n"`);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
const sseData = [
|
|
2326
|
+
message.event && `event: ${message.event}`,
|
|
2327
|
+
dataLines,
|
|
2328
|
+
message.id && `id: ${message.id}`,
|
|
2329
|
+
message.retry && `retry: ${message.retry}`
|
|
2330
|
+
].filter(Boolean).join(`
|
|
2331
|
+
`) + `
|
|
2332
|
+
|
|
2333
|
+
`;
|
|
2334
|
+
await this.write(sseData);
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
var run = async (stream, cb, onError) => {
|
|
2338
|
+
try {
|
|
2339
|
+
await cb(stream);
|
|
2340
|
+
} catch (e) {
|
|
2341
|
+
if (e instanceof Error && onError) {
|
|
2342
|
+
await onError(e, stream);
|
|
2343
|
+
await stream.writeSSE({
|
|
2344
|
+
event: "error",
|
|
2345
|
+
data: e.message
|
|
2346
|
+
});
|
|
2347
|
+
} else {
|
|
2348
|
+
console.error(e);
|
|
2349
|
+
}
|
|
2350
|
+
} finally {
|
|
2351
|
+
stream.close();
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
var contextStash = /* @__PURE__ */ new WeakMap;
|
|
2355
|
+
var streamSSE = (c, cb, onError) => {
|
|
2356
|
+
const { readable, writable } = new TransformStream;
|
|
2357
|
+
const stream = new SSEStreamingApi(writable, readable);
|
|
2358
|
+
if (isOldBunVersion()) {
|
|
2359
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
2360
|
+
if (!stream.closed) {
|
|
2361
|
+
stream.abort();
|
|
2362
|
+
}
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
contextStash.set(stream.responseReadable, c);
|
|
2366
|
+
c.header("Transfer-Encoding", "chunked");
|
|
2367
|
+
c.header("Content-Type", "text/event-stream");
|
|
2368
|
+
c.header("Cache-Control", "no-cache");
|
|
2369
|
+
c.header("Connection", "keep-alive");
|
|
2370
|
+
run(stream, cb, onError);
|
|
2371
|
+
return c.newResponse(stream.responseReadable);
|
|
2372
|
+
};
|
|
2373
|
+
|
|
2374
|
+
// src/server/routes/stream.ts
|
|
2375
|
+
function streamRoutes(db) {
|
|
2376
|
+
const app = new Hono2;
|
|
2377
|
+
app.get("/", (c) => {
|
|
2378
|
+
const { project_id, level, service } = c.req.query();
|
|
2379
|
+
return streamSSE(c, async (stream2) => {
|
|
2380
|
+
let lastId = null;
|
|
2381
|
+
const latest = db.prepare("SELECT id FROM logs ORDER BY timestamp DESC LIMIT 1").get();
|
|
2382
|
+
lastId = latest?.id ?? null;
|
|
2383
|
+
while (true) {
|
|
2384
|
+
const conditions = [];
|
|
2385
|
+
const params = {};
|
|
2386
|
+
if (lastId) {
|
|
2387
|
+
conditions.push("rowid > (SELECT rowid FROM logs WHERE id = $lastId)");
|
|
2388
|
+
params.$lastId = lastId;
|
|
2389
|
+
}
|
|
2390
|
+
if (project_id) {
|
|
2391
|
+
conditions.push("project_id = $project_id");
|
|
2392
|
+
params.$project_id = project_id;
|
|
2393
|
+
}
|
|
2394
|
+
if (level) {
|
|
2395
|
+
conditions.push("level IN (" + level.split(",").map((l, i) => `$l${i}`).join(",") + ")");
|
|
2396
|
+
level.split(",").forEach((l, i) => {
|
|
2397
|
+
params[`$l${i}`] = l;
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
if (service) {
|
|
2401
|
+
conditions.push("service = $service");
|
|
2402
|
+
params.$service = service;
|
|
2403
|
+
}
|
|
2404
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2405
|
+
const rows = db.prepare(`SELECT * FROM logs ${where} ORDER BY timestamp ASC LIMIT 50`).all(params);
|
|
2406
|
+
for (const row of rows) {
|
|
2407
|
+
await stream2.writeSSE({ data: JSON.stringify(row), id: row.id, event: row.level });
|
|
2408
|
+
lastId = row.id;
|
|
2409
|
+
}
|
|
2410
|
+
await stream2.sleep(500);
|
|
2411
|
+
}
|
|
2412
|
+
});
|
|
2413
|
+
});
|
|
2414
|
+
return app;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
1838
2417
|
// src/server/index.ts
|
|
1839
2418
|
var PORT = Number(process.env.LOGS_PORT ?? 3460);
|
|
1840
2419
|
var db = getDb();
|
|
@@ -1847,10 +2426,16 @@ app.get("/script.js", (c) => {
|
|
|
1847
2426
|
return c.text(getBrowserScript(host));
|
|
1848
2427
|
});
|
|
1849
2428
|
app.route("/api/logs", logsRoutes(db));
|
|
2429
|
+
app.route("/api/logs/stream", streamRoutes(db));
|
|
1850
2430
|
app.route("/api/projects", projectsRoutes(db));
|
|
1851
2431
|
app.route("/api/jobs", jobsRoutes(db));
|
|
2432
|
+
app.route("/api/alerts", alertsRoutes(db));
|
|
2433
|
+
app.route("/api/issues", issuesRoutes(db));
|
|
1852
2434
|
app.route("/api/perf", perfRoutes(db));
|
|
1853
|
-
app.get("/", (c) => c.json(
|
|
2435
|
+
app.get("/health", (c) => c.json(getHealth(db)));
|
|
2436
|
+
app.get("/dashboard", (c) => c.redirect("/dashboard/"));
|
|
2437
|
+
app.use("/dashboard/*", serveStatic2({ root: "./dashboard/dist", rewriteRequestPath: (p) => p.replace(/^\/dashboard/, "") }));
|
|
2438
|
+
app.get("/", (c) => c.json({ service: "@hasna/logs", port: PORT, status: "ok", dashboard: `http://localhost:${PORT}/dashboard/` }));
|
|
1854
2439
|
startScheduler(db);
|
|
1855
2440
|
console.log(`@hasna/logs server running on http://localhost:${PORT}`);
|
|
1856
2441
|
var server_default = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/logs",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -21,7 +23,15 @@
|
|
|
21
23
|
"access": "restricted",
|
|
22
24
|
"registry": "https://registry.npmjs.org/"
|
|
23
25
|
},
|
|
24
|
-
"keywords": [
|
|
26
|
+
"keywords": [
|
|
27
|
+
"logs",
|
|
28
|
+
"monitoring",
|
|
29
|
+
"mcp",
|
|
30
|
+
"ai-agents",
|
|
31
|
+
"sentry",
|
|
32
|
+
"performance",
|
|
33
|
+
"lighthouse"
|
|
34
|
+
],
|
|
25
35
|
"author": "Andrei Hasna <andrei@hasna.com>",
|
|
26
36
|
"license": "MIT",
|
|
27
37
|
"dependencies": {
|