diff --git a/configs/fibratus.yml b/configs/fibratus.yml
index d86d78952..c04cd94fc 100644
--- a/configs/fibratus.yml
+++ b/configs/fibratus.yml
@@ -313,6 +313,9 @@ output:
# Indicates whether the console output is active
enabled: true
+ # Indicates if the console output is colorized
+ colorize: true
+
# Specifies the console output format. The "pretty" format dictates that formatting is accomplished
# by replacing the specifiers in the template. The "json" format outputs the event as a raw JSON string
format: pretty
diff --git a/pkg/callstack/callstack.go b/pkg/callstack/callstack.go
index 6d5839157..ee9f1a325 100644
--- a/pkg/callstack/callstack.go
+++ b/pkg/callstack/callstack.go
@@ -1,5 +1,5 @@
/*
- * Copyright 2021-2022 by Nedim Sabic Sabic
+ * Copyright 2021-present by Nedim Sabic Sabic
* https://www.fibratus.io
* All Rights Reserved.
*
@@ -30,6 +30,15 @@ import (
"golang.org/x/sys/windows"
)
+// FrameProvenance designates the frame provenance
+type FrameProvenance uint8
+
+const (
+ Kernel FrameProvenance = iota
+ System
+ User
+)
+
// unbacked represents the identifier for unbacked regions in stack frames
const unbacked = "unbacked"
@@ -48,6 +57,20 @@ type Frame struct {
ModuleAddress va.Address // module base address
}
+// Provenance resolves the frame provenance.
+func (f Frame) Provenance() FrameProvenance {
+ if f.Addr.InSystemRange() {
+ return Kernel
+ }
+
+ mod := filepath.Base(strings.ToLower(f.Module))
+ if mod == "ntdll.dll" || mod == "kernel32.dll" || mod == "kernelbase.dll" {
+ return System
+ }
+
+ return User
+}
+
// IsUnbacked returns true if this frame is originated
// from unbacked memory section
func (f Frame) IsUnbacked() bool { return f.Module == unbacked }
diff --git a/pkg/callstack/colorize.go b/pkg/callstack/colorize.go
new file mode 100644
index 000000000..811adb352
--- /dev/null
+++ b/pkg/callstack/colorize.go
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2021-present by Nedim Sabic Sabic
+ * https://www.fibratus.io
+ * All Rights Reserved.
+ *
+ * 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
+ *
+ * http://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 callstack
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "github.com/rabbitstack/fibratus/pkg/util/colorizer"
+)
+
+// Colorize renders a callstack as a multi-line,
+// ANSI-colourised string. It works directly with the typed Frame slice so
+// no string parsing is required.
+//
+// Visual hierarchy per frame (left > right, dim > bright):
+//
+// !
+//
+// Frame tiers
+// ───────────
+//
+// kernel – frame.Addr.InSystemRange() > magenta
+// unbacked – frame.IsUnbacked() > red (highest suspicion)
+// system – system module > teal
+// user – everything else > amber
+//
+// Consecutive unresolved frames (kernel-space with no symbol) are collapsed
+// into a single dim counter line to avoid flooding the view.
+func (s Callstack) Colorize() string {
+ if s.IsEmpty() {
+ return ""
+ }
+
+ // iterate in reverse so the outermost frame comes first
+ depth := s.Depth()
+ l := s.maxAddrLength()
+
+ var idx int
+ var unresolved int
+ var b strings.Builder
+ b.Grow(depth * 100)
+
+ flushUnresolved := func() {
+ if unresolved == 0 {
+ return
+ }
+ line := fmt.Sprintf(" %s %d unresolved %s",
+ colorizer.SpanDim("▸"),
+ unresolved,
+ "frame(s)",
+ )
+ b.WriteString(colorizer.SpanDim(colorizer.Span(colorizer.Gray, line)))
+ b.WriteByte('\n')
+ unresolved = 0
+ }
+
+ for i := depth - 1; i >= 0; i-- {
+ f := s.FrameAt(i)
+
+ // frames in kernel range with no resolved symbol are unresolved so
+ // we can collapse them into a counter
+ if f.Addr.InSystemRange() && (f.Symbol == "" || f.Symbol == "?") {
+ unresolved++
+ continue
+ }
+
+ flushUnresolved()
+ idx++
+
+ // draw gutter
+ b.WriteString(colorizer.SpanDim(colorizer.Span(colorizer.Gray, fmt.Sprintf(" %3d ", idx))))
+
+ // draw address
+ addrStr := "0x" + f.Addr.String()
+ paddedAddr := addrStr + strings.Repeat(" ", l-len(addrStr))
+ b.WriteString(colorizer.SpanDim(colorizer.Span(colorizer.Gray, paddedAddr)))
+ b.WriteString(" ")
+
+ // unbacked means execution from anonymous memory which is the highest-
+ // suspicion tier, rendered red regardless of address range.
+ if f.IsUnbacked() {
+ b.WriteString(f.colorizeUnbacked())
+ b.WriteByte('\n')
+ continue
+ }
+
+ clr := f.Provenance().color()
+
+ dir := filepath.Dir(f.Module)
+ mod := filepath.Base(f.Module)
+ if dir == "." {
+ dir = ""
+ }
+ // module directory
+ if dir != "" {
+ dir += `\`
+ b.WriteString(colorizer.SpanDim(colorizer.Span(clr, dir)))
+ }
+ // module name
+ b.WriteString(colorizer.Span(clr, mod))
+
+ // symbol
+ b.WriteString(colorizer.SpanDim("!"))
+
+ sym := f.Symbol
+ if sym == "" || sym == "?" {
+ sym = "?"
+ }
+ b.WriteString(colorizer.SpanBold(clr, sym))
+
+ // offset
+ if f.Offset != 0 {
+ b.WriteString(colorizer.SpanDim(colorizer.Span(colorizer.Gray, fmt.Sprintf("+0x%x", f.Offset))))
+ }
+
+ b.WriteByte('\n')
+ }
+
+ flushUnresolved()
+
+ return strings.TrimRight(b.String(), "\n")
+}
+
+// maxAddrLength measure the widest address string
+func (s Callstack) maxAddrLength() int {
+ maxw := 0
+ for _, f := range s {
+ w := len("0x") + len(f.Addr.String())
+ maxw = max(maxw, w)
+ }
+ return maxw
+}
+
+// colorizeUnbackedFrame renders the unbacked frame.
+func (f Frame) colorizeUnbacked() string {
+ var b strings.Builder
+ b.WriteString(colorizer.SpanBold(colorizer.Red, "unbacked"))
+ b.WriteString(colorizer.SpanDim("!"))
+ b.WriteString(colorizer.SpanBold(colorizer.Red, "?"))
+ if f.Offset != 0 {
+ b.WriteString(colorizer.SpanDim(colorizer.Span(colorizer.Gray, fmt.Sprintf("+0x%x", f.Offset))))
+ }
+ return b.String()
+}
+
+// color return frame provenance color to fill the module
+// directory, module base name, and the symbol respectively.
+func (p FrameProvenance) color() uint8 {
+ switch p {
+ case Kernel:
+ return colorizer.Magenta
+ case System:
+ return colorizer.Teal
+ default:
+ return colorizer.Amber
+ }
+}
diff --git a/pkg/config/config.schema.json b/pkg/config/config.schema.json
index 0fccfc8d0..f24d078e3 100644
--- a/pkg/config/config.schema.json
+++ b/pkg/config/config.schema.json
@@ -498,6 +498,9 @@
"enabled": {
"type": "boolean"
},
+ "colorize": {
+ "type": "boolean"
+ },
"format": {
"type": "string",
"enum": [
diff --git a/pkg/event/formatter.go b/pkg/event/formatter.go
index 620ec1f3f..d1181ada7 100644
--- a/pkg/event/formatter.go
+++ b/pkg/event/formatter.go
@@ -19,12 +19,19 @@
package event
import (
+ "bytes"
"fmt"
- "github.com/rabbitstack/fibratus/pkg/util/fasttemplate"
+ "io"
"regexp"
"sort"
+ "strconv"
"strings"
+ "sync"
+ "time"
"unicode"
+
+ "github.com/rabbitstack/fibratus/pkg/util/colorizer"
+ "github.com/rabbitstack/fibratus/pkg/util/fasttemplate"
)
const (
@@ -119,6 +126,7 @@ func NewFormatter(template string) (*Formatter, error) {
if ok, pos := isTemplateBalanced(template); !ok {
return nil, fmt.Errorf("template syntax error near field #%d: %q", pos, template)
}
+
for i, field := range flds {
if len(field) > 0 {
name := sanitize(field[0])
@@ -134,6 +142,7 @@ func NewFormatter(template string) (*Formatter, error) {
}
}
}
+
// user might define the tag such as `{{ .Seq }}` or {{ .Seq}}`. We have to make sure
// inner spaces are removed before building the fast template instance
norm := normalizeTemplate(template)
@@ -141,12 +150,235 @@ func NewFormatter(template string) (*Formatter, error) {
if err != nil {
return nil, fmt.Errorf("invalid template format %q: %v", norm, err)
}
+
return &Formatter{
t: t,
expandParamsDot: tmplExpandParamsRegexp.MatchString(norm),
}, nil
}
+// ColorFormatter wraps a Formatter and re-renders each template tag with
+// ANSI colour codes before it is substituted into the output string.
+//
+// It replaces no logic in base formatter template parsing, field validation, and
+// normalisation all remain there. ColorFormatter only intercepts the moment
+// fasttemplate calls the per-tag writer function, injecting colour at that
+// exact boundary.
+//
+// When colour output is not available (piped stdout, NO_COLOR, dumb terminal,
+// pre-Win10) ColorFormatter falls back transparently to the plain formatter.
+type ColorFormatter struct {
+ f *Formatter
+ enabled bool
+
+ mu sync.Mutex
+ prevTime time.Time // timestamp of the most recently rendered event
+}
+
+// NewColorFormatter constructs a ColorFormatter backed by the given Formatter.
+// If colour is not available in the current environment the returned value
+// behaves identically to the underlying plain Formatter.
+func NewColorFormatter(f *Formatter) *ColorFormatter {
+ return &ColorFormatter{f: f, enabled: colorizer.IsAnsiEnabled()}
+}
+
+// Format renders the event according to the template. Each {{ .Field }} tag is
+// substituted with a colour-decorated string when colour output is available,
+// or with the plain field value otherwise.
+func (f *ColorFormatter) Format(e *Event) []byte {
+ if !f.enabled {
+ return f.f.Format(e)
+ }
+
+ var b bytes.Buffer
+ b.WriteString(e.Type.arrow())
+
+ // fasttemplate.Template.ExecuteFuncString calls tagWriter once per tag in
+ // document order, passing the bare tag name (e.g. ".Seq", ".Type",
+ // ".Params.file_path"). The writer writes the coloured substitution value.
+ _, _ = f.f.t.ExecuteFunc(&b, func(w io.Writer, tag string) (int, error) {
+ return io.WriteString(w, f.colourTag(tag, e))
+ })
+
+ return bytes.TrimRight(b.Bytes(), "\n")
+}
+
+// colourTag maps a bare tag name to its coloured string representation.
+func (f *ColorFormatter) colourTag(tag string, e *Event) string {
+ switch tag {
+ case seq:
+ // sequence number is ok to render as dim gray
+ return colorizer.SpanDim(colorizer.Span(colorizer.Gray, strconv.FormatUint(e.Seq, 10)))
+
+ case ts:
+ return f.colourTimestamp(e)
+
+ case cpu:
+ return colorizer.Span(colorizer.Yellow, strconv.FormatUint(uint64(e.CPU), 10))
+
+ case proc:
+ // render process name with bold green as it is the most important
+ // identity anchor on the line. Analysts scan for it first.
+ ps := e.PS
+ if ps == nil {
+ return colorizer.Span(colorizer.Gray, "N/A")
+ }
+ return colorizer.SpanBold(colorizer.Green, ps.Name)
+
+ case pid:
+ return colorizer.Span(colorizer.Green, strconv.FormatUint(uint64(e.PID), 10))
+
+ case ppid:
+ ps := e.PS
+ if ps == nil {
+ return colorizer.Span(colorizer.Gray, "N/A")
+ }
+ return colorizer.Span(colorizer.Green, strconv.FormatUint(uint64(ps.Ppid), 10))
+
+ case tid:
+ return colorizer.Span(colorizer.Green, strconv.FormatUint(uint64(e.Tid), 10))
+
+ case exe:
+ ps := e.PS
+ if ps == nil {
+ return colorizer.Span(colorizer.Gray, "N/A")
+ }
+ return colorizer.Span(colorizer.White, ps.Exe)
+
+ case pexe:
+ ps := e.PS
+ if ps == nil || ps.Parent == nil {
+ return colorizer.Span(colorizer.Gray, "N/A")
+ }
+ return colorizer.Span(colorizer.White, ps.Parent.Exe)
+
+ case cmd:
+ ps := e.PS
+ if ps == nil {
+ return colorizer.Span(colorizer.Gray, "N/A")
+ }
+ return colorizer.Span(colorizer.White, ps.Cmdline)
+
+ case pcmd:
+ ps := e.PS
+ if ps == nil || ps.Parent == nil {
+ return colorizer.Span(colorizer.Gray, "N/A")
+ }
+ return colorizer.Span(colorizer.White, ps.Parent.Cmdline)
+
+ case cwd:
+ ps := e.PS
+ if ps == nil {
+ return colorizer.Span(colorizer.Gray, "N/A")
+ }
+ return colorizer.Span(colorizer.White, ps.Cwd)
+
+ case sid:
+ ps := e.PS
+ if ps == nil {
+ return colorizer.Span(colorizer.Gray, "N/A")
+ }
+ return colorizer.Span(colorizer.Gray, ps.SID)
+
+ case pproc:
+ ps := e.PS
+ if ps == nil || ps.Parent == nil {
+ return colorizer.Span(colorizer.Gray, "N/A")
+ }
+ return colorizer.Span(colorizer.Green, ps.Parent.Name)
+
+ case typ:
+ return e.Type.color()
+
+ case cat:
+ return colorizer.Span(colorizer.Magenta, string(e.Category))
+
+ case parameters:
+ return e.Params.Colorize()
+
+ case pe:
+ ps := e.PS
+ if ps == nil || ps.PE == nil {
+ return colorizer.Span(colorizer.Gray, "N/A")
+ }
+ return colorizer.Span(colorizer.Magenta, ps.PE.String())
+
+ case cstack:
+ return fmt.Sprintf("\n%s", e.Callstack.Colorize())
+ }
+
+ return ""
+}
+
+// colourTimestamp renders the timestamp tag with:
+// - date in blue, time in cyan, tz components dim gray
+// - a Δt suffix showing the gap from the previous event, colour-coded by
+// duration: dim gray (<1ms), brighter gray (1–100ms), amber (>100ms)
+func (f *ColorFormatter) colourTimestamp(e *Event) string {
+ // compute Δt under the lock, then update prevTime.
+ f.mu.Lock()
+ var delta time.Duration
+ if !f.prevTime.IsZero() {
+ delta = max(e.Timestamp.Sub(f.prevTime), 0)
+ }
+ f.prevTime = e.Timestamp
+ f.mu.Unlock()
+
+ // split date and time into two colours so the eye
+ // can parse them independently without any delimiter
+ // change.
+ s := e.Timestamp.String()
+ // split into at most 4 parts: date, time, offset, tz-name
+ parts := strings.SplitN(s, " ", 4)
+ var b strings.Builder
+ b.Grow(len(s) + 60)
+ for i, p := range parts {
+ if i > 0 {
+ b.WriteByte(' ')
+ }
+ switch i {
+ case 0: // date
+ b.WriteString(colorizer.Span(colorizer.Blue, p))
+ case 1: // time with sub-second precision
+ b.WriteString(colorizer.Span(colorizer.Cyan, p))
+ default: // tz offset, tz name recede visually
+ b.WriteString(colorizer.SpanDim(colorizer.Span(colorizer.Gray, p)))
+ }
+ }
+
+ // Δt suffix only shown after the first event.
+ if delta > 0 {
+ b.WriteString(colorizer.Span(colorizer.Gray, " · "))
+ b.WriteString(f.colourDelta(delta))
+ }
+
+ return b.String()
+}
+
+// colourDelta formats a duration as a compact human-readable string and
+// applies a colour that encodes its significance:
+//
+// dim gray — sub-millisecond (high-frequency burst, expected noise)
+// gray — 1ms–100ms (normal inter-event cadence)
+// amber — >100ms (notable gap; something blocked or is infrequent)
+func (f *ColorFormatter) colourDelta(d time.Duration) string {
+ var s string
+ switch {
+ case d < time.Millisecond:
+ s = fmt.Sprintf("+%dµs", d.Microseconds())
+ return colorizer.SpanDim(colorizer.Span(colorizer.Gray, s))
+ case d < 100*time.Millisecond:
+ s = fmt.Sprintf("+%.2fms", float64(d.Microseconds())/1000)
+ return colorizer.Span(colorizer.Gray, s)
+ case d < time.Second:
+ s = fmt.Sprintf("+%.0fms", float64(d.Microseconds())/1000)
+ return colorizer.Span(colorizer.Amber, s)
+ default:
+ s = fmt.Sprintf("+%.2fs", d.Seconds())
+ return colorizer.SpanBold(colorizer.Amber, s)
+ }
+}
+
func sanitize(s string) string {
return strings.Map(func(r rune) rune {
if r == '{' || r == '}' || unicode.IsSpace(r) {
diff --git a/pkg/event/param.go b/pkg/event/param.go
index 82e88104f..a7d21ce20 100644
--- a/pkg/event/param.go
+++ b/pkg/event/param.go
@@ -28,7 +28,9 @@ import (
"github.com/rabbitstack/fibratus/pkg/fs"
"github.com/rabbitstack/fibratus/pkg/network"
+ "github.com/rabbitstack/fibratus/pkg/util/colorizer"
"github.com/rabbitstack/fibratus/pkg/util/key"
+ "github.com/rabbitstack/fibratus/pkg/util/ntstatus"
"github.com/rabbitstack/fibratus/pkg/util/va"
"golang.org/x/text/cases"
"golang.org/x/text/language"
@@ -741,3 +743,80 @@ func (pars Params) findParam(name string) (*Param, error) {
}
return pars[name], nil
}
+
+// Colorize renders the full parameter list for the {{.Params}} tag.
+// Each entry is formatted as: key value, with the key in
+// amber, the seperator dim, and the value semantically coloured.
+func (pars Params) Colorize() string {
+ if len(pars) == 0 {
+ return ""
+ }
+
+ // sort parameters by name
+ s := make([]*Param, 0, len(pars))
+ for _, par := range pars {
+ s = append(s, par)
+ }
+ sort.Slice(s, func(i, j int) bool { return s[i].Name < s[j].Name })
+
+ var b strings.Builder
+ b.Grow(len(pars) * 40)
+
+ for i, p := range s {
+ if i > 0 {
+ b.WriteString(colorizer.SpanDim(", "))
+ }
+ b.WriteString(colorizer.Span(colorizer.Amber, p.Name))
+ b.WriteString(colorizer.SpanDim(ParamKVDelimiter))
+ b.WriteString(p.color())
+ }
+
+ return b.String()
+}
+
+// color applies a semantic colour to a single parameter value based
+// on its type and for string types its content.
+func (p *Param) color() string {
+ switch p.Type {
+ case params.Address:
+ return colorizer.SpanDim(colorizer.Span(colorizer.Gray, "0x"+p.String()))
+
+ case params.Status:
+ v := p.String()
+
+ if v == ntstatus.Success {
+ return colorizer.Span(colorizer.Green, v)
+ }
+ return colorizer.Span(colorizer.Red, v)
+
+ case params.Int8, params.Int16, params.Int32, params.Int64,
+ params.Uint8, params.Uint16, params.Uint32, params.Uint64,
+ params.Float, params.Double:
+ return colorizer.Span(colorizer.Yellow, p.String())
+
+ case params.Bool:
+ b, ok := p.Value.(bool)
+ if !ok {
+ return colorizer.Span(colorizer.Coral, p.String())
+ }
+ if b {
+ return colorizer.Span(colorizer.Green, p.String())
+ }
+ return colorizer.Span(colorizer.Coral, p.String())
+
+ case params.UnicodeString, params.AnsiString, params.SID:
+ return colorizer.Span(colorizer.White, p.String())
+
+ case params.IPv4, params.IPv6:
+ return colorizer.Span(colorizer.Blue, p.String())
+
+ case params.Port:
+ return colorizer.Span(colorizer.Cyan, p.String())
+
+ case params.PID, params.TID:
+ return colorizer.Span(colorizer.Green, p.String())
+
+ default:
+ return colorizer.Span(colorizer.White, p.String())
+ }
+}
diff --git a/pkg/event/types_windows.go b/pkg/event/types_windows.go
index d727e9178..1812436e2 100644
--- a/pkg/event/types_windows.go
+++ b/pkg/event/types_windows.go
@@ -20,7 +20,9 @@ package event
import (
"encoding/binary"
+
"github.com/rabbitstack/fibratus/pkg/sys/etw"
+ "github.com/rabbitstack/fibratus/pkg/util/colorizer"
"github.com/rabbitstack/fibratus/pkg/util/hashers"
"golang.org/x/sys/windows"
)
@@ -635,3 +637,119 @@ func pack(g windows.GUID, id uint16) Type {
byte(id >> 8), byte(id),
}
}
+
+// color return the colorized event type to render by the color formatter.
+func (t Type) color() string {
+ switch t {
+ case CreateFile, ReadFile, CloseFile, SetFileInformation, MapViewFile, UnmapViewFile:
+ return colorizer.SpanBold(colorizer.Cyan, t.String())
+
+ case RenameFile:
+ return colorizer.SpanBold(colorizer.Amber, t.String())
+
+ case WriteFile:
+ return colorizer.SpanBold(colorizer.Teal, t.String())
+
+ case DeleteFile:
+ return colorizer.SpanBold(colorizer.Red, t.String())
+
+ case RegOpenKey, RegCreateKey, RegQueryValue, RegQueryKey:
+ return colorizer.SpanBold(colorizer.Yellow, t.String())
+
+ case RegDeleteKey, RegDeleteValue:
+ return colorizer.SpanBold(colorizer.Red, t.String())
+
+ case RegSetValue:
+ return colorizer.SpanBold(colorizer.Amber, t.String())
+
+ case CreateProcess, OpenProcess:
+ return colorizer.SpanBold(colorizer.Green, t.String())
+
+ case TerminateProcess:
+ return colorizer.SpanBold(colorizer.Red, t.String())
+
+ case CreateThread, OpenThread:
+ return colorizer.SpanBold(colorizer.Green, t.String())
+
+ case TerminateThread:
+ return colorizer.SpanBold(colorizer.Red, t.String())
+
+ case SetThreadContext:
+ return colorizer.SpanBold(colorizer.Amber, t.String())
+
+ case LoadImage, UnloadImage:
+ return colorizer.SpanBold(colorizer.Magenta, t.String())
+
+ case SendTCPv4, SendTCPv6, SendUDPv4, SendUDPv6,
+ RecvTCPv4, RecvTCPv6, RecvUDPv4, RecvUDPv6:
+ return colorizer.SpanBold(colorizer.Blue, t.String())
+
+ case ConnectTCPv4, ConnectTCPv6:
+ return colorizer.SpanBold(colorizer.Teal, t.String())
+
+ case DisconnectTCPv4, DisconnectTCPv6:
+ return colorizer.SpanBold(colorizer.Blue, t.String())
+
+ case AcceptTCPv4, AcceptTCPv6:
+ return colorizer.SpanBold(colorizer.Teal, t.String())
+
+ case QueryDNS, ReplyDNS:
+ return colorizer.SpanBold(colorizer.Indigo, t.String())
+
+ case CreateHandle, CloseHandle:
+ return colorizer.SpanBold(colorizer.Gray, t.String())
+ case DuplicateHandle:
+ return colorizer.SpanBold(colorizer.Amber, t.String())
+
+ case VirtualAlloc, VirtualFree:
+ return colorizer.SpanBold(colorizer.Magenta, t.String())
+
+ case CreateSymbolicLinkObject:
+ return colorizer.SpanBold(colorizer.Lavender, t.String())
+
+ case SubmitThreadpoolCallback, SubmitThreadpoolWork, SetThreadpoolTimer:
+ return colorizer.SpanBold(colorizer.Lavender, t.String())
+
+ default:
+ return colorizer.SpanBold(colorizer.White, t.String())
+ }
+}
+
+// arrow renders the prefix arrow according to event severity.
+// Events are grouped by destructive, mutate, read, and housekeeping
+// severities. Destructive severity covers events that irreversibly
+// alter system state: process termination, file deletion, registry
+// key deletion, code injection.
+//
+// Mutate covers write/create operations: file writes, registry value
+// sets, process creation, thread context changes.
+//
+// Read covers read/query/open operations that consume but do not
+// alter state. Finally, houskeeping covers close/cleanup
+// events that are expected noise in a healthy system.
+func (t Type) arrow() string {
+ var clr uint8
+ switch t {
+ case TerminateProcess, TerminateThread, DeleteFile, RegDeleteKey,
+ RegDeleteValue, UnloadImage, VirtualFree, UnmapViewFile:
+ clr = colorizer.Red
+
+ case CreateProcess, CreateFile, WriteFile, RenameFile, SetFileInformation,
+ RegCreateKey, RegSetValue, CreateThread, SetThreadContext, VirtualAlloc, MapViewFile,
+ DuplicateHandle, ConnectTCPv4, ConnectTCPv6, AcceptTCPv4, AcceptTCPv6,
+ SendTCPv4, SendTCPv6, SendUDPv4, SendUDPv6:
+ clr = colorizer.Amber
+
+ case ReadFile, EnumDirectory, LoadImage, RegOpenKey, RegQueryKey, RegQueryValue, OpenProcess,
+ OpenThread, CreateHandle, RecvTCPv4, RecvTCPv6, RecvUDPv4, RecvUDPv6:
+ clr = colorizer.Teal
+
+ case QueryDNS, ReplyDNS:
+ clr = colorizer.Indigo
+
+ default:
+ clr = colorizer.Gray
+ }
+
+ return colorizer.SpanBold(clr, "› ")
+}
diff --git a/pkg/outputs/console/config.go b/pkg/outputs/console/config.go
index d39a577b9..d80bf2710 100644
--- a/pkg/outputs/console/config.go
+++ b/pkg/outputs/console/config.go
@@ -25,6 +25,7 @@ const (
tmpl = "output.console.template"
paramKVDelimiter = "output.console.kv-delimiter"
enabled = "output.console.enabled"
+ colorize = "output.console.colorize"
)
// Config contains the tweaks that influence the behaviour of the console output.
@@ -33,6 +34,7 @@ type Config struct {
Template string `mapstructure:"template"`
ParamKVDelimiter string `mapstructure:"kv-delimiter"`
Enabled bool `mapstructure:"enabled"`
+ Colorize bool `mapstructure:"colorize"`
}
// AddFlags registers persistent flags.
@@ -41,4 +43,5 @@ func AddFlags(flags *pflag.FlagSet) {
flags.String(paramKVDelimiter, "", "The delimiter symbol for the params key/value pairs")
flags.String(tmpl, "", "Event formatting template")
flags.Bool(enabled, true, "Indicates if the console output is enabled")
+ flags.Bool(colorize, true, "Indicates if the console output is colorized")
}
diff --git a/pkg/outputs/console/console.go b/pkg/outputs/console/console.go
index 9eca570fb..2c91b97c5 100644
--- a/pkg/outputs/console/console.go
+++ b/pkg/outputs/console/console.go
@@ -21,9 +21,10 @@ package console
import (
"bufio"
"expvar"
+ "os"
+
"github.com/rabbitstack/fibratus/pkg/event"
"github.com/rabbitstack/fibratus/pkg/outputs"
- "os"
)
var (
@@ -40,9 +41,10 @@ const (
)
type console struct {
- writer *bufio.Writer
- formatter *event.Formatter
- format format
+ writer *bufio.Writer
+ formatter *event.Formatter
+ colorFormatter *event.ColorFormatter
+ format format
}
func init() {
@@ -59,10 +61,12 @@ func initConsole(config outputs.Config) (outputs.OutputGroup, error) {
if tmpl == "" {
tmpl = template
}
+
formatter, err := event.NewFormatter(tmpl)
if err != nil {
return outputs.Fail(err)
}
+
if cfg.ParamKVDelimiter != "" {
event.ParamKVDelimiter = cfg.ParamKVDelimiter
}
@@ -72,6 +76,11 @@ func initConsole(config outputs.Config) (outputs.OutputGroup, error) {
formatter: formatter,
format: format(cfg.Format),
}
+
+ if cfg.Colorize {
+ c.colorFormatter = event.NewColorFormatter(formatter)
+ }
+
return outputs.Success(c), nil
}
@@ -84,7 +93,11 @@ func (c *console) Publish(batch *event.Batch) error {
case json:
buf = evt.MarshalJSON()
case pretty:
- buf = c.formatter.Format(evt)
+ if c.colorFormatter != nil {
+ buf = c.colorFormatter.Format(evt)
+ } else {
+ buf = c.formatter.Format(evt)
+ }
default:
return nil
}
diff --git a/pkg/util/colorizer/colorizer.go b/pkg/util/colorizer/colorizer.go
new file mode 100644
index 000000000..6f70c1996
--- /dev/null
+++ b/pkg/util/colorizer/colorizer.go
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2021-present by Nedim Sabic Sabic
+ * https://www.fibratus.io
+ * All Rights Reserved.
+ *
+ * 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
+ *
+ * http://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 colorizer
+
+import (
+ "os"
+ "runtime"
+ "strings"
+
+ "golang.org/x/sys/windows"
+)
+
+// escape sequences
+const (
+ reset = "\033[0m"
+ bold = "\033[1m"
+ dim = "\033[2m"
+ fg = "\033[38;5;"
+)
+
+// maps logical colour names to 256-colour foreground codes.
+// All codes are tuned for dark terminal backgrounds.
+const (
+ Gray uint8 = 244
+ Blue uint8 = 75
+ Cyan uint8 = 87
+ Green uint8 = 114
+ Yellow uint8 = 221
+ Amber uint8 = 215
+ Magenta uint8 = 177
+ Red uint8 = 203
+ Coral uint8 = 209
+ Teal uint8 = 80
+ White uint8 = 252
+ Indigo uint8 = 99
+ IndigoDim uint8 = 61
+ Lavender uint8 = 183
+ LavenderDim uint8 = 140
+)
+
+// Span wraps text with a 256-colour foreground escape and a trailing reset.
+func Span(code uint8, text string) string {
+ var b strings.Builder
+ b.Grow(len(text) + 40)
+ b.WriteString(fg)
+ b.WriteString(itoa(code))
+ b.WriteByte('m')
+ b.WriteString(text)
+ b.WriteString(reset)
+ return b.String()
+}
+
+// SpanBold wraps text with bold + 256-colour foreground.
+func SpanBold(code uint8, text string) string {
+ var b strings.Builder
+ b.Grow(len(text) + 40)
+ b.WriteString(fg)
+ b.WriteString(itoa(code))
+ b.WriteByte('m')
+ b.WriteString(bold)
+ b.WriteString(text)
+ b.WriteString(reset)
+ return b.String()
+}
+
+// SpanDim wraps text with dim intensity.
+func SpanDim(text string) string {
+ var b strings.Builder
+ b.Grow(len(text) + 20)
+ b.WriteString(dim)
+ b.WriteString(text)
+ b.WriteString(reset)
+ return b.String()
+}
+
+// itoa converts a uint8 to its decimal string without importing strconv,
+// keeping this package free of extra dependencies on the hot render path.
+func itoa(n uint8) string {
+ switch {
+ case n < 10:
+ return string([]byte{'0' + n})
+ case n < 100:
+ return string([]byte{'0' + n/10, '0' + n%10})
+ default:
+ return string([]byte{'0' + n/100, '0' + (n/10)%10, '0' + n%10})
+ }
+}
+
+// IsAnsiEnabled reports whether the current process should emit ANSI codes.
+// It honours NO_COLOR (https://no-color.org), checks for a non-TTY stdout,
+// and, on Windows, enables VT-processing mode so that the same code path
+// works from Windows 10+ without any conditional compilation.
+func IsAnsiEnabled() bool {
+ if os.Getenv("NO_COLOR") != "" {
+ return false
+ }
+ if strings.ToLower(os.Getenv("TERM")) == "dumb" {
+ return false
+ }
+ fi, err := os.Stdout.Stat()
+ if err != nil || (fi.Mode()&os.ModeCharDevice) == 0 {
+ return false
+ }
+ if runtime.GOOS == "windows" {
+ return enableWindowsVT()
+ }
+ return true
+}
+
+// enableWindowsVT activates ENABLE_VIRTUAL_TERMINAL_PROCESSING on the Windows
+// console handle so that ANSI escape sequences are interpreted rather than
+// printed verbatim. Returns false on pre-Windows 10 hosts where this flag
+// is unavailable.
+func enableWindowsVT() bool {
+ handle := windows.Handle(os.Stdout.Fd())
+ var mode uint32
+ if err := windows.GetConsoleMode(handle, &mode); err != nil {
+ return false
+ }
+ const vtFlag = 0x0004 // ENABLE_VIRTUAL_TERMINAL_PROCESSING
+ if mode&vtFlag != 0 {
+ return true
+ }
+ return windows.SetConsoleMode(handle, mode|vtFlag) == nil
+}