Send logs to glog from a Go application.
go install github.com/dotcommander/glog/cmd/glog@latestglog host register --name "my-backend-service" --tag production --tag apiThis creates a glog.json config file with your API key.
# Send a single log
glog log "User registration successful" --level info --field user_id=123
# Stream logs from your application
./my-app 2>&1 | glog log --level errorSimplest option. Calls glog as a subprocess:
package main
import (
"fmt"
"os/exec"
)
func logToGLog(level, message string, fields map[string]interface{}) error {
args := []string{"log", message, "--level", level}
for k, v := range fields {
args = append(args, "--field", fmt.Sprintf("%s=%v", k, v))
}
cmd := exec.Command("glog", args...)
return cmd.Run()
}
// Usage
logToGLog("info", "Payment processed", map[string]interface{}{
"amount": 99.99,
"user_id": 456,
"payment_method": "credit_card",
})Direct HTTP API calls — better performance than subprocess:
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
type GLogClient struct {
serverURL string
apiKey string
client *http.Client
}
type LogEntry struct {
Level string `json:"level"`
Message string `json:"message"`
Fields map[string]interface{} `json:"fields,omitempty"`
Method string `json:"method,omitempty"`
Path string `json:"path,omitempty"`
Status int `json:"status_code,omitempty"`
Duration int `json:"duration_ms,omitempty"`
}
func NewGLogClient(serverURL, apiKey string) *GLogClient {
return &GLogClient{
serverURL: serverURL,
apiKey: apiKey,
client: &http.Client{
Timeout: 5 * time.Second,
},
}
}
func (c *GLogClient) SendLog(entry LogEntry) error {
jsonData, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("marshaling log: %w", err)
}
req, err := http.NewRequest("POST", c.serverURL+"/api/v1/logs", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("sending request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
return nil
}
// Usage
func main() {
client := NewGLogClient("http://localhost:6016", "your-api-key")
// Simple log
client.SendLog(LogEntry{
Level: "info",
Message: "Application started",
Fields: map[string]interface{}{
"version": "1.0.0",
"pid": 12345,
},
})
// HTTP request log
client.SendLog(LogEntry{
Level: "info",
Message: "GET /api/users",
Method: "GET",
Path: "/api/users",
Status: 200,
Duration: 45,
Fields: map[string]interface{}{
"user_count": 150,
"cache_hit": true,
},
})
}A thin wrapper that matches standard Go logging patterns:
package main
import (
"fmt"
"os"
)
// Logger provides a structured logging interface
type Logger struct {
client *GLogClient
host string
source string
}
func NewLogger(client *GLogClient, host, source string) *Logger {
return &Logger{
client: client,
host: host,
source: source,
}
}
// log is the internal logging method
func (l *Logger) log(level, message string, fields map[string]interface{}) {
if fields == nil {
fields = make(map[string]interface{})
}
// Add standard fields
fields["host"] = l.host
if l.source != "" {
fields["source"] = l.source
}
entry := LogEntry{
Level: level,
Message: message,
Fields: fields,
}
// Don't block on logging errors
go func() {
if err := l.client.SendLog(entry); err != nil {
fmt.Fprintf(os.Stderr, "Failed to send log: %v\n", err)
}
}()
}
// Convenience methods
func (l *Logger) Debug(msg string, fields map[string]interface{}) {
l.log("debug", msg, fields)
}
func (l *Logger) Info(msg string, fields map[string]interface{}) {
l.log("info", msg, fields)
}
func (l *Logger) Warn(msg string, fields map[string]interface{}) {
l.log("warn", msg, fields)
}
func (l *Logger) Error(msg string, fields map[string]interface{}) {
l.log("error", msg, fields)
}
func (l *Logger) Fatal(msg string, fields map[string]interface{}) {
l.log("fatal", msg, fields)
os.Exit(1)
}Logs all incoming requests with method, path, status, and duration:
package main
import (
"net/http"
"time"
)
// LoggingMiddleware logs all HTTP requests
type LoggingMiddleware struct {
logger *Logger
next http.Handler
}
func (m *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap response writer to capture status code
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
m.next.ServeHTTP(wrapped, r)
duration := time.Since(start).Milliseconds()
// Determine log level based on status code
level := "info"
if wrapped.statusCode >= 400 && wrapped.statusCode < 500 {
level = "warn"
} else if wrapped.statusCode >= 500 {
level = "error"
}
// Create log entry matching GLog's HTTP context format
logEntry := LogEntry{
Level: level,
Message: fmt.Sprintf("%s %s", r.Method, r.URL.Path),
Method: r.Method,
Path: r.URL.Path,
Status: wrapped.statusCode,
Duration: int(duration),
Fields: map[string]interface{}{
"remote_addr": r.RemoteAddr,
"user_agent": r.UserAgent(),
},
}
m.logger.log(logEntry.Level, logEntry.Message, logEntry.Fields)
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// Usage
func main() {
client := NewGLogClient("http://localhost:6016", "your-api-key")
logger := NewLogger(client, "api-server-1", "http-server")
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
loggedMux := &LoggingMiddleware{logger: logger, next: mux}
http.ListenAndServe(":8080", loggedMux)
}Batch logs and flush on a timer or size threshold:
package main
import (
"sync"
"time"
)
// BulkLogger batches logs for efficient sending
type BulkLogger struct {
client *GLogClient
logs []LogEntry
mux sync.Mutex
maxBatch int
ticker *time.Ticker
done chan bool
}
func NewBulkLogger(client *GLogClient, maxBatch int, flushInterval time.Duration) *BulkLogger {
bl := &BulkLogger{
client: client,
maxBatch: maxBatch,
ticker: time.NewTicker(flushInterval),
done: make(chan bool),
}
go bl.flushWorker()
return bl
}
func (bl *BulkLogger) Log(entry LogEntry) {
bl.mux.Lock()
bl.logs = append(bl.logs, entry)
shouldFlush := len(bl.logs) >= bl.maxBatch
bl.mux.Unlock()
if shouldFlush {
bl.Flush()
}
}
func (bl *BulkLogger) Flush() {
bl.mux.Lock()
if len(bl.logs) == 0 {
bl.mux.Unlock()
return
}
logsToSend := bl.logs
bl.logs = make([]LogEntry, 0, bl.maxBatch)
bl.mux.Unlock()
go func() {
if err := bl.sendBulk(logsToSend); err != nil {
fmt.Fprintf(os.Stderr, "Failed to send bulk logs: %v\n", err)
}
}()
}
func (bl *BulkLogger) sendBulk(logs []LogEntry) error {
jsonData, err := json.Marshal(map[string]interface{}{
"logs": logs,
})
if err != nil {
return err
}
req, err := http.NewRequest("POST", bl.client.serverURL+"/api/v1/logs/bulk", bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+bl.client.apiKey)
resp, err := bl.client.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
return nil
}
func (bl *BulkLogger) flushWorker() {
for {
select {
case <-bl.ticker.C:
bl.Flush()
case <-bl.done:
bl.Flush()
return
}
}
}
func (bl *BulkLogger) Stop() {
bl.ticker.Stop()
close(bl.done)
}
// Usage
func main() {
client := NewGLogClient("http://localhost:6016", "your-api-key")
bulkLogger := NewBulkLogger(client, 100, 5*time.Second)
defer bulkLogger.Stop()
// Log as normal
bulkLogger.Log(LogEntry{
Level: "info",
Message: "Application event",
Fields: map[string]interface{}{"user_id": 123},
})
}Load from environment or config file:
package main
import (
"encoding/json"
"os"
)
type Config struct {
ServerURL string `json:"server"`
APIKey string `json:"api_key"`
HostID int `json:"host_id"`
HostName string `json:"host_name"`
}
func LoadConfig() (*Config, error) {
// Priority: env vars > config file > defaults
config := &Config{
ServerURL: getEnv("GLOG_SERVER", "http://localhost:6016"),
APIKey: os.Getenv("GLOG_API_KEY"),
}
// Try loading from glog.json if API key not in env
if config.APIKey == "" {
data, err := os.ReadFile("glog.json")
if err == nil {
if err := json.Unmarshal(data, config); err != nil {
return nil, fmt.Errorf("parsing glog.json: %w", err)
}
}
}
if config.APIKey == "" {
return nil, fmt.Errorf("API key not found in GLOG_API_KEY env or glog.json")
}
return config, nil
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}Wrap the client with retry logic:
package main
import (
"time"
)
// RetryableClient wraps GLogClient with retry logic
type RetryableClient struct {
*GLogClient
maxRetries int
retryDelay time.Duration
}
func NewRetryableClient(client *GLogClient, maxRetries int, retryDelay time.Duration) *RetryableClient {
return &RetryableClient{
GLogClient: client,
maxRetries: maxRetries,
retryDelay: retryDelay,
}
}
func (c *RetryableClient) SendLogWithRetry(entry LogEntry) error {
var lastErr error
for i := 0; i < c.maxRetries; i++ {
if err := c.SendLog(entry); err != nil {
lastErr = err
time.Sleep(c.retryDelay * time.Duration(i+1)) // Exponential backoff
continue
}
return nil
}
return fmt.Errorf("failed after %d retries: %w", c.maxRetries, lastErr)
}package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestGLogIntegration(t *testing.T) {
// Create test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer test-key" {
t.Errorf("Expected Authorization header")
}
w.WriteHeader(http.StatusCreated)
}))
defer server.Close()
// Test client
client := NewGLogClient(server.URL, "test-key")
err := client.SendLog(LogEntry{
Level: "info",
Message: "Test log",
Fields: map[string]interface{}{"test": true},
})
if err != nil {
t.Errorf("SendLog failed: %v", err)
}
}- Use structured fields over embedding data in the message string.
- Use bulk logging (
BulkLogger) when sending >10 logs/second. - Log in a goroutine so logging failures never block the caller.
- Keep timeouts short (5s) so a slow glog server does not stall your app.
- Load config from environment variables (
GLOG_SERVER,GLOG_API_KEY) so the same binary works in dev and prod without changes.
- Always use structured logging: Include contextual fields
- Choose appropriate log levels: Debug for development, Info for production
- Batch high-volume logs: Use BulkLogger for performance
- Handle logging errors gracefully: Don't let logging break your app
- Include request IDs: For request tracing across services
- Set timeouts: Prevent logging from blocking your application
- Use environment-specific config: Different settings for dev/staging/prod
- Log security events: Authentication, authorization failures
- Include performance metrics: Duration, memory usage, queue sizes
- Follow 12-factor app principles: Config in environment, not code
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// Load configuration
config, err := LoadConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
os.Exit(1)
}
// Create GLog client
client := NewGLogClient(config.ServerURL, config.APIKey)
logger := NewLogger(client, config.HostName, "order-service")
// Create HTTP server with logging middleware
mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler)
mux.HandleFunc("/api/orders", orderHandler)
loggedMux := &LoggingMiddleware{logger: logger, next: mux}
server := &http.Server{
Addr: ":8080",
Handler: loggedMux,
}
// Graceful shutdown
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
logger.Info("Shutting down server", nil)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.Error("Server shutdown failed", map[string]interface{}{"error": err})
}
}()
logger.Info("Server starting", map[string]interface{}{"port": 8080})
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal("Server failed", map[string]interface{}{"error": err})
}
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func orderHandler(w http.ResponseWriter, r *http.Request) {
// Simulate order processing
time.Sleep(100 * time.Millisecond)
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"order_id": "12345"}`))
}-
"API key invalid"
- Verify API key in config or environment
- Check server URL is correct
-
"Connection refused"
- Ensure GLog server is running
- Verify network connectivity
-
Logs not appearing in dashboard
- Check log level filters
- Verify host is registered
- Look for client-side errors
-
High memory usage
- Reduce batch size
- Decrease flush interval
- Check for log spam
Enable debug logging for your client:
func (c *GLogClient) SendLog(entry LogEntry) error {
fmt.Printf("Sending log: %+v\n", entry) // Debug output
// ... existing code ...
}