mirror of
https://github.com/openziti/ziti.git
synced 2026-09-10 00:35:41 +00:00
ba68a528f5
- renames the FormatPretty --log-formatter value from "pfxlog" to "pretty"; the old name described its historical origin rather than its behavior, and pfxlog is being removed - accepts "pfxlog" as a deprecated, unadvertised alias for "pretty" via FormatPrettyAlias so existing configs and scripts keep working - advertises "pretty" in the --log-formatter help text across the run, tunnel, controller, router, and demo commands - adds the "pretty" case alongside "pfxlog" to the legacy pfxlog formatter switches so --log-formatter accepts the same values on every command - tests that the "pfxlog" alias still resolves to pretty output
185 lines
6.6 KiB
Go
185 lines
6.6 KiB
Go
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package logging
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"runtime"
|
|
)
|
|
|
|
// ReplaceAttr is the slog.HandlerOptions.ReplaceAttr callback that coerces
|
|
// the standard slog JSON output into the pfxlog-shape downstream parsers
|
|
// already consume:
|
|
//
|
|
// - the "level" key's value is the canonical lowercase name (via LevelName)
|
|
// rather than slog's uppercase default;
|
|
// - the nested "source" attr is suppressed entirely; sourceFlattener emits
|
|
// "file" and "func" as flat top-level keys instead.
|
|
//
|
|
// Modifications apply only at the top level; attrs nested inside groups are
|
|
// passed through unchanged.
|
|
func ReplaceAttr(groups []string, a slog.Attr) slog.Attr {
|
|
if len(groups) > 0 {
|
|
return a
|
|
}
|
|
switch a.Key {
|
|
case slog.LevelKey:
|
|
if lvl, ok := a.Value.Any().(slog.Level); ok {
|
|
return slog.String(slog.LevelKey, LevelName(lvl))
|
|
}
|
|
case slog.SourceKey:
|
|
return slog.Attr{}
|
|
}
|
|
return a
|
|
}
|
|
|
|
// sourceFlattener decodes a record's PC and emits "file" and "func" as flat
|
|
// top-level attrs, matching pfxlog's existing JSON shape. The slog JSONHandler
|
|
// underneath has AddSource: false; this handler is the source of the file/func
|
|
// attrs in the output.
|
|
//
|
|
// Only records with a PC are decoded here. Direct slog records carry one, set
|
|
// by slog at the call site, and it re-decodes cleanly. Bridged-logrus records
|
|
// instead arrive with PC == 0 and their location already resolved into "file"
|
|
// and "func" attrs by the bridge (logrus's Entry.Caller.PC cannot be re-decoded
|
|
// reliably, so the bridge does not forward it); those records skip the decode
|
|
// here and pass their attrs through unchanged. Records built without any PC,
|
|
// like the AsyncHandler's drop-summary line, simply carry no file/func.
|
|
type sourceFlattener struct {
|
|
parent slog.Handler
|
|
}
|
|
|
|
func (h *sourceFlattener) Enabled(ctx context.Context, level slog.Level) bool {
|
|
return h.parent.Enabled(ctx, level)
|
|
}
|
|
|
|
func (h *sourceFlattener) Handle(ctx context.Context, r slog.Record) error {
|
|
if r.PC == 0 {
|
|
return h.parent.Handle(ctx, r)
|
|
}
|
|
frames := runtime.CallersFrames([]uintptr{r.PC})
|
|
frame, _ := frames.Next()
|
|
r2 := slog.NewRecord(r.Time, r.Level, r.Message, 0)
|
|
r2.AddAttrs(slog.String("file", fmt.Sprintf("%s:%d", frame.File, frame.Line)))
|
|
r2.AddAttrs(slog.String("func", frame.Function))
|
|
r.Attrs(func(a slog.Attr) bool {
|
|
r2.AddAttrs(a)
|
|
return true
|
|
})
|
|
return h.parent.Handle(ctx, r2)
|
|
}
|
|
|
|
func (h *sourceFlattener) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
return &sourceFlattener{parent: h.parent.WithAttrs(attrs)}
|
|
}
|
|
|
|
func (h *sourceFlattener) WithGroup(name string) slog.Handler {
|
|
return &sourceFlattener{parent: h.parent.WithGroup(name)}
|
|
}
|
|
|
|
// BuildHandler constructs the standard async slog handler chain that ziti
|
|
// uses in production: a JSON handler over out, wrapped in sourceFlattener
|
|
// for the pfxlog-shape file/func keys, wrapped in an AsyncHandler so writes
|
|
// happen off the caller's goroutine. The JSON handler itself does no level
|
|
// gating; gating lives upstream in the named-logger registry (Phase 5).
|
|
//
|
|
// out defaults to os.Stderr when nil.
|
|
func BuildHandler(out io.Writer, opts AsyncOptions) (*AsyncHandler, error) {
|
|
if out == nil {
|
|
out = os.Stderr
|
|
}
|
|
json := slog.NewJSONHandler(out, &slog.HandlerOptions{
|
|
AddSource: false,
|
|
Level: LevelTrace,
|
|
ReplaceAttr: ReplaceAttr,
|
|
})
|
|
flat := &sourceFlattener{parent: json}
|
|
return NewAsyncHandler(flat, opts)
|
|
}
|
|
|
|
// BuildTextHandler constructs the async chain for plain key=value text output:
|
|
// a slog TextHandler over out (lowercased canonical level names via
|
|
// ReplaceAttr, flat file/func via sourceFlattener), wrapped in an AsyncHandler.
|
|
// This is what --log-formatter=text selects, matching the pre-slog
|
|
// logrus.TextFormatter shape (level=info msg=...) rather than the colored,
|
|
// positional pretty output.
|
|
//
|
|
// out defaults to os.Stderr when nil.
|
|
func BuildTextHandler(out io.Writer, opts AsyncOptions) (*AsyncHandler, error) {
|
|
if out == nil {
|
|
out = os.Stderr
|
|
}
|
|
text := slog.NewTextHandler(out, &slog.HandlerOptions{
|
|
AddSource: false,
|
|
Level: LevelTrace,
|
|
ReplaceAttr: ReplaceAttr,
|
|
})
|
|
flat := &sourceFlattener{parent: text}
|
|
return NewAsyncHandler(flat, opts)
|
|
}
|
|
|
|
// BuildPrettyHandler builds the async chain for ziti's pretty (pfxlog-shape)
|
|
// output: a PrettyHandler over out, wrapped in an AsyncHandler. The
|
|
// PrettyHandler resolves the caller frame from slog.Record.PC, so the bridge
|
|
// must have ReportCaller enabled (which Install does) for legacy logrus call
|
|
// sites to render their original file/func.
|
|
//
|
|
// prettyOpts defaults to DefaultPrettyOptions(): pfxlog-compatible labels,
|
|
// "github.com/openziti/" trimmed from function names, relative time since the
|
|
// start of today, and color off unless PFXLOG_USE_COLOR opts in. out defaults
|
|
// to os.Stderr.
|
|
func BuildPrettyHandler(out io.Writer, opts AsyncOptions, prettyOpts *PrettyOptions) (*AsyncHandler, error) {
|
|
if out == nil {
|
|
out = os.Stderr
|
|
}
|
|
return NewAsyncHandler(NewPrettyHandler(out, prettyOpts), opts)
|
|
}
|
|
|
|
// Recognised values for the --log-formatter flag. "" is treated as
|
|
// FormatPretty so unconfigured binaries match the pre-slog default look.
|
|
const (
|
|
FormatPretty = "pretty"
|
|
FormatJSON = "json"
|
|
FormatText = "text"
|
|
|
|
// FormatPrettyAlias is the historical name for FormatPretty, accepted for
|
|
// backward compatibility with existing configs and scripts. It is not
|
|
// advertised in flag help. The name predates removing pfxlog and describes
|
|
// its origin, not its behavior.
|
|
FormatPrettyAlias = "pfxlog"
|
|
)
|
|
|
|
// BuildHandlerForFormat picks the production handler chain by name. Unknown
|
|
// or empty formats fall back to FormatPretty so ziti's default look-and-feel
|
|
// matches the pre-slog binaries.
|
|
func BuildHandlerForFormat(out io.Writer, opts AsyncOptions, format string) (*AsyncHandler, error) {
|
|
switch format {
|
|
case FormatJSON:
|
|
return BuildHandler(out, opts)
|
|
case FormatText:
|
|
return BuildTextHandler(out, opts)
|
|
case "", FormatPretty, FormatPrettyAlias:
|
|
return BuildPrettyHandler(out, opts, nil)
|
|
default:
|
|
return nil, fmt.Errorf("logging: unknown formatter %q (want %q, %q, or %q)", format, FormatPretty, FormatJSON, FormatText)
|
|
}
|
|
}
|