From 3fed37dbab951e83927b77d98f4299aea2f9d84b Mon Sep 17 00:00:00 2001 From: simagix Date: Thu, 16 Feb 2023 17:00:29 -0500 Subject: [PATCH 1/3] consolidate audit tables --- README_DEV.md | 16 ++++-- charts_handler.go | 79 ++++++++++++++++++++++------ database.go | 19 +++++-- sqlite3.go | 52 ++++++++++++++----- sqlite3_query.go | 129 ++++++++++++++++++++++++++++++++++++++++------ stats_template.go | 124 ++++++++++++++++++++++++++++++-------------- template.go | 25 +++++---- version | 2 +- 8 files changed, 343 insertions(+), 103 deletions(-) diff --git a/README_DEV.md b/README_DEV.md index 89b5abc..37aab1b 100644 --- a/README_DEV.md +++ b/README_DEV.md @@ -35,8 +35,8 @@ The easiest way is to go to the home page `http://localhost:3721` and following - `/hatchets/{hatchet}/charts/ops?type={}` views average ops time chart, types are: - stats - counts -- `/hatchets/{hatchet}/charts/reslen?type={}` views response length chart, types are: - - ips +- `/hatchets/{hatchet}/charts/reslen-ip?ip={}` views response length by IPs chart, types are: +- `/hatchets/{hatchet}/charts/reslen-ns?ns={}` views response length by IPs chart, types are: ``` ## Query SQLite3 Database @@ -53,7 +53,7 @@ SELECT * FROM mongod_1b3d5f7; ``` ```sqlite3 -SELECT date, severity, component, context, substr(message, 1, 60) message FROM mongod_1b3d5f7; +SELECT date, severity, component, context, SUBSTR(message, 1, 60) message FROM mongod_1b3d5f7; ``` ```sqlite3 @@ -80,6 +80,16 @@ SELECT SUBSTR(date, 1, 16), COUNT(op), op, ns GROUP by SUBSTR(date, 1, 16), op, ns; ``` +### Query Long Lasting Connection Duration and Relen +```sqlite3 +SELECT ip, context, STRFTIME('%s', SUBSTR(etm,1,19))-STRFTIME('%s', SUBSTR(btm,1,19)) dur, reslen + FROM ( + SELECT MAX(a.date) etm, MIN(a.date) btm, a.context context, b.ip, SUM(a.reslen) reslen + FROM mongod_1b3d5f7 a, mongod_1b3d5f7_clients b WHERE a.context = b.context GROUP BY a.context + ) + WHERE dur > 0 AND reslen > 0 order by dur DESC, reslen DESC limit 23; +``` + ## Use SQLite3 API Different drivers are supported for most popular programming languages including Golang, NodeJS, Java, Python, and C#. diff --git a/charts_handler.go b/charts_handler.go index 249633f..507fc16 100644 --- a/charts_handler.go +++ b/charts_handler.go @@ -1,4 +1,7 @@ -// Copyright 2022-present Kuei-chun Chen. All rights reserved. +/* + * Copyright 2022-present Kuei-chun Chen. All rights reserved. + * charts_handler.go + */ package hatchet @@ -16,6 +19,14 @@ const ( BAR_CHART = "bar_chart" BUBBLE_CHART = "bubble_chart" PIE_CHART = "pie_chart" + + T_OPS = "ops" + T_RESLEN_UP = "reslen-ip" + T_OPS_COUNTS = "ops-counts" + T_CONNS_ACCEPTED = "connections-accepted" + T_CONNS_TIME = "connections-time" + T_CONNS_TOTAL = "connections-total" + T_RESLEN_NS = "reslen-ns" ) type Chart struct { @@ -27,18 +38,20 @@ type Chart struct { var charts = map[string]Chart{ "instruction": {0, "select a chart", "", ""}, - "ops": {1, "Average Operation Time", + T_OPS: {1, "Average Operation Time", "Display average operations time over a period of time", "/ops?type=stats"}, - "reslen": {2, "Response Length ", - "Display total response length from client IPs", "/reslen?type=ips"}, - "ops-counts": {3, "Operation Counts", + T_OPS_COUNTS: {2, "Operation Counts", "Display total counts of operations", "/ops?type=counts"}, - "connections-accepted": {4, "Accepted Connections", + "connections-accepted": {3, "Accepted Connections", "Display accepted connections from clients", "/connections?type=accepted"}, - "connections-time": {5, "Accepted & Ended Connections", + T_CONNS_TIME: {4, "Accepted & Ended Connections", "Display accepted vs ended connections over a period of time", "/connections?type=time"}, - "connections-total": {6, "Accepted & Ended from IPs", + T_CONNS_TOTAL: {5, "Accepted & Ended from IPs", "Display accepted vs ended connections by client IPs", "/connections?type=total"}, + T_RESLEN_UP: {6, "Response Length by IPs ", + "Display total response length by client IPs", "/reslen-ip?ip="}, + T_RESLEN_NS: {7, "Response Length by Namespaces ", + "Display total response length by namespaces", "/reslen-ns?ns="}, } // ChartsHandler responds to charts API calls @@ -65,11 +78,12 @@ func ChartsHandler(w http.ResponseWriter, r *http.Request, params httprouter.Par start, end = getStartEndDates(duration) } - if attr == "ops" { + if attr == T_OPS { chartType := r.URL.Query().Get("type") + op := r.URL.Query().Get("op") if chartType == "stats" { - chartType := "ops" - docs, err := dbase.GetAverageOpTime(duration) + chartType := T_OPS + docs, err := dbase.GetAverageOpTime(op, duration) if len(docs) > 0 { start = docs[0].Date end = docs[len(docs)-1].Date @@ -90,7 +104,7 @@ func ChartsHandler(w http.ResponseWriter, r *http.Request, params httprouter.Par return } } else if chartType == "counts" { - chartType = "ops-counts" + chartType = T_OPS_COUNTS docs, err := dbase.GetOpsCounts(duration) if err != nil { json.NewEncoder(w).Encode(map[string]interface{}{"ok": 0, "error": err.Error()}) @@ -154,13 +168,40 @@ func ChartsHandler(w http.ResponseWriter, r *http.Request, params httprouter.Par } return } - } else if attr == "reslen" { - chartType := r.URL.Query().Get("type") + } else if attr == T_RESLEN_UP { + ip := r.URL.Query().Get("ip") + chartType := attr + if dbase.GetVerbose() { + log.Println("type", chartType, "duration", duration) + } + docs, err := dbase.GetReslenByIP(ip, duration) + if err != nil { + json.NewEncoder(w).Encode(map[string]interface{}{"ok": 0, "error": err.Error()}) + return + } + templ, err := GetChartTemplate(PIE_CHART) + if err != nil { + json.NewEncoder(w).Encode(map[string]interface{}{"ok": 0, "error": err.Error()}) + return + } + chart := charts[chartType] + if ip != "" { + chart.Title += fmt.Sprintf(" (%v)", ip) + } + doc := map[string]interface{}{"Hatchet": hatchetName, "NameValues": docs, "Chart": chart, + "Type": chartType, "Summary": summary, "Start": start, "End": end} + if err = templ.Execute(w, doc); err != nil { + json.NewEncoder(w).Encode(map[string]interface{}{"ok": 0, "error": err.Error()}) + return + } + return + } else if attr == T_RESLEN_NS { + ns := r.URL.Query().Get("ns") + chartType := attr if dbase.GetVerbose() { log.Println("type", chartType, "duration", duration) } - chartType = "reslen" - docs, err := dbase.GetReslenByClients(duration) + docs, err := dbase.GetReslenByNamespace(ns, duration) if err != nil { json.NewEncoder(w).Encode(map[string]interface{}{"ok": 0, "error": err.Error()}) return @@ -170,7 +211,11 @@ func ChartsHandler(w http.ResponseWriter, r *http.Request, params httprouter.Par json.NewEncoder(w).Encode(map[string]interface{}{"ok": 0, "error": err.Error()}) return } - doc := map[string]interface{}{"Hatchet": hatchetName, "NameValues": docs, "Chart": charts[chartType], + chart := charts[chartType] + if ns != "" { + chart.Title += fmt.Sprintf(" (%v)", ns) + } + doc := map[string]interface{}{"Hatchet": hatchetName, "NameValues": docs, "Chart": chart, "Type": chartType, "Summary": summary, "Start": start, "End": end} if err = templ.Execute(w, doc); err != nil { json.NewEncoder(w).Encode(map[string]interface{}{"ok": 0, "error": err.Error()}) diff --git a/database.go b/database.go index 1be0e54..3deefe2 100644 --- a/database.go +++ b/database.go @@ -1,4 +1,8 @@ -// Copyright 2022-present Kuei-chun Chen. All rights reserved. +/* + * Copyright 2022-present Kuei-chun Chen. All rights reserved. + * database.go + */ + package hatchet type NameValue struct { @@ -6,14 +10,19 @@ type NameValue struct { Value int } +type NameValues struct { + Name string + Values []int +} + type Database interface { Begin() error Close() error Commit() error CreateMetaData() error GetAcceptedConnsCounts(duration string) ([]NameValue, error) - GetAuditData() (map[string][]NameValue, error) - GetAverageOpTime(duration string) ([]OpCount, error) + GetAuditData() (map[string][]NameValues, error) + GetAverageOpTime(op string, duration string) ([]OpCount, error) GetClientPreparedStmt() string GetConnectionStats(chartType string, duration string) ([]Remote, error) GetHatchetInfo() HatchetInfo @@ -22,7 +31,8 @@ type Database interface { GetHatchetPreparedStmt() string GetLogs(opts ...string) ([]LegacyLog, error) GetOpsCounts(duration string) ([]NameValue, error) - GetReslenByClients(duration string) ([]NameValue, error) + GetReslenByNamespace(ip string, duration string) ([]NameValue, error) + GetReslenByIP(ip string, duration string) ([]NameValue, error) GetSlowOps(orderBy string, order string, collscan bool) ([]OpStat, error) GetSlowestLogs(topN int) ([]LegacyLog, error) GetVerbose() bool @@ -36,4 +46,3 @@ type Database interface { func GetDatabase(hatchetName string) (Database, error) { return GetSQLite3DB(hatchetName) } - diff --git a/sqlite3.go b/sqlite3.go index fb9e2e2..23ba571 100644 --- a/sqlite3.go +++ b/sqlite3.go @@ -1,4 +1,7 @@ -// Copyright 2022-present Kuei-chun Chen. All rights reserved. +/* + * Copyright 2022-present Kuei-chun Chen. All rights reserved. + * sqlite3.go + */ package hatchet @@ -120,42 +123,64 @@ func (ptr *SQLite3DB) CreateMetaData() error { return err } - log.Printf("insert into %v_audit\n", ptr.hatchetName) + log.Printf("insert exception into %v_audit\n", ptr.hatchetName) istmt = fmt.Sprintf(`INSERT INTO %v_audit - SELECT 'ip', ip, SUM(accepted) open FROM %v_clients GROUP by ip ORDER BY open DESC`, ptr.hatchetName, ptr.hatchetName) + SELECT 'exception', severity, COUNT(*) count FROM %v WHERE severity IN ('W', 'E', 'F') + GROUP by severity`, ptr.hatchetName, ptr.hatchetName) if _, err = ptr.db.Exec(istmt); err != nil { return err } + log.Printf("insert failed into %v_audit\n", ptr.hatchetName) istmt = fmt.Sprintf(`INSERT INTO %v_audit - SELECT 'exception', severity, COUNT(*) count FROM %v WHERE severity IN ('W', 'E', 'F') - GROUP by severity ORDER BY count DESC`, ptr.hatchetName, ptr.hatchetName) + SELECT 'failed', SUBSTR(message, 1, INSTR(message, 'failed')+6) matched, COUNT(*) count FROM %v + WHERE message REGEXP "(\w\sfailed\s)" GROUP by matched`, ptr.hatchetName, ptr.hatchetName) if _, err = ptr.db.Exec(istmt); err != nil { return err } + log.Printf("insert op into %v_audit\n", ptr.hatchetName) istmt = fmt.Sprintf(`INSERT INTO %v_audit - SELECT 'failed', SUBSTR(message, 1, INSTR(message, 'failed')+6) failed, COUNT(*) count FROM %v - WHERE message REGEXP "(\w\sfailed\s)" GROUP by failed ORDER BY count DESC`, ptr.hatchetName, ptr.hatchetName) + SELECT 'op', op, COUNT(*) count FROM %v WHERE op != '' GROUP by op`, ptr.hatchetName, ptr.hatchetName) if _, err = ptr.db.Exec(istmt); err != nil { return err } + log.Printf("insert ip into %v_audit\n", ptr.hatchetName) istmt = fmt.Sprintf(`INSERT INTO %v_audit - SELECT 'ns', ns, COUNT(*) count FROM %v WHERE op != "" GROUP by ns ORDER BY count DESC`, ptr.hatchetName, ptr.hatchetName) + SELECT 'ip', ip, SUM(accepted) open FROM %v_clients GROUP by ip`, ptr.hatchetName, ptr.hatchetName) if _, err = ptr.db.Exec(istmt); err != nil { return err } + log.Printf("insert reslen-ip into %v_audit\n", ptr.hatchetName) istmt = fmt.Sprintf(`INSERT INTO %v_audit - SELECT 'reslen', b.ip, SUM(a.reslen) reslen FROM %v a, %v_clients b WHERE a.op != "" AND reslen > 0 AND a.context = b.context GROUP by b.ip ORDER BY reslen DESC`, + SELECT 'reslen-ip', b.ip, SUM(a.reslen) reslen FROM %v a, %v_clients b WHERE a.op != "" AND reslen > 0 AND a.context = b.context GROUP by b.ip`, ptr.hatchetName, ptr.hatchetName, ptr.hatchetName) if _, err = ptr.db.Exec(istmt); err != nil { return err } + log.Printf("insert ns into %v_audit\n", ptr.hatchetName) + istmt = fmt.Sprintf(`INSERT INTO %v_audit + SELECT 'ns', ns, COUNT(*) count FROM %v WHERE op != "" GROUP by ns`, ptr.hatchetName, ptr.hatchetName) + if _, err = ptr.db.Exec(istmt); err != nil { + return err + } + + log.Printf("insert reslen-ns into %v_audit\n", ptr.hatchetName) + istmt = fmt.Sprintf(`INSERT INTO %v_audit + SELECT 'reslen-ns', ns, SUM(reslen) reslen FROM %v WHERE ns != "" AND reslen > 0 GROUP by ns`, ptr.hatchetName, ptr.hatchetName) + if _, err = ptr.db.Exec(istmt); err != nil { + return err + } + + // commented out, the cmd takes a long time to process + log.Printf("insert duration into %v_audit\n", ptr.hatchetName) istmt = fmt.Sprintf(`INSERT INTO %v_audit - SELECT 'op', op, COUNT(*) count FROM %v WHERE op != '' GROUP by op ORDER BY count DESC`, ptr.hatchetName, ptr.hatchetName) + SELECT 'duration', context || ' (' || ip || ')', STRFTIME('%%s', SUBSTR(etm,1,19))-STRFTIME('%%s', SUBSTR(btm,1,19)) duration + FROM ( SELECT MAX(a.date) etm, MIN(a.date) btm, a.context, b.ip FROM %v a, %v_clients b WHERE a.id = b.id GROUP BY a.context) + WHERE duration > 0`, ptr.hatchetName, ptr.hatchetName, ptr.hatchetName) if _, err = ptr.db.Exec(istmt); err != nil { return err } @@ -183,15 +208,16 @@ func (ptr *SQLite3DB) GetHatchetInitStmt() string { CREATE TABLE %v_audit ( type, name, value); CREATE INDEX IF NOT EXISTS %v_idx_component ON %v (component); - CREATE INDEX IF NOT EXISTS %v_idx_context ON %v (context); + CREATE INDEX IF NOT EXISTS %v_idx_context ON %v (context,date); CREATE INDEX IF NOT EXISTS %v_idx_severity ON %v (severity); CREATE INDEX IF NOT EXISTS %v_idx_op ON %v (op,ns,filter); DROP TABLE IF EXISTS %v_clients; CREATE TABLE %v_clients( - id integer not null primary key, ip text, port text, conns integer, accepted integer, ended integer, context string)`, + id integer not null primary key, ip text, port text, conns integer, accepted integer, ended integer, context string); + CREATE INDEX IF NOT EXISTS %v_clients_idx_context ON %v_clients (context,ip);`, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, - hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName) + hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName) } // GetHatchetPreparedStmt returns prepared statement of the hatchet table diff --git a/sqlite3_query.go b/sqlite3_query.go index 83880ff..4c7d265 100644 --- a/sqlite3_query.go +++ b/sqlite3_query.go @@ -1,4 +1,7 @@ -// Copyright 2022-present Kuei-chun Chen. All rights reserved. +/* + * Copyright 2022-present Kuei-chun Chen. All rights reserved. + * sqlite3_query.go + */ package hatchet @@ -190,11 +193,15 @@ func (ptr *SQLite3DB) GetSlowestLogs(topN int) ([]LegacyLog, error) { return docs, err } -func (ptr *SQLite3DB) GetAverageOpTime(duration string) ([]OpCount, error) { +func (ptr *SQLite3DB) GetAverageOpTime(op string, duration string) ([]OpCount, error) { docs := []OpCount{} db := ptr.db durcond := "" var substr string + opcond := "op != ''" + if op != "" { + opcond = fmt.Sprintf("op = '%v'", op) + } if duration != "" { toks := strings.Split(duration, ",") durcond = fmt.Sprintf("AND date BETWEEN '%v' AND '%v'", toks[0], toks[1]) @@ -204,7 +211,7 @@ func (ptr *SQLite3DB) GetAverageOpTime(duration string) ([]OpCount, error) { substr = GetDateSubString(info.Start, info.End) } query := fmt.Sprintf(`SELECT %v, AVG(milli), COUNT(*), op, ns, filter FROM %v - WHERE op != '' %v GROUP by %v, op, ns, filter;`, substr, ptr.hatchetName, durcond, substr) + WHERE %v %v GROUP by %v, op, ns, filter;`, substr, ptr.hatchetName, opcond, durcond, substr) if ptr.verbose { log.Println(query) } @@ -373,18 +380,25 @@ func (ptr *SQLite3DB) GetOpsCounts(duration string) ([]NameValue, error) { return docs, err } -// GetReslenByClients returns total response length by connections -func (ptr *SQLite3DB) GetReslenByClients(duration string) ([]NameValue, error) { +// GetReslenByIP returns total response length by ip +func (ptr *SQLite3DB) GetReslenByIP(ip string, duration string) ([]NameValue, error) { hatchetName := ptr.hatchetName docs := []NameValue{} - var durcond string + var query, durcond, ipcond string if duration != "" { toks := strings.Split(duration, ",") durcond = fmt.Sprintf("AND a.date BETWEEN '%v' AND '%v'", toks[0], toks[1]) } - query := fmt.Sprintf(`SELECT b.ip, SUM(a.reslen) reslen FROM %v a, %v_clients b - WHERE a.op != "" AND reslen > 0 AND a.context = b.context %v GROUP by b.ip ORDER BY reslen DESC;`, - hatchetName, hatchetName, durcond) + if ip != "" { + ipcond = fmt.Sprintf("AND b.ip = '%v'", ip) + query = fmt.Sprintf(`SELECT a.context, SUM(a.reslen) reslen FROM %v a, %v_clients b + WHERE reslen > 0 AND a.context = b.context %v %v GROUP by a.context ORDER BY reslen DESC;`, + hatchetName, hatchetName, ipcond, durcond) + } else { + query = fmt.Sprintf(`SELECT b.ip, SUM(a.reslen) reslen FROM %v a, %v_clients b + WHERE reslen > 0 AND a.context = b.context %v GROUP by b.ip ORDER BY reslen DESC;`, + hatchetName, hatchetName, durcond) + } db := ptr.db if ptr.verbose { log.Println(query) @@ -406,10 +420,49 @@ func (ptr *SQLite3DB) GetReslenByClients(duration string) ([]NameValue, error) { return docs, err } -func (ptr *SQLite3DB) GetAuditData() (map[string][]NameValue, error) { +// GetReslenByNamespace returns total response length by ns +func (ptr *SQLite3DB) GetReslenByNamespace(ns string, duration string) ([]NameValue, error) { + hatchetName := ptr.hatchetName + docs := []NameValue{} + var query, durcond, nscond string + if duration != "" { + toks := strings.Split(duration, ",") + durcond = fmt.Sprintf("AND date BETWEEN '%v' AND '%v'", toks[0], toks[1]) + } + if ns != "" { + nscond = fmt.Sprintf("AND ns = '%v'", ns) + query = fmt.Sprintf(`SELECT ns, SUM(reslen) reslen FROM %v WHERE op != "" AND reslen > 0 %v %v GROUP by ns ORDER BY reslen DESC;`, + hatchetName, nscond, durcond) + } else { + query = fmt.Sprintf(`SELECT ns, SUM(reslen) reslen FROM %v WHERE op != "" AND reslen > 0 %v GROUP by ns ORDER BY reslen DESC;`, + hatchetName, durcond) + } + db := ptr.db + if ptr.verbose { + log.Println(query) + } + rows, err := db.Query(query) + if err != nil { + return docs, err + } + defer rows.Close() + for rows.Next() { + var doc NameValue + var conns float64 + if err = rows.Scan(&doc.Name, &conns); err != nil { + return docs, err + } + doc.Value = int(conns) + docs = append(docs, doc) + } + return docs, err +} + +func (ptr *SQLite3DB) GetAuditData() (map[string][]NameValues, error) { var err error - data := map[string][]NameValue{} - query := fmt.Sprintf(`SELECT type, name, value FROM %v_audit ORDER BY type, value DESC;`, ptr.hatchetName) + data := map[string][]NameValues{} + query := fmt.Sprintf(`SELECT type, name, value FROM %v_audit + WHERE type IN ('exception', 'failed', 'op', 'duration') ORDER BY type, value DESC;`, ptr.hatchetName) if ptr.verbose { log.Println(query) } @@ -418,13 +471,14 @@ func (ptr *SQLite3DB) GetAuditData() (map[string][]NameValue, error) { if err != nil { return data, err } - defer rows.Close() for rows.Next() { var category string - var doc NameValue - if err = rows.Scan(&category, &doc.Name, &doc.Value); err != nil { + var doc NameValues + var value int + if err = rows.Scan(&category, &doc.Name, &value); err != nil { return data, err } + doc.Values = append(doc.Values, value) if category == "exception" { if doc.Name == "E" { doc.Name = "Error" @@ -436,5 +490,50 @@ func (ptr *SQLite3DB) GetAuditData() (map[string][]NameValue, error) { } data[category] = append(data[category], doc) } + rows.Close() + + category := "ip" + query = fmt.Sprintf(`SELECT a.name ip, a.value count, b.value reslen FROM %v_audit a, %v_audit b WHERE a.type == '%v' AND b.type = 'reslen-ip' AND a.name = b.name ORDER BY reslen DESC;`, + ptr.hatchetName, ptr.hatchetName, category) + if ptr.verbose { + log.Println(query) + } + rows, err = db.Query(query) + if err != nil { + return data, err + } + for rows.Next() { + var doc NameValues + var val1, val2 int + if err = rows.Scan(&doc.Name, &val1, &val2); err != nil { + return data, err + } + doc.Values = append(doc.Values, val1) + doc.Values = append(doc.Values, val2) + data[category] = append(data[category], doc) + } + rows.Close() + + category = "ns" + query = fmt.Sprintf(`SELECT a.name ns, a.value count, b.value reslen FROM %v_audit a, %v_audit b WHERE a.type == '%v' AND b.type = 'reslen-ns' AND a.name = b.name ORDER BY reslen DESC;`, + ptr.hatchetName, ptr.hatchetName, category) + if ptr.verbose { + log.Println(query) + } + rows, err = db.Query(query) + if err != nil { + return data, err + } + for rows.Next() { + var doc NameValues + var val1, val2 int + if err = rows.Scan(&doc.Name, &val1, &val2); err != nil { + return data, err + } + doc.Values = append(doc.Values, val1) + doc.Values = append(doc.Values, val2) + data[category] = append(data[category], doc) + } + defer rows.Close() return data, err } diff --git a/stats_template.go b/stats_template.go index 9941842..f7e1b15 100644 --- a/stats_template.go +++ b/stats_template.go @@ -1,4 +1,8 @@ -// Copyright 2022-present Kuei-chun Chen. All rights reserved. +/* + * Copyright 2022-present Kuei-chun Chen. All rights reserved. + * stats_template.go + */ + package hatchet import ( @@ -6,6 +10,7 @@ import ( "html/template" "strings" + "github.com/simagix/gox" "golang.org/x/text/language" "golang.org/x/text/message" ) @@ -14,7 +19,7 @@ import ( func GetStatsTableTemplate(collscan bool, orderBy string, download string) (*template.Template, error) { html := headers if download == "" { - html = getContentHTML() + getFooter() + html = getContentHTML() } html += getStatsTable(collscan, orderBy, download) + "" return template.New("hatchet").Funcs(template.FuncMap{ @@ -108,81 +113,101 @@ func getStatsTable(collscan bool, orderBy string, download string) string { // GetAuditTablesTemplate returns HTML func GetAuditTablesTemplate() (*template.Template, error) { - html := headers + getContentHTML() + getFooter() - html += ` + html := headers + getContentHTML() + html += `{{$name := .Hatchet}} {{if hasData .Data "exception"}} -

Exceptions

+

Exceptions

- + {{range $n, $val := index .Data "exception"}} - + + + {{end}}
SeverityCounts
SeverityTotal
{{add $n 1}}{{$val.Name}}{{numPrinter $val.Value}}
{{add $n 1}} + {{$val.Name}} + {{getFormattedNumber $val.Values 0}}
{{end}} {{if hasData .Data "failed"}} -

Failed Operations

+

Failed Operations

- - {{$name := .Hatchet}} + {{range $n, $val := index .Data "failed"}} - + {{end}}
Failed OperationsCounts
Failed OperationTotal
{{add $n 1}} - {{$val.Name}} + {{$val.Name}} {{numPrinter $val.Value}}{{getFormattedNumber $val.Values 0}}
{{end}} {{if hasData .Data "op"}} -

Operations Stats

+

Operations Stats

- + {{range $n, $val := index .Data "op"}} - + + + {{end}}
OperationCounts
OperationTotal
{{add $n 1}}{{$val.Name}}{{numPrinter $val.Value}}
{{add $n 1}} + {{$val.Name}} + {{getFormattedNumber $val.Values 0}}
{{end}} {{if hasData .Data "ip"}} -

Connected Clients

+

Stats by IPs

- + {{range $n, $val := index .Data "ip"}} - + + + {{end}}
IPAccepted Connections
IPAccepted ConnectionsResponse Length
{{add $n 1}}{{$val.Name}}{{numPrinter $val.Value}}
{{add $n 1}} + {{$val.Name}} + {{getFormattedNumber $val.Values 0}}{{getFormattedSize $val.Values 1}}
{{end}} {{if hasData .Data "ns"}} -

Namespaces

+

Stats by Namespaces

- + {{range $n, $val := index .Data "ns"}} - + + + {{end}}
NamespaceCounts
NamespaceAccessedResponse Length
{{add $n 1}}{{$val.Name}}{{numPrinter $val.Value}}
{{add $n 1}} + {{$val.Name}} + {{getFormattedNumber $val.Values 0}}{{getFormattedSize $val.Values 1}}
{{end}} -{{if hasData .Data "reslen"}} -

Response Length

+{{if hasData .Data "duration"}} +

Top N Long Lasting Connections

- - {{range $n, $val := index .Data "reslen"}} - + + {{range $n, $val := index .Data "duration"}} + {{if lt $n 23}} + + + + {{end}} + {{end}}
IPResponse Length (bytes)
{{add $n 1}}{{$val.Name}}{{numPrinter $val.Value}}
ContextDuration
{{add $n 1}}{{$val.Name}} + {{getFormattedDuration $val.Values 0}}
{{end}} @@ -193,11 +218,34 @@ func GetAuditTablesTemplate() (*template.Template, error) { "add": func(a int, b int) int { return a + b }, - "hasData": func(data map[string][]NameValue, key string) bool { + "hasData": func(data map[string][]NameValues, key string) bool { return len(data[key]) > 0 }, "numPrinter": func(n interface{}) string { printer := message.NewPrinter(language.English) return printer.Sprintf("%v", ToInt(n)) + }, + "getContext": func(s string) string { + toks := strings.Split(s, " ") + if len(toks) == 0 { + return s + } + return toks[0] + }, + "getFormattedNumber": func(numbers []int, i int) string { + printer := message.NewPrinter(language.English) + return printer.Sprintf("%v", numbers[i]) + }, + "getDurationFromSeconds": func(s int) string { + return gox.GetDurationFromSeconds(float64(s)) + }, + "getFormattedDuration": func(numbers []int, i int) string { + return gox.GetDurationFromSeconds(float64(numbers[i])) + }, + "getStorageSize": func(s int) string { + return gox.GetStorageSize(float64(s)) + }, + "getFormattedSize": func(numbers []int, i int) string { + return gox.GetStorageSize(numbers[i]) }}).Parse(html) } diff --git a/template.go b/template.go index 78ba8b3..566919a 100644 --- a/template.go +++ b/template.go @@ -1,4 +1,8 @@ -// Copyright 2022-present Kuei-chun Chen. All rights reserved. +/* + * Copyright 2022-present Kuei-chun Chen. All rights reserved. + * template.go + */ + package hatchet import ( @@ -71,15 +75,16 @@ const headers = ` } tr:nth-child(even) {background-color: #fff;} tr:nth-child(odd) {background-color: #f2f2f2;} - .api { + ul, ol { font-family: Consolas, monaco, monospace; + font-size: .8em; } .btn { background-color: transparent; border: none; outline:none; color: #4285F4; - padding: 5px 10px; + padding: 5px 5px; cursor: pointer; font-size: 16px; } @@ -297,16 +302,14 @@ func getMainPage() string {
-

{{.Version}}

-

Reports

- - - - - + + + + +
TitleDescription
AuditSecurity and audits
ChartsStats charts
SearchSearch logs
StatsSlow operations summary
TopNSlowest 25 operations
AuditDisplay information on security audits and performance metrics
ChartsA number of charts are available for security audits and performance metrics
SearchPowerful log searching function with key metrics highlighted
StatsSummary of slow operational query patterns and duration
TopNDisplay the slowest 23 operation logs

Charts

@@ -340,7 +343,7 @@ func getMainPage() string {
  • /api/hatchet/v1.0/hatchets/{hatchet}/stats/audit
  • /api/hatchet/v1.0/hatchets/{hatchet}/stats/slowops[?COLLSCAN={bool}&orderBy={str}]
  • -

    @simagix

    +


    {{.Version}}

    ` template += fmt.Sprintf(``, CHEN_ICO) return template diff --git a/version b/version index 9325c3c..9e11b32 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.3.0 \ No newline at end of file +0.3.1 From 45930d169b4f9f06ade6190b0c9f8251f8a2d491 Mon Sep 17 00:00:00 2001 From: simagix Date: Sat, 18 Feb 2023 15:34:55 -0500 Subject: [PATCH 2/3] smart assistant --- README_DEV.md | 4 +- audit_template.go | 321 ++++++++++++++++++++++++++++++++++++++++++ charts_handler.go | 4 +- charts_template.go | 4 +- database.go | 3 +- hatchet.go | 11 +- images.go | 12 ++ legacy.go | 26 +++- logv2.go | 26 +++- sqlite3.go | 32 ++++- sqlite3_query.go | 103 ++++++++++++-- sqlite3_query_test.go | 3 - stats_handler.go | 7 +- stats_template.go | 141 +------------------ template.go | 19 ++- utils.go | 11 +- 16 files changed, 527 insertions(+), 200 deletions(-) create mode 100644 audit_template.go create mode 100644 images.go delete mode 100644 sqlite3_query_test.go diff --git a/README_DEV.md b/README_DEV.md index 37aab1b..3c231d9 100644 --- a/README_DEV.md +++ b/README_DEV.md @@ -45,7 +45,7 @@ The database file is *data/hatchet.db*; use the *sqlite3* command as below: sqlite3 ./data/hatchet.db ``` -After a log file is processed, 3 tables are created in the SQLite3 database. Part of the table name are from the processed log file. For example, a table *mongod*_{hex} (e.g., mongod_1b3d5f7) is created after a log file $HOME/Downloads/**mongod**.log.gz is processed. The other 3 tables are 1) mongod_{hex}_ops stores stats of slow ops, 2) mongod_{hex}_clients stores clients information, and 3) mongod_{hex}_audit keeps audit data. A few SQL commands follow. +After a log file is processed, 3 tables are created in the SQLite3 database. Part of the table name are from the processed log file. For example, a table *mongod*_{hex} (e.g., mongod_1b3d5f7) is created after a log file $HOME/Downloads/**mongod**.log.gz is processed. The other 4 tables are 1) mongod_{hex}_ops stores stats of slow ops, 2) mongod_{hex}_clients stores clients information, 3) mongod_{hex}_audit keeps audit data, and 4) mongod_{hex}_drivers to store driver information. A few SQL commands follow. ### Query All Data ```sqlite3 @@ -85,7 +85,7 @@ SELECT SUBSTR(date, 1, 16), COUNT(op), op, ns SELECT ip, context, STRFTIME('%s', SUBSTR(etm,1,19))-STRFTIME('%s', SUBSTR(btm,1,19)) dur, reslen FROM ( SELECT MAX(a.date) etm, MIN(a.date) btm, a.context context, b.ip, SUM(a.reslen) reslen - FROM mongod_1b3d5f7 a, mongod_1b3d5f7_clients b WHERE a.context = b.context GROUP BY a.context + FROM mongod_1b3d5f7 a, mongod_1b3d5f7_clients b WHERE a.ip = b.ip GROUP BY a.context ) WHERE dur > 0 AND reslen > 0 order by dur DESC, reslen DESC limit 23; ``` diff --git a/audit_template.go b/audit_template.go new file mode 100644 index 0000000..65958bb --- /dev/null +++ b/audit_template.go @@ -0,0 +1,321 @@ +/* + * Copyright 2022-present Kuei-chun Chen. All rights reserved. + * audit_template.go + */ + +package hatchet + +import ( + "fmt" + "html/template" + "strings" + "time" + + "github.com/simagix/gox" + "golang.org/x/text/language" + "golang.org/x/text/message" +) + +// GetAuditTablesTemplate returns HTML +func GetAuditTablesTemplate() (*template.Template, error) { + html := headers + getContentHTML() + html += `{{$name := .Hatchet}} +
    + + +
    + {{getInfoSummary .Info}}

    {{getStatsSummary .Data}}

    +{{if hasData .Data "exception"}} +

    Exceptions

    + + + {{range $n, $val := index .Data "exception"}} + + + + {{end}} +
    SeverityTotal
    {{add $n 1}} + {{$val.Name}} + {{getFormattedNumber $val.Values 0}}
    +{{end}} + +{{if hasData .Data "failed"}} +

    Failed Operations

    + + + {{range $n, $val := index .Data "failed"}} + + + + + {{end}} +
    Failed OperationTotal
    {{add $n 1}} + {{$val.Name}} + {{getFormattedNumber $val.Values 0}}
    +{{end}} + +{{if hasData .Data "op"}} +

    Operations Stats

    + + + {{range $n, $val := index .Data "op"}} + + + + {{end}} +
    OperationTotal
    {{add $n 1}} + {{$val.Name}} + {{getFormattedNumber $val.Values 0}}
    +{{end}} + +{{if hasData .Data "ip"}} +

    Stats by IPs

    + + + {{range $n, $val := index .Data "ip"}} + + + + {{end}} +
    IPAccepted ConnectionsResponse Length
    {{add $n 1}} + {{$val.Name}} + {{getFormattedNumber $val.Values 0}}{{getFormattedSize $val.Values 1}}
    +{{end}} + +{{if hasData .Data "ns"}} +

    Stats by Namespaces

    + + + {{range $n, $val := index .Data "ns"}} + + + + {{end}} +
    NamespaceAccessedResponse Length
    {{add $n 1}} + {{$val.Name}} + {{getFormattedNumber $val.Values 0}}{{getFormattedSize $val.Values 1}}
    +{{end}} + +{{if hasData .Data "duration"}} +

    Top N Long Lasting Connections

    + + + {{range $n, $val := index .Data "duration"}} + {{if lt $n 23}} + + + + + {{end}} + {{end}} +
    ContextDuration
    {{add $n 1}}{{$val.Name}} + {{getFormattedDuration $val.Values 0}}
    +{{end}} +

    @simagix

    + ` + html += "" + return template.New("hatchet").Funcs(template.FuncMap{ + "add": func(a int, b int) int { + return a + b + }, + "hasData": func(data map[string][]NameValues, key string) bool { + return len(data[key]) > 0 + }, + "numPrinter": func(n interface{}) string { + printer := message.NewPrinter(language.English) + return printer.Sprintf("%v", ToInt(n)) + }, + "getContext": func(s string) string { + toks := strings.Split(s, " ") + if len(toks) == 0 { + return s + } + return toks[0] + }, + "getTheGuyImage": func() string { + return SIMONE_PNG + }, + "getFormattedNumber": func(numbers []int, i int) string { + printer := message.NewPrinter(language.English) + return printer.Sprintf("%v", numbers[i]) + }, + "getDurationFromSeconds": func(s int) string { + return gox.GetDurationFromSeconds(float64(s)) + }, + "getFormattedDuration": func(numbers []int, i int) string { + return gox.GetDurationFromSeconds(float64(numbers[i])) + }, + "getStorageSize": func(s int) string { + return gox.GetStorageSize(float64(s)) + }, + "getFormattedSize": func(numbers []int, i int) string { + return gox.GetStorageSize(numbers[i]) + }, + "getInfoSummary": func(info HatchetInfo) template.HTML { + var html = "Hey there! My name is Simone and I am your assistant today. " + if info.Version == "" { + html += "There is not enough information in the log to determine what MongoDB version is used." + } else { + html += fmt.Sprintf("So, the server running MongoDB is using the %v edition of version %v", info.Module, info.Version) + if strings.Compare(MIN_MONGO_VER, info.Version) == 1 { + html += ", which is kinda old now. It's probably a good idea to upgrade to a more recent version of MongoDB soon. " + } else { + html += ". " + } + if info.Arch != "" && info.OS != "" { + html += fmt.Sprintf("The server is on a %v architecture server that running the %v operating system. ", info.Arch, info.OS) + } + if info.Module != "enterprise" { + html += fmt.Sprintf("Although %v edition works, it is recommended to upgrade to the enterprise edition or migrate to Atlas. ", info.Module) + } + if info.Start != "" && info.End != "" { + layout := "2006-01-02T15:04:05" + startTime := info.Start[:19] + endTime := info.End[:19] + stime, _ := time.Parse(layout, startTime) + etime, _ := time.Parse(layout, endTime) + seconds := etime.Unix() - stime.Unix() + days := gox.GetDurationFromSeconds(float64(seconds)) + if days == " 1.0 days" { + days = "1 day" + } + html += fmt.Sprintf("The first log was dated on %v, it ran for %v, ending on %v. ", + stime.Format(time.ANSIC), days, etime.Format(time.ANSIC)) + if seconds > (24*60*60 + 5*60) { + html += fmt.Sprintf("The duration of %v is greater than a day for a single log file, and rotating logs regularly is recommended. ", + gox.GetDurationFromSeconds(float64(seconds))) + } + } + if info.Provider != "" && info.Region != "" { + html += fmt.Sprintf("Bravo for the decision to host your servers on Atlas %s in the %s region. ", + info.Provider, info.Region) + html += "A gentle reminder to ensure your appplication servers reside in the same region which can save you more than a few bucks from data transfer charges. " + } + if len(info.Drivers) == 1 { + for _, driver := range info.Drivers { + for key, value := range driver { + html += "For the application driver, I found a driver information and it was " + html += fmt.Sprintf("%s version %s. ", key, value) + } + } + html += "You should confirm the driver you use is compatible with the MongoDB server version. " + } else if len(info.Drivers) > 1 { + html += "It looks like your applications have used a number of drivers, and they are " + cnt := 0 + for _, driver := range info.Drivers { + for key, value := range driver { + cnt++ + if cnt == len(info.Drivers) { + html += fmt.Sprintf("and %s version %s. ", key, value) + } else { + html += fmt.Sprintf("%s version %s, ", key, value) + } + } + } + html += "You should double check the drivers you use are compatible with the MongoDB server version. " + } + } + return template.HTML(html) + }, + "getStatsSummary": func(data map[string][]NameValues) template.HTML { + var html string + printer := message.NewPrinter(language.English) + var totalImpact float64 + for key, docs := range data { + if key == "exception" && len(docs) > 0 { + html += printer.Sprintf("Hmmm, I found %d warning (or more severe) ", len(docs)) + if len(docs) < 2 { + html += "message. " + } else { + html += "messages. " + } + } else if key == "ip" && len(docs) > 0 { + conns := 0 + for _, doc := range docs { + conns += doc.Values[0] + } + html += printer.Sprintf("During the time, there were a total of %d accepted connections from %d ", conns, len(docs)) + if len(docs) < 2 { + html += "client. " + } else { + html += "different clients. " + } + } else if key == "ns" && len(docs) > 0 { + count := 0 + for _, doc := range docs { + count += doc.Values[0] + } + html += printer.Sprintf("As many as %d different namespaces were accessed a total of %d times. ", len(docs), count) + reslen := 0 + for _, doc := range docs { + reslen += doc.Values[1] + } + html += printer.Sprintf("The total response length was around %v. ", gox.GetStorageSize(reslen)) + } else if key == "stats" && len(docs) > 0 { + for _, doc := range docs { + if doc.Name == "maxConns" { + milli := doc.Values[0] + if milli == 0 { + html += "I didn't find any connections information. " + continue + } + html += printer.Sprintf("At one point, the number of opened connections reached %d", doc.Values[0]) + if milli > 1000 { + mem := milli * (1024 * 1024) + html += printer.Sprintf(", which could take up about %s of memory. ", gox.GetStorageSize(float64(mem))) + } else { + html += ". " + } + } else if doc.Name == "maxMilli" { + milli := doc.Values[0] + html += "The slowest operation took " + if milli < 1000 { + html += printer.Sprintf("%d milliseconds. ", doc.Values[0]) + } else { + seconds := float64(milli) / 1000 + html += printer.Sprintf("%s. ", gox.GetDurationFromSeconds(seconds)) + } + } else if doc.Name == "avgMilli" { + milli := doc.Values[0] + html += printer.Sprintf("Moreover, the average operation time was %d milliseconds", milli) + if milli > 100 { + html += `, where operation time greater than 100 milliseconds is, IMO, "slow". ` + } else { + html += ". " + } + } else if doc.Name == "totalMilli" { + seconds := float64(doc.Values[0]) / 1000 + if seconds < (10 * 60) { // should be calculated with duration + html += printer.Sprintf("The total impact time from slowest operations was %s. ", gox.GetDurationFromSeconds(seconds)) + } else if seconds < (60 * 60) { + html += printer.Sprintf("The total impact time from slowest operations was, ouch,%s. ", gox.GetDurationFromSeconds(seconds)) + } else { + totalImpact = seconds + } + } + } + } + } + if totalImpact > 0 { + html += "OMG, the total impact from slowest operations was " + html += printer.Sprintf("%s, this may be a problem of lacking resources. Please review the sizing training videos below. ", gox.GetDurationFromSeconds(totalImpact)) + html += "
    " + html += `` + html += `` + html += "
    " + } + html += "

    Check out the details below for additional information about this server." + return template.HTML(html) + }}).Parse(html) +} diff --git a/charts_handler.go b/charts_handler.go index 507fc16..a079a62 100644 --- a/charts_handler.go +++ b/charts_handler.go @@ -42,7 +42,7 @@ var charts = map[string]Chart{ "Display average operations time over a period of time", "/ops?type=stats"}, T_OPS_COUNTS: {2, "Operation Counts", "Display total counts of operations", "/ops?type=counts"}, - "connections-accepted": {3, "Accepted Connections", + T_CONNS_ACCEPTED: {3, "Accepted Connections", "Display accepted connections from clients", "/connections?type=accepted"}, T_CONNS_TIME: {4, "Accepted & Ended Connections", "Display accepted vs ended connections over a period of time", "/connections?type=time"}, @@ -130,7 +130,7 @@ func ChartsHandler(w http.ResponseWriter, r *http.Request, params httprouter.Par log.Println("type", chartType, "duration", duration) } if chartType == "" || chartType == "accepted" { - chartType = "connections-accepted" + chartType = T_CONNS_ACCEPTED docs, err := dbase.GetAcceptedConnsCounts(duration) if err != nil { json.NewEncoder(w).Encode(map[string]interface{}{"ok": 0, "error": err.Error()}) diff --git a/charts_template.go b/charts_template.go index 23984e6..76a2db7 100644 --- a/charts_template.go +++ b/charts_template.go @@ -157,9 +157,9 @@ func getConnectionsChart() string { {{range $i, $v := .Remote}} {{if eq $ctype "connections-time"}} - [new Date("{{$v.Value}}"), {{$v.Accepted}}, {{$v.Ended}}], + [new Date("{{$v.IP}}"), {{$v.Accepted}}, {{$v.Ended}}], {{else}} - ['{{$v.Value}}', {{$v.Accepted}}, {{$v.Ended}}], + ['{{$v.IP}}', {{$v.Accepted}}, {{$v.Ended}}], {{end}} {{end}} ]); diff --git a/database.go b/database.go index 3deefe2..e09cb12 100644 --- a/database.go +++ b/database.go @@ -24,7 +24,7 @@ type Database interface { GetAuditData() (map[string][]NameValues, error) GetAverageOpTime(op string, duration string) ([]OpCount, error) GetClientPreparedStmt() string - GetConnectionStats(chartType string, duration string) ([]Remote, error) + GetConnectionStats(chartType string, duration string) ([]RemoteClient, error) GetHatchetInfo() HatchetInfo GetHatchetInitStmt() string GetHatchetNames() ([]string, error) @@ -37,6 +37,7 @@ type Database interface { GetSlowestLogs(topN int) ([]LegacyLog, error) GetVerbose() bool InsertClientConn(index int, doc *Logv2Info) error + InsertDriver(index int, doc *Logv2Info) error InsertLog(index int, end string, doc *Logv2Info, stat *OpStat) error SearchLogs(opts ...string) ([]LegacyLog, error) SetVerbose(v bool) diff --git a/hatchet.go b/hatchet.go index d2e98fe..04f51a8 100644 --- a/hatchet.go +++ b/hatchet.go @@ -1,4 +1,7 @@ -// Copyright 2022-present Kuei-chun Chen. All rights reserved. +/* + * Copyright 2022-present Kuei-chun Chen. All rights reserved. + * hatchet.go + */ package hatchet @@ -16,11 +19,7 @@ import ( "github.com/mattn/go-sqlite3" ) -const ( - CHEN_ICO = `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` - HATCHET_PNG = `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` - SQLITE3_FILE = "./data/hatchet.db" -) +const SQLITE3_FILE = "./data/hatchet.db" func Run(fullVersion string) { dbfile := flag.String("dbfile", SQLITE3_FILE, "database file name") diff --git a/images.go b/images.go new file mode 100644 index 0000000..dd82289 --- /dev/null +++ b/images.go @@ -0,0 +1,12 @@ +/* + * Copyright 2022-present Kuei-chun Chen. All rights reserved. + * hatchet.go + */ + +package hatchet + +const ( + CHEN_ICO = `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` + SIMONE_PNG = `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` + HATCHET_PNG = `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` +) diff --git a/legacy.go b/legacy.go index 6e35630..1f9ac8e 100644 --- a/legacy.go +++ b/legacy.go @@ -1,4 +1,7 @@ -// Copyright 2022-present Kuei-chun Chen. All rights reserved. +/* + * Copyright 2022-present Kuei-chun Chen. All rights reserved. + * legacy.go + */ package hatchet @@ -47,16 +50,16 @@ func AddLegacyString(doc *Logv2Info) error { } } } else if doc.Component == "NETWORK" { - remote := Remote{} + remote := RemoteClient{} for _, attr := range doc.Attr { if attr.Key == "remote" { toks := strings.Split(attr.Value.(string), ":") - remote.Value = toks[0] + remote.IP = toks[0] remote.Port = toks[1] if doc.Msg == "Connection ended" { remote.Ended = 1 arr = append(arr, fmt.Sprintf("%v", attr.Value)) - } else { + } else if doc.Msg == "Connection accepted" { remote.Accepted = 1 arr = append(arr, fmt.Sprintf("from %v", attr.Value)) } @@ -70,10 +73,21 @@ func AddLegacyString(doc *Logv2Info) error { } else if attr.Key == "doc" { b, _ := bson.MarshalExtJSON(attr.Value, false, false) arr = append(arr, string(b)) + if doc.Msg == "client metadata" { + + data, ok := attr.Value.(bson.D) + if ok { + driver, ok := data.Map()["driver"].(bson.D) + if ok { + remote.Driver, _ = driver.Map()["name"].(string) + remote.Version, _ = driver.Map()["version"].(string) + } + } + } } } - if remote.Value != "" { - doc.Remote = &remote + if remote.IP != "" { + doc.Client = &remote } } else { for _, attr := range doc.Attr { diff --git a/logv2.go b/logv2.go index e77ebea..24caebc 100644 --- a/logv2.go +++ b/logv2.go @@ -1,4 +1,7 @@ -// Copyright 2022-present Kuei-chun Chen. All rights reserved. +/* + * Copyright 2022-present Kuei-chun Chen. All rights reserved. + * logv2.go + */ package hatchet @@ -60,7 +63,7 @@ type Logv2Info struct { Attributes Attributes Message string // remaining legacy message - Remote *Remote + Client *RemoteClient } type Attributes struct { @@ -74,12 +77,15 @@ type Attributes struct { Type string `json:"type" bson:"type"` } -type Remote struct { +type RemoteClient struct { Accepted int `json:"accepted"` Conns int `json:"conns"` Ended int `json:"ended"` + IP string `json:"value"` Port string `json:"port"` - Value string `json:"value"` + + Driver string // driver name + Version string // driver version } // OpStat stores performance data @@ -111,6 +117,10 @@ type HatchetInfo struct { OS string Start string Version string + + Drivers []map[string]string + Provider string + Region string } // Analyze analyzes logs from a file @@ -221,8 +231,12 @@ func (ptr *Logv2) Analyze(filename string) error { start = end } dbase.InsertLog(index, end, &doc, stat) - if doc.Remote != nil { - dbase.InsertClientConn(index, &doc) + if doc.Client != nil { + if (doc.Client.Accepted + doc.Client.Ended) > 0 { // record connections + dbase.InsertClientConn(index, &doc) + } else if doc.Client.Driver != "" { + dbase.InsertDriver(index, &doc) + } } } if ptr.legacy { diff --git a/sqlite3.go b/sqlite3.go index 23ba571..4482c61 100644 --- a/sqlite3.go +++ b/sqlite3.go @@ -13,6 +13,7 @@ import ( type SQLite3DB struct { clientStmt *sql.Stmt // {hatchet}_clients + driverStmt *sql.Stmt // {hatchet}_drivers db *sql.DB dbfile string hatchetName string @@ -68,6 +69,9 @@ func (ptr *SQLite3DB) Begin() error { if ptr.clientStmt, err = ptr.tx.Prepare(ptr.GetClientPreparedStmt()); err != nil { return err } + if ptr.driverStmt, err = ptr.tx.Prepare(ptr.GetDriverPreparedStmt()); err != nil { + return err + } return err } @@ -101,8 +105,15 @@ func (ptr *SQLite3DB) InsertLog(index int, end string, doc *Logv2Info, stat *OpS func (ptr *SQLite3DB) InsertClientConn(index int, doc *Logv2Info) error { var err error - rmt := doc.Remote - _, err = ptr.clientStmt.Exec(index, rmt.Value, rmt.Port, rmt.Conns, rmt.Accepted, rmt.Ended, doc.Context) + client := doc.Client + _, err = ptr.clientStmt.Exec(index, client.IP, client.Port, client.Conns, client.Accepted, client.Ended, doc.Context) + return err +} + +func (ptr *SQLite3DB) InsertDriver(index int, doc *Logv2Info) error { + var err error + client := doc.Client + _, err = ptr.driverStmt.Exec(index, client.IP, client.Driver, client.Version) return err } @@ -175,7 +186,6 @@ func (ptr *SQLite3DB) CreateMetaData() error { return err } - // commented out, the cmd takes a long time to process log.Printf("insert duration into %v_audit\n", ptr.hatchetName) istmt = fmt.Sprintf(`INSERT INTO %v_audit SELECT 'duration', context || ' (' || ip || ')', STRFTIME('%%s', SUBSTR(etm,1,19))-STRFTIME('%%s', SUBSTR(btm,1,19)) duration @@ -184,7 +194,6 @@ func (ptr *SQLite3DB) CreateMetaData() error { if _, err = ptr.db.Exec(istmt); err != nil { return err } - return err } @@ -212,12 +221,17 @@ func (ptr *SQLite3DB) GetHatchetInitStmt() string { CREATE INDEX IF NOT EXISTS %v_idx_severity ON %v (severity); CREATE INDEX IF NOT EXISTS %v_idx_op ON %v (op,ns,filter); + DROP TABLE IF EXISTS %v_drivers; + CREATE TABLE %v_drivers ( + id integer not null primary key, ip text, driver text, version text); + DROP TABLE IF EXISTS %v_clients; CREATE TABLE %v_clients( id integer not null primary key, ip text, port text, conns integer, accepted integer, ended integer, context string); CREATE INDEX IF NOT EXISTS %v_clients_idx_context ON %v_clients (context,ip);`, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, - hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName) + hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName, hatchetName) + } // GetHatchetPreparedStmt returns prepared statement of the hatchet table @@ -227,8 +241,14 @@ func (ptr *SQLite3DB) GetHatchetPreparedStmt() string { VALUES(?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?)`, ptr.hatchetName) } -// GetClientPreparedStmt returns prepared statement of client table +// GetClientPreparedStmt returns prepared statement of clients table func (ptr *SQLite3DB) GetClientPreparedStmt() string { return fmt.Sprintf(`INSERT INTO %v_clients (id, ip, port, conns, accepted, ended, context) VALUES(?,?,?,?,?, ?,?)`, ptr.hatchetName) } + +// GetDriverPreparedStmt returns prepared statement of drivers table +func (ptr *SQLite3DB) GetDriverPreparedStmt() string { + return fmt.Sprintf(`INSERT INTO %v_drivers (id, ip, driver, version) + VALUES(?,?,?,?)`, ptr.hatchetName) +} diff --git a/sqlite3_query.go b/sqlite3_query.go index 4c7d265..9f39d75 100644 --- a/sqlite3_query.go +++ b/sqlite3_query.go @@ -8,6 +8,7 @@ package hatchet import ( "fmt" "log" + "regexp" "strings" ) @@ -239,13 +240,49 @@ func (ptr *SQLite3DB) GetHatchetInfo() HatchetInfo { if err != nil { return info } - defer rows.Close() if rows.Next() { if err = rows.Scan(&info.Name, &info.Version, &info.Module, &info.OS, &info.Arch, &info.Start, &info.End); err != nil { return info } } + if rows != nil { + rows.Close() + } + + query = fmt.Sprintf(`SELECT message FROM %v WHERE component = 'CONTROL' AND message LIKE '%%provider:%%region:%%';`, + ptr.hatchetName) + if ptr.verbose { + log.Println(query) + } + rows, err = db.Query(query) + if err == nil && rows.Next() { + var message string + if err = rows.Scan(&message); err == nil { + re := regexp.MustCompile(`.*(provider: "(\w+)", region: "(\w+)",).*`) + matches := re.FindStringSubmatch(message) + info.Provider = matches[2] + info.Region = matches[3] + } + } + if rows != nil { + rows.Close() + } + + query = fmt.Sprintf(`SELECT DISTINCT driver, version FROM %v_drivers;`, ptr.hatchetName) + if ptr.verbose { + log.Println(query) + } + rows, err = db.Query(query) + for err == nil && rows.Next() { + var driver, version string + if err = rows.Scan(&driver, &version); err == nil { + info.Drivers = append(info.Drivers, map[string]string{driver: version}) + } + } + if rows != nil { + rows.Close() + } return info } @@ -305,9 +342,9 @@ func (ptr *SQLite3DB) GetAcceptedConnsCounts(duration string) ([]NameValue, erro } // GetConnectionStats returns stats data of accepted and ended -func (ptr *SQLite3DB) GetConnectionStats(chartType string, duration string) ([]Remote, error) { +func (ptr *SQLite3DB) GetConnectionStats(chartType string, duration string) ([]RemoteClient, error) { hatchetName := ptr.hatchetName - docs := []Remote{} + docs := []RemoteClient{} var query, durcond string var substr string if duration != "" { @@ -336,10 +373,10 @@ func (ptr *SQLite3DB) GetConnectionStats(chartType string, duration string) ([]R } defer rows.Close() for rows.Next() { - var doc Remote + var doc RemoteClient var accepted float64 var ended float64 - if err = rows.Scan(&doc.Value, &accepted, &ended); err != nil { + if err = rows.Scan(&doc.IP, &accepted, &ended); err != nil { return docs, err } doc.Accepted = int(accepted) @@ -460,14 +497,50 @@ func (ptr *SQLite3DB) GetReslenByNamespace(ns string, duration string) ([]NameVa func (ptr *SQLite3DB) GetAuditData() (map[string][]NameValues, error) { var err error + db := ptr.db data := map[string][]NameValues{} - query := fmt.Sprintf(`SELECT type, name, value FROM %v_audit - WHERE type IN ('exception', 'failed', 'op', 'duration') ORDER BY type, value DESC;`, ptr.hatchetName) + query := fmt.Sprintf(`SELECT MAX(conns) FROM %v_clients;`, ptr.hatchetName) if ptr.verbose { log.Println(query) } - db := ptr.db rows, err := db.Query(query) + category := "stats" + if err == nil && rows.Next() { + var doc NameValues + var value int + _ = rows.Scan(&value) + doc.Name = "maxConns" + doc.Values = append(doc.Values, value) + data[category] = append(data[category], doc) + } + if rows != nil { + rows.Close() + } + + query = fmt.Sprintf(`SELECT MAX(max_ms), SUM(count), SUM(total_ms) FROM %v_ops;`, ptr.hatchetName) + if ptr.verbose { + log.Println(query) + } + rows, err = db.Query(query) + if err == nil && rows.Next() { + var maxMilli, total, totalMilli int + if err = rows.Scan(&maxMilli, &total, &totalMilli); err != nil { + return data, err + } + data[category] = append(data[category], NameValues{"maxMilli", []int{maxMilli}}) + data[category] = append(data[category], NameValues{"avgMilli", []int{totalMilli / total}}) + data[category] = append(data[category], NameValues{"totalMilli", []int{totalMilli}}) + } + if rows != nil { + rows.Close() + } + + query = fmt.Sprintf(`SELECT type, name, value FROM %v_audit + WHERE type IN ('exception', 'failed', 'op', 'duration') ORDER BY type, value DESC;`, ptr.hatchetName) + if ptr.verbose { + log.Println(query) + } + rows, err = db.Query(query) if err != nil { return data, err } @@ -490,9 +563,11 @@ func (ptr *SQLite3DB) GetAuditData() (map[string][]NameValues, error) { } data[category] = append(data[category], doc) } - rows.Close() + if rows != nil { + rows.Close() + } - category := "ip" + category = "ip" query = fmt.Sprintf(`SELECT a.name ip, a.value count, b.value reslen FROM %v_audit a, %v_audit b WHERE a.type == '%v' AND b.type = 'reslen-ip' AND a.name = b.name ORDER BY reslen DESC;`, ptr.hatchetName, ptr.hatchetName, category) if ptr.verbose { @@ -512,7 +587,9 @@ func (ptr *SQLite3DB) GetAuditData() (map[string][]NameValues, error) { doc.Values = append(doc.Values, val2) data[category] = append(data[category], doc) } - rows.Close() + if rows != nil { + rows.Close() + } category = "ns" query = fmt.Sprintf(`SELECT a.name ns, a.value count, b.value reslen FROM %v_audit a, %v_audit b WHERE a.type == '%v' AND b.type = 'reslen-ns' AND a.name = b.name ORDER BY reslen DESC;`, @@ -534,6 +611,8 @@ func (ptr *SQLite3DB) GetAuditData() (map[string][]NameValues, error) { doc.Values = append(doc.Values, val2) data[category] = append(data[category], doc) } - defer rows.Close() + if rows != nil { + rows.Close() + } return data, err } diff --git a/sqlite3_query_test.go b/sqlite3_query_test.go deleted file mode 100644 index ae886a6..0000000 --- a/sqlite3_query_test.go +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright 2022-present Kuei-chun Chen. All rights reserved. - -package hatchet diff --git a/stats_handler.go b/stats_handler.go index 921404b..10aee79 100644 --- a/stats_handler.go +++ b/stats_handler.go @@ -1,4 +1,7 @@ -// Copyright 2022-present Kuei-chun Chen. All rights reserved. +/* + * Copyright 2022-present Kuei-chun Chen. All rights reserved. + * stats_handler.go + */ package hatchet @@ -42,7 +45,7 @@ func StatsHandler(w http.ResponseWriter, r *http.Request, params httprouter.Para json.NewEncoder(w).Encode(map[string]interface{}{"ok": 0, "error": err.Error()}) return } - doc := map[string]interface{}{"Hatchet": hatchetName, "Summary": summary, "Data": data} + doc := map[string]interface{}{"Hatchet": hatchetName, "Info": info, "Summary": summary, "Data": data} if err = templ.Execute(w, doc); err != nil { json.NewEncoder(w).Encode(map[string]interface{}{"ok": 0, "error": err.Error()}) return diff --git a/stats_template.go b/stats_template.go index f7e1b15..eb5a7c2 100644 --- a/stats_template.go +++ b/stats_template.go @@ -10,11 +10,12 @@ import ( "html/template" "strings" - "github.com/simagix/gox" "golang.org/x/text/language" "golang.org/x/text/message" ) +const MIN_MONGO_VER = "5.0" + // GetStatsTableTemplate returns HTML func GetStatsTableTemplate(collscan bool, orderBy string, download string) (*template.Template, error) { html := headers @@ -111,141 +112,3 @@ func getStatsTable(collscan bool, orderBy string, download string) string { return html } -// GetAuditTablesTemplate returns HTML -func GetAuditTablesTemplate() (*template.Template, error) { - html := headers + getContentHTML() - html += `{{$name := .Hatchet}} -{{if hasData .Data "exception"}} -

    Exceptions

    - - - {{range $n, $val := index .Data "exception"}} - - - - {{end}} -
    SeverityTotal
    {{add $n 1}} - {{$val.Name}} - {{getFormattedNumber $val.Values 0}}
    -{{end}} - -{{if hasData .Data "failed"}} -

    Failed Operations

    - - - {{range $n, $val := index .Data "failed"}} - - - - - {{end}} -
    Failed OperationTotal
    {{add $n 1}} - {{$val.Name}} - {{getFormattedNumber $val.Values 0}}
    -{{end}} - -{{if hasData .Data "op"}} -

    Operations Stats

    - - - {{range $n, $val := index .Data "op"}} - - - - {{end}} -
    OperationTotal
    {{add $n 1}} - {{$val.Name}} - {{getFormattedNumber $val.Values 0}}
    -{{end}} - -{{if hasData .Data "ip"}} -

    Stats by IPs

    - - - {{range $n, $val := index .Data "ip"}} - - - - {{end}} -
    IPAccepted ConnectionsResponse Length
    {{add $n 1}} - {{$val.Name}} - {{getFormattedNumber $val.Values 0}}{{getFormattedSize $val.Values 1}}
    -{{end}} - -{{if hasData .Data "ns"}} -

    Stats by Namespaces

    - - - {{range $n, $val := index .Data "ns"}} - - - - {{end}} -
    NamespaceAccessedResponse Length
    {{add $n 1}} - {{$val.Name}} - {{getFormattedNumber $val.Values 0}}{{getFormattedSize $val.Values 1}}
    -{{end}} - -{{if hasData .Data "duration"}} -

    Top N Long Lasting Connections

    - - - {{range $n, $val := index .Data "duration"}} - {{if lt $n 23}} - - - - {{end}} - - {{end}} -
    ContextDuration
    {{add $n 1}}{{$val.Name}} - {{getFormattedDuration $val.Values 0}}
    -{{end}} -

    @simagix

    - ` - html += "" - return template.New("hatchet").Funcs(template.FuncMap{ - "add": func(a int, b int) int { - return a + b - }, - "hasData": func(data map[string][]NameValues, key string) bool { - return len(data[key]) > 0 - }, - "numPrinter": func(n interface{}) string { - printer := message.NewPrinter(language.English) - return printer.Sprintf("%v", ToInt(n)) - }, - "getContext": func(s string) string { - toks := strings.Split(s, " ") - if len(toks) == 0 { - return s - } - return toks[0] - }, - "getFormattedNumber": func(numbers []int, i int) string { - printer := message.NewPrinter(language.English) - return printer.Sprintf("%v", numbers[i]) - }, - "getDurationFromSeconds": func(s int) string { - return gox.GetDurationFromSeconds(float64(s)) - }, - "getFormattedDuration": func(numbers []int, i int) string { - return gox.GetDurationFromSeconds(float64(numbers[i])) - }, - "getStorageSize": func(s int) string { - return gox.GetStorageSize(float64(s)) - }, - "getFormattedSize": func(numbers []int, i int) string { - return gox.GetStorageSize(numbers[i]) - }}).Parse(html) -} diff --git a/template.go b/template.go index 566919a..d8023e3 100644 --- a/template.go +++ b/template.go @@ -190,12 +190,12 @@ const headers = ` font-size: 1em; padding: 2px 2px; } - .rotate45:hover { - -webkit-transform: rotate(45deg); - -moz-transform: rotate(45deg); - -o-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); + .rotate23:hover { + -webkit-transform: rotate(23deg); + -moz-transform: rotate(23deg); + -o-transform: rotate(23deg); + -ms-transform: rotate(23deg); + transform: rotate(23deg); } input[type="checkbox"] { accent-color: red; @@ -207,6 +207,11 @@ const headers = ` color: #DB4437; cursor: hand; } + .summary { + background-color: black; + color: #CCC; + padding: 5px; + } @@ -293,7 +298,7 @@ func getMainPage() string {
    -

    Hatchet - MongoDB JSON Log Analyzer

    +

    Hatchet - MongoDB JSON Log Analyzer

    - - -
    -
    ` +
    + + + +
    +
    + + ` return template.New("hatchet").Funcs(template.FuncMap{ "descr": func(v OpCount) template.HTML { @@ -84,6 +86,7 @@ func getOpStatsChart() string { // 'hAxis': { textPosition: 'none' }, 'hAxis': { slantedText: true, slantedTextAngle: 30 }, 'vAxis': {title: '{{.VAxisLabel}}', minValue: 0}, + 'width': '100%', 'height': 480, 'titleTextStyle': {'fontSize': 20}, {{if eq $ctype "ops"}} @@ -123,6 +126,7 @@ func getPieChart() string { var options = { 'backgroundColor': { 'fill': 'transparent' }, 'title': '{{.Chart.Title}}', + 'width': '100%', 'height': 480, 'titleTextStyle': {'fontSize': 20}, 'slices': {}, @@ -169,11 +173,16 @@ func getConnectionsChart() string { 'title': '{{.Chart.Title}}', 'hAxis': { slantedText: true, slantedTextAngle: 30 }, 'vAxis': {title: 'Count', minValue: 0}, + 'width': '100%', 'height': 480, 'titleTextStyle': {'fontSize': 20}, 'legend': { 'position': 'right' } }; // Instantiate and draw our chart, passing in some options. + {{if eq $ctype "connections-time"}} + var chart = new google.visualization.LineChart(document.getElementById('hatchetChart')); + {{else}} var chart = new google.visualization.ColumnChart(document.getElementById('hatchetChart')); + {{end}} chart.draw(data, options); } diff --git a/images.go b/images.go index dd82289..a508b4a 100644 --- a/images.go +++ b/images.go @@ -7,6 +7,7 @@ package hatchet const ( CHEN_ICO = `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` + SAGE_PNG = `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` SIMONE_PNG = `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` HATCHET_PNG = `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` ) diff --git a/legacy.go b/legacy.go index 1f9ac8e..d59bf87 100644 --- a/legacy.go +++ b/legacy.go @@ -74,7 +74,6 @@ func AddLegacyString(doc *Logv2Info) error { b, _ := bson.MarshalExtJSON(attr.Value, false, false) arr = append(arr, string(b)) if doc.Msg == "client metadata" { - data, ok := attr.Value.(bson.D) if ok { driver, ok := data.Map()["driver"].(bson.D) diff --git a/logs_template.go b/logs_template.go index 430e3b8..48f6581 100644 --- a/logs_template.go +++ b/logs_template.go @@ -53,6 +53,9 @@ func GetLogTableTemplate(attr string) (*template.Template, error) { }, "highlightLog": func(log string, params ...string) template.HTML { return template.HTML(highlightLog(log, params...)) + }, + "formatDateTime": func(str string) string { + return strings.Replace(str, "T", " ", 1) }}).Parse(html) } @@ -90,7 +93,7 @@ func getSlowOpsLogsTable() string { {{range $n, $value := .Logs}} {{ add $n 1 }} - {{ $value.Timestamp }} + {{ formatDateTime $value.Timestamp }} {{ $value.Severity }} {{ $value.Component }} {{ $value.Context }} @@ -98,7 +101,7 @@ func getSlowOpsLogsTable() string { {{end}} -

    @simagix

    +

    @simagix

    ` return template @@ -106,35 +109,34 @@ func getSlowOpsLogsTable() string { func getLegacyLogsTable() string { template := ` -
    -
    +
    -
    +
    -
    +
    -
    +
    -
    +
    -
    +

    {{ if .Logs }} {{if .HasMore}} + class="btn" style="float: right; clear: right"> {{end}} @@ -150,7 +152,7 @@ func getLegacyLogsTable() string { {{range $n, $value := .Logs}} - + @@ -160,7 +162,7 @@ func getLegacyLogsTable() string {
    {{ add $n $seq }}{{ $value.Timestamp }}{{ formatDateTime $value.Timestamp }} {{ $value.Severity }} {{ $value.Component }} {{ $value.Context }}
    {{if .HasMore}} + class="btn" style="float: right; clear: right;"> {{end}}

    @simagix

    {{end}} diff --git a/sqlite3.go b/sqlite3.go index 4482c61..ea2f569 100644 --- a/sqlite3.go +++ b/sqlite3.go @@ -186,6 +186,7 @@ func (ptr *SQLite3DB) CreateMetaData() error { return err } + /* logs don't present trusted data from context, ignored log.Printf("insert duration into %v_audit\n", ptr.hatchetName) istmt = fmt.Sprintf(`INSERT INTO %v_audit SELECT 'duration', context || ' (' || ip || ')', STRFTIME('%%s', SUBSTR(etm,1,19))-STRFTIME('%%s', SUBSTR(btm,1,19)) duration @@ -194,6 +195,7 @@ func (ptr *SQLite3DB) CreateMetaData() error { if _, err = ptr.db.Exec(istmt); err != nil { return err } + */ return err } diff --git a/sqlite3_audit.go b/sqlite3_audit.go new file mode 100644 index 0000000..fdab798 --- /dev/null +++ b/sqlite3_audit.go @@ -0,0 +1,149 @@ +/* + * Copyright 2022-present Kuei-chun Chen. All rights reserved. + * sqlite3_audit.go + */ + +package hatchet + +import ( + "fmt" + "log" +) + +func (ptr *SQLite3DB) GetAuditData() (map[string][]NameValues, error) { + var err error + db := ptr.db + data := map[string][]NameValues{} + // get max connection counts + query := fmt.Sprintf(`SELECT MAX(conns) FROM %v_clients;`, ptr.hatchetName) + if ptr.verbose { + log.Println(query) + } + rows, err := db.Query(query) + category := "stats" + if err == nil && rows.Next() { + var doc NameValues + var value int + _ = rows.Scan(&value) + doc.Name = "maxConns" + doc.Values = append(doc.Values, value) + data[category] = append(data[category], doc) + rows.Close() + } + + // get max operation time + query = fmt.Sprintf(`SELECT IFNULL(MAX(max_ms), 0), IFNULL(SUM(count), 0), IFNULL(SUM(total_ms), 0) FROM %v_ops;`, ptr.hatchetName) + if ptr.verbose { + log.Println(query) + } + rows, err = db.Query(query) + if err == nil && rows.Next() { + var maxMilli, total, totalMilli int + if err = rows.Scan(&maxMilli, &total, &totalMilli); err != nil { + return data, err + } + data[category] = append(data[category], NameValues{"maxMilli", []int{maxMilli}}) + data[category] = append(data[category], NameValues{"avgMilli", []int{totalMilli / total}}) + data[category] = append(data[category], NameValues{"totalMilli", []int{totalMilli}}) + rows.Close() + } + + // get max operation time + category = "collscan" + query = fmt.Sprintf(`SELECT IFNULL(MAX(max_ms), 0), IFNULL(SUM(count), 0), IFNULL(SUM(total_ms), 0) FROM %v_ops WHERE _index = 'COLLSCAN';`, ptr.hatchetName) + if ptr.verbose { + log.Println(query) + } + rows, err = db.Query(query) + if err == nil && rows.Next() { + var maxMilli, count, totalMilli int + if err = rows.Scan(&maxMilli, &count, &totalMilli); err != nil { + return data, err + } + if count > 0 { + data[category] = append(data[category], NameValues{"count", []int{count}}) + data[category] = append(data[category], NameValues{"maxMilli", []int{maxMilli}}) + data[category] = append(data[category], NameValues{"totalMilli", []int{totalMilli}}) + } + rows.Close() + } + + // get audit data + query = fmt.Sprintf(`SELECT type, name, value FROM %v_audit + WHERE type IN ('exception', 'failed', 'op', 'duration') ORDER BY type, value DESC;`, ptr.hatchetName) + if ptr.verbose { + log.Println(query) + } + rows, err = db.Query(query) + for err == nil && rows.Next() { + var category string + var doc NameValues + var value int + if err = rows.Scan(&category, &doc.Name, &value); err != nil { + return data, err + } + doc.Values = append(doc.Values, value) + if category == "exception" { + if doc.Name == "E" { + doc.Name = "Error" + } else if doc.Name == "F" { + doc.Name = "Fatal" + } else if doc.Name == "W" { + doc.Name = "Warn" + } + } + data[category] = append(data[category], doc) + } + if rows != nil { + rows.Close() + } + + category = "ip" + query = fmt.Sprintf(`SELECT a.name ip, a.value count, b.value reslen FROM %v_audit a, %v_audit b WHERE a.type == '%v' AND b.type = 'reslen-ip' AND a.name = b.name ORDER BY reslen DESC;`, + ptr.hatchetName, ptr.hatchetName, category) + if ptr.verbose { + log.Println(query) + } + rows, err = db.Query(query) + if err != nil { + return data, err + } + for rows.Next() { + var doc NameValues + var val1, val2 int + if err = rows.Scan(&doc.Name, &val1, &val2); err != nil { + return data, err + } + doc.Values = append(doc.Values, val1) + doc.Values = append(doc.Values, val2) + data[category] = append(data[category], doc) + } + if rows != nil { + rows.Close() + } + + category = "ns" + query = fmt.Sprintf(`SELECT a.name ns, a.value count, b.value reslen FROM %v_audit a, %v_audit b WHERE a.type == '%v' AND b.type = 'reslen-ns' AND a.name = b.name ORDER BY reslen DESC;`, + ptr.hatchetName, ptr.hatchetName, category) + if ptr.verbose { + log.Println(query) + } + rows, err = db.Query(query) + if err != nil { + return data, err + } + for rows.Next() { + var doc NameValues + var val1, val2 int + if err = rows.Scan(&doc.Name, &val1, &val2); err != nil { + return data, err + } + doc.Values = append(doc.Values, val1) + doc.Values = append(doc.Values, val2) + data[category] = append(data[category], doc) + } + if rows != nil { + rows.Close() + } + return data, err +} diff --git a/sqlite3_query.go b/sqlite3_query.go index 9f39d75..3d97977 100644 --- a/sqlite3_query.go +++ b/sqlite3_query.go @@ -494,125 +494,3 @@ func (ptr *SQLite3DB) GetReslenByNamespace(ns string, duration string) ([]NameVa } return docs, err } - -func (ptr *SQLite3DB) GetAuditData() (map[string][]NameValues, error) { - var err error - db := ptr.db - data := map[string][]NameValues{} - query := fmt.Sprintf(`SELECT MAX(conns) FROM %v_clients;`, ptr.hatchetName) - if ptr.verbose { - log.Println(query) - } - rows, err := db.Query(query) - category := "stats" - if err == nil && rows.Next() { - var doc NameValues - var value int - _ = rows.Scan(&value) - doc.Name = "maxConns" - doc.Values = append(doc.Values, value) - data[category] = append(data[category], doc) - } - if rows != nil { - rows.Close() - } - - query = fmt.Sprintf(`SELECT MAX(max_ms), SUM(count), SUM(total_ms) FROM %v_ops;`, ptr.hatchetName) - if ptr.verbose { - log.Println(query) - } - rows, err = db.Query(query) - if err == nil && rows.Next() { - var maxMilli, total, totalMilli int - if err = rows.Scan(&maxMilli, &total, &totalMilli); err != nil { - return data, err - } - data[category] = append(data[category], NameValues{"maxMilli", []int{maxMilli}}) - data[category] = append(data[category], NameValues{"avgMilli", []int{totalMilli / total}}) - data[category] = append(data[category], NameValues{"totalMilli", []int{totalMilli}}) - } - if rows != nil { - rows.Close() - } - - query = fmt.Sprintf(`SELECT type, name, value FROM %v_audit - WHERE type IN ('exception', 'failed', 'op', 'duration') ORDER BY type, value DESC;`, ptr.hatchetName) - if ptr.verbose { - log.Println(query) - } - rows, err = db.Query(query) - if err != nil { - return data, err - } - for rows.Next() { - var category string - var doc NameValues - var value int - if err = rows.Scan(&category, &doc.Name, &value); err != nil { - return data, err - } - doc.Values = append(doc.Values, value) - if category == "exception" { - if doc.Name == "E" { - doc.Name = "Error" - } else if doc.Name == "F" { - doc.Name = "Fatal" - } else if doc.Name == "W" { - doc.Name = "Warn" - } - } - data[category] = append(data[category], doc) - } - if rows != nil { - rows.Close() - } - - category = "ip" - query = fmt.Sprintf(`SELECT a.name ip, a.value count, b.value reslen FROM %v_audit a, %v_audit b WHERE a.type == '%v' AND b.type = 'reslen-ip' AND a.name = b.name ORDER BY reslen DESC;`, - ptr.hatchetName, ptr.hatchetName, category) - if ptr.verbose { - log.Println(query) - } - rows, err = db.Query(query) - if err != nil { - return data, err - } - for rows.Next() { - var doc NameValues - var val1, val2 int - if err = rows.Scan(&doc.Name, &val1, &val2); err != nil { - return data, err - } - doc.Values = append(doc.Values, val1) - doc.Values = append(doc.Values, val2) - data[category] = append(data[category], doc) - } - if rows != nil { - rows.Close() - } - - category = "ns" - query = fmt.Sprintf(`SELECT a.name ns, a.value count, b.value reslen FROM %v_audit a, %v_audit b WHERE a.type == '%v' AND b.type = 'reslen-ns' AND a.name = b.name ORDER BY reslen DESC;`, - ptr.hatchetName, ptr.hatchetName, category) - if ptr.verbose { - log.Println(query) - } - rows, err = db.Query(query) - if err != nil { - return data, err - } - for rows.Next() { - var doc NameValues - var val1, val2 int - if err = rows.Scan(&doc.Name, &val1, &val2); err != nil { - return data, err - } - doc.Values = append(doc.Values, val1) - doc.Values = append(doc.Values, val2) - data[category] = append(data[category], doc) - } - if rows != nil { - rows.Close() - } - return data, err -} diff --git a/template.go b/template.go index d8023e3..2df297e 100644 --- a/template.go +++ b/template.go @@ -35,75 +35,88 @@ const headers = ` @@ -232,18 +242,17 @@ func getContentHTML() string { }
    - +
    Hatchet
    - - - - +
    Audit
    +
    Stats
    +
    Top N
    +
    Search