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

@ -2,7 +2,6 @@ package internal
import (
"database/sql"
"errors"
"fmt"
"io"
"net"
@ -103,7 +102,7 @@ type RuleRow struct {
}
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, ?);`
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 := `
SELECT
SELECT
started, clientIp, protocol, domain, totalTimeMs, error, recurseRoundTripTimeMs, recurseUpstreamIp, status
FROM
log
WHERE
id > ? AND started > ? AND started < ?
id > ? AND started > ? AND started < ?
ORDER BY started DESC
LIMIT ?;
`
@ -213,7 +212,6 @@ func (ss *Sqlite) GetLog(in GetLogInput) ([]QueryLog, error) {
var ql []QueryLog
for rows.Next() {
var q QueryLog
var errStr string
var started string
rows.Scan(
@ -222,16 +220,12 @@ func (ss *Sqlite) GetLog(in GetLogInput) ([]QueryLog, error) {
&q.Protocol,
&q.Domain,
&q.TotalTimeMs,
&errStr,
&q.Error,
&q.RecurseRoundTripTimeMs,
&q.RecurseUpstreamIP,
&q.Status,
)
if errStr != "" {
q.Error = errors.New(errStr)
}
if q.Started, err = time.Parse(ISO8601, started); err != nil {
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"
FROM log
GROUP BY %s, strftime('%%s', started) / (%d)
ORDER BY started desc;
ORDER BY started ASC;
`
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 {
sql := `
INSERT INTO log
INSERT INTO log
(started, clientIp, protocol, domain, totalTimeMs, error, recurseRoundTripTimeMs, recurseUpstreamIp, status)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?);
`
var errStr string
if ql.Error != nil {
errStr = ql.Error.Error()
}
if _, err := ss.DB.Exec(sql,
ql.Started.Format(ISO8601),
ql.ClientIP,
ql.Protocol,
ql.Domain,
ql.TotalTimeMs,
errStr,
ql.Error,
ql.RecurseRoundTripTimeMs,
ql.RecurseUpstreamIP,
ql.Status,
@ -386,6 +376,7 @@ func initTable(db *sql.DB) error {
status TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS rules (
id INTEGER PRIMARY KEY,
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_expression ON rules (expression);
CREATE TABLE IF NOT EXISTS recursors (
id INTEGER PRIMARY KEY,
ipAddress TEXT NOT NULL,
@ -407,6 +399,27 @@ func initTable(db *sql.DB) error {
weight INT NOT NULL
);
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 {

@ -1,11 +1,17 @@
dev: clean .bin/gopherhole .bin/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:
@rm -rf .bin/gopherhole
clobber:
@rm -rf .bin
@rm -rf .bin ./client/node_modules
test:
dig -p 5353 twitter.com @localhost

Loading…
Cancel
Save