some style tweaks to the frontend, added search bar, fixed bug with errors not showing correctly

pull/1/head
Adam Veldhousen 3 years ago
parent 36ad084621
commit f76004a261
Signed by: adam
GPG Key ID: 6DB29003C6DD1E4B

7
.gitignore vendored

@ -1,2 +1,7 @@
.bin .bin
.idea .idea
client/node_modules/
client/public/build/
.DS_Store

6
client/.gitignore vendored

@ -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"
}
}

Binary file not shown.

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/*"]
}

@ -29,6 +29,7 @@ func (dm *DomainManager) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
} }
var resolved Resolved var resolved Resolved
var err error
// lookup in cache // lookup in cache
if dest := dm.LookupRecord(q.Name); dest != nil { if dest := dm.LookupRecord(q.Name); dest != nil {
responseMessage = new(dns.Msg) responseMessage = new(dns.Msg)
@ -39,7 +40,7 @@ func (dm *DomainManager) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
responseMessage = rule.CreateAnswer(q.Name) responseMessage = rule.CreateAnswer(q.Name)
responseMessage.Authoritative = true responseMessage.Authoritative = true
ql.Status = CustomRule ql.Status = CustomRule
} else if resolved, ql.Error = dm.Recursors.Resolve(r); ql.Error == nil { } else if resolved, err = dm.Recursors.Resolve(r); err == nil {
dm.SaveAnswers(q.Name, resolved.Message.Answer) dm.SaveAnswers(q.Name, resolved.Message.Answer)
responseMessage = resolved.Message responseMessage = resolved.Message
ql.RecurseUpstreamIP = resolved.UpstreamUsed ql.RecurseUpstreamIP = resolved.UpstreamUsed
@ -47,6 +48,10 @@ func (dm *DomainManager) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
ql.Status = RecursedUpstream ql.Status = RecursedUpstream
} }
if err != nil {
ql.Error = err.Error()
}
responseMessage.SetReply(r) responseMessage.SetReply(r)
responseMessage.RecursionAvailable = true responseMessage.RecursionAvailable = true
responseMessage.Compress = true responseMessage.Compress = true
@ -82,6 +87,6 @@ type QueryLog struct {
TotalTimeMs int TotalTimeMs int
RecurseRoundTripTimeMs int RecurseRoundTripTimeMs int
RecurseUpstreamIP string RecurseUpstreamIP string
Error error Error string
Status ResponseStatus Status ResponseStatus
} }

@ -2,7 +2,6 @@ package internal
import ( import (
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -103,7 +102,7 @@ type RuleRow struct {
} }
func (ss *Sqlite) AddRule(rr RuleRow) error { func (ss *Sqlite) AddRule(rr RuleRow) error {
sql := `INSERT INTO rules (name, expression, answerType, answerValue, ttl, weight, enabled, created) sql := `INSERT INTO rules (name, expression, answerType, answerValue, ttl, weight, enabled, created)
VALUES (?, ? , ?, ?, ?, ?, 1, ?);` VALUES (?, ? , ?, ?, ?, ?, 1, ?);`
if _, err := ss.Exec(sql, rr.Name, rr.Value, rr.Answer.Type, rr.Answer.Value, rr.TTL, rr.Weight, time.Now().UTC().Format(ISO8601)); err != nil { if _, err := ss.Exec(sql, rr.Name, rr.Value, rr.Answer.Type, rr.Answer.Value, rr.TTL, rr.Weight, time.Now().UTC().Format(ISO8601)); err != nil {
@ -194,12 +193,12 @@ func (ss *Sqlite) GetLog(in GetLogInput) ([]QueryLog, error) {
} }
sql := ` sql := `
SELECT SELECT
started, clientIp, protocol, domain, totalTimeMs, error, recurseRoundTripTimeMs, recurseUpstreamIp, status started, clientIp, protocol, domain, totalTimeMs, error, recurseRoundTripTimeMs, recurseUpstreamIp, status
FROM FROM
log log
WHERE WHERE
id > ? AND started > ? AND started < ? id > ? AND started > ? AND started < ?
ORDER BY started DESC ORDER BY started DESC
LIMIT ?; LIMIT ?;
` `
@ -213,7 +212,6 @@ func (ss *Sqlite) GetLog(in GetLogInput) ([]QueryLog, error) {
var ql []QueryLog var ql []QueryLog
for rows.Next() { for rows.Next() {
var q QueryLog var q QueryLog
var errStr string
var started string var started string
rows.Scan( rows.Scan(
@ -222,16 +220,12 @@ func (ss *Sqlite) GetLog(in GetLogInput) ([]QueryLog, error) {
&q.Protocol, &q.Protocol,
&q.Domain, &q.Domain,
&q.TotalTimeMs, &q.TotalTimeMs,
&errStr, &q.Error,
&q.RecurseRoundTripTimeMs, &q.RecurseRoundTripTimeMs,
&q.RecurseUpstreamIP, &q.RecurseUpstreamIP,
&q.Status, &q.Status,
) )
if errStr != "" {
q.Error = errors.New(errStr)
}
if q.Started, err = time.Parse(ISO8601, started); err != nil { if q.Started, err = time.Parse(ISO8601, started); err != nil {
return nil, fmt.Errorf("could not parse time '%s': %v", started, err) return nil, fmt.Errorf("could not parse time '%s': %v", started, err)
} }
@ -294,7 +288,7 @@ func (ss *Sqlite) GetLogAggregate(la LogAggregateInput) ([]LogAggregateDataPoint
strftime('%%s', started)/(%d) as "timeWindow" strftime('%%s', started)/(%d) as "timeWindow"
FROM log FROM log
GROUP BY %s, strftime('%%s', started) / (%d) GROUP BY %s, strftime('%%s', started) / (%d)
ORDER BY started desc; ORDER BY started ASC;
` `
sql = fmt.Sprintf(sql, column, timeWindow, column, timeWindow) sql = fmt.Sprintf(sql, column, timeWindow, column, timeWindow)
@ -328,22 +322,18 @@ func (ss *Sqlite) GetLogAggregate(la LogAggregateInput) ([]LogAggregateDataPoint
func (ss *Sqlite) Log(ql QueryLog) error { func (ss *Sqlite) Log(ql QueryLog) error {
sql := ` sql := `
INSERT INTO log INSERT INTO log
(started, clientIp, protocol, domain, totalTimeMs, error, recurseRoundTripTimeMs, recurseUpstreamIp, status) (started, clientIp, protocol, domain, totalTimeMs, error, recurseRoundTripTimeMs, recurseUpstreamIp, status)
VALUES VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?); (?, ?, ?, ?, ?, ?, ?, ?, ?);
` `
var errStr string
if ql.Error != nil {
errStr = ql.Error.Error()
}
if _, err := ss.DB.Exec(sql, if _, err := ss.DB.Exec(sql,
ql.Started.Format(ISO8601), ql.Started.Format(ISO8601),
ql.ClientIP, ql.ClientIP,
ql.Protocol, ql.Protocol,
ql.Domain, ql.Domain,
ql.TotalTimeMs, ql.TotalTimeMs,
errStr, ql.Error,
ql.RecurseRoundTripTimeMs, ql.RecurseRoundTripTimeMs,
ql.RecurseUpstreamIP, ql.RecurseUpstreamIP,
ql.Status, ql.Status,
@ -386,6 +376,7 @@ func initTable(db *sql.DB) error {
status TEXT NOT NULL status TEXT NOT NULL
); );
CREATE TABLE IF NOT EXISTS rules ( CREATE TABLE IF NOT EXISTS rules (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
@ -400,6 +391,7 @@ func initTable(db *sql.DB) error {
CREATE UNIQUE INDEX IF NOT EXISTS idx_rules_name ON rules (name); CREATE UNIQUE INDEX IF NOT EXISTS idx_rules_name ON rules (name);
CREATE UNIQUE INDEX IF NOT EXISTS idx_rules_expression ON rules (expression); CREATE UNIQUE INDEX IF NOT EXISTS idx_rules_expression ON rules (expression);
CREATE TABLE IF NOT EXISTS recursors ( CREATE TABLE IF NOT EXISTS recursors (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
ipAddress TEXT NOT NULL, ipAddress TEXT NOT NULL,
@ -407,6 +399,27 @@ func initTable(db *sql.DB) error {
weight INT NOT NULL weight INT NOT NULL
); );
CREATE UNIQUE INDEX IF NOT EXISTS idx_recursors_ipAddress ON recursors (ipAddress); CREATE UNIQUE INDEX IF NOT EXISTS idx_recursors_ipAddress ON recursors (ipAddress);
CREATE TABLE IF NOT EXISTS ruleslist (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
url TEXT NOT NULL,
lastLoadedTs TEXT NOT NULL,
weight INT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_ruleslist_name ON ruleslist (name);
CREATE UNIQUE INDEX IF NOT EXISTS idx_ruleslist_url ON ruleslist (url);
CREATE TABLE IF NOT EXISTS ruleslist_entry (
id INTEGER PRIMARY KEY,
ruleslistId INTEGER NOT NULL,
expression TEXT NOT NULL,
ipAddress TEXT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_ruleslist_entry_expression ON ruleslist_entry (expression);
CREATE UNIQUE INDEX IF NOT EXISTS idx_ruleslist_entry_ruleslistId ON ruleslist_entry (id, ruleslistId);
` `
if _, err := db.Exec(sql); err != nil { if _, err := db.Exec(sql); err != nil {

@ -1,11 +1,17 @@
dev: clean .bin/gopherhole .bin/config.json dev: clean .bin/gopherhole .bin/config.json
cd .bin && ./gopherhole -config config.json cd .bin && ./gopherhole -config config.json
client-dev: client/node_modules
cd ./client && npm run dev
client/node_modules:
cd ./client && npm install
clean: clean:
@rm -rf .bin/gopherhole @rm -rf .bin/gopherhole
clobber: clobber:
@rm -rf .bin @rm -rf .bin ./client/node_modules
test: test:
dig -p 5353 twitter.com @localhost dig -p 5353 twitter.com @localhost

Loading…
Cancel
Save