Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,7 @@ _testmain.go
/test/garbage/*.out
/test/pass.out
/test/run.out
/test/times.out
/test/times.out

# ignore the vendor directory
vendor
8 changes: 7 additions & 1 deletion internal/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/doganarif/govisual/internal/model"
"github.com/doganarif/govisual/internal/store"
"github.com/doganarif/govisual/internal/utils"
)

// PathMatcher defines an interface for checking if a path should be ignored
Expand Down Expand Up @@ -39,7 +40,7 @@ func (w *responseWriter) Write(b []byte) (int, error) {
}

// Wrap wraps an http.Handler with the request visualization middleware
func Wrap(handler http.Handler, store store.Store, logRequestBody, logResponseBody bool, pathMatcher PathMatcher) http.Handler {
func Wrap(handler http.Handler, store store.Store, logRequestBody, logResponseBody bool, logToConsole bool, pathMatcher PathMatcher) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if the path should be ignored
if pathMatcher != nil && pathMatcher.ShouldIgnorePath(r.URL.Path) {
Expand Down Expand Up @@ -115,6 +116,11 @@ func Wrap(handler http.Handler, store store.Store, logRequestBody, logResponseBo
reqLog.ResponseBody = resWriter.buffer.String()
}

// Log to console if enabled
if logToConsole {
utils.LogRequest(reqLog)
}

// Store the request log
store.Add(reqLog)
})
Expand Down
109 changes: 109 additions & 0 deletions internal/utils/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package utils

import (
"fmt"
"net/http"
"time"

"github.com/doganarif/govisual/internal/model"
)

const (
Comment thread
erancihan marked this conversation as resolved.
Outdated
green = "\033[32m"
white = "\033[37m"
red = "\033[31m"
blue = "\033[34m"
yellow = "\033[33m"
gray = "\033[90m"
black = "\033[30m"
magenta = "\033[35m"
cyan = "\033[36m"
reset = "\033[0m"
)

func colorizeMethod(method string) string {
if method == "" {
return ""
}

var color string
switch method {
case http.MethodGet:
color = blue
case http.MethodPost:
color = green
case http.MethodPut:
color = yellow
case http.MethodDelete:
color = red
case http.MethodPatch:
color = magenta
case http.MethodHead:
color = gray
case http.MethodOptions:
color = cyan
case http.MethodTrace:
color = white
default:
color = black
}

return fmt.Sprintf("[%s%-7s%s]", color, method, reset)
}

func colorizeStatus(status int) string {
if status < 100 || status > 599 {
return fmt.Sprintf("[%s%3d%s]", red, status, reset)
}

var color string
switch {
case status >= http.StatusContinue && status < http.StatusOK:
color = gray
case status >= http.StatusOK && status < http.StatusMultipleChoices:
color = green
case status >= http.StatusMultipleChoices && status < http.StatusBadRequest:
color = white
case status >= http.StatusBadRequest && status < http.StatusInternalServerError:
color = yellow
default:
color = red
}

return fmt.Sprintf("[%s%3d%s]", color, status, reset)
}

func colorizeDuration(duration time.Duration) string {
if duration < 0 {
return fmt.Sprintf("%s%13v%s", red, duration, reset)
}

var color string
switch {
case duration < 500*time.Millisecond:
color = green
case duration < 1*time.Second:
color = yellow
default:
color = red
}

return fmt.Sprintf("%s%13v%s", color, duration, reset)
}

func LogRequest(reqLog *model.RequestLog) {
// This function logs the request details based on the configuration
if reqLog == nil {
fmt.Println("Warning: Attempted to log nil request log, ignoring")
return
}

fmt.Printf(
"[VIS] %v %s%s %s %#v\n",
reqLog.Timestamp.Format("2006-01-02 15:04:05"),
colorizeMethod(reqLog.Method),
colorizeStatus(reqLog.StatusCode),
colorizeDuration(time.Since(reqLog.Timestamp)),
reqLog.Path,
)
}
9 changes: 9 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type Config struct {

LogResponseBody bool

LogRequestToConsole bool

IgnorePaths []string

// OpenTelemetry configuration
Expand Down Expand Up @@ -76,6 +78,12 @@ func WithResponseBodyLogging(enabled bool) Option {
}
}

func WithConsoleLogging(enabled bool) Option {
return func(c *Config) {
c.LogRequestToConsole = enabled
}
}
Comment thread
erancihan marked this conversation as resolved.

// WithIgnorePaths sets the path patterns to ignore
func WithIgnorePaths(patterns ...string) Option {
return func(c *Config) {
Expand Down Expand Up @@ -197,6 +205,7 @@ func defaultConfig() *Config {
DashboardPath: "/__viz",
LogRequestBody: false,
LogResponseBody: false,
LogRequestToConsole: false,
IgnorePaths: []string{},
EnableOpenTelemetry: false,
ServiceName: "govisual",
Expand Down
75 changes: 75 additions & 0 deletions test/example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main
Comment thread
erancihan marked this conversation as resolved.
Outdated

import (
"net/http"
"strconv"
"time"

"github.com/doganarif/govisual"
)

func main() {
mux := http.NewServeMux()

// Add your routes
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Example handler logic
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message": "Hello, user!"}`))
})

// add delay simulation route
mux.HandleFunc("/dalay/{duration}", func(w http.ResponseWriter, r *http.Request) {
durationStr := r.PathValue("duration")
duration, err := strconv.Atoi(durationStr)
if err != nil || duration < 0 {
http.Error(w, "Invalid duration", http.StatusBadRequest)
return
}
time.Sleep(time.Duration(duration) * time.Millisecond)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message": "Delay completed"}`))
})

// add status code simulation route
mux.HandleFunc("/status/{code}", func(w http.ResponseWriter, r *http.Request) {
codeStr := r.PathValue("code")
code, err := strconv.Atoi(codeStr)
if err != nil || code < 100 || code > 599 {
http.Error(w, "Invalid status code", http.StatusBadRequest)
return
}
w.WriteHeader(code)
w.Write([]byte(`{"message": "Status code set"}`))
})

// add status code simulation route with delay
mux.HandleFunc("/status/{code}/delay/{duration}", func(w http.ResponseWriter, r *http.Request) {
codeStr := r.PathValue("code")
code, err := strconv.Atoi(codeStr)
if err != nil || code < 100 || code > 599 {
http.Error(w, "Invalid status code", http.StatusBadRequest)
return
}
durationStr := r.PathValue("duration")
duration, err := strconv.Atoi(durationStr)
if err != nil || duration < 0 {
http.Error(w, "Invalid duration", http.StatusBadRequest)
return
}
time.Sleep(time.Duration(duration) * time.Millisecond)
w.WriteHeader(code)
w.Write([]byte(`{"message": "Status code set with delay"}`))
})

// Wrap with GoVisual
handler := govisual.Wrap(
mux,
govisual.WithRequestBodyLogging(true),
govisual.WithResponseBodyLogging(true),
govisual.WithConsoleLogging(true),
)

http.ListenAndServe(":8080", handler)
}
2 changes: 1 addition & 1 deletion wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func Wrap(handler http.Handler, opts ...Option) http.Handler {
})

// Create middleware wrapper
wrapped := middleware.Wrap(handler, requestStore, config.LogRequestBody, config.LogResponseBody, config)
wrapped := middleware.Wrap(handler, requestStore, config.LogRequestBody, config.LogResponseBody, config.LogRequestToConsole, config)

// Initialize OpenTelemetry if enabled
if config.EnableOpenTelemetry {
Expand Down