pull/1/head
parent
36ad084621
commit
f76004a261
@ -1,2 +1,7 @@
|
|||||||
.bin
|
.bin
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
client/node_modules/
|
||||||
|
client/public/build/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
/node_modules/
|
||||||
|
/public/build/
|
||||||
|
.vscode/
|
||||||
|
.DS_Store
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
@ -0,0 +1,10 @@
|
|||||||
|
# Svelte TypeScript Tailwindcss Setup
|
||||||
|
|
||||||
|
svelte template based on the default svelte/template with enabled typescript and tailwindcss support.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx degit munxar/svelte-template my-svelte-project
|
||||||
|
cd my-svelte-project
|
||||||
|
npm i
|
||||||
|
npm run dev
|
||||||
|
```
|
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "svelte-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"build": "NODE_ENV=production rollup -c",
|
||||||
|
"dev": "rollup -c -w",
|
||||||
|
"start": "sirv public",
|
||||||
|
"validate": "svelte-check"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-commonjs": "^17.0.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||||
|
"@rollup/plugin-typescript": "^8.1.0",
|
||||||
|
"@tsconfig/svelte": "^1.0.0",
|
||||||
|
"autoprefixer": "^10.2.3",
|
||||||
|
"postcss": "^8.2.4",
|
||||||
|
"rollup": "^2.3.4",
|
||||||
|
"rollup-plugin-css-only": "^3.1.0",
|
||||||
|
"rollup-plugin-livereload": "^2.0.0",
|
||||||
|
"rollup-plugin-svelte": "^7.0.0",
|
||||||
|
"rollup-plugin-terser": "^7.0.0",
|
||||||
|
"svelte": "^3.38.2",
|
||||||
|
"svelte-check": "^1.0.0",
|
||||||
|
"svelte-preprocess": "^4.6.3",
|
||||||
|
"tailwindcss": "^2.0.2",
|
||||||
|
"tslib": "^2.0.0",
|
||||||
|
"typescript": "^4.1.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||||
|
"bootstrap": "^5.0.0",
|
||||||
|
"chart.js": "^3.2.1",
|
||||||
|
"date-fns": "^2.21.3",
|
||||||
|
"fa-svelte": "^3.1.0",
|
||||||
|
"normalize.css": "^8.0.1",
|
||||||
|
"sirv-cli": "^1.0.0",
|
||||||
|
"svelte-chartjs": "^1.0.1",
|
||||||
|
"svelte-routing": "^1.6.0",
|
||||||
|
"sveltestrap": "^4.2.1"
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 3.1 KiB |
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
|
||||||
|
<title>Svelte app</title>
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
<link rel="stylesheet" href="/build/bundle.css" />
|
||||||
|
|
||||||
|
<script defer src="/build/bundle.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body></body>
|
||||||
|
</html>
|
@ -0,0 +1,74 @@
|
|||||||
|
import svelte from "rollup-plugin-svelte";
|
||||||
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
|
import resolve from "@rollup/plugin-node-resolve";
|
||||||
|
import livereload from "rollup-plugin-livereload";
|
||||||
|
import { terser } from "rollup-plugin-terser";
|
||||||
|
import sveltePreprocess from "svelte-preprocess";
|
||||||
|
import typescript from "@rollup/plugin-typescript";
|
||||||
|
import css from "rollup-plugin-css-only";
|
||||||
|
|
||||||
|
const production = !process.env.ROLLUP_WATCH;
|
||||||
|
|
||||||
|
function serve() {
|
||||||
|
let server;
|
||||||
|
|
||||||
|
function toExit() {
|
||||||
|
if (server) server.kill(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
writeBundle() {
|
||||||
|
if (server) return;
|
||||||
|
server = require("child_process").spawn(
|
||||||
|
"npm",
|
||||||
|
["run", "start", "--", "--dev"],
|
||||||
|
{
|
||||||
|
stdio: ["ignore", "inherit", "inherit"],
|
||||||
|
shell: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
process.on("SIGTERM", toExit);
|
||||||
|
process.on("exit", toExit);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: "src/main.ts",
|
||||||
|
output: {
|
||||||
|
sourcemap: true,
|
||||||
|
format: "iife",
|
||||||
|
name: "app",
|
||||||
|
file: "public/build/bundle.js",
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
svelte({
|
||||||
|
// add postcss config with tailwind
|
||||||
|
preprocess: sveltePreprocess({
|
||||||
|
postcss: {
|
||||||
|
plugins: [require("tailwindcss"), require("autoprefixer")],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
compilerOptions: {
|
||||||
|
dev: !production,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
css({ output: "bundle.css" }),
|
||||||
|
resolve({
|
||||||
|
browser: true,
|
||||||
|
dedupe: ["svelte"],
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
typescript({
|
||||||
|
sourceMap: !production,
|
||||||
|
inlineSources: !production,
|
||||||
|
}),
|
||||||
|
!production && serve(),
|
||||||
|
!production && livereload("public"),
|
||||||
|
production && terser(),
|
||||||
|
],
|
||||||
|
watch: {
|
||||||
|
clearScreen: false,
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
|
import Tailwind from "./Tailwind.svelte";
|
||||||
|
|
||||||
|
import { Router, Link, Route } from "svelte-routing";
|
||||||
|
import Home from "./routes/Home.svelte";
|
||||||
|
|
||||||
|
export let url = "";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Tailwind />
|
||||||
|
<main class="flex w-full h-full">
|
||||||
|
<Router {url}>
|
||||||
|
<nav class="bg-gray-700 text-gray-300 w-1/8 py-5 px-2 shadow-md">
|
||||||
|
<Link to="/">Home</Link>
|
||||||
|
</nav>
|
||||||
|
<div class="flex-column py-5 px-5 w-full h-full overflow-y-auto">
|
||||||
|
<Route path="/" component={Home} />
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
</main>
|
@ -0,0 +1,14 @@
|
|||||||
|
<style global lang="postcss">
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
@apply w-full h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,25 @@
|
|||||||
|
import { sub } from 'date-fns'
|
||||||
|
import { formatDate } from './metrics';
|
||||||
|
|
||||||
|
|
||||||
|
export const getLogs = async function ({
|
||||||
|
start = sub(new Date(), { hours: 12 }),
|
||||||
|
end = new Date(),
|
||||||
|
filter = ""
|
||||||
|
} = {}) {
|
||||||
|
try {
|
||||||
|
const data = await fetch(`http://localhost:8080/api/v1/metrics/log?filter=${filter}`, {
|
||||||
|
"method": "GET",
|
||||||
|
"headers": { "Accept": "application/json" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const { success, payload } = await data.json();
|
||||||
|
if (success) {
|
||||||
|
return { payload, start, end, filter };
|
||||||
|
}
|
||||||
|
return { error: payload };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
import { sub, format } from 'date-fns';
|
||||||
|
import { readable } from "svelte/store";
|
||||||
|
|
||||||
|
export const formatDate = date => format(date, "yyyy-MM-dd HH:mm:ss.SSS");
|
||||||
|
|
||||||
|
/*
|
||||||
|
take something like `clientIp:127.0.0.1 protocol:udp domain:google.com`
|
||||||
|
`(prop:expr)*
|
||||||
|
*/
|
||||||
|
export const parseTerms = (terms: string = "") :[string] | [] => {
|
||||||
|
|
||||||
|
const matches = terms.match(/([a-z]+[:>][\w\.]+)*/ig);
|
||||||
|
|
||||||
|
const exprs = matches.reduce((agg, term) => {
|
||||||
|
|
||||||
|
}, []);
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchMetrics = async function ({
|
||||||
|
start = sub(new Date(), { hours: 8 }),
|
||||||
|
end = new Date(),
|
||||||
|
key = "domain",
|
||||||
|
interval = "30",
|
||||||
|
} = {}) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`http://localhost:8080/api/v1/metrics/stats?start=${formatDate(
|
||||||
|
start
|
||||||
|
)}&end=${formatDate(end)}&key=${key}&interval=${interval}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: { Accept: "application/json" }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { success, payload } = await response.json();
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return { error: payload };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { payload, interval, start, end, key };
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return { error: e };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const metricsStore = readable([], (set) => {
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
const { error, payload } = await fetchMetrics({});
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(payload);
|
||||||
|
set(payload);
|
||||||
|
}, 15 * 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
});
|
@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
InputGroupText,
|
||||||
|
} from "sveltestrap";
|
||||||
|
export let label = "label";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<InputGroup class="flex flex-row">
|
||||||
|
<InputGroupAddon addonType="prepend">
|
||||||
|
<InputGroupText>{label}</InputGroupText>
|
||||||
|
</InputGroupAddon>
|
||||||
|
<Input bsSize="sm" type="date" />
|
||||||
|
<Input bsSize="sm" type="time" />
|
||||||
|
</InputGroup>
|
@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { Column, Table } from "sveltestrap";
|
||||||
|
import { getLogs } from "../api/logs";
|
||||||
|
|
||||||
|
let rows = [];
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
const { payload } = await getLogs();
|
||||||
|
console.log(payload);
|
||||||
|
rows = payload || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-column text-sm">
|
||||||
|
{#if rows.length > 0}
|
||||||
|
<Table {rows} let:row hover bordered>
|
||||||
|
<Column header="Started">
|
||||||
|
{row.Started}
|
||||||
|
</Column>
|
||||||
|
<Column header="Domain">
|
||||||
|
{row.Domain}
|
||||||
|
</Column>
|
||||||
|
<Column header="IP">
|
||||||
|
{row.ClientIP}
|
||||||
|
</Column>
|
||||||
|
<Column header="Status">
|
||||||
|
{row.Status}
|
||||||
|
</Column>
|
||||||
|
<Column header="Protocol">
|
||||||
|
{row.Protocol}
|
||||||
|
</Column>
|
||||||
|
<Column header="Error">
|
||||||
|
{#if row.Error}
|
||||||
|
{row.Error}
|
||||||
|
{/if}
|
||||||
|
</Column>
|
||||||
|
<Column header="Round Trip Ms">
|
||||||
|
{row.RecurseRoundTripTimeMs}
|
||||||
|
</Column>
|
||||||
|
<Column header="Upstream Used">
|
||||||
|
{row.RecurseUpstreamIP}
|
||||||
|
</Column>
|
||||||
|
<Column header="Total Time Ms">
|
||||||
|
{row.TotalTimeMs}
|
||||||
|
</Column>
|
||||||
|
</Table>
|
||||||
|
{/if}
|
||||||
|
</div>
|
@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Dropdown,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownToggle,
|
||||||
|
Form,
|
||||||
|
FormGroup,
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
InputGroupText,
|
||||||
|
Label,
|
||||||
|
Input,
|
||||||
|
} from "sveltestrap";
|
||||||
|
import DatetimePicker from "./DatetimePicker.svelte";
|
||||||
|
|
||||||
|
let menuItem = [
|
||||||
|
{ label: "Domain Name", key: "domain" },
|
||||||
|
{ label: "Client IP", key: "clientIp" },
|
||||||
|
{ label: "Protocol", key: "protocol" },
|
||||||
|
{ label: "Status", key: "status" },
|
||||||
|
];
|
||||||
|
|
||||||
|
let selected = menuItem[0];
|
||||||
|
const selectItem = (selectedItem) => {
|
||||||
|
selected = selectedItem;
|
||||||
|
handleOnChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
let searchFilter = "";
|
||||||
|
let startDate = new Date();
|
||||||
|
let endDate = new Date();
|
||||||
|
|
||||||
|
const handleOnChange = () => {
|
||||||
|
if (onChange) {
|
||||||
|
const event = {
|
||||||
|
terms: searchFilter,
|
||||||
|
start: startDate,
|
||||||
|
end: endDate,
|
||||||
|
key: selected.key,
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export let onChange = (evt) =>
|
||||||
|
console.log("no handler bound to SearchOptions.svelte: ", evt);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full flex flex-row">
|
||||||
|
<section class="w-full flex flex-row">
|
||||||
|
<FormGroup class="flex-grow flex flex-row mx-2">
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupAddon addonType="prepend">
|
||||||
|
<InputGroupText>Search</InputGroupText>
|
||||||
|
</InputGroupAddon>
|
||||||
|
<Input
|
||||||
|
id="search"
|
||||||
|
type="search"
|
||||||
|
on:change={handleOnChange}
|
||||||
|
bind:value={searchFilter}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup class="flex flex-row">
|
||||||
|
<DatetimePicker label="Start" />
|
||||||
|
<DatetimePicker label="End" />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup class="mx-2">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownToggle caret>By {selected.label}</DropdownToggle>
|
||||||
|
<DropdownMenu>
|
||||||
|
{#each menuItem as item}
|
||||||
|
<DropdownItem active={selected.key === item.key}>
|
||||||
|
<span on:click={() => selectItem(item)}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</DropdownItem>
|
||||||
|
{/each}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</FormGroup>
|
||||||
|
</section>
|
||||||
|
</div>
|
@ -0,0 +1,66 @@
|
|||||||
|
<script>
|
||||||
|
import { fetchMetrics } from "../api/metrics.ts";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { Chart, registerables } from "chart.js";
|
||||||
|
Chart.register(...registerables);
|
||||||
|
|
||||||
|
let canvas;
|
||||||
|
onMount(async () => {
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
const { labels, datasets } = await fetchAndPrepareMetrics();
|
||||||
|
|
||||||
|
console.log(labels);
|
||||||
|
console.log(datasets);
|
||||||
|
var c = new Chart(ctx, {
|
||||||
|
type: "line",
|
||||||
|
data: { labels, datasets },
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: `Top Requests`,
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
stacked: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchAndPrepareMetrics = async () => {
|
||||||
|
const { error, payload } = await fetchMetrics({ key: "clientIp" });
|
||||||
|
if (error) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartData = payload.reduce((agg, x) => {
|
||||||
|
let root = agg[x.Header] || {
|
||||||
|
labels: [],
|
||||||
|
dataset: {
|
||||||
|
label: x.Header,
|
||||||
|
borderColor: "rgb(75,192,192)",
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
root.dataset.data = root.dataset.data.concat(x.Count);
|
||||||
|
root.labels = root.labels.concat(x.Time);
|
||||||
|
agg[x.Header] = root;
|
||||||
|
return agg;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const finalChartData = Object.keys(chartData).map((x) => chartData[x]);
|
||||||
|
const finalChartLabels = finalChartData[0].labels;
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: finalChartLabels,
|
||||||
|
datasets: finalChartData.map((x) => x.dataset),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="position: relative; max-height: 512px; height: 512px; width: 100%;">
|
||||||
|
<canvas width="100%" height="100%" bind:this={canvas} />
|
||||||
|
</div>
|
@ -0,0 +1,7 @@
|
|||||||
|
import App from "./App.svelte";
|
||||||
|
|
||||||
|
const app = new App({
|
||||||
|
target: document.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import LogViewer from "../components/LogViewer.svelte";
|
||||||
|
import SearchOptions from "../components/SearchOptions.svelte";
|
||||||
|
import TimeChart from "../components/TimeChart.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="w-full">
|
||||||
|
<header>
|
||||||
|
<h1>Home</h1>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<SearchOptions />
|
||||||
|
</section>
|
||||||
|
<section class="m-10">
|
||||||
|
<TimeChart />
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<LogViewer />
|
||||||
|
</section>
|
||||||
|
</section>
|
@ -0,0 +1,24 @@
|
|||||||
|
const { tailwindExtractor } = require("tailwindcss/lib/lib/purgeUnusedStyles");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
purge: {
|
||||||
|
content: ["src/**/*.svelte", "public/index.html"],
|
||||||
|
options: {
|
||||||
|
defaultExtractor: (content) => [
|
||||||
|
...tailwindExtractor(content),
|
||||||
|
...[...content.matchAll(/(?:class:)*([\w\d-/:%.]+)/gm)].map(
|
||||||
|
([_match, group, ..._rest]) => group
|
||||||
|
),
|
||||||
|
],
|
||||||
|
keyframes: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
darkMode: false, // or 'media' or 'class'
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
|
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
||||||
|
}
|
Loading…
Reference in new issue