Files
opencloud/vendor/github.com/olekukonko/ll/dbg.go
dependabot[bot] 57fdbb2d4c build(deps): bump github.com/olekukonko/tablewriter from 1.1.3 to 1.1.4
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.1.3 to 1.1.4.
- [Release notes](https://github.com/olekukonko/tablewriter/releases)
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.1.3...v1.1.4)

---
updated-dependencies:
- dependency-name: github.com/olekukonko/tablewriter
  dependency-version: 1.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-12 16:39:40 +01:00

283 lines
7.0 KiB
Go

package ll
import (
"container/list"
"fmt"
"os"
"runtime"
"strings"
"sync"
"github.com/olekukonko/ll/lx"
)
// -----------------------------------------------------------------------------
// Global Cache Instance
// -----------------------------------------------------------------------------
// sourceCache caches up to 128 source files using LRU eviction.
var sourceCache = newFileLRU(128)
// -----------------------------------------------------------------------------
// File-Level LRU Cache
// -----------------------------------------------------------------------------
type fileLRU struct {
capacity int
mu sync.Mutex
list *list.List
items map[string]*list.Element
}
type fileItem struct {
key string
lines []string
}
func newFileLRU(capacity int) *fileLRU {
if capacity <= 0 {
capacity = 1
}
return &fileLRU{
capacity: capacity,
list: list.New(),
items: make(map[string]*list.Element, capacity),
}
}
// getLine retrieves a specific 1-indexed line from a file.
func (c *fileLRU) getLine(file string, line int) (string, bool) {
c.mu.Lock()
defer c.mu.Unlock()
// 1. Cache Hit
if elem, ok := c.items[file]; ok {
c.list.MoveToFront(elem)
item := elem.Value.(*fileItem)
if item.lines == nil {
return "", false
}
return nthLine(item.lines, line)
}
// 2. Cache Miss - Read File
// Release lock during I/O to avoid blocking other loggers
c.mu.Unlock()
data, err := os.ReadFile(file)
c.mu.Lock()
// 3. Double-check (another goroutine might have loaded it while unlocked)
if elem, ok := c.items[file]; ok {
c.list.MoveToFront(elem)
item := elem.Value.(*fileItem)
if item.lines == nil {
return "", false
}
return nthLine(item.lines, line)
}
var lines []string
if err == nil {
lines = strings.Split(string(data), "\n")
}
// 4. Store (Positive or Negative Cache)
item := &fileItem{
key: file,
lines: lines,
}
elem := c.list.PushFront(item)
c.items[file] = elem
// 5. Evict if needed
if c.list.Len() > c.capacity {
old := c.list.Back()
if old != nil {
c.list.Remove(old)
delete(c.items, old.Value.(*fileItem).key)
}
}
if lines == nil {
return "", false
}
return nthLine(lines, line)
}
// nthLine returns the 1-indexed line from slice.
func nthLine(lines []string, n int) (string, bool) {
if n <= 0 || n > len(lines) {
return "", false
}
return strings.TrimSuffix(lines[n-1], "\r"), true
}
// -----------------------------------------------------------------------------
// Logger Debug Implementation
// -----------------------------------------------------------------------------
// Dbg logs debug information including source file, line number,
// and the best-effort extracted expression.
//
// Example:
//
// x := 42
// logger.Dbg("val", x)
// Output: [file.go:123] "val" = "val", x = 42
func (l *Logger) Dbg(values ...interface{}) {
if !l.shouldLog(lx.LevelInfo) {
return
}
l.dbg(2, values...)
}
func (l *Logger) dbg(skip int, values ...interface{}) {
file, line, ok := callerFrame(skip)
if !ok {
// Fallback if we can't get frame
var sb strings.Builder
sb.WriteString("[?:?] ")
for i, v := range values {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(fmt.Sprintf("%+v", v))
}
l.log(lx.LevelInfo, lx.ClassText, sb.String(), nil, false)
return
}
shortFile := file
if idx := strings.LastIndex(file, "/"); idx >= 0 {
shortFile = file[idx+1:]
}
srcLine, hit := sourceCache.getLine(file, line)
var expr string
if hit && srcLine != "" {
// Attempt to extract the text inside Dbg(...)
if a := strings.Index(srcLine, "Dbg("); a >= 0 {
rest := srcLine[a+len("Dbg("):]
if b := strings.LastIndex(rest, ")"); b >= 0 {
expr = strings.TrimSpace(rest[:b])
}
} else {
// Fallback: extract first (...) group if Dbg isn't explicit prefix
a := strings.Index(srcLine, "(")
b := strings.LastIndex(srcLine, ")")
if a >= 0 && b > a {
expr = strings.TrimSpace(srcLine[a+1 : b])
}
}
}
// Format output
var outBuilder strings.Builder
outBuilder.WriteString(fmt.Sprintf("[%s:%d] ", shortFile, line))
// Attempt to split expressions to map 1:1 with values
var parts []string
if expr != "" {
parts = splitExpressions(expr)
}
// If the number of extracted expressions matches the number of values,
// print them as "expr = value". Otherwise, fall back to "expr = val1, val2".
if len(parts) == len(values) {
for i, v := range values {
if i > 0 {
outBuilder.WriteString(", ")
}
outBuilder.WriteString(fmt.Sprintf("%s = %+v", parts[i], v))
}
} else {
if expr != "" {
outBuilder.WriteString(expr)
outBuilder.WriteString(" = ")
}
for i, v := range values {
if i > 0 {
outBuilder.WriteString(", ")
}
outBuilder.WriteString(fmt.Sprintf("%+v", v))
}
}
l.log(lx.LevelInfo, lx.ClassDbg, outBuilder.String(), nil, false)
}
// splitExpressions splits a comma-separated string of expressions,
// respecting nested parentheses, brackets, braces, and quotes.
// Example: "a, fn(b, c), d" -> ["a", "fn(b, c)", "d"]
func splitExpressions(s string) []string {
var parts []string
var current strings.Builder
depth := 0 // Tracks nested (), [], {}
inQuote := false // Tracks string literals
var quoteChar rune
for _, r := range s {
switch {
case inQuote:
current.WriteRune(r)
if r == quoteChar {
// We rely on the fact that valid Go source won't have unescaped quotes easily
// accessible here without complex parsing, but for simple Dbg calls this suffices.
// A robust parser handles `\"`, but simple state toggling covers 99% of debug cases.
inQuote = false
}
case r == '"' || r == '\'':
inQuote = true
quoteChar = r
current.WriteRune(r)
case r == '(' || r == '{' || r == '[':
depth++
current.WriteRune(r)
case r == ')' || r == '}' || r == ']':
depth--
current.WriteRune(r)
case r == ',' && depth == 0:
// Split point
parts = append(parts, strings.TrimSpace(current.String()))
current.Reset()
default:
current.WriteRune(r)
}
}
if current.Len() > 0 {
parts = append(parts, strings.TrimSpace(current.String()))
}
return parts
}
// -----------------------------------------------------------------------------
// Caller Resolution
// -----------------------------------------------------------------------------
// callerFrame walks stack frames until it finds the first frame
// outside the ll package.
func callerFrame(skip int) (file string, line int, ok bool) {
// +2 to skip callerFrame + dbg itself.
pcs := make([]uintptr, 32)
n := runtime.Callers(skip+2, pcs)
if n == 0 {
return "", 0, false
}
frames := runtime.CallersFrames(pcs[:n])
for {
fr, more := frames.Next()
// fr.Function looks like: "github.com/you/mod/ll.(*Logger).Dbg"
// We want the first frame that is NOT inside package ll.
if fr.Function == "" || !strings.Contains(fr.Function, "/ll.") && !strings.Contains(fr.Function, ".ll.") {
return fr.File, fr.Line, true
}
if !more {
// Fallback: return the last frame we saw
return fr.File, fr.Line, fr.File != ""
}
}
}