1
0
mirror of https://github.com/taigrr/log-socket synced 2026-03-20 16:02:28 -07:00

34 Commits

Author SHA1 Message Date
f86c2dbf46 enable namespaced logging 2025-11-10 16:07:28 -05:00
Daniel Merja
9c571ca955 Update screenshot image in the browser directory 2025-06-29 19:53:24 -07:00
Daniel Merja
45ba154d1e Update Go version to 1.24.4 and overhaul viewer.html with a complete redesign for improved log display and user interaction. (#13) 2025-06-29 02:11:12 -07:00
c89dda8333 error -> warn for broken connection 2025-06-29 00:43:55 -07:00
41aad10e6c add upgrader option 2025-06-29 00:10:09 -07:00
64ab2564c1 upgrade workflow to match go.mod 2025-05-01 18:30:13 -07:00
1c00bc90b8 upgrade deps, readme 2025-05-01 18:27:46 -07:00
f114098a8c uodate go version 2023-08-26 22:00:42 -07:00
Ethan Holz
2571dbe347 Add Github Actions for automated testing (#12)
* ci: Added initial github action for automated testing

* fix: changed go version

* ci: updated to change job name to test
2023-05-13 23:27:20 -07:00
80b80758de Add Default() func 2023-03-29 14:26:06 -07:00
0fa4de7961 Revert "add ability to embed other loggers of varying functionality"
This reverts commit 50c507c8f4.
2023-03-29 14:06:00 -07:00
50c507c8f4 add ability to embed other loggers of varying functionality 2023-03-29 14:02:57 -07:00
e21fb8a614 make pass gofumpt 2022-10-14 22:23:42 -07:00
2492509b6b extract browser from string var, upgrade deps 2022-10-14 22:14:47 -07:00
a1e960366e Merge pull request #11 from ethanholz/log-parity
feat: Updated to include print and added ln variants for all logs
2022-10-13 08:05:39 -07:00
Ethan Holz
430181e3a2 feat: Updated to include print and added ln variants for all logs 2022-10-13 09:59:04 -05:00
147a8cb30b Merge pull request #10 from ethanholz/README
Added a README
2022-10-10 13:38:59 -07:00
Ethan Holz
8e044e3993 docs: Updated README to include running example. 2022-10-10 12:16:52 -05:00
Ethan Holz
e97d37012e docs: Added inital README 2022-10-10 12:09:29 -05:00
ebef59a9a8 actually set value for depth 2021-08-23 21:58:05 -07:00
b3ceb12277 fixes fileinfo reflection issue 2021-08-23 21:45:08 -07:00
45cad34fdc fixes fileinfo reflection issue 2021-08-23 21:44:56 -07:00
f0c16a0c56 fixes fileinfo reflection issue 2021-08-23 21:36:49 -07:00
c18854598d Added a logger type 2021-08-23 21:25:01 -07:00
63ed3a2ad7 logger => log 2021-08-23 21:12:21 -07:00
f51ec53a89 Adds Notice logging level to support NATS 2021-08-23 20:58:15 -07:00
d21c91379e update go compiler target version 2021-08-17 10:07:07 -07:00
af2116af48 add sponsorship 2021-07-05 21:03:17 -07:00
b65a10e7a8 detect tls presence based on incoming request for websocket 2021-07-05 16:10:50 -07:00
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
19 changed files with 2578 additions and 497 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: taigrr # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

23
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Go package
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.21"
- name: Install dependencies
run: go get .
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...

501
CRUSH.md Normal file
View File

@@ -0,0 +1,501 @@
# CRUSH.md - Log Socket v2 Development Guide
This document provides context and conventions for working on **log-socket v2** - a real-time log viewer with WebSocket support and namespace filtering, written in Go.
## Project Overview
Log Socket v2 is a Go library and standalone application that provides:
- Real-time log streaming via WebSocket
- **Namespace-based log organization** (NEW in v2)
- Web-based log viewer with namespace filtering
- Support for multiple log levels (TRACE, DEBUG, INFO, NOTICE, WARN, ERROR, PANIC, FATAL)
- Client architecture allowing multiple subscribers to filtered log streams
**Key insight**: This is both a library (importable Go package) and a standalone application. The main.go serves as an example implementation.
## Essential Commands
### Build
```bash
go build -v ./...
```
### Run Server
```bash
# Default (runs on 0.0.0.0:8080)
go run main.go
# Custom address
go run main.go -addr localhost:8080
```
Once running, open browser to `http://localhost:8080` to view logs.
### Test
```bash
go test -v ./...
```
### Install
```bash
go install github.com/taigrr/log-socket/v2@latest
```
### Dependencies
```bash
go get .
```
## Project Structure
```
.
├── main.go # Example server with multiple namespaces
├── log/ # Core logging package
│ ├── log.go # Package-level logging functions + namespace tracking
│ ├── logger.go # Logger type with namespace support
│ ├── types.go # Type definitions (includes Namespace fields)
│ └── log_test.go # Tests
├── ws/ # WebSocket server
│ ├── server.go # LogSocketHandler with namespace filtering
│ └── namespaces.go # HTTP handler for namespace list API
└── browser/ # Web UI
├── browser.go # HTTP handler serving embedded HTML
└── viewer.html # Embedded web interface with namespace filter
```
## Major Changes in v2
### Module Path
- **v1**: `github.com/taigrr/log-socket`
- **v2**: `github.com/taigrr/log-socket/v2`
### Namespace Support
**Core concept**: Namespaces allow organizing logs by component, service, or domain (e.g., "api", "database", "auth").
#### Types Changes
**Entry** now includes:
```go
type Entry struct {
Timestamp time.Time `json:"timestamp"`
Output string `json:"output"`
File string `json:"file"`
Level string `json:"level"`
Namespace string `json:"namespace"` // NEW
level Level
}
```
**Client** now uses a slice for filtering:
```go
type Client struct {
LogLevel Level `json:"level"`
Namespaces []string `json:"namespaces"` // Empty = all namespaces
writer LogWriter
initialized bool
}
```
**Logger** has namespace field:
```go
type Logger struct {
FileInfoDepth int
Namespace string // NEW
}
```
#### API Changes
**CreateClient** now variadic:
```go
// v1
func CreateClient() *Client
// v2
func CreateClient(namespaces ...string) *Client
// Examples:
client := log.CreateClient() // All namespaces
client := log.CreateClient("api") // Single namespace
client := log.CreateClient("api", "database") // Multiple namespaces
```
**NewLogger** constructor added:
```go
func NewLogger(namespace string) *Logger
// Example:
apiLogger := log.NewLogger("api")
apiLogger.Info("API request received")
```
### Namespace Tracking
Global namespace registry tracks all used namespaces:
```go
var (
namespaces map[string]bool
namespacesMux sync.RWMutex
)
func GetNamespaces() []string
```
Namespaces are automatically registered when logs are created.
### WebSocket Changes
**Query parameter for filtering**:
```
ws://localhost:8080/ws?namespaces=api,database
```
The handler parses comma-separated namespace list and creates a filtered client.
### Web UI Changes
- Namespace filter input field added to controls
- Namespace column added to log table
- Reconnect button to apply namespace filter
- WebSocket URL includes namespace query parameter
## Code Organization & Architecture
### Log Package (`log/`)
Dual API remains, but with namespace support:
1. **Package-level functions**: Use "default" namespace
- `log.Info()`, `log.Debug()`, etc.
- All entry creations include `Namespace: DefaultNamespace`
2. **Logger instances**: Use custom namespace
- Create with `log.NewLogger(namespace)`
- All entry creations include `Namespace: l.Namespace`
### Client Architecture (Updated)
**Client filtering by namespace**:
1. **Empty Namespaces slice**: Receives all logs regardless of namespace
2. **Non-empty Namespaces**: Only receives logs matching one of the specified namespaces
**matchesNamespace helper**:
```go
func (c *Client) matchesNamespace(namespace string) bool {
// Empty Namespaces slice means match all
if len(c.Namespaces) == 0 {
return true
}
for _, ns := range c.Namespaces {
if ns == namespace {
return true
}
}
return false
}
```
**Entry flow with namespace filtering**:
1. Log function called with namespace
2. `Entry` created with namespace field
3. Namespace registered in global map
4. `createLog()` sends to all clients
5. Each client checks `matchesNamespace()`
6. Only matching clients receive the entry
### WebSocket Handler (`ws/`)
**Namespace parameter parsing**:
```go
namespacesParam := r.URL.Query().Get("namespaces")
var namespaces []string
if namespacesParam != "" {
namespaces = strings.Split(namespacesParam, ",")
}
lc := logger.CreateClient(namespaces...)
```
**Namespaces API handler** (`ws/namespaces.go`):
```go
func NamespacesHandler(w http.ResponseWriter, r *http.Request) {
namespaces := logger.GetNamespaces()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"namespaces": namespaces,
})
}
```
### Browser Package (`browser/`)
Still embeds viewer.html, but HTML now includes:
- Namespace filter input
- Namespace column in grid (5 columns instead of 4)
- `reconnectWithNamespace()` method
- WebSocket URL construction with query parameter
## Go Version & Dependencies
- **Go version**: 1.24.4 (specified in go.mod)
- **Only external dependency**: `github.com/gorilla/websocket v1.5.3`
## Naming Conventions & Style
### Namespaces
- Use lowercase strings: `"api"`, `"database"`, `"auth"`, `"cache"`
- Default constant: `DefaultNamespace = "default"`
- Comma-separated in query params: `?namespaces=api,database`
### Log Levels
Unchanged from v1 - still use uppercase strings and iota constants.
### Variable Names
- Use descriptive names (`apiLogger`, `dbLogger`, `namespaces`)
- Exception: Loop variables, short-lived scopes
## Important Patterns & Gotchas
### 1. Namespace Tracking is Automatic
When any log is created, its namespace is automatically added to the global registry:
```go
func createLog(e Entry) {
// Track namespace
namespacesMux.Lock()
namespaces[e.Namespace] = true
namespacesMux.Unlock()
// ... rest of function
}
```
**No manual registration needed** - just log and the namespace appears.
### 2. Empty Namespace List = All Logs
Both for clients and WebSocket connections:
```go
client := log.CreateClient() // Gets ALL logs
ws://localhost:8080/ws // Gets ALL logs
```
This is the default behavior to maintain backward compatibility.
### 3. Client Namespace Filtering is Inclusive (OR)
If a client has multiple namespaces, it receives logs matching ANY of them:
```go
client := log.CreateClient("api", "database")
// Receives logs from "api" OR "database", not "auth"
```
### 4. Namespace Field Always Set
All logging functions set namespace:
- Package functions: `DefaultNamespace`
- Logger methods: `l.Namespace`
**Never nil or empty** - there's always a namespace.
### 5. WebSocket Namespace Reconnection
Changing namespace filter requires reconnecting WebSocket:
```javascript
reconnectWithNamespace() {
if (this.ws) {
this.ws.onclose = null; // Prevent auto-reconnect
this.ws.close();
this.ws = null;
}
this.reconnectAttempts = 0;
this.connectWebSocket(); // Creates new connection with new filter
}
```
UI provides "Reconnect" button for this purpose.
### 6. Stderr Client Uses All Namespaces
The built-in stderr client (created in `init()`) listens to all namespaces:
```go
stderrClient = CreateClient(DefaultNamespace)
```
But only prints logs matching its own namespace in `logStdErr()`:
```go
if e.level >= c.LogLevel && c.matchesNamespace(e.Namespace) {
fmt.Fprintf(os.Stderr, "%s\t%s\t[%s]\t%s\t%s\n", ...)
}
```
**Wait, that's a bug!** The stderr client is created with `DefaultNamespace` but should be created with no namespaces to see all logs. Let me check this.
Actually looking at the code:
```go
stderrClient = CreateClient(DefaultNamespace)
```
This means stderr client only sees "default" namespace logs. This might be intentional, but seems like a bug. Should probably be:
```go
stderrClient = CreateClient() // No args = all namespaces
```
### 7. Grid Layout Updated
The log viewer grid changed from 4 to 5 columns:
```css
/* v1 */
grid-template-columns: 180px 80px 1fr 120px;
/* v2 */
grid-template-columns: 180px 80px 100px 1fr 120px;
```
Order: Timestamp, Level, Namespace, Message, Source
## Testing
### Test Updates for v2
All `CreateClient()` calls in tests now pass namespace:
```go
// v1
c := CreateClient()
// v2
c := CreateClient("test")
c := CreateClient(DefaultNamespace)
```
Tests verify namespace appears in output (see stderr format).
### Running Tests
```bash
go test -v ./...
```
All existing tests pass with namespace support added.
## CI/CD
GitHub Actions workflow (`.github/workflows/ci.yaml`):
- Still uses Go 1.21 (should update to 1.24.4 to match go.mod)
- No changes needed for v2 functionality
## Common Tasks
### Adding a New Namespace
No code changes needed! Just create a logger:
```go
cacheLogger := log.NewLogger("cache")
cacheLogger.Info("Cache initialized")
```
Namespace automatically tracked and available via API.
### Creating Namespace-Specific Client
```go
// Subscribe only to API logs
apiClient := log.CreateClient("api")
defer apiClient.Destroy()
for {
entry := apiClient.Get()
// Only receives logs from "api" namespace
processAPILog(entry)
}
```
### Filtering WebSocket by Namespace
Frontend:
```javascript
// Set namespace filter
document.getElementById('namespaceFilter').value = 'api,database';
// Click reconnect button or call:
logViewer.reconnectWithNamespace();
```
Backend automatically creates filtered client based on query param.
### Getting All Active Namespaces
```go
namespaces := log.GetNamespaces()
// Returns: ["default", "api", "database", "auth", ...]
```
Or via HTTP:
```bash
GET /api/namespaces
```
Returns:
```json
{
"namespaces": ["default", "api", "database", "auth"]
}
```
## Migration from v1 to v2
### Import Paths
```go
// v1
import "github.com/taigrr/log-socket/log"
import "github.com/taigrr/log-socket/ws"
import "github.com/taigrr/log-socket/browser"
// v2
import "github.com/taigrr/log-socket/v2/log"
import "github.com/taigrr/log-socket/v2/ws"
import "github.com/taigrr/log-socket/v2/browser"
```
### CreateClient Calls
```go
// v1
client := log.CreateClient()
// v2 - same behavior
client := log.CreateClient() // Empty = all namespaces
// v2 - new filtering capability
client := log.CreateClient("api")
client := log.CreateClient("api", "database")
```
### Default() Logger
```go
// v1
logger := log.Default()
// v2 - uses default namespace
logger := log.Default()
// v2 - new namespaced option
logger := log.NewLogger("api")
```
### WebSocket URL
```
v1: ws://host/ws
v2: ws://host/ws # All namespaces (backward compatible)
v2: ws://host/ws?namespaces=api # Filtered
v2: ws://host/ws?namespaces=api,database # Multiple
```
## Repository Context
- **License**: Check LICENSE file
- **Funding**: GitHub sponsors (.github/FUNDING.yml)
- **Open Source**: github.com/taigrr/log-socket
- **Version**: v2.x.x (major version bump for breaking changes)
## Future Agent Notes
- **v2 is a breaking change**: Major version bump follows Go modules convention
- Namespace support is the primary new feature
- Backward compatible behavior: empty namespace list = all logs
- Namespace tracking is automatic via global registry
- Web UI has been significantly updated for namespace support
- Possible bug: stderr client might need to use `CreateClient()` instead of `CreateClient(DefaultNamespace)` to see all logs
- All tests updated and passing
- Example in main.go demonstrates multiple namespaces

12
LICENSE Normal file
View File

@@ -0,0 +1,12 @@
Copyright (C) 2019-2025 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.

233
README.md Normal file
View File

@@ -0,0 +1,233 @@
# Log Socket v2
A real-time log viewer with WebSocket support and namespace filtering, written in Go.
## What's New in v2
**Breaking Changes:**
- Module path changed to `github.com/taigrr/log-socket/v2`
- `CreateClient()` now accepts variadic `namespaces ...string` parameter
- `Logger` type now includes `Namespace` field
- New `NewLogger(namespace string)` constructor for namespaced loggers
**New Features:**
- **Namespace support**: Organize logs by namespace (api, database, auth, etc.)
- **Namespace filtering**: Subscribe to specific namespaces via WebSocket
- **Frontend namespace selector**: Filter logs by namespace in the web UI
- **Namespace API**: GET `/api/namespaces` to list all active namespaces
## Features
- Real-time log streaming via WebSocket
- Web-based log viewer with filtering capabilities
- **Namespace-based log organization**
- Support for multiple log levels (TRACE, DEBUG, INFO, WARN, ERROR, PANIC, FATAL)
- Color-coded log levels for better visibility
- Auto-scrolling with toggle option
- Log download functionality
- Log clearing capability
- File source tracking for each log entry
## Installation
```bash
go install github.com/taigrr/log-socket/v2@latest
```
## Quick Start
```go
package main
import (
"net/http"
logger "github.com/taigrr/log-socket/v2/log"
"github.com/taigrr/log-socket/v2/ws"
"github.com/taigrr/log-socket/v2/browser"
)
func main() {
defer logger.Flush()
// Set up HTTP handlers
http.HandleFunc("/ws", ws.LogSocketHandler)
http.HandleFunc("/api/namespaces", ws.NamespacesHandler)
http.HandleFunc("/", browser.LogSocketViewHandler)
// Use default namespace
logger.Info("Application started")
// Create namespaced loggers
apiLogger := logger.NewLogger("api")
dbLogger := logger.NewLogger("database")
apiLogger.Info("API server ready")
dbLogger.Debug("Database connected")
logger.Fatal(http.ListenAndServe(":8080", nil))
}
```
## Usage
### Starting the Server
```bash
log-socket
```
By default, the server runs on `0.0.0.0:8080`. Specify a different address:
```bash
log-socket -addr localhost:8080
```
### Web Interface
Open your browser and navigate to `http://localhost:8080`
**Namespace Filtering:**
- The namespace dropdown is automatically populated from `/api/namespaces`
- Select one or more namespaces to filter (hold Ctrl/Cmd to multi-select)
- Default is "All Namespaces" (shows everything)
- Click "Reconnect" to apply the filter
## API
### Logging Interface
The package provides two ways to log:
#### 1. Package-level functions (default namespace)
```go
logger.Trace("trace message")
logger.Debug("debug message")
logger.Info("info message")
logger.Notice("notice message")
logger.Warn("warning message")
logger.Error("error message")
logger.Panic("panic message") // Logs and panics
logger.Fatal("fatal message") // Logs and exits
```
Each has formatted (`f`) and line (`ln`) variants:
```go
logger.Infof("User %s logged in", username)
logger.Infoln("This adds a newline")
```
#### 2. Namespaced loggers
```go
apiLogger := logger.NewLogger("api")
apiLogger.Info("Request received")
dbLogger := logger.NewLogger("database")
dbLogger.Warn("Slow query detected")
```
### Creating Clients with Namespace Filters
```go
// Listen to all namespaces
client := logger.CreateClient()
// Listen to specific namespace
client := logger.CreateClient("api")
// Listen to multiple namespaces
client := logger.CreateClient("api", "database", "auth")
```
### WebSocket API
#### Log Stream Endpoint
**URL:** `ws://localhost:8080/ws`
**Query Parameters:**
- `namespaces` (optional): Comma-separated list of namespaces to filter
**Examples:**
```
ws://localhost:8080/ws # All namespaces
ws://localhost:8080/ws?namespaces=api # Only "api" namespace
ws://localhost:8080/ws?namespaces=api,database # Multiple namespaces
```
**Message Format:**
```json
{
"timestamp": "2024-11-10T15:42:49.777298-05:00",
"output": "API request received",
"file": "main.go:42",
"level": "INFO",
"namespace": "api"
}
```
#### Namespaces List Endpoint
**URL:** `GET http://localhost:8080/api/namespaces`
**Response:**
```json
{
"namespaces": ["default", "api", "database", "auth"]
}
```
## Web Interface Features
- **Namespace Dropdown**: Dynamically populated from `/api/namespaces`, multi-select support
- **Text Search**: Filter logs by content, level, namespace, or source file
- **Auto-scroll**: Toggle auto-scrolling with checkbox
- **Download**: Save all logs as a JSON file
- **Clear**: Remove all logs from the viewer
- **Color Coding**: Different log levels are color-coded
- **Reconnect**: Reconnect WebSocket with new namespace filter
## Migration from v1
### Import Path
```go
// v1
import "github.com/taigrr/log-socket/log"
// v2
import "github.com/taigrr/log-socket/v2/log"
```
### CreateClient Changes
```go
// v1
client := log.CreateClient()
// v2 - specify namespace(s) or leave empty for all
client := log.CreateClient() // All namespaces
client := log.CreateClient("api") // Single namespace
client := log.CreateClient("api", "db") // Multiple namespaces
```
### New Logger Constructor
```go
// v2 only - create namespaced logger
apiLogger := log.NewLogger("api")
apiLogger.Info("Message in api namespace")
```
## Dependencies
- [gorilla/websocket](https://github.com/gorilla/websocket) for WebSocket support
## Notes
The web interface is provided as an example implementation. Users are encouraged to customize it for their specific needs. The WebSocket endpoint (`/ws`) can be consumed by any WebSocket client.
## License
See LICENSE file for details.

24
browser/browser.go Normal file
View File

@@ -0,0 +1,24 @@
package browser
import (
_ "embed"
"html/template"
"net/http"
"strings"
)
//go:embed viewer.html
var webpage string
func LogSocketViewHandler(w http.ResponseWriter, r *http.Request) {
wsResource := r.Host + r.URL.Path
if r.TLS != nil {
wsResource = "wss://" + wsResource
} else {
wsResource = "ws://" + wsResource
}
wsResource = strings.TrimSuffix(wsResource, "/") + "/ws"
homeTemplate.Execute(w, wsResource)
}
var homeTemplate = template.Must(template.New("").Parse(webpage))

BIN
browser/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

706
browser/viewer.html Normal file
View File

@@ -0,0 +1,706 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Log Viewer</title>
<style>
:root {
--primary-color: #2196F3;
--error-color: #f44336;
--warning-color: #ff9800;
--success-color: #4caf50;
--info-color: #2196F3;
--debug-color: #9c27b0;
--trace-color: #607d8b;
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #333333;
--text-secondary: #666666;
--border-color: #e0e0e0;
--shadow: 0 2px 4px rgba(0,0,0,0.1);
--shadow-hover: 0 4px 8px rgba(0,0,0,0.15);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background-color: var(--bg-secondary);
color: var(--text-primary);
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
background: var(--bg-primary);
padding: 20px;
border-radius: 8px;
box-shadow: var(--shadow);
margin-bottom: 20px;
}
.header h1 {
margin: 0 0 20px 0;
color: var(--text-primary);
font-size: 24px;
font-weight: 600;
}
.controls {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.search-container {
position: relative;
flex: 1;
min-width: 250px;
}
.search-input {
width: 100%;
padding: 10px 15px;
border: 2px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s ease;
}
.search-input:focus {
outline: none;
border-color: var(--primary-color);
}
.checkbox-container {
display: flex;
align-items: center;
gap: 8px;
user-select: none;
}
.checkbox-container input[type="checkbox"] {
width: 16px;
height: 16px;
}
.checkbox-container label {
font-size: 14px;
color: var(--text-secondary);
cursor: pointer;
}
.log-container {
background: var(--bg-primary);
border-radius: 8px;
box-shadow: var(--shadow);
overflow: hidden;
margin-bottom: 20px;
}
.log-header {
background: var(--bg-secondary);
padding: 12px 0;
border-bottom: 1px solid var(--border-color);
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.log-table {
width: 100%;
border-collapse: collapse;
}
.log-row {
display: grid;
grid-template-columns: 180px 80px 100px 1fr 120px;
gap: 15px;
padding: 10px 15px;
border-bottom: 1px solid var(--border-color);
transition: background-color 0.2s ease;
}
.log-row:hover {
background-color: rgba(0,0,0,0.02);
}
.log-cell {
display: flex;
align-items: center;
font-size: 13px;
word-break: break-word;
}
.log-cell.timestamp {
font-family: 'Monaco', 'Menlo', monospace;
color: var(--text-secondary);
font-size: 12px;
}
.log-cell.level {
font-weight: 600;
text-align: center;
justify-content: center;
}
.log-cell.output {
font-family: 'Monaco', 'Menlo', monospace;
white-space: pre-wrap;
}
.log-cell.source {
font-size: 11px;
color: var(--text-secondary);
}
.log-level-error { background-color: #ffebee; color: var(--error-color); }
.log-level-warn { background-color: #fff3e0; color: var(--warning-color); }
.log-level-info { background-color: #e3f2fd; color: var(--info-color); }
.log-level-debug { background-color: #f3e5f5; color: var(--debug-color); }
.log-level-trace { background-color: #eceff1; color: var(--trace-color); }
.log-viewer {
height: 60vh;
overflow-y: auto;
overflow-x: hidden;
}
.log-viewer::-webkit-scrollbar {
width: 8px;
}
.log-viewer::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
.log-viewer::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
}
.log-viewer::-webkit-scrollbar-thumb:hover {
background: #999;
}
.actions {
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-hover);
}
.btn:active {
transform: translateY(0);
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-danger {
background-color: var(--error-color);
color: white;
}
.status-bar {
background: var(--bg-primary);
padding: 10px 20px;
border-radius: 6px;
box-shadow: var(--shadow);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--text-secondary);
}
.connection-status {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--error-color);
}
.status-indicator.connected {
background-color: var(--success-color);
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: var(--text-secondary);
}
.empty-state h3 {
margin: 0 0 10px 0;
font-size: 18px;
}
.empty-state p {
margin: 0;
font-size: 14px;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.controls {
flex-direction: column;
align-items: stretch;
}
.log-row {
grid-template-columns: 1fr;
gap: 5px;
}
.log-cell.timestamp::before {
content: 'Time: ';
font-weight: 600;
}
.log-cell.level::before {
content: 'Level: ';
font-weight: 600;
}
.log-cell.source::before {
content: 'Source: ';
font-weight: 600;
}
.log-header {
display: none;
}
}
</style>
</head>
<body>
<div class="container">
<header class="header">
<h1>📊 Log Viewer</h1>
<div class="controls">
<div class="search-container">
<select
id="namespaceFilter"
class="search-input"
multiple
size="1"
aria-label="Filter by namespace"
>
<option value="">All Namespaces (loading...)</option>
</select>
</div>
<div class="search-container">
<input
type="text"
id="search"
class="search-input"
placeholder="🔍 Filter logs..."
aria-label="Filter logs"
>
</div>
<div class="checkbox-container">
<input type="checkbox" id="shouldScroll" checked>
<label for="shouldScroll">Auto-scroll</label>
</div>
<button id="reconnectBtn" class="btn btn-primary">
🔄 Reconnect
</button>
</div>
</header>
<div class="log-container">
<div class="log-header">
<div class="log-row">
<div class="log-cell">Timestamp</div>
<div class="log-cell">Level</div>
<div class="log-cell">Namespace</div>
<div class="log-cell">Message</div>
<div class="log-cell">Source</div>
</div>
</div>
<div id="logViewer" class="log-viewer">
<div id="emptyState" class="empty-state">
<h3>No logs yet</h3>
<p>Waiting for log entries...</p>
</div>
</div>
</div>
<div class="actions">
<button id="downloadBtn" class="btn btn-primary">
📥 Download Logs
</button>
<button id="clearBtn" class="btn btn-danger">
🗑️ Clear Logs
</button>
</div>
<div class="status-bar">
<div class="connection-status">
<span class="status-indicator" id="statusIndicator"></span>
<span id="connectionStatus">Connecting...</span>
</div>
<div id="logCount">0 logs</div>
</div>
</div>
<script>
class LogViewer {
constructor() {
this.ws = null;
this.logs = [];
this.filteredLogs = [];
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.initializeElements();
this.attachEventListeners();
this.fetchNamespaces();
this.connectWebSocket();
this.startAutoScroll();
}
initializeElements() {
this.logViewer = document.getElementById('logViewer');
this.emptyState = document.getElementById('emptyState');
this.namespaceFilter = document.getElementById('namespaceFilter');
this.searchInput = document.getElementById('search');
this.scrollCheckbox = document.getElementById('shouldScroll');
this.downloadBtn = document.getElementById('downloadBtn');
this.clearBtn = document.getElementById('clearBtn');
this.reconnectBtn = document.getElementById('reconnectBtn');
this.statusIndicator = document.getElementById('statusIndicator');
this.connectionStatus = document.getElementById('connectionStatus');
this.logCount = document.getElementById('logCount');
}
attachEventListeners() {
this.searchInput.addEventListener('input', this.debounce(() => this.filterLogs(), 300));
this.namespaceFilter.addEventListener('change', () => this.reconnectWithNamespace());
this.downloadBtn.addEventListener('click', () => this.downloadLogs());
this.clearBtn.addEventListener('click', () => this.clearLogs());
this.reconnectBtn.addEventListener('click', () => this.reconnectWithNamespace());
}
async fetchNamespaces() {
try {
const response = await fetch('/api/namespaces');
const data = await response.json();
this.updateNamespaceFilter(data.namespaces || []);
} catch (error) {
console.error('Failed to fetch namespaces:', error);
}
}
updateNamespaceFilter(namespaces) {
// Clear existing options
this.namespaceFilter.innerHTML = '';
// Add "All" option
const allOption = document.createElement('option');
allOption.value = '';
allOption.textContent = 'All Namespaces';
allOption.selected = true;
this.namespaceFilter.appendChild(allOption);
// Add namespace options
namespaces.sort().forEach(ns => {
const option = document.createElement('option');
option.value = ns;
option.textContent = ns;
this.namespaceFilter.appendChild(option);
});
}
connectWebSocket() {
if (this.ws) return;
try {
let wsUrl = "{{.}}";
// Get selected namespace options from multi-select
const selectedOptions = Array.from(this.namespaceFilter.selectedOptions)
.map(opt => opt.value)
.filter(val => val !== ''); // Remove empty "All" option
// Add namespace filter if specific namespaces selected
if (selectedOptions.length > 0) {
const namespaces = selectedOptions.join(',');
const separator = wsUrl.includes('?') ? '&' : '?';
wsUrl += `${separator}namespaces=${encodeURIComponent(namespaces)}`;
}
this.ws = new WebSocket(wsUrl);
this.updateConnectionStatus('Connecting...', false);
this.ws.onopen = () => {
this.isConnected = true;
this.reconnectAttempts = 0;
this.updateConnectionStatus('Connected', true);
};
this.ws.onmessage = (event) => {
try {
const logEntry = JSON.parse(event.data);
this.addLogEntry(logEntry);
} catch (error) {
console.error('Failed to parse log entry:', error);
}
};
this.ws.onclose = () => {
this.isConnected = false;
this.ws = null;
this.updateConnectionStatus('Disconnected', false);
this.scheduleReconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.updateConnectionStatus('Connection Error', false);
};
} catch (error) {
console.error('Failed to create WebSocket:', error);
this.updateConnectionStatus('Connection Failed', false);
this.scheduleReconnect();
}
}
reconnectWithNamespace() {
if (this.ws) {
this.ws.onclose = null; // Prevent auto-reconnect
this.ws.close();
this.ws = null;
}
this.reconnectAttempts = 0;
this.connectWebSocket();
}
scheduleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
this.updateConnectionStatus(`Reconnecting in ${Math.ceil(delay/1000)}s...`, false);
setTimeout(() => {
this.connectWebSocket();
}, delay);
} else {
this.updateConnectionStatus('Connection Failed', false);
}
}
addLogEntry(entry) {
this.logs.push(entry);
this.updateLogCount();
if (this.matchesFilter(entry)) {
this.renderLogEntry(entry);
this.hideEmptyState();
}
}
renderLogEntry(entry) {
const logRow = document.createElement('div');
logRow.className = `log-row log-level-${entry.level.toLowerCase()}`;
logRow.innerHTML = `
<div class="log-cell timestamp">${this.formatTimestamp(entry.timestamp)}</div>
<div class="log-cell level">${entry.level}</div>
<div class="log-cell namespace">${this.escapeHtml(entry.namespace || 'default')}</div>
<div class="log-cell output">${this.escapeHtml(entry.output)}</div>
<div class="log-cell source">${this.escapeHtml(entry.file || 'N/A')}</div>
`;
this.logViewer.appendChild(logRow);
}
formatTimestamp(timestamp) {
try {
const date = new Date(timestamp);
return date.toLocaleString();
} catch {
return timestamp;
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
filterLogs() {
const query = this.searchInput.value.toLowerCase().trim();
// Clear current display
this.clearLogDisplay();
if (!query) {
// Show all logs
this.logs.forEach(log => this.renderLogEntry(log));
} else {
// Show filtered logs
const filtered = this.logs.filter(log =>
this.matchesQuery(log, query)
);
filtered.forEach(log => this.renderLogEntry(log));
}
if (this.logViewer.children.length === 0 ||
(this.logViewer.children.length === 1 && this.logViewer.contains(this.emptyState))) {
this.showEmptyState();
} else {
this.hideEmptyState();
}
}
matchesFilter(entry) {
const query = this.searchInput.value.toLowerCase().trim();
return !query || this.matchesQuery(entry, query);
}
matchesQuery(entry, query) {
return (
entry.output.toLowerCase().includes(query) ||
entry.level.toLowerCase().includes(query) ||
(entry.namespace && entry.namespace.toLowerCase().includes(query)) ||
(entry.file && entry.file.toLowerCase().includes(query)) ||
entry.timestamp.toLowerCase().includes(query)
);
}
clearLogDisplay() {
while (this.logViewer.firstChild && this.logViewer.firstChild !== this.emptyState) {
this.logViewer.removeChild(this.logViewer.firstChild);
}
}
showEmptyState() {
if (!this.logViewer.contains(this.emptyState)) {
this.logViewer.appendChild(this.emptyState);
}
}
hideEmptyState() {
if (this.logViewer.contains(this.emptyState)) {
this.logViewer.removeChild(this.emptyState);
}
}
clearLogs() {
if (!confirm('Are you sure you want to clear all logs?')) return;
this.logs = [];
this.clearLogDisplay();
this.showEmptyState();
this.updateLogCount();
}
downloadLogs() {
if (this.logs.length === 0) {
alert('No logs to download');
return;
}
const dataStr = JSON.stringify(this.logs, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(dataBlob);
link.download = `logs-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
}
updateConnectionStatus(status, connected) {
this.connectionStatus.textContent = status;
this.statusIndicator.classList.toggle('connected', connected);
}
updateLogCount() {
const count = this.logs.length;
this.logCount.textContent = `${count} log${count !== 1 ? 's' : ''}`;
}
startAutoScroll() {
const autoScroll = () => {
if (this.scrollCheckbox.checked && this.isConnected) {
this.logViewer.scrollTop = this.logViewer.scrollHeight;
}
requestAnimationFrame(autoScroll);
};
autoScroll();
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
}
// Initialize the log viewer when the page loads
document.addEventListener('DOMContentLoaded', () => {
new LogViewer();
});
</script>
</body>
</html>

6
go.mod
View File

@@ -1,5 +1,5 @@
module github.com/taigrr/log-socket
module github.com/taigrr/log-socket/v2
go 1.16
go 1.25.4
require github.com/gorilla/websocket v1.4.2
require github.com/gorilla/websocket v1.5.3

4
go.sum
View File

@@ -1,2 +1,2 @@
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

551
log/log.go Normal file
View File

@@ -0,0 +1,551 @@
package log
import (
"errors"
"fmt"
"os"
"runtime"
"strings"
"sync"
"time"
)
var (
clients []*Client
sliceTex sync.Mutex
stderrClient *Client
cleanup sync.Once
stderrFinished chan bool
namespaces map[string]bool
namespacesMux sync.RWMutex
)
func init() {
namespaces = make(map[string]bool)
stderrClient = CreateClient(DefaultNamespace)
stderrClient.SetLogLevel(LTrace)
stderrFinished = make(chan bool, 1)
go stderrClient.logStdErr()
}
func (c *Client) logStdErr() {
for e := range c.writer {
if e.level >= c.LogLevel && c.matchesNamespace(e.Namespace) {
fmt.Fprintf(os.Stderr, "%s\t%s\t[%s]\t%s\t%s\n", e.Timestamp.String(), e.Level, e.Namespace, e.Output, e.File)
}
}
stderrFinished <- true
}
func (c *Client) matchesNamespace(namespace string) bool {
// Empty Namespaces slice means match all
if len(c.Namespaces) == 0 {
return true
}
for _, ns := range c.Namespaces {
if ns == namespace {
return true
}
}
return false
}
func CreateClient(namespaces ...string) *Client {
var client Client
client.initialized = true
client.Namespaces = namespaces
client.writer = make(LogWriter, 1000)
sliceTex.Lock()
clients = append(clients, &client)
sliceTex.Unlock()
return &client
}
func Flush() {
cleanup.Do(func() {
close(stderrClient.writer)
<-stderrFinished
stderrClient.Destroy()
})
}
func (c *Client) Destroy() error {
var otherClients []*Client
if !c.initialized {
panic(errors.New("cannot delete uninitialized client, did you use CreateClient?"))
}
sliceTex.Lock()
c.writer = nil
c.initialized = false
for _, x := range clients {
if x.initialized {
otherClients = append(otherClients, x)
}
}
clients = otherClients
sliceTex.Unlock()
return nil
}
func (c *Client) GetLogLevel() Level {
if !c.initialized {
panic(errors.New("cannot get level for uninitialized client, use CreateClient instead"))
}
return c.LogLevel
}
func createLog(e Entry) {
// Track namespace
namespacesMux.Lock()
namespaces[e.Namespace] = true
namespacesMux.Unlock()
sliceTex.Lock()
for _, c := range clients {
func(c *Client, e Entry) {
// Filter by namespace if client has filters specified
if !c.matchesNamespace(e.Namespace) {
return
}
select {
case c.writer <- e:
// try to clear out one of the older entries
default:
select {
case <-c.writer:
c.writer <- e
default:
}
}
}(c, e)
}
sliceTex.Unlock()
}
// GetNamespaces returns a list of all namespaces that have been used
func GetNamespaces() []string {
namespacesMux.RLock()
defer namespacesMux.RUnlock()
result := make([]string, 0, len(namespaces))
for ns := range namespaces {
result = append(result, ns)
}
return result
}
func SetLogLevel(level Level) {
stderrClient.LogLevel = level
}
// SetLogLevel set log level of logger
func (c *Client) SetLogLevel(level Level) {
if !c.initialized {
panic(errors.New("cannot set level for uninitialized client, use CreateClient instead"))
}
c.LogLevel = level
}
func (c *Client) Get() Entry {
if !c.initialized {
panic(errors.New("cannot get logs for uninitialized client, did you use CreateClient?"))
}
return <-c.writer
}
// Trace prints out logs on trace level
func Trace(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "TRACE",
Namespace: DefaultNamespace,
level: LTrace,
}
createLog(e)
}
// Formatted print for Trace
func Tracef(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "TRACE",
level: LTrace,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Trace prints out logs on trace level with newline
func Traceln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "TRACE",
level: LTrace,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Debug prints out logs on debug level
func Debug(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "DEBUG",
level: LDebug,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Formatted print for Debug
func Debugf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "DEBUG",
level: LDebug,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Debug prints out logs on debug level with a newline
func Debugln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "DEBUG",
level: LDebug,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Info prints out logs on info level
func Info(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "INFO",
level: LInfo,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Formatted print for Info
func Infof(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "INFO",
level: LInfo,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Info prints out logs on info level with a newline
func Infoln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "INFO",
level: LInfo,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Info prints out logs on info level
func Notice(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "NOTICE",
level: LNotice,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Formatted print for Info
func Noticef(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "NOTICE",
level: LNotice,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Info prints out logs on info level with a newline
func Noticeln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "NOTICE",
level: LNotice,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Warn prints out logs on warn level
func Warn(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "WARN",
level: LWarn,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Formatted print for Warn
func Warnf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "WARN",
level: LWarn,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Newline print for Warn
func Warnln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "WARN",
level: LWarn,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Error prints out logs on error level
func Error(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "ERROR",
level: LError,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Formatted print for error
func Errorf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "ERROR",
level: LError,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Error prints out logs on error level with a newline
func Errorln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "ERROR",
level: LError,
Namespace: DefaultNamespace,
}
createLog(e)
}
// Panic prints out logs on panic level
func Panic(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "PANIC",
level: LPanic,
Namespace: DefaultNamespace,
}
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))
}
// Formatted print for panic
func Panicf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "PANIC",
level: LPanic,
Namespace: DefaultNamespace,
}
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))
}
func Panicln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "PANIC",
level: LPanic,
Namespace: DefaultNamespace,
}
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
func Fatal(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "FATAL",
level: LFatal,
Namespace: DefaultNamespace,
}
createLog(e)
Flush()
os.Exit(1)
}
// Formatted print for fatal
func Fatalf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "FATAL",
level: LFatal,
Namespace: DefaultNamespace,
}
createLog(e)
Flush()
os.Exit(1)
}
func Fatalln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "FATAL",
level: LFatal,
Namespace: DefaultNamespace,
}
createLog(e)
Flush()
os.Exit(1)
}
func Print(args ...any) {
Info(args...)
}
func Printf(format string, args ...any) {
Infof(format, args...)
}
func Println(args ...any) {
Infoln(args...)
}
// fileInfo for getting which line in which file
func fileInfo(skip int) string {
_, file, line, ok := runtime.Caller(skip)
if !ok {
file = "<???>"
line = 1
} else {
slash := strings.LastIndex(file, "/")
if slash >= 0 {
file = file[slash+1:]
}
}
return fmt.Sprintf("%s:%d", file, line)
}

View File

@@ -1,4 +1,4 @@
package logger
package log
import (
"strconv"
@@ -13,7 +13,7 @@ func TestCreateDestroy(t *testing.T) {
t.Errorf("Expected 1 client, but found %d", len(clients))
}
// Create a new client, ensure it's added
c := CreateClient()
c := CreateClient("test")
if len(clients) != 2 {
t.Errorf("Expected 2 clients, but found %d", len(clients))
}
@@ -27,7 +27,7 @@ func TestCreateDestroy(t *testing.T) {
// SetLogLevel set log level of logger
func TestSetLogLevel(t *testing.T) {
logLevels := [...]Level{LTrace, LDebug, LInfo, LWarn, LError, LPanic, LFatal}
c := CreateClient()
c := CreateClient("test")
for _, x := range logLevels {
c.SetLogLevel(x)
if c.GetLogLevel() != x {
@@ -38,7 +38,7 @@ func TestSetLogLevel(t *testing.T) {
}
func BenchmarkDebugSerial(b *testing.B) {
c := CreateClient()
c := CreateClient("test")
var x sync.WaitGroup
x.Add(b.N)
for i := 0; i < b.N; i++ {
@@ -55,8 +55,7 @@ func BenchmarkDebugSerial(b *testing.B) {
// Trace ensure logs come out in the right order
func TestOrder(t *testing.T) {
testString := "Testing trace: "
var c *Client
c = CreateClient()
c := CreateClient(DefaultNamespace)
c.SetLogLevel(LTrace)
for i := 0; i < 5000; i++ {
@@ -86,6 +85,15 @@ func TestInfo(t *testing.T) {
// }
}
// Print prints out logs on info level
func TestPrint(t *testing.T) {
// if logLevel >= LInfo {
// entry := logger.WithFields(logrus.Fields{})
// entry.Data["file"] = fileInfo(2)
// entry.Info(args...)
// }
}
// Warn prints out logs on warn level
func TestWarn(t *testing.T) {
// if logLevel >= LWarn {
@@ -121,6 +129,7 @@ func TestPanic(t *testing.T) {
// entry.Panic(args...)
// }
}
func TestFlush(t *testing.T) {
defer Flush()
}

396
log/logger.go Normal file
View File

@@ -0,0 +1,396 @@
package log
import (
"errors"
"fmt"
"os"
"time"
)
func Default() *Logger {
return &Logger{FileInfoDepth: 0, Namespace: DefaultNamespace}
}
func NewLogger(namespace string) *Logger {
if namespace == "" {
namespace = DefaultNamespace
}
return &Logger{FileInfoDepth: 0, Namespace: namespace}
}
func (l *Logger) SetInfoDepth(depth int) {
l.FileInfoDepth = depth
}
// Trace prints out logs on trace level
func (l Logger) Trace(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "TRACE",
level: LTrace,
Namespace: l.Namespace,
}
createLog(e)
}
// Formatted print for Trace
func (l Logger) Tracef(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "TRACE",
level: LTrace,
Namespace: l.Namespace,
}
createLog(e)
}
// Trace prints out logs on trace level with newline
func (l Logger) Traceln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "TRACE",
level: LTrace,
Namespace: l.Namespace,
}
createLog(e)
}
// Debug prints out logs on debug level
func (l Logger) Debug(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "DEBUG",
level: LDebug,
Namespace: l.Namespace,
}
createLog(e)
}
// Formatted print for Debug
func (l Logger) Debugf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "DEBUG",
level: LDebug,
Namespace: l.Namespace,
}
createLog(e)
}
// Info prints out logs on info level
func (l Logger) Info(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "INFO",
level: LInfo,
Namespace: l.Namespace,
}
createLog(e)
}
// Formatted print for Info
func (l Logger) Infof(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "INFO",
level: LInfo,
Namespace: l.Namespace,
}
createLog(e)
}
// Info prints out logs on info level with newline
func (l Logger) Infoln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "INFO",
level: LInfo,
Namespace: l.Namespace,
}
createLog(e)
}
// Notice prints out logs on notice level
func (l Logger) Notice(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "NOTICE",
level: LNotice,
Namespace: l.Namespace,
}
createLog(e)
}
// Formatted print for Notice
func (l Logger) Noticef(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "NOTICE",
level: LNotice,
Namespace: l.Namespace,
}
createLog(e)
}
// Notice prints out logs on notice level with newline
func (l Logger) Noticeln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "NOTICE",
level: LNotice,
Namespace: l.Namespace,
}
createLog(e)
}
// Warn prints out logs on warn level
func (l Logger) Warn(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "WARN",
level: LWarn,
Namespace: l.Namespace,
}
createLog(e)
}
// Formatted print for Warn
func (l Logger) Warnf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "WARN",
level: LWarn,
Namespace: l.Namespace,
}
createLog(e)
}
// Warn prints out logs on warn level with a newline
func (l Logger) Warnln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "WARN",
level: LWarn,
Namespace: l.Namespace,
}
createLog(e)
}
// Error prints out logs on error level
func (l Logger) Error(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "ERROR",
level: LError,
Namespace: l.Namespace,
}
createLog(e)
}
// Formatted print for error
func (l Logger) Errorf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "ERROR",
level: LError,
Namespace: l.Namespace,
}
createLog(e)
}
// Error prints out logs on error level with a new line
func (l Logger) Errorln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "ERROR",
level: LError,
Namespace: l.Namespace,
}
createLog(e)
}
// Panic prints out logs on panic level
func (l Logger) Panic(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "PANIC",
level: LPanic,
Namespace: l.Namespace,
}
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))
}
// Formatted print for panic
func (l Logger) Panicf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "PANIC",
level: LPanic,
Namespace: l.Namespace,
}
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))
}
// Panic prints out logs on panic level with a newline
func (l Logger) Panicln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "PANIC",
level: LPanic,
Namespace: l.Namespace,
}
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
func (l Logger) Fatal(args ...any) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "FATAL",
level: LFatal,
Namespace: l.Namespace,
}
createLog(e)
Flush()
os.Exit(1)
}
// Formatted print for fatal
func (l Logger) Fatalf(format string, args ...any) {
output := fmt.Sprintf(format, args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "FATAL",
level: LFatal,
Namespace: l.Namespace,
}
createLog(e)
Flush()
os.Exit(1)
}
// Fatal prints fatal level with a new line
func (l Logger) Fatalln(args ...any) {
output := fmt.Sprintln(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(l.FileInfoDepth),
Level: "FATAL",
level: LFatal,
Namespace: l.Namespace,
}
createLog(e)
Flush()
os.Exit(1)
}
// Handles print to info
func (l Logger) Print(args ...any) {
l.Info(args...)
}
// Handles formatted print to info
func (l Logger) Printf(format string, args ...any) {
l.Infof(format, args...)
}
// Handles print to info with new line
func (l Logger) Println(args ...any) {
l.Infoln(args...)
}

40
log/types.go Normal file
View File

@@ -0,0 +1,40 @@
package log
import "time"
const (
LTrace Level = iota
LDebug
LInfo
LNotice
LWarn
LError
LPanic
LFatal
)
const DefaultNamespace = "default"
type (
LogWriter chan Entry
Level int
Client struct {
LogLevel Level `json:"level"`
Namespaces []string `json:"namespaces"` // Empty slice means all namespaces
writer LogWriter
initialized bool
}
Entry struct {
Timestamp time.Time `json:"timestamp"`
Output string `json:"output"`
File string `json:"file"`
Level string `json:"level"`
Namespace string `json:"namespace"`
level Level
}
Logger struct {
FileInfoDepth int
Namespace string
}
)

View File

@@ -1,254 +0,0 @@
package logger
import (
"errors"
"fmt"
"os"
"runtime"
"strings"
"sync"
"time"
)
const (
LTrace Level = iota
LDebug
LInfo
LWarn
LError
LPanic
LFatal
)
var (
clients []*Client
sliceTex sync.Mutex
stderrClient *Client
cleanup sync.Once
stderrFinished chan bool
)
func init() {
stderrClient = CreateClient()
stderrClient.SetLogLevel(LTrace)
stderrFinished = make(chan bool, 1)
go stderrClient.logStdErr()
}
func (c *Client) logStdErr() {
for {
select {
case e, more := <-c.writer:
if e.level >= c.LogLevel {
fmt.Fprintf(os.Stderr, "%s\t%s\t%s\t%s\n", e.Timestamp.String(), e.Level, e.Output, e.File)
}
if !more {
stderrFinished <- true
return
}
}
}
}
func CreateClient() *Client {
var client Client
client.initialized = true
client.writer = make(LogWriter, 1000)
sliceTex.Lock()
clients = append(clients, &client)
sliceTex.Unlock()
return &client
}
func Flush() {
cleanup.Do(func() {
close(stderrClient.writer)
<-stderrFinished
stderrClient.Destroy()
})
}
func (c *Client) Destroy() error {
var otherClients []*Client
if !c.initialized {
panic(errors.New("Cannot delete uninitialized client, did you use CreateClient?"))
}
sliceTex.Lock()
c.writer = nil
c.initialized = false
for _, x := range clients {
if x.initialized {
otherClients = append(otherClients, x)
}
}
clients = otherClients
sliceTex.Unlock()
return nil
}
func (c *Client) GetLogLevel() Level {
if !c.initialized {
panic(errors.New("Cannot get level for uninitialized client, use CreateClient instead"))
}
return c.LogLevel
}
func createLog(e Entry) {
sliceTex.Lock()
for _, c := range clients {
func(c *Client, e Entry) {
select {
case c.writer <- e:
// try to clear out one of the older entries
default:
select {
case <-c.writer:
c.writer <- e
default:
}
}
}(c, e)
}
sliceTex.Unlock()
}
func SetLogLevel(level Level) {
stderrClient.LogLevel = level
}
// SetLogLevel set log level of logger
func (c *Client) SetLogLevel(level Level) {
if !c.initialized {
panic(errors.New("Cannot set level for uninitialized client, use CreateClient instead"))
}
c.LogLevel = level
}
func (c *Client) Get() Entry {
if !c.initialized {
panic(errors.New("Cannot get logs for uninitialized client, did you use CreateClient?"))
}
return <-c.writer
}
// Trace prints out logs on trace level
func Trace(args ...interface{}) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "TRACE",
level: LTrace,
}
createLog(e)
// entry := logger.WithFields(logrus.Fields{})
// entry.Data["file"] = fileInfo(2)
// entry.Debug(args...)
}
// Debug prints out logs on debug level
func Debug(args ...interface{}) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "DEBUG",
level: LDebug,
}
createLog(e)
}
// Info prints out logs on info level
func Info(args ...interface{}) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "INFO",
level: LInfo,
}
createLog(e)
}
// Warn prints out logs on warn level
func Warn(args ...interface{}) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "WARN",
level: LWarn,
}
createLog(e)
}
// Error prints out logs on error level
func Error(args ...interface{}) {
output := fmt.Sprint(args...)
e := Entry{
Timestamp: time.Now(),
Output: output,
File: fileInfo(2),
Level: "ERROR",
level: LError,
}
createLog(e)
}
// Panic prints out logs on panic level
func Panic(args ...interface{}) {
output := fmt.Sprint(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
func Fatal(args ...interface{}) {
output := fmt.Sprint(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
func fileInfo(skip int) string {
_, file, line, ok := runtime.Caller(skip)
if !ok {
file = "<???>"
line = 1
} else {
slash := strings.LastIndex(file, "/")
if slash >= 0 {
file = file[slash+1:]
}
}
return fmt.Sprintf("%s:%d", file, line)
}

View File

@@ -1,20 +0,0 @@
package logger
import "time"
type LogWriter chan Entry
type Level int
type Client struct {
LogLevel Level `json:"level"`
writer LogWriter
initialized bool
}
type Entry struct {
Timestamp time.Time `json:"timestamp"`
Output string `json:"output"`
File string `json:"file"`
Level string `json:"level"`
level Level
}

232
main.go
View File

@@ -2,226 +2,46 @@ package main
import (
"flag"
"html/template"
"log"
"net/http"
"time"
"github.com/taigrr/log-socket/logger"
"github.com/taigrr/log-socket/ws"
"github.com/taigrr/log-socket/v2/browser"
logger "github.com/taigrr/log-socket/v2/log"
"github.com/taigrr/log-socket/v2/ws"
)
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() {
// Create loggers for different namespaces
apiLogger := logger.NewLogger("api")
dbLogger := logger.NewLogger("database")
authLogger := logger.NewLogger("auth")
for {
logger.Info("This is an info log!")
logger.Trace("This is a trace log!")
logger.Debug("This is a debug log!")
logger.Warn("This is a warn log!")
logger.Error("This is an error log!")
time.Sleep(10 * time.Second)
logger.Info("This is a default namespace log!")
apiLogger.Info("API request received")
apiLogger.Debug("Processing API call")
dbLogger.Info("Database query executed")
dbLogger.Warn("Slow query detected")
authLogger.Info("User authentication successful")
authLogger.Error("Failed login attempt detected")
logger.Trace("This is a trace log in default namespace!")
logger.Warn("This is a warning in default namespace!")
time.Sleep(2 * time.Second)
}
}
func main() {
defer logger.Flush()
flag.Parse()
http.HandleFunc("/logs", ws.LogSocketHandler)
http.HandleFunc("/", home)
http.HandleFunc("/ws", ws.LogSocketHandler)
http.HandleFunc("/api/namespaces", ws.NamespacesHandler)
http.HandleFunc("/", browser.LogSocketViewHandler)
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>
`))

17
ws/namespaces.go Normal file
View File

@@ -0,0 +1,17 @@
package ws
import (
"encoding/json"
"net/http"
logger "github.com/taigrr/log-socket/v2/log"
)
// NamespacesHandler returns a JSON list of all namespaces that have been used
func NamespacesHandler(w http.ResponseWriter, r *http.Request) {
namespaces := logger.GetNamespaces()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"namespaces": namespaces,
})
}

View File

@@ -3,32 +3,43 @@ package ws
import (
"encoding/json"
"net/http"
"strings"
"github.com/gorilla/websocket"
"github.com/taigrr/log-socket/logger"
logger "github.com/taigrr/log-socket/v2/log"
)
// var addr = flag.String("addr", "localhost:8080", "http service address")
var upgrader = websocket.Upgrader{} // use default options
func SetUpgrader(u websocket.Upgrader) {
upgrader = u
}
func LogSocketHandler(w http.ResponseWriter, r *http.Request) {
// Get namespaces from query parameter, comma-separated
// Empty or missing means all namespaces
namespacesParam := r.URL.Query().Get("namespaces")
var namespaces []string
if namespacesParam != "" {
namespaces = strings.Split(namespacesParam, ",")
}
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
logger.Error("upgrade:", err)
return
}
defer c.Close()
lc := logger.CreateClient()
lc := logger.CreateClient(namespaces...)
defer lc.Destroy()
lc.SetLogLevel(logger.LTrace)
logger.Info("Websocket client attached.")
for {
logEvent := lc.Get()
logJSON, err := json.Marshal(logEvent)
logJSON, _ := json.Marshal(logEvent)
err = c.WriteMessage(websocket.TextMessage, logJSON)
if err != nil {
logger.Error("write:", err)
logger.Warn("write:", err)
break
}
}