From 17651d989de553e4331e5656f96737733377103f Mon Sep 17 00:00:00 2001 From: Edward Viaene Date: Mon, 23 Sep 2024 21:01:01 -0500 Subject: [PATCH] UI work --- pkg/observability/logs.go | 16 ++- pkg/observability/logs_test.go | 4 +- pkg/observability/types.go | 24 +++- webapp/.gitignore | 4 +- webapp/src/Routes/Logs/Logs.tsx | 211 ++++++++++++++++++++++++++++++++ 5 files changed, 250 insertions(+), 9 deletions(-) create mode 100644 webapp/src/Routes/Logs/Logs.tsx diff --git a/pkg/observability/logs.go b/pkg/observability/logs.go index 5f266b9..1021748 100644 --- a/pkg/observability/logs.go +++ b/pkg/observability/logs.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "math" + "sort" "strings" "time" ) @@ -13,9 +14,11 @@ func (o *Observability) getLogs(fromDate, endDate time.Time, pos int64, maxLogLi Enabled: true, Environments: []string{"dev", "qa", "prod"}, LogEntries: []LogEntry{}, - Keys: make(map[KeyValue]int), + Keys: KeyValueInt{}, } + keys := make(map[KeyValue]int) + logFiles := []string{} if maxLogLines == 0 { @@ -65,7 +68,7 @@ func (o *Observability) getLogs(fromDate, endDate time.Time, pos int64, maxLogLi logEntryResponse.LogEntries = append(logEntryResponse.LogEntries, logEntry) for k, v := range logMessage.Data { if k != "log" { - logEntryResponse.Keys[KeyValue{Key: k, Value: v}] += 1 + keys[KeyValue{Key: k, Value: v}] += 1 } } } @@ -81,6 +84,15 @@ func (o *Observability) getLogs(fromDate, endDate time.Time, pos int64, maxLogLi logEntryResponse.NextPos = pos } + for k, v := range keys { + logEntryResponse.Keys = append(logEntryResponse.Keys, KeyValueTotal{ + Key: k.Key, + Value: k.Value, + Total: v, + }) + } + sort.Sort(logEntryResponse.Keys) + return logEntryResponse, nil } diff --git a/pkg/observability/logs_test.go b/pkg/observability/logs_test.go index 4cec612..9d11a47 100644 --- a/pkg/observability/logs_test.go +++ b/pkg/observability/logs_test.go @@ -74,8 +74,8 @@ func TestFloatToDate(t *testing.T) { func TestKeyValue(t *testing.T) { logEntryResponse := LogEntryResponse{ - Keys: map[KeyValue]int{ - {Key: "k", Value: "v"}: 4, + Keys: KeyValueInt{ + {Key: "k", Value: "v", Total: 4}, }, } out, err := json.Marshal(logEntryResponse) diff --git a/pkg/observability/types.go b/pkg/observability/types.go index af818be..46db9a1 100644 --- a/pkg/observability/types.go +++ b/pkg/observability/types.go @@ -46,8 +46,13 @@ type LogEntry struct { Data string `json:"data"` } -type KeyValueInt map[KeyValue]int +type KeyValueInt []KeyValueTotal +type KeyValueTotal struct { + Key string + Value string + Total int +} type KeyValue struct { Key string Value string @@ -55,10 +60,23 @@ type KeyValue struct { func (kv KeyValueInt) MarshalJSON() ([]byte, error) { res := "[" - for k, v := range kv { - res += `{ "key" : "` + k.Key + `", "value": "` + k.Value + `", "total": ` + strconv.Itoa(v) + ` },` + for _, v := range kv { + res += `{ "key" : "` + v.Key + `", "value": "` + v.Value + `", "total": ` + strconv.Itoa(v.Total) + ` },` } res = strings.TrimRight(res, ",") res += "]" return []byte(res), nil } + +func (kv KeyValueInt) Len() int { + return len(kv) +} +func (kv KeyValueInt) Less(i, j int) bool { + if kv[i].Key == kv[j].Key { + return kv[i].Value < kv[j].Value + } + return kv[i].Key < kv[j].Key +} +func (kv KeyValueInt) Swap(i, j int) { + kv[i], kv[j] = kv[j], kv[i] +} diff --git a/webapp/.gitignore b/webapp/.gitignore index f3f691e..ddfda02 100644 --- a/webapp/.gitignore +++ b/webapp/.gitignore @@ -1,5 +1,5 @@ # Logs -logs +/logs *.log npm-debug.log* yarn-debug.log* @@ -129,4 +129,4 @@ dist .yarn/install-state.gz .pnp.* -.DS_Store \ No newline at end of file +.DS_Store diff --git a/webapp/src/Routes/Logs/Logs.tsx b/webapp/src/Routes/Logs/Logs.tsx new file mode 100644 index 0000000..3e3c777 --- /dev/null +++ b/webapp/src/Routes/Logs/Logs.tsx @@ -0,0 +1,211 @@ +import { Card, Container, Text, Table, Title, Button, Grid, Select, Popover, Group, TextInput, rem, ActionIcon, Checkbox, Highlight} from "@mantine/core"; +import { AppSettings } from "../../Constants/Constants"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { useAuthContext } from "../../Auth/Auth"; +import { Link, useSearchParams } from "react-router-dom"; +import { TbArrowRight, TbSearch, TbSettings } from "react-icons/tb"; +import { DatePickerInput } from "@mantine/dates"; +import { useEffect, useState } from "react"; +import React from "react"; + +type LogsDataResponse = { + enabled: boolean; + logEntries: LogEntry[]; + environments: string[]; + nextPos: number; + keys: Keys[]; +} +type LogEntry = { + data: string; + timestamp: string; +} +type Keys = { + key: string; + value: string; + total: number; +} +type Tag = { + key: string; + value: string; +} + +function getDate(date:Date) { + var dd = String(date.getDate()).padStart(2, '0'); + var mm = String(date.getMonth() + 1).padStart(2, '0'); //January is 0! + var yyyy = date.getFullYear(); + return yyyy + "-" + mm + '-' + dd; +} + +export function Logs() { + const {authInfo} = useAuthContext(); + const timezoneOffset = new Date().getTimezoneOffset() * -1 + const [currentQueryParameters] = useSearchParams(); + const dateParam = currentQueryParameters.get("date") + const environmentParam = currentQueryParameters.get("environment") + const [tags, setTags] = useState([]) + const [search, setSearch] = useState("") + const [searchParam, setSearchParam] = useState("") + const [logsDate, setLogsDate] = useState(dateParam === null ? new Date() : new Date(dateParam)); + const [environment, setEnvironment] = useState(environmentParam === null ? "all" : environmentParam) + const { isPending, fetchNextPage, hasNextPage, error, data } = useInfiniteQuery({ + queryKey: ['logs', environment, logsDate, tags, searchParam], + queryFn: async ({ pageParam }) => + fetch(AppSettings.url + '/observability/logs?environment='+(environment === undefined || environment === "" ? "all" : environment)+'&fromDate='+(logsDate == undefined ? getDate(new Date()) : getDate(logsDate)) + '&endDate='+(logsDate == undefined ? getDate(new Date()) : getDate(logsDate)) + "&pos="+pageParam+"&offset="+timezoneOffset+"&tags="+encodeURIComponent(tags.map(t => t.key + "=" + t.value).join(","))+"&search="+encodeURIComponent(searchParam), { + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + authInfo.token + }, + }).then((res) => { + return res.json() + } + ), + initialPageParam: 0, + getNextPageParam: (lastRequest) => lastRequest.nextPos === -1 ? null : lastRequest.nextPos, + }) + + const captureEnter = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + setSearchParam(search) + } + } + + useEffect(() => { + const handleScroll = () => { + const { scrollTop, clientHeight, scrollHeight } = + document.documentElement; + if (scrollTop + clientHeight >= scrollHeight - 20) { + fetchNextPage(); + } + }; + + window.addEventListener("scroll", handleScroll); + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, [fetchNextPage]) + + if(isPending) return "Loading..." + if(error) return 'A backend error has occurred: ' + error.message + + if(data.pages.length === 0 || !data.pages[0].enabled) { // show disabled page if not enabled + return ( + + + Logs + + + + { !data.pages[0].enabled ? + "Logs are not enabled." + : + null + } + + + + + + + + + ) + } + + const rows = data.pages.map((group, groupIndex) => ( + + {group.logEntries.map((row, i) => ( + + {row.timestamp} + {searchParam === "" ? row.data : {row.data}} + + ))} + + )); + return ( + + + Logs + + + + } + rightSection={ + setSearchParam(search)}> + + + } + onKeyDown={(e) => captureEnter(e)} + onChange={(e) => setSearch(e.currentTarget.value)} + value={search} + /> + + + + + +