1
0
mirror of https://github.com/taigrr/log-socket synced 2025-01-18 04:53:14 -08:00

6 Commits

Author SHA1 Message Date
a07d3a11df minor cleanup, adds support for formatted prints 2021-07-03 08:45:19 -07:00
5f18def7fa Adds license 2021-04-09 11:09:18 -07:00
2c7cf0494e Adds autoscroll feature to browser 2021-04-09 10:39:23 -07:00
b744f083dc general refactoring 2021-04-09 10:05:59 -07:00
1ea76f4ab1 breaks browser out into package 2021-04-09 09:47:49 -07:00
2e01f23b30 Allow setting of stderr level 2021-04-09 09:40:25 -07:00
5 changed files with 345 additions and 212 deletions

12
LICENSE Normal file
View File

@@ -0,0 +1,12 @@
Copyright (C) 2019-2021 by Tai Groot <tai@taigrr.com>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

212
browser/browser.go Normal file
View File

@@ -0,0 +1,212 @@
package browser
import (
"html/template"
"net/http"
"strings"
)
func LogSocketViewHandler(w http.ResponseWriter, r *http.Request) {
wsResource := "ws://" + r.Host + r.URL.Path
wsResource = strings.TrimSuffix(wsResource, "/") + "/ws"
homeTemplate.Execute(w, wsResource)
}
var homeTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<center>
<input type="text" id="search" onkeyup="filterTable()" placeholder="Filter...">
<input type="checkbox" id="shouldScroll" checked>Enable Autoscroll<br>
<table id="logHeaders" style="text-align:left; width:80%;" >
<tbody>
<tr class="header">
<th style="width:20%;">TimeStamp</th>
<th style="width:5%;">Level</th>
<th style="width:65%;">Output</th>
<th style="width:10%;">Source</th>
</tr>
</tbody>
</table>
<div id="tableWrapper">
<table id="logs" style="text-align:left; width:100%;" >
<tbody id="tbodylogs">
<tr class="header">
<th style="width:20%;"></th>
<th style="width:5%;"></th>
<th style="width:65%;"></th>
<th style="width:10%;"></th>
</tr>
</tbody>
</table>
</div>
<br>
<input class="button" type="button" id="download" value="Download Logs" style="background-color:#3f51b5;"/>
<input class="button" type="button" id="delete" value="Delete Logs" style="background-color:#f44336"/>
</center>
</body>
<footer>
<style>
#tableWrapper{
overflow-y: scroll;
display: flow-root;
width: 80%;
height: 80vh;
}
td,tr{
height: min-content;
}
.button{
display: inline-block;
width: 5vw;
height: 5vh;
}
</style>
<script>
var logTable = document.getElementById("logs");
var logTableB = document.getElementById("tbodylogs");
var ws = null;
var application = "demo-commit"
var logs = [];
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function download(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function openSocket() {
if (ws) {
return false;
}
ws = new WebSocket("{{.}}");
ws.onclose = async function(evt) {
ws = null;
while(ws == null){
openSocket()
await sleep(5000);
}
}
ws.onmessage = function(evt) {
var entry = JSON.parse(evt.data)
logs.push(entry)
var row = document.createElement('tr');
var ts = document.createElement('td');
var tst = document.createTextNode(entry.timestamp);
ts.appendChild(tst);
row.appendChild(ts);
var ts = document.createElement('td');
var tst = document.createTextNode(entry.level);
ts.appendChild(tst);
row.appendChild(ts);
var ts = document.createElement('td');
var tst = document.createTextNode(entry.output);
ts.appendChild(tst);
row.appendChild(ts);
var ts = document.createElement('td');
var tst = document.createTextNode(entry.file);
ts.appendChild(tst);
row.appendChild(ts);
var bg="";
switch(entry.level){
case "INFO":
bg="white";
break;
case "ERROR":
bg="#f44336";
break;
case "WARN":
bg="#fb8c00"
break;
case "TRACE":
bg="#E1F5FE"
break;
case "DEBUG":
bg="#B3E5FC"
break;
default:
bg="white"
break;
}
row.style.backgroundColor=bg
logTableB.append(row)
filterTable()
}
ws.onerror = function(evt) {
if (evt != null && evt.data != null){
// handle error here
}
}
}
function clearTable(){
if(!window.confirm("Are you sure you want to delete all logs?")){
return
}
logs = []
while (logTableB.childNodes.length > 1) {
logTableB.removeChild(logTableB.childNodes[1]);
}
}
function filterTable() {
var cols, input, filter, table, tr, td, i, txtValue, w;
input = document.getElementById("search");
filter = input.value;
table = logTableB;
tr = table.getElementsByTagName("tr");
for (i = 1; i < tr.length; i++) {
cols = tr[i].getElementsByTagName("td");
var visible = false;
for (w = 0; w < cols.length; w++){
if (!visible && cols[w]) {
td = cols[w]
txtValue = td.textContent || td.innerText;
if (txtValue.indexOf(filter) > -1) {
visible = true
}
}
}
if(visible){
tr[i].style.display = "";
} else {
tr[i].style.display = "none";
}
}
}
function pageScroll() {
if (document.getElementById('shouldScroll').checked) {
document.getElementById('tableWrapper').scrollBy(0,10);
}
setTimeout(pageScroll,10);
}
document.getElementById("delete").addEventListener("click", function(){
clearTable()
}, false);
document.getElementById("download").addEventListener("click", function(){
download(application+'.json',JSON.stringify(logs));
}, false);
openSocket();
pageScroll();
</script>
</footer>
</html>
`))

View File

@@ -10,16 +10,6 @@ import (
"time" "time"
) )
const (
LTrace Level = iota
LDebug
LInfo
LWarn
LError
LPanic
LFatal
)
var ( var (
clients []*Client clients []*Client
sliceTex sync.Mutex sliceTex sync.Mutex
@@ -112,6 +102,10 @@ func createLog(e Entry) {
sliceTex.Unlock() sliceTex.Unlock()
} }
func SetLogLevel(level Level) {
stderrClient.LogLevel = level
}
// SetLogLevel set log level of logger // SetLogLevel set log level of logger
func (c *Client) SetLogLevel(level Level) { func (c *Client) SetLogLevel(level Level) {
if !c.initialized { if !c.initialized {
@@ -138,9 +132,19 @@ func Trace(args ...interface{}) {
level: LTrace, level: LTrace,
} }
createLog(e) createLog(e)
// entry := logger.WithFields(logrus.Fields{}) }
// entry.Data["file"] = fileInfo(2)
// entry.Debug(args...) // Formatted print for Trace
func Tracef(format string, args ...interface{}) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "TRACE",
level: LTrace,
}
createLog(e)
} }
// Debug prints out logs on debug level // Debug prints out logs on debug level
@@ -154,7 +158,19 @@ func Debug(args ...interface{}) {
level: LDebug, level: LDebug,
} }
createLog(e) createLog(e)
}
// Formatted print for Debug
func Debugf(format string, args ...interface{}) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "DEBUG",
level: LDebug,
}
createLog(e)
} }
// Info prints out logs on info level // Info prints out logs on info level
@@ -170,6 +186,19 @@ func Info(args ...interface{}) {
createLog(e) createLog(e)
} }
// Formatted print for Info
func Infof(format string, args ...interface{}) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "INFO",
level: LInfo,
}
createLog(e)
}
// Warn prints out logs on warn level // Warn prints out logs on warn level
func Warn(args ...interface{}) { func Warn(args ...interface{}) {
output := fmt.Sprint(args...) output := fmt.Sprint(args...)
@@ -183,6 +212,19 @@ func Warn(args ...interface{}) {
createLog(e) createLog(e)
} }
// Formatted print for Warn
func Warnf(format string, args ...interface{}) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "WARN",
level: LWarn,
}
createLog(e)
}
// Error prints out logs on error level // Error prints out logs on error level
func Error(args ...interface{}) { func Error(args ...interface{}) {
output := fmt.Sprint(args...) output := fmt.Sprint(args...)
@@ -196,6 +238,19 @@ func Error(args ...interface{}) {
createLog(e) createLog(e)
} }
// Formatted print for error
func Errorf(format string, args ...interface{}) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "ERROR",
level: LError,
}
createLog(e)
}
// Panic prints out logs on panic level // Panic prints out logs on panic level
func Panic(args ...interface{}) { func Panic(args ...interface{}) {
output := fmt.Sprint(args...) output := fmt.Sprint(args...)
@@ -219,6 +274,29 @@ func Panic(args ...interface{}) {
panic(errors.New(output)) panic(errors.New(output))
} }
// Formatted print for panic
func Panicf(format string, args ...interface{}) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "PANIC",
level: LPanic,
}
createLog(e)
if len(args) >= 0 {
switch args[0].(type) {
case error:
panic(args[0])
default:
// falls through to default below
}
}
Flush()
panic(errors.New(output))
}
// Fatal prints out logs on fatal level // Fatal prints out logs on fatal level
func Fatal(args ...interface{}) { func Fatal(args ...interface{}) {
output := fmt.Sprint(args...) output := fmt.Sprint(args...)
@@ -234,6 +312,21 @@ func Fatal(args ...interface{}) {
os.Exit(1) os.Exit(1)
} }
// Formatted print for fatal
func Fatalf(format string, args ...interface{}) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "FATAL",
level: LFatal,
}
createLog(e)
Flush()
os.Exit(1)
}
// fileInfo for getting which line in which file // fileInfo for getting which line in which file
func fileInfo(skip int) string { func fileInfo(skip int) string {
_, file, line, ok := runtime.Caller(skip) _, file, line, ok := runtime.Caller(skip)

View File

@@ -5,6 +5,16 @@ import "time"
type LogWriter chan Entry type LogWriter chan Entry
type Level int type Level int
const (
LTrace Level = iota
LDebug
LInfo
LWarn
LError
LPanic
LFatal
)
type Client struct { type Client struct {
LogLevel Level `json:"level"` LogLevel Level `json:"level"`
writer LogWriter writer LogWriter

204
main.go
View File

@@ -2,21 +2,16 @@ package main
import ( import (
"flag" "flag"
"html/template"
"log"
"net/http" "net/http"
"time" "time"
"github.com/taigrr/log-socket/browser"
"github.com/taigrr/log-socket/logger" "github.com/taigrr/log-socket/logger"
"github.com/taigrr/log-socket/ws" "github.com/taigrr/log-socket/ws"
) )
var addr = flag.String("addr", "0.0.0.0:8080", "http service address") var addr = flag.String("addr", "0.0.0.0:8080", "http service address")
func home(w http.ResponseWriter, r *http.Request) {
homeTemplate.Execute(w, "ws://"+r.Host+"/logs")
}
func generateLogs() { func generateLogs() {
for { for {
logger.Info("This is an info log!") logger.Info("This is an info log!")
@@ -24,204 +19,15 @@ func generateLogs() {
logger.Debug("This is a debug log!") logger.Debug("This is a debug log!")
logger.Warn("This is a warn log!") logger.Warn("This is a warn log!")
logger.Error("This is an error log!") logger.Error("This is an error log!")
time.Sleep(10 * time.Second) time.Sleep(2 * time.Second)
} }
} }
func main() { func main() {
defer logger.Flush() defer logger.Flush()
flag.Parse() flag.Parse()
http.HandleFunc("/logs", ws.LogSocketHandler) http.HandleFunc("/ws", ws.LogSocketHandler)
http.HandleFunc("/", home) http.HandleFunc("/", browser.LogSocketViewHandler)
go generateLogs() go generateLogs()
log.Fatal(http.ListenAndServe(*addr, nil)) logger.Fatal(http.ListenAndServe(*addr, nil))
} }
var homeTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<center>
<input type="text" id="search" onkeyup="filterTable()" placeholder="Filter...">
<table id="logHeaders" style="text-align:left; width:80%;" >
<tbody>
<tr class="header">
<th style="width:20%;">TimeStamp</th>
<th style="width:5%;">Level</th>
<th style="width:65%;">Output</th>
<th style="width:10%;">Source</th>
</tr>
</tbody>
</table>
<div id="tableWrapper">
<table id="logs" style="text-align:left; width:100%;" >
<tbody id="tbodylogs">
<tr class="header">
<th style="width:20%;"></th>
<th style="width:5%;"></th>
<th style="width:65%;"></th>
<th style="width:10%;"></th>
</tr>
</tbody>
</table>
</div>
<br>
<input class="button" type="button" id="download" value="Download Logs" style="background-color:#3f51b5;"/>
<input class="button" type="button" id="delete" value="Delete Logs" style="background-color:#f44336"/>
</center>
</body>
<footer>
<style>
#tableWrapper{
overflow-y: scroll;
display: flow-root;
width: 80%;
height: 80vh;
}
td,tr{
height: min-content;
}
.button{
display: inline-block;
width: 5vw;
height: 5vh;
}
</style>
<script>
var logTable = document.getElementById("logs");
var logTableB = document.getElementById("tbodylogs");
var ws = null;
var application = "demo-commit"
var logs = [];
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function download(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function openSocket() {
if (ws) {
return false;
}
ws = new WebSocket("{{.}}");
ws.onclose = async function(evt) {
ws = null;
while(ws == null){
openSocket()
await sleep(5000);
}
}
ws.onmessage = function(evt) {
var entry = JSON.parse(evt.data)
logs.push(entry)
var row = document.createElement('tr');
var ts = document.createElement('td');
var tst = document.createTextNode(entry.timestamp);
ts.appendChild(tst);
row.appendChild(ts);
var ts = document.createElement('td');
var tst = document.createTextNode(entry.level);
ts.appendChild(tst);
row.appendChild(ts);
var ts = document.createElement('td');
var tst = document.createTextNode(entry.output);
ts.appendChild(tst);
row.appendChild(ts);
var ts = document.createElement('td');
var tst = document.createTextNode(entry.file);
ts.appendChild(tst);
row.appendChild(ts);
var bg="";
switch(entry.level){
case "INFO":
bg="white";
break;
case "ERROR":
bg="#f44336";
break;
case "WARN":
bg="#fb8c00"
break;
case "TRACE":
bg="#E1F5FE"
break;
case "DEBUG":
bg="#B3E5FC"
break;
default:
bg="white"
break;
}
row.style.backgroundColor=bg
logTableB.append(row)
filterTable()
}
ws.onerror = function(evt) {
if (evt != null && evt.data != null){
// handle error here
}
}
}
function clearTable(){
if(!window.confirm("Are you sure you want to delete all logs?")){
return
}
logs = []
while (logTableB.childNodes.length > 1) {
logTableB.removeChild(logTableB.childNodes[1]);
}
}
function filterTable() {
var cols, input, filter, table, tr, td, i, txtValue, w;
input = document.getElementById("search");
filter = input.value;
table = logTableB;
tr = table.getElementsByTagName("tr");
for (i = 1; i < tr.length; i++) {
cols = tr[i].getElementsByTagName("td");
var visible = false;
for (w = 0; w < cols.length; w++){
if (!visible && cols[w]) {
td = cols[w]
txtValue = td.textContent || td.innerText;
if (txtValue.indexOf(filter) > -1) {
visible = true
}
}
}
if(visible){
tr[i].style.display = "";
} else {
tr[i].style.display = "none";
}
}
}
document.getElementById("delete").addEventListener("click", function(){
clearTable()
}, false);
document.getElementById("download").addEventListener("click", function(){
download(application+'.json',JSON.stringify(logs));
}, false);
openSocket();
</script>
</footer>
</html>
`))