go/mqtt/internal/log_utils.go (97 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package internal
import (
"context"
"fmt"
"log/slog"
"reflect"
"github.com/Azure/iot-operations-sdks/go/internal/log"
"github.com/eclipse/paho.golang/paho"
"github.com/iancoleman/strcase"
)
type Logger struct{ log.Logger }
func (l Logger) Packet(ctx context.Context, name string, packet any) {
// This is expensive; bail out if we don't need it.
if !l.Enabled(ctx, slog.LevelDebug) {
return
}
val := realValue(reflect.ValueOf(packet))
if missingValue(val) {
l.Log(ctx, slog.LevelWarn, fmt.Sprintf("%s not available", name))
} else {
l.Log(ctx, slog.LevelDebug, name, reflectAttrs(val)...)
}
}
func reflectAttrs(val reflect.Value) []slog.Attr {
typ := val.Type()
num := typ.NumField()
var attrs []slog.Attr
for i := range num {
f := typ.Field(i)
if !f.IsExported() {
continue
}
attrs = append(attrs, reflectAttr(
strcase.ToSnake(f.Name),
realValue(val.Field(i)),
)...)
}
return attrs
}
func reflectAttr(name string, val reflect.Value) []slog.Attr {
// Ignore zero values to keep the log cleaner.
if missingValue(val) {
return nil
}
switch name {
// Paho's struct nesting is not particularly useful to log.
case "properties":
return reflectAttrs(val)
// Subscriptions are one-at-a-time for the session client.
case "subscriptions":
if subs, ok := val.Interface().([]paho.SubscribeOptions); ok {
return reflectAttrs(reflect.ValueOf(subs[0]))
}
case "topics":
if topics, ok := val.Interface().([]string); ok {
return []slog.Attr{slog.String("topic", topics[0])}
}
case "reasons":
if reasons, ok := val.Interface().([]byte); ok {
return []slog.Attr{slog.Int("reason_code", int(reasons[0]))}
}
// Fix QoS not being actually PascalCased.
case "qo_s":
return []slog.Attr{slog.Any("qos", val.Interface())}
// Do not log secrets.
case "password", "auth_data":
return nil
}
switch v := val.Interface().(type) {
case []byte:
return []slog.Attr{slog.String(name, string(v))}
case paho.UserProperties:
if len(v) == 0 {
return nil
}
attrs := make([]any, len(v))
for i, p := range v {
attrs[i] = slog.String(p.Key, p.Value)
}
return []slog.Attr{slog.Group(name, attrs...)}
}
if val.Kind() == reflect.Struct {
as := reflectAttrs(val)
if len(as) == 0 {
return nil
}
cpy := make([]any, len(as))
for i, a := range as {
cpy[i] = a
}
return []slog.Attr{slog.Group(name, cpy...)}
}
return []slog.Attr{slog.Any(name, val.Interface())}
}
func realValue(val reflect.Value) reflect.Value {
for val.Kind() == reflect.Pointer {
val = val.Elem()
}
return val
}
func missingValue(val reflect.Value) bool {
return val.Kind() == reflect.Invalid || val.IsZero()
}