Files
opencloud/vendor/github.com/olekukonko/ll/inspector.go
dependabot[bot] 063217c3e6 build(deps): bump github.com/olekukonko/tablewriter from 1.1.1 to 1.1.2
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.1.1 to 1.1.2.
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.1.1...v1.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-14 17:03:18 +01:00

240 lines
7.5 KiB
Go

package ll
import (
"encoding/json"
"fmt"
"reflect"
"runtime"
"strings"
"unsafe"
"github.com/olekukonko/ll/lx"
)
// Inspector is a utility for Logger that provides advanced inspection and logging of data
// in human-readable JSON format. It uses reflection to access and represent unexported fields,
// nested structs, embedded structs, and pointers, making it useful for debugging complex data structures.
type Inspector struct {
logger *Logger
}
// NewInspector returns a new Inspector instance associated with the provided logger.
func NewInspector(logger *Logger) *Inspector {
return &Inspector{logger: logger}
}
// Log outputs the given values as indented JSON at the Info level, prefixed with the caller's
// file name and line number. It handles structs (including unexported fields, nested, and embedded),
// pointers, errors, and other types. The skip parameter determines how many stack frames to skip
// when identifying the caller; typically set to 2 to account for the call to Log and its wrapper.
//
// Example usage within a Logger method:
//
// o := NewInspector(l)
// o.Log(2, someStruct) // Logs JSON representation with caller info
func (o *Inspector) Log(skip int, values ...interface{}) {
// Skip if logger is suspended or Info level is disabled
if o.logger.suspend.Load() || !o.logger.shouldLog(lx.LevelInfo) {
return
}
// Retrieve caller information for logging context
_, file, line, ok := runtime.Caller(skip)
if !ok {
o.logger.log(lx.LevelError, lx.ClassText, "Inspector: Unable to parse runtime caller", nil, false)
return
}
// Extract short filename for concise output
shortFile := file
if idx := strings.LastIndex(file, "/"); idx >= 0 {
shortFile = file[idx+1:]
}
// Process each value individually
for _, value := range values {
var jsonData []byte
var err error
// Use reflection for struct types to handle unexported and nested fields
val := reflect.ValueOf(value)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() == reflect.Struct {
valueMap := o.structToMap(val)
jsonData, err = json.MarshalIndent(valueMap, "", " ")
} else if errVal, ok := value.(error); ok {
// Special handling for errors to represent them as a simple map
value = map[string]string{"error": errVal.Error()}
jsonData, err = json.MarshalIndent(value, "", " ")
} else {
// Fall back to standard JSON marshaling for non-struct types
jsonData, err = json.MarshalIndent(value, "", " ")
}
if err != nil {
o.logger.log(lx.LevelError, lx.ClassText, fmt.Sprintf("Inspector: JSON encoding error: %v", err), nil, false)
continue
}
// Construct log message with file, line, and JSON data
msg := fmt.Sprintf("[%s:%d] INSPECT: %s", shortFile, line, string(jsonData))
o.logger.log(lx.LevelInfo, lx.ClassText, msg, nil, false)
}
}
// structToMap recursively converts a struct's reflect.Value to a map[string]interface{}.
// It includes unexported fields (named with parentheses), prefixes pointers with '*',
// flattens anonymous embedded structs without json tags, and uses unsafe pointers to access
// unexported primitive fields when reflect.CanInterface() returns false.
func (o *Inspector) structToMap(val reflect.Value) map[string]interface{} {
result := make(map[string]interface{})
if !val.IsValid() {
return result
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
// Determine field name: prefer json tag if present and not "-", else use struct field name
baseName := fieldType.Name
jsonTag := fieldType.Tag.Get("json")
hasJsonTag := false
if jsonTag != "" {
if idx := strings.Index(jsonTag, ","); idx != -1 {
jsonTag = jsonTag[:idx]
}
if jsonTag != "-" {
baseName = jsonTag
hasJsonTag = true
}
}
// Enclose unexported field names in parentheses
fieldName := baseName
if !fieldType.IsExported() {
fieldName = "(" + baseName + ")"
}
// Handle pointer fields
isPtr := fieldType.Type.Kind() == reflect.Ptr
if isPtr {
fieldName = "*" + fieldName
if field.IsNil() {
result[fieldName] = nil
continue
}
field = field.Elem()
}
// Recurse for struct fields
if field.Kind() == reflect.Struct {
subMap := o.structToMap(field)
isNested := !fieldType.Anonymous || hasJsonTag
if isNested {
result[fieldName] = subMap
} else {
// Flatten embedded struct fields into the parent map, avoiding overwrites
for k, v := range subMap {
if _, exists := result[k]; !exists {
result[k] = v
}
}
}
} else {
// Handle primitive fields
if field.CanInterface() {
result[fieldName] = field.Interface()
} else {
// Use unsafe access for unexported primitives
ptr := getDataPtr(field)
switch field.Kind() {
case reflect.String:
result[fieldName] = *(*string)(ptr)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
result[fieldName] = o.getIntFromUnexportedField(field)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
result[fieldName] = o.getUintFromUnexportedField(field)
case reflect.Float32, reflect.Float64:
result[fieldName] = o.getFloatFromUnexportedField(field)
case reflect.Bool:
result[fieldName] = *(*bool)(ptr)
default:
result[fieldName] = fmt.Sprintf("*unexported %s*", field.Type().String())
}
}
}
}
return result
}
// emptyInterface represents the internal structure of an empty interface{}.
// This is used for unsafe pointer manipulation to access unexported field data.
type emptyInterface struct {
typ unsafe.Pointer
word unsafe.Pointer
}
// getDataPtr returns an unsafe.Pointer to the underlying data of a reflect.Value.
// This enables direct access to unexported fields via unsafe operations.
func getDataPtr(v reflect.Value) unsafe.Pointer {
return (*emptyInterface)(unsafe.Pointer(&v)).word
}
// getIntFromUnexportedField extracts a signed integer value from an unexported field
// using unsafe pointer access. It supports int, int8, int16, int32, and int64 kinds,
// returning the value as int64. Returns 0 for unsupported kinds.
func (o *Inspector) getIntFromUnexportedField(field reflect.Value) int64 {
ptr := getDataPtr(field)
switch field.Kind() {
case reflect.Int:
return int64(*(*int)(ptr))
case reflect.Int8:
return int64(*(*int8)(ptr))
case reflect.Int16:
return int64(*(*int16)(ptr))
case reflect.Int32:
return int64(*(*int32)(ptr))
case reflect.Int64:
return *(*int64)(ptr)
}
return 0
}
// getUintFromUnexportedField extracts an unsigned integer value from an unexported field
// using unsafe pointer access. It supports uint, uint8, uint16, uint32, and uint64 kinds,
// returning the value as uint64. Returns 0 for unsupported kinds.
func (o *Inspector) getUintFromUnexportedField(field reflect.Value) uint64 {
ptr := getDataPtr(field)
switch field.Kind() {
case reflect.Uint:
return uint64(*(*uint)(ptr))
case reflect.Uint8:
return uint64(*(*uint8)(ptr))
case reflect.Uint16:
return uint64(*(*uint16)(ptr))
case reflect.Uint32:
return uint64(*(*uint32)(ptr))
case reflect.Uint64:
return *(*uint64)(ptr)
}
return 0
}
// getFloatFromUnexportedField extracts a floating-point value from an unexported field
// using unsafe pointer access. It supports float32 and float64 kinds, returning the value
// as float64. Returns 0 for unsupported kinds.
func (o *Inspector) getFloatFromUnexportedField(field reflect.Value) float64 {
ptr := getDataPtr(field)
switch field.Kind() {
case reflect.Float32:
return float64(*(*float32)(ptr))
case reflect.Float64:
return *(*float64)(ptr)
}
return 0
}