├── .gitignore ├── tests ├── scripts │ ├── printf.env │ ├── stacktracealways.env │ ├── stacktraceregion.env │ ├── compare_benchmarks.sh │ ├── run_benchmarks.sh │ └── assert.sh ├── internal │ ├── capture_output.go │ ├── randstr.go │ └── parselines.go ├── stacktraceerror │ └── printf_error_test.go ├── stacktraceregiononerror │ └── region_on_error_test.go ├── stacktracealways │ └── printf_always_test.go ├── printf │ └── printf_test.go ├── benchmark_results │ ├── printf.baseline.bench.txt │ ├── stacktracealways.baseline.bench.txt │ └── stacktraceregion.baseline.bench.txt └── stacktraceregion │ └── region_test.go ├── go.mod ├── examples ├── example01 │ ├── run.sh │ ├── run_with_stacktrace.sh │ ├── run_with_linker_flags.sh │ └── main.go ├── example02 │ ├── run.sh │ └── main.go ├── example03 │ ├── main.go │ └── run.sh ├── example04 │ ├── run.sh │ └── main.go └── README.md ├── .github └── workflows │ ├── tests.yml │ └── precommit.yml ├── CONTRIBUTING.md ├── LICENSE ├── colorize.go ├── noop.go ├── docs.go ├── Makefile ├── trace.go ├── printf.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.bin 2 | *.objdump 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /tests/scripts/printf.env: -------------------------------------------------------------------------------- 1 | export DLG_NO_WARN=1 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vvvvv/dlg 2 | 3 | go 1.24.5 4 | -------------------------------------------------------------------------------- /tests/scripts/stacktracealways.env: -------------------------------------------------------------------------------- 1 | export DLG_NO_WARN=1 2 | export DLG_STACKTRACE=ALWAYS 3 | -------------------------------------------------------------------------------- /tests/scripts/stacktraceregion.env: -------------------------------------------------------------------------------- 1 | export DLG_NO_WARN=1 2 | export DLG_STACKTRACE=REGION,ALWAYS 3 | -------------------------------------------------------------------------------- /examples/example01/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -f ./example01.dlg.bin 4 | go build -tags dlg -o example01.dlg.bin 5 | ./example01.dlg.bin 6 | -------------------------------------------------------------------------------- /examples/example01/run_with_stacktrace.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -f ./example01.dlg.bin 4 | go build -tags dlg -o example01.dlg.bin 5 | DLG_STACKTRACE=ERROR ./example01.dlg.bin 6 | -------------------------------------------------------------------------------- /examples/example02/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -f example02.dlg.bin 4 | go build -tags dlg -o example02.dlg.bin -ldflags "-X 'github.com/vvvvv/dlg.DLG_STACKTRACE=ERROR'" 5 | ./example02.dlg.bin 6 | -------------------------------------------------------------------------------- /examples/example03/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/vvvvv/dlg" 7 | ) 8 | 9 | func main() { 10 | fmt.Println("hello world") 11 | dlg.Printf("hello from dlg") 12 | } 13 | -------------------------------------------------------------------------------- /examples/example01/run_with_linker_flags.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -f example01_linker_flags.dlg.bin 4 | go build -tags dlg -ldflags "-X 'github.com/vvvvv/dlg.DLG_STACKTRACE=ERROR'" -o example01_linker_flags.dlg.bin 5 | ./example01_linker_flags.dlg.bin 6 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-go@v5 11 | with: 12 | go-version: '1.x' 13 | - name: Run tests 14 | run: make test 15 | -------------------------------------------------------------------------------- /tests/internal/capture_output.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | ) 8 | 9 | func CaptureOutput(fn func()) string { 10 | stderr := os.Stderr 11 | r, w, _ := os.Pipe() 12 | os.Stderr = w 13 | defer func() { 14 | os.Stderr = stderr 15 | }() 16 | 17 | fn() 18 | 19 | w.Close() 20 | var buf bytes.Buffer 21 | io.Copy(&buf, r) 22 | 23 | return buf.String() 24 | } 25 | -------------------------------------------------------------------------------- /examples/example04/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -f ./example04.dlg.bin 4 | go build -tags dlg -o example04.dlg.bin 5 | 6 | printf '==== STARTING WITH DLG_STACKTRACE=REGION,ALWAYS ====\n\n' 7 | DLG_NO_WARN=1 DLG_STACKTRACE=REGION,ALWAYS ./example04.dlg.bin 8 | printf '\n\n' 9 | 10 | printf '==== STARTING WITH DLG_STACKTRACE=REGION,ERROR ====\n\n' 11 | DLG_NO_WARN=1 DLG_STACKTRACE=REGION,ERROR ./example04.dlg.bin 12 | -------------------------------------------------------------------------------- /examples/example01/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/vvvvv/dlg" 7 | ) 8 | 9 | func risky() error { 10 | return fmt.Errorf("unexpected error") 11 | } 12 | 13 | func main() { 14 | fmt.Println("starting...") 15 | 16 | dlg.Printf("executing risky operation") 17 | err := risky() 18 | if err != nil { 19 | dlg.Printf("something failed: %v", err) 20 | } 21 | 22 | dlg.Printf("continuing") 23 | } 24 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains simple programs demonstrating how to use dlg. 4 | - **example01** - Demonstrates how to enable stack traces at runtime or via linker flags. 5 | - **example02** - Demonstrates how to write a custom, concurrency-safe writer. 6 | - **example03** - Demonstrates that logging calls are removed from production builds by inspecting the generated assembly. 7 | - **example04** - Demonstrates tracing regions. 8 | 9 | Run the `run.sh` script inside any example directory to try it. 10 | -------------------------------------------------------------------------------- /examples/example04/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/vvvvv/dlg" 7 | ) 8 | 9 | func risky() error { 10 | return fmt.Errorf("unexpected error") 11 | } 12 | 13 | func main() { 14 | fmt.Println("starting...") 15 | 16 | dlg.Printf("executing risky operation") 17 | err := risky() 18 | if err != nil { 19 | dlg.Printf("something failed but we don't trace it: %v", err) 20 | } 21 | 22 | dlg.StartTrace() 23 | dlg.Printf("now we trace it") 24 | dlg.Printf("where did that error come from again?: %v", err) 25 | dlg.StopTrace() 26 | 27 | dlg.Printf("continuing") 28 | } 29 | -------------------------------------------------------------------------------- /examples/example03/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -f ./example03.bin 4 | rm -f ./example03.dlg.bin 5 | rm -f ./example03.objdump 6 | rm -f ./example03.dlg.objdump 7 | 8 | go build -o example03.bin 9 | go build -tags dlg -o example03.dlg.bin 10 | 11 | go tool objdump -S example03.bin >example03.objdump 12 | go tool objdump -S example03.dlg.bin >example03.dlg.objdump 13 | 14 | printf 'Lines containing "dlg" in disassembly (no build tag): %5d (see: %s)\n' "$(grep 'dlg' ./example03.objdump | grep -v '^TEXT' | wc -l)" 'example03.objdump' 15 | printf 'Lines containing "dlg" in disassembly (dlg build tag): %5d (see: %s)\n' "$(grep 'dlg' ./example03.dlg.objdump | grep -v '^TEXT' | wc -l)" 'example03.dlg.objdump' 16 | -------------------------------------------------------------------------------- /tests/stacktraceerror/printf_error_test.go: -------------------------------------------------------------------------------- 1 | //go:build dlg 2 | 3 | package dlg_test_stacktrace_error 4 | 5 | import ( 6 | "errors" 7 | "testing" 8 | 9 | "github.com/vvvvv/dlg" 10 | "github.com/vvvvv/dlg/tests/internal" 11 | ) 12 | 13 | func TestPrintfStackTraceOnError(t *testing.T) { 14 | out := internal.CaptureOutput(func() { 15 | dlg.Printf("message with error: %v", errors.New("some error")) 16 | }) 17 | 18 | lines := internal.ParseLines([]byte(out)) 19 | 20 | if len(lines) > 1 { 21 | // This should only happen if there's something wrong with internal.ParseLines 22 | t.Fatalf("Too many lines: %+v", lines) 23 | } 24 | 25 | got := lines[0] 26 | 27 | want := struct { 28 | line string 29 | trace bool 30 | }{ 31 | "message with error: some error", true, 32 | } 33 | 34 | if got.Line() != want.line || got.HasTrace() != want.trace { 35 | t.Errorf("Mismatch: want: %q (stacktrace: %v) ; got: %q (stacktrace: %v)", want.line, want.trace, got.Line(), got.HasTrace()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/internal/randstr.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | ) 7 | 8 | var encoder = base64.StdEncoding.WithPadding(base64.NoPadding) 9 | 10 | func RandomString(n int) string { 11 | randomBytes := make([]byte, n) 12 | _, err := rand.Read(randomBytes) 13 | if err != nil { 14 | panic(err) 15 | } 16 | dst := make([]byte, base64.StdEncoding.EncodedLen(len(randomBytes))) 17 | 18 | encoder.Encode(dst, randomBytes) 19 | 20 | return string(dst[:n]) 21 | } 22 | 23 | func RandomStrings(n int) []string { 24 | s := make([]string, 1000) 25 | 26 | for i := 0; i < len(s); i++ { 27 | s[i] = RandomString(n) 28 | } 29 | 30 | return s 31 | } 32 | 33 | func RandomStringWithFormatting(n int) string { 34 | s := RandomString(n) 35 | 36 | s += " %v" 37 | return s 38 | } 39 | 40 | func RandomStringsWithFormatting(n int) []string { 41 | s := make([]string, 1000) 42 | 43 | for i := 0; i < len(s); i++ { 44 | s[i] = RandomStringWithFormatting(n) 45 | } 46 | 47 | return s 48 | } 49 | -------------------------------------------------------------------------------- /examples/example02/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "sync" 8 | 9 | "github.com/vvvvv/dlg" 10 | ) 11 | 12 | type SafeWriter struct { 13 | w io.Writer 14 | mu sync.Mutex 15 | } 16 | 17 | func (s *SafeWriter) Write(b []byte) (int, error) { 18 | return s.w.Write(b) 19 | } 20 | 21 | func (s *SafeWriter) Lock() { 22 | s.mu.Lock() 23 | } 24 | 25 | func (s *SafeWriter) Unlock() { 26 | s.mu.Unlock() 27 | } 28 | 29 | func main() { 30 | // Using a bytes.Buffer for demostrative purposes 31 | sink := &bytes.Buffer{} 32 | // But we could also do: 33 | // sink, err := os.Create("debug.log") 34 | // if err != nil { 35 | // panic(err) 36 | // } 37 | // defer sink.Close() 38 | 39 | writer := &SafeWriter{ 40 | w: sink, 41 | } 42 | 43 | dlg.SetOutput(writer) 44 | 45 | var wg sync.WaitGroup 46 | for i := 0; i < 10; i++ { 47 | wg.Add(1) 48 | go func() { 49 | defer wg.Done() 50 | for n := 0; n < 5; n++ { 51 | dlg.Printf("from goroutine #%v: message %v", i, n) 52 | } 53 | }() 54 | } 55 | wg.Wait() 56 | 57 | fmt.Print(sink.String()) 58 | } 59 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Running Tests 2 | 3 | This repository contains a script for integration tests (`tests/assert.sh`). 4 | These tests verify that the library compiles down to a no-op with no references to `dlg` when the build tag is omitted. 5 | 6 | Because of these extra checks and the use of build tags, running `go test` on its own will not execute every test case. 7 | Instead use the provided `Makefile`: 8 | `make test` runs all Go unit tests + the integration tests. 9 | 10 | ## Pre-commit Hook 11 | 12 | If you're developing in an environment where bash is available, you may use this script as a git pre-commit hook (.git/hooks/pre-commit): 13 | 14 | ```bash 15 | #!/usr/bin/env bash 16 | 17 | set -euo pipefail 18 | 19 | # Format code 20 | if command -v gofmt &>/dev/null; then 21 | gofmt -w ./ 22 | fi 23 | 24 | typeset -i errors=0 25 | 26 | # Checking if build tags are formatted properly 27 | while IFS= read -r f; do 28 | first_line="$(sed -n '1p' "${f}")" 29 | if [[ "$first_line" =~ ^//.*dlg.*$ ]]; then 30 | if [[ ! "$first_line" =~ ^//go:build\ [^\ ] ]]; then 31 | printf 'Error: Malformed build tag.\n' 32 | printf ' File: %s\n' "${f}" 33 | printf ' Found: %s\n' "${first_line}" 34 | printf ' Expected: //go:build \n' 35 | errors=$(( errors + 1)) 36 | fi 37 | fi 38 | done < <(git diff --cached --name-only --diff-filter=ACMR) 39 | 40 | if (( errors )); then 41 | echo 'Commit rejected' 42 | exit 1 43 | fi 44 | 45 | exit 0 46 | ``` 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, vvvvv 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /tests/stacktraceregiononerror/region_on_error_test.go: -------------------------------------------------------------------------------- 1 | //go:build dlg 2 | 3 | package stacktraceregiononerror_test 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/vvvvv/dlg" 10 | "github.com/vvvvv/dlg/tests/internal" 11 | ) 12 | 13 | func noTrace() { 14 | dlg.StartTrace() 15 | defer dlg.StopTrace() 16 | 17 | dlg.Printf("don't trace this") 18 | } 19 | 20 | func traceOnError() { 21 | dlg.StartTrace() 22 | err := fmt.Errorf("this error") 23 | 24 | dlg.Printf("don't trace this") 25 | dlg.Printf("trace %v", err) 26 | dlg.StopTrace() 27 | 28 | dlg.Printf("don't trace this") 29 | } 30 | 31 | func TestPrintfStackTraceRegion(t *testing.T) { 32 | type exp struct { 33 | line string 34 | trace bool 35 | } 36 | 37 | tcs := []struct { 38 | name string 39 | fn func() 40 | exp []exp 41 | }{ 42 | { 43 | name: "don't trace if no error is given", 44 | fn: noTrace, 45 | exp: []exp{ 46 | { 47 | "don't trace this", false, 48 | }, 49 | }, 50 | }, 51 | { 52 | name: "trace on error", 53 | fn: traceOnError, 54 | exp: []exp{ 55 | { 56 | "don't trace this", false, 57 | }, 58 | { 59 | "trace this error", true, 60 | }, 61 | { 62 | "don't trace this", false, 63 | }, 64 | }, 65 | }, 66 | } 67 | 68 | for _, tc := range tcs { 69 | t.Run(tc.name, func(t *testing.T) { 70 | out := internal.CaptureOutput(tc.fn) 71 | lines := internal.ParseLines([]byte(out)) 72 | 73 | if len(lines) != len(tc.exp) { 74 | t.Fatalf("Testcase must contain all output; expected: %v ; got: %v", len(lines), len(tc.exp)) 75 | } 76 | 77 | for i := 0; i < len(tc.exp); i++ { 78 | want := tc.exp[i] 79 | got := lines[i] 80 | 81 | if want.line != got.Line() || want.trace != got.HasTrace() { 82 | t.Errorf("Mismatch: want: %q (stacktrace: %v) ; got: %q (stacktrace: %v)\n%+v", want.line, want.trace, got.Line(), got.HasTrace(), got) 83 | } 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/scripts/compare_benchmarks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Get the root directory of this project 5 | pkg_root() { 6 | typeset project_root 7 | project_root="$(realpath -- "$0")" 8 | project_root="${project_root%/*}/" 9 | 10 | while [[ -n "${project_root}" ]]; do 11 | typeset go_mod_file="${project_root}/go.mod" 12 | if [[ -f "${go_mod_file}" ]] && grep --quiet 'module github.com/vvvvv/dlg' "${go_mod_file}" 2>/dev/null; then 13 | break 14 | fi 15 | project_root="${project_root%/*}" 16 | done 17 | printf '%s' "${project_root}" 18 | } 19 | 20 | typeset project_root benchmark_output_dir current_label baseline_label 21 | project_root="$(pkg_root)" 22 | benchmark_output_dir="${OUT:-${project_root}/tests/benchmark_results/}" 23 | 24 | # Labels 25 | current_label="${NAME:-new}" 26 | baseline_label="${BASELINE:-baseline}" 27 | 28 | # Output file for diff results 29 | typeset comparison_output_file="${benchmark_output_dir}bench.diff.txt" 30 | 31 | typeset significance_level change_threshold 32 | significance_level="${ALPHA:-0.05}" 33 | change_threshold="${THRESHOLD:-5}" # percent 34 | 35 | # Benchmark pkgs to execute 36 | typeset -a benchmark_pkgs=( 37 | "${project_root}/tests/printf/" 38 | "${project_root}/tests/stacktraceregion/" 39 | "${project_root}/tests/stacktracealways/" 40 | ) 41 | 42 | # Clear previous comparison file 43 | truncate -s 0 "${comparison_output_file}" 44 | 45 | # Compare each benchmark file 46 | for benchmark in "${benchmark_pkgs[@]}"; do 47 | typeset benchmark_base_name 48 | benchmark_base_name="$(basename "${benchmark}")" 49 | 50 | typeset baseline_file="${benchmark_output_dir}${benchmark_base_name}.${baseline_label}.bench.txt" 51 | typeset current_file="${benchmark_output_dir}${benchmark_base_name}.${current_label}.bench.txt" 52 | 53 | benchstat \ 54 | -alpha="${significance_level}" \ 55 | "${baseline_file}" \ 56 | "${current_file}" | 57 | tee -a "${comparison_output_file}" 58 | done 59 | -------------------------------------------------------------------------------- /tests/scripts/run_benchmarks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # Get the root directory of this project 5 | pkg_root() { 6 | typeset project_root 7 | project_root="$(realpath -- "$0")" 8 | project_root="${project_root%/*}/" 9 | 10 | while [[ -n "${project_root}" ]]; do 11 | typeset go_mod_file="${project_root}/go.mod" 12 | if [[ -f "${go_mod_file}" ]] && grep --quiet 'module github.com/vvvvv/dlg' "${go_mod_file}" 2>/dev/null; then 13 | break 14 | fi 15 | project_root="${project_root%/*}" 16 | done 17 | printf '%s' "${project_root}" 18 | } 19 | 20 | typeset project_root benchmark_output_dir benchmark_label 21 | project_root="$(pkg_root)" 22 | 23 | # Directory to store benchmark results 24 | benchmark_output_dir="${OUTDIR:-${project_root}/tests/benchmark_results/}" 25 | 26 | # Label for the benchmark run (affects output file names) 27 | # Files are named: ..bench.txt 28 | benchmark_label="${NAME:-new}" 29 | 30 | # Benchmark settings 31 | typeset run_count bench_time max_procs 32 | run_count="${COUNT:-10}" 33 | bench_time="${BENCHTIME:-200ms}" 34 | max_procs="${GOMAXPROCS:-}" 35 | 36 | if [[ -n "${max_procs}" ]]; then 37 | export "${max_procs}" 38 | fi 39 | 40 | # Benchmark pkgs to execute 41 | typeset -a benchmark_pkgs=( 42 | "${project_root}/tests/printf/" 43 | "${project_root}/tests/stacktraceregion/" 44 | "${project_root}/tests/stacktracealways/" 45 | ) 46 | 47 | # Run each benchmark file 48 | for benchmark in "${benchmark_pkgs[@]}"; do 49 | typeset benchmark_base_name 50 | benchmark_base_name="$(basename "${benchmark}")" 51 | 52 | if [[ -n "${benchmark_label}" ]]; then 53 | typeset benchmark_result_file="${benchmark_output_dir}${benchmark_base_name}.${benchmark_label}.bench.txt" 54 | else 55 | typeset benchmark_result_file="${benchmark_output_dir}${benchmark_base_name}.bench.txt" 56 | fi 57 | 58 | # Load environment variables specific to this benchmark 59 | source "${project_root}/tests/scripts/${benchmark_base_name}.env" 60 | 61 | # Run benchmark and save results 62 | go test \ 63 | -tags dlg \ 64 | -run=^$ \ 65 | -bench=. \ 66 | -benchmem \ 67 | -count="${run_count}" \ 68 | -benchtime="${bench_time}" \ 69 | "${benchmark}" | 70 | tee "${benchmark_result_file}" 71 | done 72 | -------------------------------------------------------------------------------- /.github/workflows/precommit.yml: -------------------------------------------------------------------------------- 1 | name: Precommit 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | precommit: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | # Fetch entire commit history to get base ref for PRs 12 | fetch-depth: 0 13 | - uses: actions/setup-go@v5 14 | with: 15 | go-version: '1.x' 16 | - name: Run pre-commit checks 17 | run: | 18 | #!/usr/bin/env bash 19 | set -euo pipefail 20 | 21 | # Track overall success 22 | typeset -i overall_success=0 23 | 24 | # 1. Check code formatting 25 | echo "Checking code formatting..." 26 | if command -v gofmt &>/dev/null; then 27 | if ! gofmt_output=$(gofmt -l . 2>&1); then 28 | echo "Error: gofmt failed" 29 | overall_success=1 30 | elif [[ -n "$gofmt_output" ]]; then 31 | echo "Error: Unformatted Go files found:" 32 | echo "$gofmt_output" 33 | overall_success=1 34 | fi 35 | fi 36 | 37 | # 2. Check build tags in all Go files 38 | echo "Checking build tags..." 39 | typeset -i tag_errors=0 40 | # Find all Go files in the repository 41 | while IFS= read -r -d $'\0' file; do 42 | first_line=$(head -n1 "$file") 43 | if [[ "$first_line" =~ ^//.*dlg.*$ ]]; then 44 | if [[ ! "$first_line" =~ ^//go:build\ [^\ ] ]]; then 45 | printf '\nError: Malformed build tag:\n' 46 | printf ' File: %s\n' "$file" 47 | printf ' Found: %s\n' "$first_line" 48 | printf ' Expected: //go:build \n' 49 | tag_errors=$((tag_errors + 1)) 50 | fi 51 | fi 52 | done < <(find . -name '*.go' -print0) 53 | 54 | if ((tag_errors > 0)); then 55 | echo "Found $tag_errors build tag errors" 56 | overall_success=1 57 | fi 58 | 59 | # Final result 60 | if ((overall_success == 0)); then 61 | echo "All checks passed" 62 | exit 0 63 | else 64 | echo "Checks failed" 65 | exit 1 66 | fi 67 | -------------------------------------------------------------------------------- /colorize.go: -------------------------------------------------------------------------------- 1 | //go:build dlg 2 | 3 | package dlg 4 | 5 | import ( 6 | "bytes" 7 | "strings" 8 | ) 9 | 10 | // colorizeOrDont gets set when -ldflags "-X 'github.com/vvvvv/dlg.DLG_COLOR=1'" 11 | var colorizeOrDont = func(buf *[]byte) {} 12 | 13 | // resetColorOrDont gets set when -ldflags "-X 'github.com/vvvvv/dlg.DLG_COLOR=1'" 14 | var resetColorOrDont = func(buf *[]byte) {} 15 | 16 | func colorize(buf *[]byte) { 17 | *buf = append(*buf, termColor...) 18 | } 19 | 20 | // colorResets resets the terminal color sequence by appending \033[0m . 21 | func colorReset(buf *[]byte) { 22 | // *buf = append(*buf, resetColor...) 23 | *buf = append(*buf, 27, 91, 48, 109) 24 | } 25 | 26 | // colorArgToTermColor converts a string to a terminal color escape sequence. 27 | // It accepts: 28 | // 29 | // "red" 30 | // "YELLOW" 31 | // "Black" 32 | // "\033[38;5;2m" 33 | // string([]byte{27, 91, 51, 56, 59, 53, 59, 49, 109}) 34 | // "3" 35 | // 36 | // The function does only naive validation and doesn't guaranty the returned sequence is a valid terminal escape sequence. 37 | func colorArgToTermColor(arg string) (color []byte, ok bool) { 38 | c := bytes.TrimSpace([]byte(arg)) 39 | 40 | if len(c) > 0 { 41 | if c[0] == '\\' || c[0] == 27 { 42 | // Starts with \ or ESC, assume it's a escaped term color 43 | if len(c) >= 4 && bytes.Equal(c[:4], []byte("\\033")) { 44 | _ = c[3:] 45 | c = c[3:] 46 | c[0] = 27 47 | ok = true 48 | } 49 | return c, ok 50 | } else { 51 | is_number := (c[0] >= 48 && c[0] <= 57) 52 | is_alpha := (c[0] >= 65 && c[0] <= 122) 53 | 54 | color = make([]byte, 0, 32) 55 | // ESC 56 | color = append(color, 27) 57 | // Start of esc sequence 58 | color = append(color, "[38;5;"...) 59 | if is_number { 60 | // Assume it's a terminal color code e.g. 61 | // 1 for red 62 | ok = true 63 | color = append(color, c...) 64 | } else if is_alpha { 65 | // Assume it's a color name 66 | name := strings.ToLower(arg) 67 | for i, n := range []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"} { 68 | if name == n { 69 | ok = true 70 | color = append(color, byte('0'+i)) 71 | } 72 | } 73 | } 74 | // Add suffix 75 | if color[len(color)-1] == ';' { 76 | color = append(color, "1m"...) 77 | } else { 78 | color = append(color, ";1m"...) 79 | } 80 | } 81 | } 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /tests/internal/parselines.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | type logline struct { 11 | line string 12 | trace string 13 | } 14 | 15 | func (l logline) String() string { 16 | if l.trace != "" { 17 | return fmt.Sprintf("%s\n%s\n", l.line, l.trace) 18 | } 19 | return fmt.Sprintf("%s\n", l.line) 20 | } 21 | 22 | func (l logline) Line() string { 23 | lines := logSingleLineRegexp.FindStringSubmatch(l.line) 24 | if len(lines) > 1 { 25 | return strings.TrimSpace(lines[1]) 26 | } 27 | 28 | return "" 29 | } 30 | 31 | func (l logline) HasTrace() bool { 32 | return l.trace != "" 33 | } 34 | 35 | var ( 36 | // Gotta love writing regexes 37 | // This matches: 38 | // v everything until there (including possible stack traces) 39 | // 00:09:45 [4µs] main.go:16: foo 01:19:55 [8s] main.go:36: bar 40 | logLineRegexp = regexp.MustCompile(`\d{2}:\d{2}:\d{2}\s+\[[^\]]+\]\s+\S+\.go:\d+:\s+.*?`) 41 | traceRegexp = regexp.MustCompile(`(?:\S+\([^)]*\)\s+\S+\.go:\d+\s+\+0x[0-9A-Fa-f]+\s+)+`) 42 | 43 | logSingleLineRegexp = regexp.MustCompile(`\d{2}:\d{2}:\d{2}\s+\[\d+\.?\d*.?s\]\s+\w+\.go:\d+:\s(.*)$`) 44 | ) 45 | 46 | // ParseLines parses dlg.Printf output into line, trace. 47 | // This is done to simplify tests and should never be used outside of tests. 48 | func ParseLines(b []byte) []logline { 49 | b = bytes.ReplaceAll(b, []byte("\n"), []byte(" ")) 50 | loglines := make([]logline, 0, 32) 51 | 52 | for len(b) != 0 { 53 | // Find log line 54 | lineLoc := logLineRegexp.FindIndex(b) 55 | 56 | if lineLoc == nil { 57 | break 58 | } 59 | var ( 60 | from int 61 | to int 62 | ) 63 | 64 | from = lineLoc[0] 65 | 66 | if next := logLineRegexp.FindIndex(b[from+1:]); next == nil { 67 | // No other match can be found. 68 | // This means we're at the last line -> b[from:len(b)] 69 | to = len(b) 70 | } else { 71 | // Slice to the beginning of the next match 72 | to = from + 1 + next[0] 73 | } 74 | 75 | if traceLoc := traceRegexp.FindIndex(b[from:to]); traceLoc == nil { 76 | // There's no stack trace 77 | loglines = append(loglines, logline{ 78 | line: string(b[from:to]), 79 | }) 80 | } else { 81 | // Found stack trace which means: 82 | // line: starts at 'from' ; ends at the beginning of the stacktrace 83 | // trace: starts at the beginning of the stacktrace ; ends at 'to' 84 | loglines = append(loglines, logline{ 85 | line: string(b[from:traceLoc[0]]), 86 | trace: string(b[traceLoc[0]:to]), 87 | }) 88 | } 89 | 90 | b = b[to:] 91 | } 92 | 93 | return loglines 94 | } 95 | -------------------------------------------------------------------------------- /tests/stacktracealways/printf_always_test.go: -------------------------------------------------------------------------------- 1 | //go:build dlg 2 | 3 | package dlg_test_stacktrace_always 4 | 5 | import ( 6 | "bytes" 7 | "testing" 8 | 9 | "github.com/vvvvv/dlg" 10 | "github.com/vvvvv/dlg/tests/internal" 11 | ) 12 | 13 | func TestPrintfStackTraceAlways(t *testing.T) { 14 | out := internal.CaptureOutput(func() { 15 | dlg.Printf("test message") 16 | }) 17 | 18 | lines := internal.ParseLines([]byte(out)) 19 | 20 | if len(lines) > 1 { 21 | // This should only happen if there's something wrong with internal.ParseLines 22 | t.Fatalf("Too many lines: %+v", lines) 23 | } 24 | 25 | got := lines[0] 26 | 27 | want := struct { 28 | line string 29 | trace bool 30 | }{ 31 | "test message", true, 32 | } 33 | 34 | if got.Line() != want.line || got.HasTrace() != want.trace { 35 | t.Errorf("Mismatch: want: %q (stacktrace: %v) ; got: %q (stacktrace: %v)", want.line, want.trace, got.Line(), got.HasTrace()) 36 | } 37 | } 38 | 39 | func BenchmarkPrintfTraceAlways16(b *testing.B) { 40 | var buf bytes.Buffer 41 | dlg.SetOutput(&buf) 42 | 43 | s := internal.RandomStrings(16) 44 | 45 | for i := 0; i < b.N; i++ { 46 | buf.Reset() 47 | dlg.Printf(s[i%len(s)]) 48 | } 49 | } 50 | 51 | func BenchmarkPrintfTraceAlways64(b *testing.B) { 52 | var buf bytes.Buffer 53 | dlg.SetOutput(&buf) 54 | 55 | s := internal.RandomStrings(64) 56 | 57 | for i := 0; i < b.N; i++ { 58 | buf.Reset() 59 | dlg.Printf(s[i%len(s)]) 60 | } 61 | } 62 | 63 | func BenchmarkPrintfTraceAlways128(b *testing.B) { 64 | var buf bytes.Buffer 65 | dlg.SetOutput(&buf) 66 | 67 | s := internal.RandomStrings(128) 68 | 69 | for i := 0; i < b.N; i++ { 70 | buf.Reset() 71 | dlg.Printf(s[i%len(s)]) 72 | } 73 | } 74 | 75 | func BenchmarkPrintfWithFormattingTraceAlways16(b *testing.B) { 76 | var buf bytes.Buffer 77 | dlg.SetOutput(&buf) 78 | 79 | s := internal.RandomStrings(16) 80 | 81 | for i := 0; i < b.N; i++ { 82 | buf.Reset() 83 | dlg.Printf(s[i%len(s)], i) 84 | } 85 | } 86 | 87 | func BenchmarkPrintfWithFormattingTraceAlways64(b *testing.B) { 88 | var buf bytes.Buffer 89 | dlg.SetOutput(&buf) 90 | 91 | s := internal.RandomStrings(64) 92 | 93 | for i := 0; i < b.N; i++ { 94 | buf.Reset() 95 | dlg.Printf(s[i%len(s)], i) 96 | } 97 | } 98 | 99 | func BenchmarkPrintfWithFormattingTraceAlways128(b *testing.B) { 100 | var buf bytes.Buffer 101 | dlg.SetOutput(&buf) 102 | 103 | s := internal.RandomStrings(128) 104 | 105 | for i := 0; i < b.N; i++ { 106 | buf.Reset() 107 | dlg.Printf(s[i%len(s)], i) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /noop.go: -------------------------------------------------------------------------------- 1 | //go:build !dlg 2 | 3 | package dlg 4 | 5 | import ( 6 | "io" 7 | ) 8 | 9 | /* 10 | Printf writes a formatted message to standard error when built with the dlg build tag. Formatting uses the same verbs as fmt (see https://pkg.go.dev/fmt#hdr-Printing). 11 | It also supports optional stack trace generation, configurable at runtime via environment variables. 12 | 13 | In builds without the dlg tag, Printf is a no-op. 14 | */ 15 | func Printf(fmt string, v ...any) {} 16 | 17 | /* 18 | SetOutput sets the output destination for Printf. 19 | While Printf itself is safe for concurrent use, this guarantee does not extend to custom writers. 20 | To ensure concurrency safety, the provided writer should implement [sync.Locker]. 21 | 22 | Calls to SetOutput should ideally be made at program initialization as they affect the logger globally. 23 | Changing outputs during program execution, while concurrency-safe, may cause logs to temporarily continue appearing in the previous output. Eventually, all logs will be written to the new output. 24 | For consistent output behavior, avoid changing writers after logging has started. 25 | */ 26 | func SetOutput(w io.Writer) {} 27 | 28 | /* 29 | StartTrace begins a tracing region. 30 | 31 | Tracing regions allow you to limit stack trace generation to specific parts of your code. 32 | When DLG_STACKTRACE is set to "REGION,ALWAYS", Printf calls inside an active tracing region 33 | will always include a stack trace. When set to "REGION,ERROR", only Printf calls inside the 34 | region that include an error argument will include a stack trace. 35 | 36 | A tracing region can be started with an optional key (any comparable value). If a key is 37 | provided, only a matching StopTrace call with the same key will end that region. Without a key, 38 | StopTrace ends the most recent active tracing region started in the same scope. 39 | 40 | Tracing regions follow LIFO (last-in, first-out) order, and their scope is tied to the function 41 | that called StartTrace. Even if StopTrace is called in a nested function, the region remains 42 | active until closed in the starting scope-unless explicitly stopped by a matching key. 43 | 44 | In builds without the dlg tag, StartTrace is a no-op. 45 | */ 46 | func StartTrace(v ...any) {} 47 | 48 | /* 49 | StopTrace ends a tracing region. 50 | 51 | If called without arguments, StopTrace ends the most recent active tracing region started 52 | in the same scope, regardless of whether it was keyed. If a key is provided, StopTrace ends 53 | only the region that was started with the matching key, allowing you to close regions from 54 | other functions or scopes. 55 | 56 | Tracing regions are closed in LIFO (last-in, first-out) order. 57 | 58 | In builds without the dlg tag, StopTrace is a no-op. 59 | */ 60 | func StopTrace(v ...any) {} 61 | -------------------------------------------------------------------------------- /docs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package dlg implements a zero-cost debug logger that compiles down to a no-op unless built using the build tag dlg. 3 | The package exports just two functions: dlg.Printf(format string, v ...any) and dlg.SetOutput(w io.Writer). 4 | The format string uses the same verbs as fmt (see https://pkg.go.dev/fmt#hdr-Printing). 5 | 6 | Printf defaults to writing to standard error but SetOutput may be used to change the output destination. 7 | While Printf itself is safe for concurrent use, this guarantee does not extend to custom output writers. 8 | To ensure concurrency safety, the provided writer should implement sync.Locker (see https://pkg.go.dev/sync#Locker). 9 | 10 | Printf also supports optional stack trace generation, either if Printf contains an error, or on every call. 11 | Stack traces can be activated either at runtime by setting Environment variables or at compile time via a linker flag. 12 | 13 | Usage: 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/vvvvv/dlg" 21 | ) 22 | 23 | func risky() error { 24 | return fmt.Errorf("unexpected error") 25 | } 26 | 27 | func main() { 28 | fmt.Println("starting...") 29 | 30 | dlg.Printf("executing risky operation") 31 | err := risky() 32 | if err != nil { 33 | dlg.Printf("something failed: %v", err) 34 | } 35 | 36 | dlg.Printf("continuing") 37 | } 38 | 39 | Compiling without the dlg build tag: 40 | 41 | go build -o example 42 | ./example 43 | 44 | starting... 45 | 46 | Compiling with dlg activated: 47 | 48 | go build -tags=dlg -o example 49 | ./example 50 | 51 | ​ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 52 | ​ * * * * * * * * * * * * * * DEBUG BUILD * * * * * * * * * * * * * * 53 | ​ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 54 | ​ - DLG_STACKTRACE=ERROR show stack traces on errors 55 | ​ - DLG_STACKTRACE=ALWAYS show stack traces always 56 | ​ - DLG_NO_WARN=1 disable this message (use at your own risk) 57 | 58 | starting... 59 | 01:28:27 [2µs] main.go:16: executing risky operation 60 | 01:28:27 [21µs] main.go:19: something failed: unexpected error 61 | 01:28:27 [23µs] main.go:22: continuing 62 | 63 | Note: To abbreviate the example output below, the debug banner has been omitted. 64 | 65 | Including stack traces in the output we simply set an environment variable when executing the binary: 66 | 67 | DLG_STACKTRACE=ERROR ./example 68 | 69 | starting... 70 | 01:31:34 [2µs] main.go:16: executing risky operation 71 | 01:31:34 [21µs] main.go:19: something failed: unexpected error 72 | main.main() 73 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example01/main.go:19 +0xc0 74 | 75 | 01:31:34 [38µs] main.go:22: continuing 76 | 77 | Including stack traces every time Printf is called: 78 | 79 | DLG_STACKTRACE=ALWAYS ./example 80 | 81 | starting... 82 | 01:35:47 [2µs] main.go:16: executing risky operation 83 | main.main() 84 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example01/main.go:16 +0x6c 85 | 86 | 01:35:47 [34µs] main.go:19: something failed: unexpected error 87 | main.main() 88 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example01/main.go:19 +0xc0 89 | 90 | 01:35:47 [41µs] main.go:22: continuing 91 | main.main() 92 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example01/main.go:22 +0xdc 93 | 94 | Compiling with stack traces activated: 95 | 96 | go build -tags dlg -ldflags "-X 'github.com/vvvvv/dlg.DLG_STACKTRACE=ERROR'" 97 | 98 | Package Configuration: 99 | 100 | Outputs a stack trace if any of the arguments passed to Printf is of type error: 101 | 102 | export DLG_STACKTRACE=ERROR 103 | // or 104 | -ldflags "-X 'github.com/vvvvv/dlg.DLG_STACKTRACE=ERROR'" 105 | 106 | Outputs a stack trace *on every call* to Printf, regardless of the arguments: 107 | 108 | export DLG_STACKTRACE=ALWAYS 109 | // or 110 | -ldflags "-X 'github.com/vvvvv/dlg.DLG_STACKTRACE=ALWAYS'" 111 | 112 | Suppresses the debug banner printed at startup: 113 | 114 | // Note: DLG_NO_WARN cannot be set via -ldflags. This decision was made so debug builds will never accidentally land in production. 115 | DLG_NO_WARN=1 116 | */ 117 | package dlg 118 | -------------------------------------------------------------------------------- /tests/scripts/assert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # set -x 4 | 5 | _err(){ 6 | printf '%s ' "$@" >&2 7 | printf '\n' >&2 8 | exit 200 9 | } 10 | 11 | typeset -i ok_tests=0 failed_tests=0 total_tests=0 12 | 13 | _test_header(){ 14 | total_tests=$(( total_tests + 1)) 15 | printf -- ' --- Testing %s\n' "${1}" 16 | } 17 | 18 | _test_ok(){ 19 | ok_tests=$(( ok_tests + 1)) 20 | printf -- ' %-8s\n\n' 'OK' 21 | } 22 | 23 | _test_failed(){ 24 | failed_tests=$(( failed_tests + 1)) 25 | printf -- ' %-10s%s\n' 'FAILED' "${1}" 26 | if [[ ! -z "${2:+x}" ]]; then 27 | while read -r line; do 28 | printf -- ' %s\n' "${line}" 29 | 30 | done <<<"${2}" 31 | fi 32 | if [[ ! -z "${3:+x}" ]]; then 33 | printf -- ' %s\n\n' "${3}" 34 | fi 35 | printf '\n' 36 | } 37 | 38 | _test_synopses(){ 39 | printf '\n' 40 | printf -- ' Ran %s tests\n %-8s%4s\n %-8s%4s\n' "${total_tests}" "OK" "${ok_tests}" "FAILED" "${failed_tests}" 41 | exit "${failed_tests}" 42 | } 43 | 44 | typeset old_pwd="${PWD}" 45 | 46 | # Get absolute path to this packages root dir. 47 | # Starting from this scripts parent dir we traverse towards root. 48 | # The directory which contains this packages go.mod is the root dir. 49 | typeset pkg_root 50 | pkg_root="$(realpath -- "$0")" 51 | pkg_root="${pkg_root%/*}/" 52 | 53 | while [[ -n "${pkg_root}" ]]; do 54 | typeset go_mod="${pkg_root}/go.mod" 55 | if [[ -f "${go_mod}" ]] && grep --quiet 'module github.com/vvvvv/dlg' "${go_mod}" 2>/dev/null; then 56 | break 57 | fi 58 | pkg_root="${pkg_root%/*}" 59 | done 60 | 61 | if [[ -z "${pkg_root}" ]]; then 62 | _err "Failed to find go module root directory" 63 | fi 64 | 65 | # Version of dlg to test against 66 | # Used for the test codes go.mod file 67 | typeset version="${DLG_VERSION:-$(( cd "${pkg_root}"; git rev-list --all 2>/dev/null || echo "v0.0.0" ) | head -n1 )}" 68 | version="v0.0.0" #TODO: use the git hash 69 | 70 | # Temp dir for building the test code 71 | typeset tmp_dir 72 | tmp_dir="$(mktemp -d)" 73 | 74 | # This dir is going to be removed on error/exit - better make sure it isn't empty 75 | if [[ "$?" -ne 0 ]]; then 76 | _err "Failed to create tmp dir" 77 | fi 78 | 79 | # Cleanup temp files on error/exit 80 | trap "cd ${old_pwd}; rm -rf ${tmp_dir}" SIGINT SIGTERM EXIT 81 | 82 | # cd into tmp_dir to start building the test code 83 | cd "${tmp_dir}" 84 | 85 | typeset test_mod_name="assert_no_dlg" 86 | 87 | # Create go module 88 | go mod init "${test_mod_name}" 2>/dev/null 89 | 90 | if [[ "$?" -ne 0 || ! -s 'go.mod' ]]; then 91 | _err "Failed to initialize go module" 92 | fi 93 | 94 | 95 | # Change go.mod file so the local dlg package is being used 96 | cat >> go.mod < ${pkg_root} 101 | 102 | END 103 | 104 | typeset test_str="hi from fmt.Println" 105 | 106 | cat > main.go <&1)" 130 | if [[ "$?" -ne 0 ]]; then 131 | _test_failed "${go_build_out}" 132 | else 133 | _test_ok 134 | fi 135 | 136 | 137 | _test_header "if test string is being output" 138 | typeset test_output 139 | test_output="$(./${bin_name})" 140 | if [[ "${test_output}" != "${test_str}" ]]; then 141 | _test_failed "expected: '${test_str}' ; got: '${test_output}'" 142 | else 143 | _test_ok 144 | fi 145 | 146 | 147 | _test_header "if dlg API is not in compiled output when build without dlg tag" 148 | go tool objdump "${bin_name}" 2>/dev/null 1> objdump 149 | if grep --quiet 'dlg.Printf' 'objdump'; then 150 | # if ! go tool objdump "${bin_name}" | grep --quiet 'main'; then 151 | _test_failed "expected binary to not contain any reference to dlg.Printf but got:" "$(grep -A2 -B2 'dlg.Printf' 'objdump' )" 152 | else 153 | _test_ok 154 | fi 155 | 156 | # Delete binary to recompile with dlg tag 157 | rm "${bin_name}" 158 | 159 | _test_header "if debug banner is being printed when build with dlg" 160 | go_build_out="$(go build -tags dlg -o "${bin_name}" 2>&1)" 161 | test_output="$(./${bin_name} 2>&1)" 162 | if ! grep --quiet 'DEBUG BUILD' <<<"${test_output}"; then 163 | _test_failed "expected DEBUG BUILD OUTPUT" 164 | else 165 | _test_ok 166 | fi 167 | 168 | _test_synopses 169 | -------------------------------------------------------------------------------- /tests/printf/printf_test.go: -------------------------------------------------------------------------------- 1 | //go:build dlg 2 | 3 | package dlg_test 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "os" 9 | "regexp" 10 | "strings" 11 | "sync" 12 | "testing" 13 | 14 | "github.com/vvvvv/dlg" 15 | "github.com/vvvvv/dlg/tests/internal" 16 | ) 17 | 18 | var ( 19 | bannerRegexp = regexp.MustCompile(`^[\*\s]+DEBUG BUILD[\*\s]+$`) 20 | logLineRegexp = regexp.MustCompile(`\d{2}:\d{2}:\d{2} \[.*\] printf_test\.go:\d+: test message`) 21 | ) 22 | 23 | func TestPrintfBasic(t *testing.T) { 24 | out := internal.CaptureOutput(func() { 25 | dlg.Printf("test %s", "message") 26 | }) 27 | 28 | matched := logLineRegexp.MatchString(out) 29 | if !matched { 30 | t.Errorf("Output format mismatch. Got: %q ; Want: %q", out, "test message") 31 | } 32 | } 33 | 34 | func TestPrintfNoDebugBanner(t *testing.T) { 35 | out := internal.CaptureOutput(func() { 36 | dlg.Printf("different %s message", "test") 37 | }) 38 | 39 | matched := bannerRegexp.MatchString(out) 40 | if matched { 41 | t.Errorf("Expected no Debug Banner: Got: %q", out) 42 | } 43 | } 44 | 45 | func TestSetOutput(t *testing.T) { 46 | var buf bytes.Buffer 47 | 48 | dlg.SetOutput(&buf) 49 | defer dlg.SetOutput(os.Stderr) 50 | 51 | want := "custom output target" 52 | 53 | noOut := internal.CaptureOutput(func() { 54 | dlg.Printf(want) 55 | }) 56 | 57 | if strings.Contains(noOut, want) { 58 | t.Error("Output was written to stderr but shouldn't") 59 | } 60 | 61 | out := buf.String() 62 | 63 | if !strings.Contains(out, want) { 64 | t.Errorf("Expected output in Writer: Got: %q ; Want: %q", out, want) 65 | } 66 | } 67 | 68 | func TestPrintfConcurrentWriter(t *testing.T) { 69 | buf := struct { 70 | sync.Mutex 71 | bytes.Buffer 72 | }{} 73 | 74 | dlg.SetOutput(&buf) 75 | defer dlg.SetOutput(os.Stderr) 76 | 77 | n := 100 78 | 79 | var wg sync.WaitGroup 80 | for i := range n { 81 | wg.Add(1) 82 | go func() { 83 | defer wg.Done() 84 | dlg.Printf("message from #%v", i) 85 | }() 86 | } 87 | wg.Wait() 88 | 89 | logLines := strings.Split(buf.String(), "\n") 90 | logLines = logLines[:len(logLines)-1] // last element contains empty string 91 | 92 | if len(logLines) != n { 93 | t.Errorf("Expected %v log lines but got: %v", n, len(logLines)) 94 | } 95 | 96 | for n := 0; n < len(logLines); n++ { 97 | found := false 98 | want := fmt.Sprintf("message from %v", n) 99 | for _, line := range logLines { 100 | if strings.ContainsAny(line, want) { 101 | found = true 102 | } 103 | } 104 | 105 | if !found { 106 | t.Errorf("Expected log line %q not in buffer.", want) 107 | } 108 | 109 | } 110 | } 111 | 112 | func BenchmarkPrintf16(b *testing.B) { 113 | var buf bytes.Buffer 114 | dlg.SetOutput(&buf) 115 | 116 | s := internal.RandomStrings(16) 117 | 118 | for i := 0; i < b.N; i++ { 119 | buf.Reset() 120 | dlg.Printf(s[i%len(s)]) 121 | } 122 | } 123 | 124 | func BenchmarkPrintf64(b *testing.B) { 125 | var buf bytes.Buffer 126 | dlg.SetOutput(&buf) 127 | 128 | s := internal.RandomStrings(64) 129 | 130 | for i := 0; i < b.N; i++ { 131 | buf.Reset() 132 | dlg.Printf(s[i%len(s)]) 133 | } 134 | } 135 | 136 | func BenchmarkPrintf128(b *testing.B) { 137 | var buf bytes.Buffer 138 | dlg.SetOutput(&buf) 139 | 140 | s := internal.RandomStrings(128) 141 | 142 | for i := 0; i < b.N; i++ { 143 | buf.Reset() 144 | dlg.Printf(s[i%len(s)]) 145 | } 146 | } 147 | 148 | func BenchmarkPrintfWithFormatting16(b *testing.B) { 149 | var buf bytes.Buffer 150 | dlg.SetOutput(&buf) 151 | 152 | s := internal.RandomStrings(16) 153 | 154 | for i := 0; i < b.N; i++ { 155 | buf.Reset() 156 | dlg.Printf(s[i%len(s)], i) 157 | } 158 | } 159 | 160 | func BenchmarkPrintfWithFormatting64(b *testing.B) { 161 | var buf bytes.Buffer 162 | dlg.SetOutput(&buf) 163 | 164 | s := internal.RandomStrings(64) 165 | 166 | for i := 0; i < b.N; i++ { 167 | buf.Reset() 168 | dlg.Printf(s[i%len(s)], i) 169 | } 170 | } 171 | 172 | func BenchmarkPrintfWithFormatting128(b *testing.B) { 173 | var buf bytes.Buffer 174 | dlg.SetOutput(&buf) 175 | 176 | s := internal.RandomStrings(128) 177 | 178 | for i := 0; i < b.N; i++ { 179 | buf.Reset() 180 | dlg.Printf(s[i%len(s)], i) 181 | } 182 | } 183 | 184 | type safeBuffer struct { 185 | sync.Mutex 186 | bytes.Buffer 187 | } 188 | 189 | var safeBuf = &safeBuffer{} 190 | 191 | // TODO: find out whats happening here 192 | // sometimes there's a huge spike in Bytes/Op - why? 193 | // func BenchmarkPrintf128Parallel(b *testing.B) { 194 | // s := internal.RandomStrings(64) 195 | // strCount := len(s) 196 | // dlg.SetOutput(safeBuf) 197 | // 198 | // b.RunParallel(func(pb *testing.PB) { 199 | // i := 0 200 | // for pb.Next() { 201 | // if i == strCount { 202 | // i = 0 203 | // } 204 | // str := s[i] 205 | // dlg.Printf(str) 206 | // } 207 | // }) 208 | // } 209 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | REL_MAKEFILE_PATH := $(lastword $(MAKEFILE_LIST)) 4 | PROJECT_ROOT := $(abspath $(dir $(REL_MAKEFILE_PATH))) 5 | GO := go 6 | GO_BIN := $(shell $(GO) env GOPATH)/bin 7 | BENCHSTAT := $(GO_BIN)/benchstat 8 | 9 | # Tests 10 | GOFLAGS ?= 11 | TESTFLAGS ?= -count=1 12 | GO_TEST := $(GO) test $(GOFLAGS) $(TESTFLAGS) 13 | SCRIPTS_DIR := $(PROJECT_ROOT)/tests/scripts 14 | TESTS_DIR := $(PROJECT_ROOT)/tests 15 | 16 | # Benchmarks 17 | BENCH_COUNT ?= 10 18 | BENCH_TIME ?= 200ms 19 | BENCH_OUTPUT_LABEL ?= new 20 | BENCH_BASELINE_LABEL ?= baseline 21 | GOMAXPROCS ?= 22 | BENCH_DIR := $(join $(TESTS_DIR)/, benchmark_results) 23 | COVER_DIR := $(join $(TESTS_DIR)/, coverage) 24 | COVER_MERGED_DIR := $(join $(COVER_DIR)/, merged) 25 | 26 | # Env Vars for running test suits/code coverage 27 | ENV_printf := DLG_NO_WARN=1 28 | ENV_stacktraceerror := DLG_NO_WARN=1 DLG_STACKTRACE=ERROR 29 | ENV_stacktracealways := DLG_NO_WARN=1 DLG_STACKTRACE=ALWAYS 30 | ENV_stacktraceregion := DLG_NO_WARN=1 DLG_STACKTRACE=REGION,ALWAYS 31 | ENV_stacktraceregiononerror := DLG_NO_WARN=1 DLG_STACKTRACE=REGION,ERROR 32 | 33 | # Run a test suite and set the correct environment 34 | define run_test 35 | $2 $(GO_TEST) -tags dlg $(TESTS_DIR)/$1 || exit_code=1; 36 | endef 37 | 38 | # Run code coverage and set the correct environment 39 | define run_code_coverage 40 | mkdir -p $(COVER_DIR)/$1; \ 41 | $2 $(GO) test -tags dlg -count=1 -covermode=atomic -coverpkg ./ ./tests/$1 -args -test.gocoverdir=$(COVER_DIR)/$1 || exit_code=1; 42 | endef 43 | 44 | .PHONY: help 45 | help: ## Show this help 46 | @awk 'BEGIN { FS=":.*## "; print "Targets:" } \ 47 | /^[a-zA-Z0-9_.-]+:.*## / { \ 48 | printf(" %-25s %s\n", $$1, $$2) \ 49 | }' $(REL_MAKEFILE_PATH) 50 | 51 | .PHONY: install-deps 52 | install-deps: $(BENCHSTAT) ## Install dependencies 53 | 54 | $(BENCHSTAT): 55 | $(GO) install golang.org/x/perf/cmd/benchstat@latest 56 | 57 | # 58 | # Tests 59 | # 60 | .PHONY: test 61 | test: ## Run all tests 62 | @exit_code=0; \ 63 | $(call run_test,printf,$(ENV_printf)) \ 64 | $(call run_test,stacktraceerror,$(ENV_stacktraceerror)) \ 65 | $(call run_test,stacktracealways,$(ENV_stacktracealways)) \ 66 | $(call run_test,stacktraceregion,$(ENV_stacktraceregion)) \ 67 | $(call run_test,stacktraceregiononerror,$(ENV_stacktraceregiononerror)) \ 68 | $(SCRIPTS_DIR)/assert.sh || exit_code=1; \ 69 | exit $$exit_code 70 | 71 | # 72 | # Benchmarks 73 | # 74 | $(BENCH_DIR): 75 | @mkdir -p $@ 76 | 77 | .PHONY: benchmark 78 | benchmark: $(BENCH_DIR) ## Run all benchmarks 79 | NAME="$(BENCH_OUTPUT_LABEL)" \ 80 | COUNT="$(BENCH_COUNT)" \ 81 | BENCHTIME="$(BENCH_TIME)" \ 82 | GOMAXPROCS="$(GOMAXPROCS)" \ 83 | $(SCRIPTS_DIR)/run_benchmarks.sh 84 | 85 | .PHONY: benchmark-baseline 86 | benchmark-baseline: $(BENCH_DIR) ## Record a new baseline benchmark set 87 | BASELINE="$(BENCH_BASELINE_LABEL)" \ 88 | NAME="$(BENCH_OUTPUT_LABEL)" \ 89 | COUNT="$(BENCH_COUNT)" \ 90 | BENCHTIME="$(BENCH_TIME)" \ 91 | GOMAXPROCS="$(GOMAXPROCS)" \ 92 | $(SCRIPTS_DIR)/run_benchmarks.sh 93 | 94 | .PHONY: benchmark-compare 95 | benchmark-compare: $(BENCHSTAT) | $(BENCH_DIR) ## Compare benchmarks to baseline benchmarks 96 | NAME="$(BENCH_OUTPUT_LABEL)" \ 97 | BASELINE="$(BENCH_BASELINE_LABEL)" \ 98 | $(SCRIPTS_DIR)/compare_benchmarks.sh 99 | 100 | # 101 | # Code coverage 102 | # 103 | $(COVER_DIR) $(COVER_MERGED_DIR): 104 | @mkdir -p $@ 105 | 106 | .PHONY: coverage-run 107 | coverage-run: | $(COVER_DIR) ## Run code coverage 108 | @exit_code=0; \ 109 | $(call run_code_coverage,printf,$(ENV_printf)) \ 110 | $(call run_code_coverage,stacktraceerror,$(ENV_stacktraceerror)) \ 111 | $(call run_code_coverage,stacktracealways,$(ENV_stacktracealways)) \ 112 | $(call run_code_coverage,stacktraceregion,$(ENV_stacktraceregion)) \ 113 | $(call run_code_coverage,stacktraceregiononerror,$(ENV_stacktraceregiononerror)) \ 114 | exit $$exit_code 115 | 116 | .PHONY: coverage-merge 117 | coverage-merge: | $(COVER_MERGED_DIR) ## Merge code coverage and merge into one report 118 | @$(GO) tool covdata merge \ 119 | -i=$(COVER_DIR)/printf,$(COVER_DIR)/stacktraceerror,$(COVER_DIR)/stacktracealways,$(COVER_DIR)/stacktraceregion,$(COVER_DIR)/stacktraceregiononerror \ 120 | -o=$(COVER_MERGED_DIR) 121 | @$(GO) tool covdata textfmt -i=$(COVER_MERGED_DIR) -o=$(COVER_DIR)/merged.cover 122 | @$(GO) tool cover -html=$(COVER_DIR)/merged.cover -o $(COVER_DIR)/coverage.html 123 | @echo "Coverage report generated: $(COVER_DIR)/coverage.html" 124 | @$(GO) tool cover -func=$(COVER_DIR)/merged.cover | grep total | awk '{printf ("Total coverage: %s\n", $$3) }'; 125 | 126 | .PHONY: coverage 127 | coverage: coverage-run coverage-merge ## Run coverage and merge 128 | 129 | # 130 | # Clean 131 | # 132 | 133 | .PHONY: clean-coverage 134 | clean-coverage: ## Remove coverage data 135 | @rm -rf \ 136 | $(COVER_DIR)/printf \ 137 | $(COVER_DIR)/stacktraceerror \ 138 | $(COVER_DIR)/stacktracealways \ 139 | $(COVER_DIR)/stacktraceregion \ 140 | $(COVER_DIR)/stacktraceregiononerror \ 141 | $(COVER_MERGED_DIR) \ 142 | $(COVER_DIR)/merged.cover \ 143 | $(COVER_DIR)/coverage.html 144 | @echo "Cleaned up coverage data" 145 | 146 | .PHONY: clean-examples 147 | clean-examples: ## Remove example build artifacts 148 | @rm -rf \ 149 | $(PROJECT_ROOT)/examples/example01/example01.dlg.bin \ 150 | $(PROJECT_ROOT)/examples/example01/example01_linker_flags.dlg.bin \ 151 | $(PROJECT_ROOT)/examples/example02/example02.dlg.bin \ 152 | $(PROJECT_ROOT)/examples/example03/example03.bin \ 153 | $(PROJECT_ROOT)/examples/example03/example03.dlg.bin \ 154 | $(PROJECT_ROOT)/examples/example03/example03.dlg.objdump \ 155 | $(PROJECT_ROOT)/examples/example03/example03.objdump 156 | @echo "Cleaned up examples" 157 | 158 | .PHONY: clean-benchmark-results 159 | clean-benchmark-results: ## Remove benchmark results except baseline 160 | @find '$(BENCH_DIR)' -type f ! -name '*.baseline.bench.txt' -exec rm -f {} +; 161 | @echo "Cleaned up benchmark results" 162 | 163 | .PHONY: clean 164 | clean: clean-examples clean-coverage clean-benchmark-results ## Clean all 165 | 166 | -------------------------------------------------------------------------------- /tests/benchmark_results/printf.baseline.bench.txt: -------------------------------------------------------------------------------- 1 | goos: darwin 2 | goarch: arm64 3 | pkg: github.com/vvvvv/dlg/tests/printf 4 | cpu: Apple M3 Pro 5 | BenchmarkPrintf16-12 664735 327.5 ns/op 296 B/op 4 allocs/op 6 | BenchmarkPrintf16-12 699079 324.6 ns/op 296 B/op 4 allocs/op 7 | BenchmarkPrintf16-12 688155 325.0 ns/op 296 B/op 4 allocs/op 8 | BenchmarkPrintf16-12 692906 327.2 ns/op 296 B/op 4 allocs/op 9 | BenchmarkPrintf16-12 694678 327.0 ns/op 296 B/op 4 allocs/op 10 | BenchmarkPrintf16-12 692240 326.9 ns/op 296 B/op 4 allocs/op 11 | BenchmarkPrintf16-12 688887 327.8 ns/op 296 B/op 4 allocs/op 12 | BenchmarkPrintf16-12 694276 327.3 ns/op 296 B/op 4 allocs/op 13 | BenchmarkPrintf16-12 695316 347.8 ns/op 296 B/op 4 allocs/op 14 | BenchmarkPrintf16-12 697944 327.0 ns/op 296 B/op 4 allocs/op 15 | BenchmarkPrintf64-12 673754 332.3 ns/op 296 B/op 4 allocs/op 16 | BenchmarkPrintf64-12 677025 339.8 ns/op 296 B/op 4 allocs/op 17 | BenchmarkPrintf64-12 677685 334.1 ns/op 296 B/op 4 allocs/op 18 | BenchmarkPrintf64-12 671916 333.1 ns/op 296 B/op 4 allocs/op 19 | BenchmarkPrintf64-12 673714 334.9 ns/op 296 B/op 4 allocs/op 20 | BenchmarkPrintf64-12 678349 333.1 ns/op 296 B/op 4 allocs/op 21 | BenchmarkPrintf64-12 682383 332.7 ns/op 296 B/op 4 allocs/op 22 | BenchmarkPrintf64-12 676278 331.4 ns/op 296 B/op 4 allocs/op 23 | BenchmarkPrintf64-12 685608 332.4 ns/op 296 B/op 4 allocs/op 24 | BenchmarkPrintf64-12 675603 332.4 ns/op 296 B/op 4 allocs/op 25 | BenchmarkPrintf128-12 661794 336.9 ns/op 296 B/op 4 allocs/op 26 | BenchmarkPrintf128-12 673754 337.5 ns/op 296 B/op 4 allocs/op 27 | BenchmarkPrintf128-12 675603 335.4 ns/op 296 B/op 4 allocs/op 28 | BenchmarkPrintf128-12 676786 334.8 ns/op 296 B/op 4 allocs/op 29 | BenchmarkPrintf128-12 669805 333.9 ns/op 296 B/op 4 allocs/op 30 | BenchmarkPrintf128-12 675421 335.7 ns/op 296 B/op 4 allocs/op 31 | BenchmarkPrintf128-12 664927 336.0 ns/op 296 B/op 4 allocs/op 32 | BenchmarkPrintf128-12 669260 335.0 ns/op 296 B/op 4 allocs/op 33 | BenchmarkPrintf128-12 672440 335.0 ns/op 296 B/op 4 allocs/op 34 | BenchmarkPrintf128-12 659544 335.5 ns/op 296 B/op 4 allocs/op 35 | BenchmarkPrintfWithFormatting16-12 446034 495.1 ns/op 328 B/op 6 allocs/op 36 | BenchmarkPrintfWithFormatting16-12 464365 492.7 ns/op 328 B/op 6 allocs/op 37 | BenchmarkPrintfWithFormatting16-12 465800 492.3 ns/op 328 B/op 6 allocs/op 38 | BenchmarkPrintfWithFormatting16-12 467080 497.0 ns/op 328 B/op 6 allocs/op 39 | BenchmarkPrintfWithFormatting16-12 463980 498.8 ns/op 328 B/op 6 allocs/op 40 | BenchmarkPrintfWithFormatting16-12 462486 496.1 ns/op 328 B/op 6 allocs/op 41 | BenchmarkPrintfWithFormatting16-12 457092 491.1 ns/op 328 B/op 6 allocs/op 42 | BenchmarkPrintfWithFormatting16-12 456482 496.0 ns/op 328 B/op 6 allocs/op 43 | BenchmarkPrintfWithFormatting16-12 469586 491.3 ns/op 328 B/op 6 allocs/op 44 | BenchmarkPrintfWithFormatting16-12 462556 494.3 ns/op 328 B/op 6 allocs/op 45 | BenchmarkPrintfWithFormatting64-12 425763 513.0 ns/op 385 B/op 6 allocs/op 46 | BenchmarkPrintfWithFormatting64-12 438255 510.3 ns/op 385 B/op 6 allocs/op 47 | BenchmarkPrintfWithFormatting64-12 437659 516.1 ns/op 385 B/op 6 allocs/op 48 | BenchmarkPrintfWithFormatting64-12 432968 509.7 ns/op 385 B/op 6 allocs/op 49 | BenchmarkPrintfWithFormatting64-12 435420 512.6 ns/op 385 B/op 6 allocs/op 50 | BenchmarkPrintfWithFormatting64-12 427963 509.3 ns/op 385 B/op 6 allocs/op 51 | BenchmarkPrintfWithFormatting64-12 432108 511.4 ns/op 385 B/op 6 allocs/op 52 | BenchmarkPrintfWithFormatting64-12 432850 510.0 ns/op 385 B/op 6 allocs/op 53 | BenchmarkPrintfWithFormatting64-12 429542 513.1 ns/op 385 B/op 6 allocs/op 54 | BenchmarkPrintfWithFormatting64-12 441537 511.1 ns/op 384 B/op 6 allocs/op 55 | BenchmarkPrintfWithFormatting128-12 403702 553.0 ns/op 449 B/op 6 allocs/op 56 | BenchmarkPrintfWithFormatting128-12 405630 551.8 ns/op 449 B/op 6 allocs/op 57 | BenchmarkPrintfWithFormatting128-12 398064 551.3 ns/op 449 B/op 6 allocs/op 58 | BenchmarkPrintfWithFormatting128-12 398780 551.1 ns/op 449 B/op 6 allocs/op 59 | BenchmarkPrintfWithFormatting128-12 401959 553.9 ns/op 449 B/op 6 allocs/op 60 | BenchmarkPrintfWithFormatting128-12 400141 551.9 ns/op 449 B/op 6 allocs/op 61 | BenchmarkPrintfWithFormatting128-12 405122 552.3 ns/op 449 B/op 6 allocs/op 62 | BenchmarkPrintfWithFormatting128-12 389916 553.0 ns/op 449 B/op 6 allocs/op 63 | BenchmarkPrintfWithFormatting128-12 403200 550.4 ns/op 449 B/op 6 allocs/op 64 | BenchmarkPrintfWithFormatting128-12 403502 552.8 ns/op 449 B/op 6 allocs/op 65 | PASS 66 | ok github.com/vvvvv/dlg/tests/printf 13.849s 67 | -------------------------------------------------------------------------------- /tests/benchmark_results/stacktracealways.baseline.bench.txt: -------------------------------------------------------------------------------- 1 | goos: darwin 2 | goarch: arm64 3 | pkg: github.com/vvvvv/dlg/tests/stacktracealways 4 | cpu: Apple M3 Pro 5 | BenchmarkPrintfTraceAlways16-12 156034 1488 ns/op 561 B/op 6 allocs/op 6 | BenchmarkPrintfTraceAlways16-12 158491 1486 ns/op 561 B/op 6 allocs/op 7 | BenchmarkPrintfTraceAlways16-12 156771 1481 ns/op 561 B/op 6 allocs/op 8 | BenchmarkPrintfTraceAlways16-12 160177 1493 ns/op 561 B/op 6 allocs/op 9 | BenchmarkPrintfTraceAlways16-12 153790 1492 ns/op 561 B/op 6 allocs/op 10 | BenchmarkPrintfTraceAlways16-12 155814 1500 ns/op 561 B/op 6 allocs/op 11 | BenchmarkPrintfTraceAlways16-12 155017 1490 ns/op 561 B/op 6 allocs/op 12 | BenchmarkPrintfTraceAlways16-12 156136 1482 ns/op 561 B/op 6 allocs/op 13 | BenchmarkPrintfTraceAlways16-12 152907 1489 ns/op 561 B/op 6 allocs/op 14 | BenchmarkPrintfTraceAlways16-12 157628 1490 ns/op 561 B/op 6 allocs/op 15 | BenchmarkPrintfTraceAlways64-12 155919 1498 ns/op 562 B/op 6 allocs/op 16 | BenchmarkPrintfTraceAlways64-12 158004 1492 ns/op 562 B/op 6 allocs/op 17 | BenchmarkPrintfTraceAlways64-12 159169 1500 ns/op 562 B/op 6 allocs/op 18 | BenchmarkPrintfTraceAlways64-12 155601 1498 ns/op 562 B/op 6 allocs/op 19 | BenchmarkPrintfTraceAlways64-12 151498 1497 ns/op 562 B/op 6 allocs/op 20 | BenchmarkPrintfTraceAlways64-12 154580 1492 ns/op 562 B/op 6 allocs/op 21 | BenchmarkPrintfTraceAlways64-12 156847 1489 ns/op 562 B/op 6 allocs/op 22 | BenchmarkPrintfTraceAlways64-12 153283 1501 ns/op 562 B/op 6 allocs/op 23 | BenchmarkPrintfTraceAlways64-12 156454 1495 ns/op 562 B/op 6 allocs/op 24 | BenchmarkPrintfTraceAlways64-12 155238 1494 ns/op 562 B/op 6 allocs/op 25 | BenchmarkPrintfTraceAlways128-12 154558 1505 ns/op 563 B/op 6 allocs/op 26 | BenchmarkPrintfTraceAlways128-12 154057 1497 ns/op 563 B/op 6 allocs/op 27 | BenchmarkPrintfTraceAlways128-12 152409 1499 ns/op 563 B/op 6 allocs/op 28 | BenchmarkPrintfTraceAlways128-12 155166 1505 ns/op 563 B/op 6 allocs/op 29 | BenchmarkPrintfTraceAlways128-12 155720 1504 ns/op 563 B/op 6 allocs/op 30 | BenchmarkPrintfTraceAlways128-12 151924 1504 ns/op 563 B/op 6 allocs/op 31 | BenchmarkPrintfTraceAlways128-12 157722 1499 ns/op 563 B/op 6 allocs/op 32 | BenchmarkPrintfTraceAlways128-12 155509 1503 ns/op 563 B/op 6 allocs/op 33 | BenchmarkPrintfTraceAlways128-12 155644 1493 ns/op 563 B/op 6 allocs/op 34 | BenchmarkPrintfTraceAlways128-12 155073 1499 ns/op 563 B/op 6 allocs/op 35 | BenchmarkPrintfWithFormattingTraceAlways16-12 165030 1415 ns/op 593 B/op 8 allocs/op 36 | BenchmarkPrintfWithFormattingTraceAlways16-12 164479 1413 ns/op 593 B/op 8 allocs/op 37 | BenchmarkPrintfWithFormattingTraceAlways16-12 161323 1410 ns/op 593 B/op 8 allocs/op 38 | BenchmarkPrintfWithFormattingTraceAlways16-12 166334 1417 ns/op 593 B/op 8 allocs/op 39 | BenchmarkPrintfWithFormattingTraceAlways16-12 165150 1416 ns/op 593 B/op 8 allocs/op 40 | BenchmarkPrintfWithFormattingTraceAlways16-12 162272 1417 ns/op 593 B/op 8 allocs/op 41 | BenchmarkPrintfWithFormattingTraceAlways16-12 164107 1424 ns/op 593 B/op 8 allocs/op 42 | BenchmarkPrintfWithFormattingTraceAlways16-12 168145 1416 ns/op 593 B/op 8 allocs/op 43 | BenchmarkPrintfWithFormattingTraceAlways16-12 165014 1416 ns/op 593 B/op 8 allocs/op 44 | BenchmarkPrintfWithFormattingTraceAlways16-12 168630 1409 ns/op 593 B/op 8 allocs/op 45 | BenchmarkPrintfWithFormattingTraceAlways64-12 161521 1440 ns/op 650 B/op 8 allocs/op 46 | BenchmarkPrintfWithFormattingTraceAlways64-12 160111 1447 ns/op 650 B/op 8 allocs/op 47 | BenchmarkPrintfWithFormattingTraceAlways64-12 160725 1448 ns/op 650 B/op 8 allocs/op 48 | BenchmarkPrintfWithFormattingTraceAlways64-12 160179 1439 ns/op 650 B/op 8 allocs/op 49 | BenchmarkPrintfWithFormattingTraceAlways64-12 163059 1440 ns/op 650 B/op 8 allocs/op 50 | BenchmarkPrintfWithFormattingTraceAlways64-12 161158 1442 ns/op 650 B/op 8 allocs/op 51 | BenchmarkPrintfWithFormattingTraceAlways64-12 159277 1446 ns/op 650 B/op 8 allocs/op 52 | BenchmarkPrintfWithFormattingTraceAlways64-12 160363 1435 ns/op 650 B/op 8 allocs/op 53 | BenchmarkPrintfWithFormattingTraceAlways64-12 162991 1440 ns/op 650 B/op 8 allocs/op 54 | BenchmarkPrintfWithFormattingTraceAlways64-12 158264 1441 ns/op 650 B/op 8 allocs/op 55 | BenchmarkPrintfWithFormattingTraceAlways128-12 155191 1484 ns/op 716 B/op 8 allocs/op 56 | BenchmarkPrintfWithFormattingTraceAlways128-12 158889 1478 ns/op 716 B/op 8 allocs/op 57 | BenchmarkPrintfWithFormattingTraceAlways128-12 156639 1487 ns/op 716 B/op 8 allocs/op 58 | BenchmarkPrintfWithFormattingTraceAlways128-12 158084 1484 ns/op 716 B/op 8 allocs/op 59 | BenchmarkPrintfWithFormattingTraceAlways128-12 159184 1482 ns/op 716 B/op 8 allocs/op 60 | BenchmarkPrintfWithFormattingTraceAlways128-12 157016 1486 ns/op 716 B/op 8 allocs/op 61 | BenchmarkPrintfWithFormattingTraceAlways128-12 156504 1482 ns/op 716 B/op 8 allocs/op 62 | BenchmarkPrintfWithFormattingTraceAlways128-12 157998 1480 ns/op 716 B/op 8 allocs/op 63 | BenchmarkPrintfWithFormattingTraceAlways128-12 155112 1482 ns/op 716 B/op 8 allocs/op 64 | BenchmarkPrintfWithFormattingTraceAlways128-12 156367 1479 ns/op 716 B/op 8 allocs/op 65 | PASS 66 | ok github.com/vvvvv/dlg/tests/stacktracealways 14.960s 67 | -------------------------------------------------------------------------------- /tests/benchmark_results/stacktraceregion.baseline.bench.txt: -------------------------------------------------------------------------------- 1 | goos: darwin 2 | goarch: arm64 3 | pkg: github.com/vvvvv/dlg/tests/stacktraceregion 4 | cpu: Apple M3 Pro 5 | BenchmarkPrintfWithRegion16-12 97921 2357 ns/op 1314 B/op 8 allocs/op 6 | BenchmarkPrintfWithRegion16-12 99145 2357 ns/op 1314 B/op 8 allocs/op 7 | BenchmarkPrintfWithRegion16-12 99495 2361 ns/op 1314 B/op 8 allocs/op 8 | BenchmarkPrintfWithRegion16-12 102667 2360 ns/op 1313 B/op 8 allocs/op 9 | BenchmarkPrintfWithRegion16-12 99608 2356 ns/op 1313 B/op 8 allocs/op 10 | BenchmarkPrintfWithRegion16-12 100038 2352 ns/op 1314 B/op 8 allocs/op 11 | BenchmarkPrintfWithRegion16-12 101982 2349 ns/op 1313 B/op 8 allocs/op 12 | BenchmarkPrintfWithRegion16-12 101299 2352 ns/op 1313 B/op 8 allocs/op 13 | BenchmarkPrintfWithRegion16-12 101356 2353 ns/op 1314 B/op 8 allocs/op 14 | BenchmarkPrintfWithRegion16-12 99660 2354 ns/op 1313 B/op 8 allocs/op 15 | BenchmarkPrintfWithRegion64-12 100665 2355 ns/op 1314 B/op 8 allocs/op 16 | BenchmarkPrintfWithRegion64-12 99675 2358 ns/op 1314 B/op 8 allocs/op 17 | BenchmarkPrintfWithRegion64-12 100634 2359 ns/op 1313 B/op 8 allocs/op 18 | BenchmarkPrintfWithRegion64-12 98767 2363 ns/op 1313 B/op 8 allocs/op 19 | BenchmarkPrintfWithRegion64-12 100252 2355 ns/op 1314 B/op 8 allocs/op 20 | BenchmarkPrintfWithRegion64-12 99363 2366 ns/op 1314 B/op 8 allocs/op 21 | BenchmarkPrintfWithRegion64-12 100363 2353 ns/op 1314 B/op 8 allocs/op 22 | BenchmarkPrintfWithRegion64-12 99505 2367 ns/op 1314 B/op 8 allocs/op 23 | BenchmarkPrintfWithRegion64-12 100146 2355 ns/op 1314 B/op 8 allocs/op 24 | BenchmarkPrintfWithRegion64-12 99794 2368 ns/op 1314 B/op 8 allocs/op 25 | BenchmarkPrintfWithRegionStartStop16-12 71858 3268 ns/op 1939 B/op 15 allocs/op 26 | BenchmarkPrintfWithRegionStartStop16-12 72444 3270 ns/op 1939 B/op 15 allocs/op 27 | BenchmarkPrintfWithRegionStartStop16-12 72727 3264 ns/op 1939 B/op 15 allocs/op 28 | BenchmarkPrintfWithRegionStartStop16-12 72656 3257 ns/op 1939 B/op 15 allocs/op 29 | BenchmarkPrintfWithRegionStartStop16-12 72261 3266 ns/op 1939 B/op 15 allocs/op 30 | BenchmarkPrintfWithRegionStartStop16-12 73155 3258 ns/op 1939 B/op 15 allocs/op 31 | BenchmarkPrintfWithRegionStartStop16-12 72758 3270 ns/op 1939 B/op 15 allocs/op 32 | BenchmarkPrintfWithRegionStartStop16-12 72054 3265 ns/op 1939 B/op 15 allocs/op 33 | BenchmarkPrintfWithRegionStartStop16-12 72150 3270 ns/op 1939 B/op 15 allocs/op 34 | BenchmarkPrintfWithRegionStartStop16-12 72757 3264 ns/op 1939 B/op 15 allocs/op 35 | BenchmarkPrintfWithRegionStartStop64-12 72344 3261 ns/op 1941 B/op 15 allocs/op 36 | BenchmarkPrintfWithRegionStartStop64-12 72361 3261 ns/op 1941 B/op 15 allocs/op 37 | BenchmarkPrintfWithRegionStartStop64-12 71476 3284 ns/op 1941 B/op 15 allocs/op 38 | BenchmarkPrintfWithRegionStartStop64-12 72309 3279 ns/op 1941 B/op 15 allocs/op 39 | BenchmarkPrintfWithRegionStartStop64-12 72030 3277 ns/op 1941 B/op 15 allocs/op 40 | BenchmarkPrintfWithRegionStartStop64-12 72450 3260 ns/op 1941 B/op 15 allocs/op 41 | BenchmarkPrintfWithRegionStartStop64-12 71744 3264 ns/op 1941 B/op 15 allocs/op 42 | BenchmarkPrintfWithRegionStartStop64-12 71991 3282 ns/op 1941 B/op 15 allocs/op 43 | BenchmarkPrintfWithRegionStartStop64-12 72802 3280 ns/op 1941 B/op 15 allocs/op 44 | BenchmarkPrintfWithRegionStartStop64-12 71098 3273 ns/op 1942 B/op 15 allocs/op 45 | BenchmarkPrintfWithRegionStartStopWithKey16-12 73630 3223 ns/op 1803 B/op 19 allocs/op 46 | BenchmarkPrintfWithRegionStartStopWithKey16-12 74010 3215 ns/op 1803 B/op 19 allocs/op 47 | BenchmarkPrintfWithRegionStartStopWithKey16-12 73924 3213 ns/op 1803 B/op 19 allocs/op 48 | BenchmarkPrintfWithRegionStartStopWithKey16-12 72540 3235 ns/op 1803 B/op 19 allocs/op 49 | BenchmarkPrintfWithRegionStartStopWithKey16-12 73609 3206 ns/op 1803 B/op 19 allocs/op 50 | BenchmarkPrintfWithRegionStartStopWithKey16-12 73185 3209 ns/op 1803 B/op 19 allocs/op 51 | BenchmarkPrintfWithRegionStartStopWithKey16-12 73526 3246 ns/op 1803 B/op 19 allocs/op 52 | BenchmarkPrintfWithRegionStartStopWithKey16-12 73690 3233 ns/op 1803 B/op 19 allocs/op 53 | BenchmarkPrintfWithRegionStartStopWithKey16-12 73581 3234 ns/op 1803 B/op 19 allocs/op 54 | BenchmarkPrintfWithRegionStartStopWithKey16-12 70851 3236 ns/op 1803 B/op 19 allocs/op 55 | BenchmarkPrintfWithRegionStartStopWithKey64-12 72043 3232 ns/op 1805 B/op 19 allocs/op 56 | BenchmarkPrintfWithRegionStartStopWithKey64-12 73339 3213 ns/op 1805 B/op 19 allocs/op 57 | BenchmarkPrintfWithRegionStartStopWithKey64-12 72700 3249 ns/op 1805 B/op 19 allocs/op 58 | BenchmarkPrintfWithRegionStartStopWithKey64-12 72750 3230 ns/op 1805 B/op 19 allocs/op 59 | BenchmarkPrintfWithRegionStartStopWithKey64-12 72753 3238 ns/op 1805 B/op 19 allocs/op 60 | BenchmarkPrintfWithRegionStartStopWithKey64-12 72826 3246 ns/op 1805 B/op 19 allocs/op 61 | BenchmarkPrintfWithRegionStartStopWithKey64-12 72552 3232 ns/op 1805 B/op 19 allocs/op 62 | BenchmarkPrintfWithRegionStartStopWithKey64-12 72879 3229 ns/op 1805 B/op 19 allocs/op 63 | BenchmarkPrintfWithRegionStartStopWithKey64-12 73000 3240 ns/op 1805 B/op 19 allocs/op 64 | BenchmarkPrintfWithRegionStartStopWithKey64-12 72884 3238 ns/op 1805 B/op 19 allocs/op 65 | PASS 66 | ok github.com/vvvvv/dlg/tests/stacktraceregion 16.052s 67 | -------------------------------------------------------------------------------- /trace.go: -------------------------------------------------------------------------------- 1 | //go:build dlg 2 | 3 | package dlg 4 | 5 | import ( 6 | "reflect" 7 | "runtime" 8 | "sync" 9 | "sync/atomic" 10 | ) 11 | 12 | const maxFrames = 64 13 | 14 | var pcPool = sync.Pool{ 15 | New: func() any { return make([]uintptr, maxFrames) }, 16 | } 17 | 18 | // writeStack appends a formatted stack trace to the provided byte buffer. 19 | // It captures stack frames starting from the actual caller until reaching go internal frames. 20 | // The stack trace contains: 21 | // 1. The caller function name (e.g. main.main() ) 22 | // 2. The file path and line number (e.g. main.go:69) 23 | // 3. The PC offset from the function entry in hexadecimal 24 | func writeStack(buf *[]byte) { 25 | // calldepth skips n frames to report the correct file and line number 26 | // 0 = runtime -> extern.go 27 | // 1 = writeStack -> trace.go 28 | // 2 = Printf -> printf.go 29 | // 3 = callerFn 30 | const calldepth = 3 31 | 32 | pcs := pcPool.Get().([]uintptr) 33 | 34 | n := runtime.Callers(calldepth, pcs) 35 | if n > maxFrames { 36 | n = maxFrames 37 | } 38 | pcs = pcs[:n] 39 | 40 | frames := runtime.CallersFrames(pcs) 41 | for { 42 | frame, more := frames.Next() 43 | 44 | fnName := frame.Function 45 | if isAtRuntimeCalldepth(fnName) { 46 | // If we've reached go internal frames don't descend any deeper. 47 | break 48 | } 49 | 50 | if fnName == "" { 51 | fnName = "unknown" 52 | } 53 | 54 | // Caller function name 55 | // e.g. main.main() 56 | *buf = append(*buf, fnName...) 57 | *buf = append(*buf, "()\n\t"...) 58 | 59 | // File name:line number 60 | *buf = append(*buf, frame.File...) 61 | *buf = append(*buf, ':') 62 | pad(buf, frame.Line, -1) 63 | 64 | // PC offset in hex 65 | off := uintptr(0) 66 | if frame.Entry != 0 && frame.PC >= frame.Entry { 67 | off = frame.PC - frame.Entry 68 | } 69 | *buf = append(*buf, " +0x"...) 70 | appendHex(buf, uint64(off)) 71 | *buf = append(*buf, '\n') 72 | 73 | if !more { 74 | break 75 | } 76 | } 77 | 78 | pcPool.Put(pcs[:cap(pcs)]) 79 | } 80 | 81 | // isAtRuntimeCalldepth checks if a frame's function is at runtime level depth. 82 | func isAtRuntimeCalldepth(fn string) bool { 83 | return fn == "runtime.main" || fn == "runtime.goexit" || fn == "testing.tRunner" 84 | } 85 | 86 | // appendHex converts n into hexadecimal for n >= 0 and appends it to buf. 87 | // Negative numbers are not handled. 88 | func appendHex(buf *[]byte, n uint64) { 89 | const hexd = "0123456789abcdef" 90 | var b [16]byte 91 | i := len(b) - 1 92 | 93 | _ = hexd[i] 94 | for ; (n > 0xF) && i >= 0; i-- { 95 | b[i] = hexd[n&0xF] 96 | n = n >> 4 97 | } 98 | b[i] = hexd[n&0xF] 99 | *buf = append(*buf, b[i:]...) 100 | } 101 | 102 | type caller struct { 103 | key []any 104 | id string 105 | pc uintptr 106 | lpc uintptr 107 | runFuncForPC uintptr 108 | frameEntry uintptr 109 | frameFuncEntry uintptr 110 | } 111 | 112 | var ( 113 | callersMu sync.RWMutex 114 | callersStore atomic.Value 115 | traceCount int32 116 | ) 117 | 118 | func deleteItemAt[T any](s []T, idx int) []T { 119 | _ = s[idx] 120 | res := make([]T, len(s)-1) 121 | copy(res[:idx], s[:idx]) 122 | copy(res[idx:], s[idx+1:]) 123 | return res 124 | } 125 | 126 | func StartTrace(v ...any) { 127 | startTrace(2, v) 128 | } 129 | 130 | // startTrace marks the current caller as a tracing region. 131 | // 132 | // skip controls how many stack frames above startTrace to skip before capturing 133 | // the region entry in order to get the actual callsite. 134 | // key is an optional identifier which may later be used to stop the matching region via stopTrace. 135 | // 136 | // Internally the function records the caller's function identifier and entry PC, 137 | // appending a new entry to callersStore. 138 | // 139 | // On error this function fails silently. 140 | func startTrace(skip int, key []any) { 141 | pc := make([]uintptr, 1) 142 | n := runtime.Callers(skip+1, pc) 143 | if n == 0 { 144 | return 145 | } 146 | 147 | pc = pc[:n] 148 | frames := runtime.CallersFrames(pc) 149 | 150 | frame, _ := frames.Next() 151 | if frame.Function == "" || frame.Func == nil { 152 | // We cannot identify the function. 153 | // This may happen in FFI. 154 | return 155 | } 156 | 157 | c := caller{key: key, id: frame.Function, pc: frame.Entry} 158 | 159 | callersMu.Lock() 160 | defer callersMu.Unlock() 161 | 162 | callers := callersStore.Load().([]caller) 163 | 164 | newCallers := make([]caller, len(callers)+1) 165 | copy(newCallers, callers) 166 | newCallers[len(callers)] = c 167 | callersStore.Store(newCallers) 168 | 169 | atomic.AddInt32(&traceCount, 1) 170 | } 171 | 172 | func StopTrace(v ...any) { 173 | stopTrace(2, v) 174 | } 175 | 176 | // stopTrace closes a previously started tracing region. 177 | // 178 | // It closes the the most recent started region. 179 | // If key is non-nil, the most recent region whose key matches is closed. 180 | // 181 | // On success callersStore is updated. 182 | // On error this function fails silently. 183 | func stopTrace(skip int, key []any) { 184 | if tc := atomic.LoadInt32(&traceCount); tc == 0 { 185 | // TODO: maybe panic here? 186 | return 187 | } 188 | 189 | var newCallers []caller 190 | 191 | if key != nil { 192 | // Check if we find a region with the matching key. 193 | // If we don't find one return. 194 | callersMu.Lock() 195 | defer callersMu.Unlock() 196 | callers := callersStore.Load().([]caller) 197 | for i := len(callers) - 1; i >= 0; i-- { 198 | c := callers[i] 199 | 200 | if reflect.DeepEqual(c.key, key) { 201 | // Found it. 202 | newCallers = deleteItemAt(callers, i) 203 | callersStore.Store(newCallers) 204 | atomic.AddInt32(&traceCount, -1) 205 | return 206 | } 207 | } 208 | 209 | // TODO: should this panic or fail silently? 210 | return 211 | } 212 | 213 | pc := make([]uintptr, 1) 214 | n := runtime.Callers(skip+1, pc) 215 | if n == 0 { 216 | return 217 | } 218 | 219 | pc = pc[:n] 220 | frames := runtime.CallersFrames(pc) 221 | 222 | frame, _ := frames.Next() 223 | if frame.Function == "" || frame.Func == nil { 224 | // We cannot identify the function. 225 | // This may happen in FFI. 226 | return 227 | } 228 | 229 | callersMu.Lock() 230 | defer callersMu.Unlock() 231 | callers := callersStore.Load().([]caller) 232 | 233 | // Check if this frame has an open region. 234 | for i := len(callers) - 1; i >= 0; i-- { 235 | c := callers[i] 236 | if c.id == frame.Function && c.pc == frame.Entry { 237 | // Found it. 238 | newCallers = deleteItemAt(callers, i) 239 | callersStore.Store(newCallers) 240 | atomic.AddInt32(&traceCount, -1) 241 | return 242 | } 243 | } 244 | return 245 | } 246 | 247 | // inTracingRegion reports whether any frame in the current call stack is inside a tracked tracing region. 248 | func inTracingRegion(skip int) bool { 249 | callers := callersStore.Load().([]caller) 250 | if len(callers) == 0 { 251 | return false 252 | } 253 | 254 | pcs := make([]uintptr, maxFrames) 255 | n := runtime.Callers(skip+1, pcs[:]) 256 | if n == 0 { 257 | return false 258 | } 259 | 260 | frames := runtime.CallersFrames(pcs[:n]) 261 | for i := 0; i < n; i++ { 262 | frame, more := frames.Next() 263 | if frame.Func == nil { 264 | // We don't know which function we're in. 265 | // This may happen in FFI 266 | // TODO: should we just break here? 267 | continue 268 | } 269 | entry := frame.Entry 270 | 271 | for j := len(callers) - 1; j >= 0; j-- { 272 | if callers[j].pc == entry { 273 | return true 274 | } 275 | } 276 | if !more { 277 | break 278 | } 279 | } 280 | return false 281 | } 282 | -------------------------------------------------------------------------------- /printf.go: -------------------------------------------------------------------------------- 1 | //go:build dlg 2 | 3 | package dlg 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "os" 9 | "runtime" 10 | "strings" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | ) 15 | 16 | const stackBufSize = 1024 17 | 18 | var ( 19 | writeOutput atomic.Value 20 | // Initial Printf buffer size 21 | bufSize = 128 22 | 23 | // DLG_STACKTRACE and DLG_COLORS must be set using linker flags only: 24 | // e.g. go build -tags dlg -ldflags "-X github.com/vvvvv/dlg.DLG_STACKTRACE=ALWAYS" 25 | // Packages importing dlg MUST NOT read from or write to this variable - doing so won't have any effect and will result in a compilation error when the dlg build tag is omitted. 26 | DLG_STACKTRACE = "" 27 | DLG_COLOR = "" 28 | 29 | termColor []byte 30 | ) 31 | 32 | // Include stack trace on error or on every call to Printf 33 | var stackflags = 0 34 | 35 | const ( 36 | onerror = 1 << iota 37 | always 38 | region 39 | ) 40 | 41 | // Buffers for Printf 42 | var bufPool = sync.Pool{ 43 | New: func() interface{} { return make([]byte, 0, bufSize) }, 44 | } 45 | 46 | func Printf(f string, v ...any) { 47 | b := bufPool.Get().([]byte) 48 | 49 | formatInfo(&b) 50 | if len(v) == 0 && strings.IndexByte(f, '%') < 0 { 51 | // If there's no formatting we take a fast path 52 | b = append(b, f...) 53 | b = append(b, '\n') 54 | } else { 55 | f += "\n" // Without this we get an 'non-constant format string in call' error when v is left empty. Annoying 56 | b = fmt.Appendf(b, f, v...) 57 | } 58 | 59 | if stackflags != 0 && 60 | ((stackflags&onerror != 0 && hasError(v)) || 61 | (stackflags&always != 0)) { 62 | 63 | if (stackflags®ion != 0 && inTracingRegion(1)) || (stackflags®ion == 0) { 64 | writeStack(&b) 65 | } 66 | } 67 | 68 | writeOut := writeOutput.Load().(writeOutputFn) 69 | writeOut(b) 70 | 71 | // Remove buffers with a capacity greater than 32kb from the sync.Pool in order to keep the footprint small 72 | if cap(b) >= (1 << 15) { 73 | b = nil 74 | } else { 75 | b = b[:0] 76 | } 77 | 78 | bufPool.Put(b) 79 | } 80 | 81 | // Set on package init 82 | var timeStart time.Time 83 | 84 | // formatInfo appends timestamp, elapsed time, and source location to the buffer. 85 | func formatInfo(buf *[]byte) { 86 | now := time.Now().UTC() 87 | since := now.Sub(timeStart).String() 88 | 89 | // Time in HH:MM:SS 90 | h, min, sec := now.Clock() 91 | padTime(buf, h, ':') 92 | padTime(buf, min, ':') 93 | padTime(buf, sec, 0) 94 | 95 | // Elapsed time 96 | elapsed(buf, &since) 97 | 98 | // Source file, line number 99 | callsite(buf) 100 | } 101 | 102 | // padTime formats hours, minutes, seconds as two digit values 103 | // with an optional delimiter suffix. 104 | func padTime(buf *[]byte, i int, delim byte) { 105 | if i < 10 { 106 | *buf = append(*buf, '0') 107 | *buf = append(*buf, byte('0'+i)) 108 | } else { 109 | q := i / 10 110 | *buf = append(*buf, byte('0'+q)) 111 | *buf = append(*buf, byte('0'+i-(q*10))) 112 | } 113 | if delim != 0 { 114 | *buf = append(*buf, delim) 115 | } 116 | } 117 | 118 | func elapsed(buf *[]byte, since *string) { 119 | *buf = append(*buf, " ["...) 120 | *buf = append(*buf, *since...) 121 | *buf = append(*buf, "] "...) 122 | } 123 | 124 | // pad i with zeros according to the specified width 125 | func pad(buf *[]byte, i int, width int) { 126 | width -= 1 127 | var b [20]byte 128 | bp := len(b) - 1 129 | // _ = b[bp] 130 | for ; (i >= 10 || width >= 1) && bp >= 0; width, bp = width-1, bp-1 { 131 | q := i / 10 132 | b[bp] = byte('0' + i - q*10) 133 | i = q 134 | } 135 | b[bp] = byte('0' + i) 136 | *buf = append(*buf, b[bp:]...) 137 | } 138 | 139 | // callsite Appends the filename and line number. 140 | // It optionally colors the output. 141 | func callsite(buf *[]byte) { 142 | // Calldepth skips n frames for reporting the correct file and line number 143 | // 0 = runtime -> extern.go 144 | // 1 = callsite -> printf.go 145 | // 2 = formatInfo -> printf.go 146 | // 3 = Printf -> printf.go 147 | // 4 = callerFn 148 | const calldepth = 4 149 | pcs := make([]uintptr, 1) 150 | n := runtime.Callers(calldepth, pcs) 151 | 152 | fileName := "no_file" 153 | lineNr := 0 154 | if n != 0 { 155 | frames := runtime.CallersFrames(pcs) 156 | frame, _ := frames.Next() 157 | 158 | fileName = frame.File 159 | for i := len(fileName) - 1; i > 0; i-- { 160 | if fileName[i] == '/' { 161 | fileName = fileName[i+1:] 162 | break 163 | } 164 | } 165 | 166 | lineNr = frame.Line 167 | } 168 | 169 | // File name:line number 170 | colorizeOrDont(buf) 171 | *buf = append(*buf, fileName...) 172 | *buf = append(*buf, ':') 173 | pad(buf, lineNr, -1) 174 | resetColorOrDont(buf) 175 | *buf = append(*buf, ": "...) 176 | } 177 | 178 | // hasError returns whether any argument is an error. 179 | func hasError(args []any) bool { 180 | for i := len(args) - 1; i >= 0; i-- { 181 | if _, ok := args[i].(error); ok { 182 | return true 183 | } 184 | } 185 | return false 186 | } 187 | 188 | type writeOutputFn func([]byte) (int, error) 189 | 190 | // defaultWriteOutput is the default output writer. 191 | // This function gets set by SetOutput. 192 | var defaultWriteOutput = func(buf []byte) (n int, err error) { 193 | return os.Stderr.Write(buf) 194 | } 195 | 196 | // SetOutput sets the output destination for Printf. 197 | // Defaults to os.Stderr. 198 | func SetOutput(w io.Writer) { 199 | if w == nil { 200 | w = io.Discard 201 | } 202 | 203 | var fn writeOutputFn 204 | if locker, ok := w.(sync.Locker); ok { 205 | fn = func(buf []byte) (n int, err error) { 206 | locker.Lock() 207 | n, err = w.Write(buf) 208 | locker.Unlock() 209 | return 210 | } 211 | } else { 212 | fn = func(buf []byte) (int, error) { 213 | return w.Write(buf) 214 | } 215 | } 216 | 217 | writeOutput.Store(fn) 218 | } 219 | 220 | func env(name string) (v string, ok bool) { 221 | const envPrefix = "DLG_" 222 | v, ok = os.LookupEnv(envPrefix + name) 223 | return strings.ToLower(v), ok 224 | } 225 | 226 | func init() { 227 | defer func() { 228 | timeStart = time.Now().UTC() 229 | }() 230 | 231 | // Warmup runtime.Caller and pre init the buffer pool. 232 | defer func() { 233 | // Warmup runtime.Caller 234 | runtime.Caller(0) 235 | 236 | // Pre init buffer pool 237 | bufPool.Put(make([]byte, 0, bufSize)) 238 | 239 | // Initialize trace region store 240 | callers := make([]caller, 0, 16) 241 | callersStore.Store(callers) 242 | }() 243 | 244 | defer func() { 245 | var writeOut writeOutputFn = defaultWriteOutput 246 | writeOutput.Store(writeOut) 247 | }() 248 | 249 | // Controls whether the debug banner is printed at startup. 250 | // Set to any value (even empty) other than "0" to suppress the message. 251 | noWarning, ok := env("NO_WARN") 252 | if !ok || noWarning == "0" { 253 | // TODO: decide whether to check if we're running in a tty and if this should cause the flag to be ignored 254 | fmt.Fprint( 255 | os.Stderr, 256 | `* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 257 | * * * * * * * * * * * * * * DEBUG BUILD * * * * * * * * * * * * * * 258 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 259 | - DLG_STACKTRACE=ERROR show stack traces on errors 260 | - DLG_STACKTRACE=ALWAYS show stack traces always 261 | - DLG_STACKTRACE=REGION show stack traces in trace regions 262 | - DLG_NO_WARN=1 disable this message (use at your own risk) 263 | 264 | `) 265 | } 266 | 267 | // Check if output should get colorized 268 | if color, ok := colorArgToTermColor(DLG_COLOR); ok { 269 | if _, noColor := os.LookupEnv("NO_COLOR"); noColor { 270 | // Respect NO_COLOR 271 | } else { 272 | termColor = color 273 | colorizeOrDont = colorize 274 | resetColorOrDont = colorReset 275 | } 276 | } 277 | 278 | // check if stack traces should get generated 279 | stacktrace := DLG_STACKTRACE 280 | if stacktrace == "" { 281 | if stacktrace, ok = env("STACKTRACE"); !ok { 282 | return 283 | } 284 | } 285 | 286 | // TODO: Should we fail hard if there's an invalid stack trace argument? 287 | // Notify the user about unrecognized stack trace arguments but use valid ones regardless. 288 | var err error 289 | stackflags, err = parseTraceArgs(stacktrace) 290 | if stackflags != 0 { 291 | // Increase initial buffer size to accommodate stack traces 292 | bufSize += int(stackBufSize) 293 | } 294 | 295 | if err != nil { 296 | fmt.Fprintf(os.Stderr, " dlg: Invalid Argument %v\n", err) 297 | } 298 | } 299 | 300 | func parseTraceArgs(arg string) (stackflags int, err error) { 301 | args := strings.Split(strings.ToLower(arg), ",") 302 | 303 | if len(args) > 2 { 304 | err = fmt.Errorf("DLG_STACKTRACE: too many arguments") 305 | return 306 | } 307 | 308 | // Should be a []error but errors.Join separates errors with newlines. 309 | // We want them seperated by ',' so we have to fall back to strings.Join here. 310 | var invalidArgsErr []string 311 | 312 | for i := 0; i < len(args); i++ { 313 | flag, parseErr := parseTraceOption(args[i]) 314 | if parseErr != nil { 315 | invalidArgsErr = append(invalidArgsErr, parseErr.Error()) 316 | } else { 317 | stackflags |= flag 318 | } 319 | } 320 | 321 | if len(invalidArgsErr) > 0 { 322 | err = fmt.Errorf("DLG_STACKTRACE: %s.\n", strings.Join(invalidArgsErr, ", ")) 323 | } 324 | return 325 | } 326 | 327 | func parseTraceOption(opt string) (stackflag int, err error) { 328 | const ( 329 | traceOptError = "err" 330 | traceOptAlways = "alw" 331 | traceOptRegion = "reg" 332 | ) 333 | 334 | err = fmt.Errorf("invalid argument %q", opt) 335 | if len(opt) < 3 { 336 | return 337 | } 338 | 339 | switch opt[:3] { 340 | case traceOptError: 341 | stackflag |= onerror 342 | err = nil 343 | case traceOptAlways: 344 | stackflag |= always 345 | err = nil 346 | case traceOptRegion: 347 | stackflag |= region 348 | err = nil 349 | } 350 | 351 | return 352 | } 353 | -------------------------------------------------------------------------------- /tests/stacktraceregion/region_test.go: -------------------------------------------------------------------------------- 1 | //go:build dlg 2 | 3 | package stacktraceregion_test 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/vvvvv/dlg" 11 | "github.com/vvvvv/dlg/tests/internal" 12 | ) 13 | 14 | func noTrace() { 15 | dlg.Printf("no trace") 16 | } 17 | 18 | func traceRegion() { 19 | dlg.StartTrace() 20 | defer dlg.StopTrace() 21 | 22 | dlg.Printf("trace this") 23 | } 24 | 25 | func traceRegionUntilStop() { 26 | dlg.StartTrace() 27 | 28 | dlg.Printf("trace this") 29 | dlg.StopTrace() 30 | 31 | dlg.Printf("don't trace this") 32 | } 33 | 34 | func traceClosure() { 35 | dlg.StartTrace() 36 | 37 | dlg.Printf("trace this") 38 | 39 | f := func() { 40 | dlg.Printf("trace closure 1") 41 | } 42 | 43 | g := func() { 44 | dlg.Printf("don't trace closure 2") 45 | } 46 | 47 | h := func() { 48 | dlg.StartTrace() 49 | defer dlg.StopTrace() 50 | 51 | dlg.Printf("trace closure 2") 52 | } 53 | 54 | dlg.Printf("trace this too") 55 | 56 | f() // trace this 57 | dlg.StopTrace() 58 | g() // don't trace this 59 | h() // trace this 60 | 61 | dlg.Printf("don't trace this") 62 | } 63 | 64 | func traceDeepStack() { 65 | f := func() { 66 | dlg.StartTrace() 67 | defer dlg.StopTrace() 68 | func() { 69 | func() { 70 | func() { 71 | dlg.Printf("trace deep stack call") 72 | }() 73 | }() 74 | }() 75 | } 76 | 77 | f() 78 | } 79 | 80 | func stopTraceOnlyAtScope() { 81 | dlg.StartTrace() 82 | 83 | f := func() { 84 | dlg.Printf("trace closure") 85 | dlg.StopTrace() 86 | } 87 | 88 | f() 89 | dlg.Printf("trace this") 90 | 91 | dlg.StopTrace() 92 | 93 | dlg.Printf("don't trace this") 94 | } 95 | 96 | func stopTraceIgnoreUnnecessaryCalls() { 97 | dlg.StopTrace() 98 | dlg.StopTrace() 99 | dlg.StartTrace() 100 | dlg.Printf("trace this") 101 | dlg.StopTrace() 102 | dlg.Printf("don't trace this") 103 | } 104 | 105 | func stopTraceOnKeySimple() { 106 | dlg.StartTrace(1) 107 | 108 | f := func() { 109 | dlg.Printf("trace this") 110 | dlg.StopTrace(1) 111 | } 112 | 113 | f() 114 | 115 | dlg.Printf("don't trace this") 116 | } 117 | 118 | func stopTraceOnKeyStruct() { 119 | dlg.StartTrace(struct{}{}) 120 | 121 | dlg.Printf("trace this") 122 | 123 | dlg.StopTrace("invalid key") 124 | 125 | dlg.Printf("trace this too") 126 | 127 | dlg.StopTrace(struct{}{}) 128 | 129 | dlg.Printf("don't trace this") 130 | } 131 | 132 | func stopTraceOnlyOnValidKey() { 133 | dlg.StartTrace("foo") 134 | 135 | dlg.Printf("trace this") 136 | 137 | dlg.StopTrace("invalid key") 138 | dlg.Printf("trace this too") 139 | 140 | dlg.StopTrace("foo") 141 | dlg.Printf("don't trace this") 142 | } 143 | 144 | func stopTraceNoKeyStopsActiveRegion() { 145 | dlg.StartTrace("abc") 146 | 147 | dlg.Printf("trace this") 148 | 149 | dlg.StopTrace() 150 | 151 | dlg.Printf("don't trace this") 152 | } 153 | 154 | func stopTraceNoKeyStopsOnlyScopeRegion() { 155 | dlg.StartTrace("abc") 156 | 157 | dlg.Printf("trace this") 158 | 159 | f := func() { 160 | dlg.StopTrace() 161 | 162 | dlg.Printf("trace this too") 163 | } 164 | 165 | f() 166 | 167 | dlg.Printf("trace this aswell") 168 | 169 | dlg.StopTrace() 170 | dlg.Printf("don't trace this") 171 | } 172 | 173 | func startTracingRegionOrPrintf(start bool, key any) { 174 | if start { 175 | dlg.StartTrace(key) 176 | return 177 | } 178 | dlg.Printf("start region or printf") 179 | } 180 | 181 | func tracingRegionAcrossGoroutines() { 182 | key := "key" 183 | started := make(chan struct{}) 184 | go func() { 185 | // start a region inside go routine 186 | startTracingRegionOrPrintf(true, key) 187 | close(started) 188 | }() 189 | 190 | <-started // region is active 191 | 192 | dlg.Printf("don't trace this") 193 | 194 | startTracingRegionOrPrintf(false, nil) 195 | 196 | dlg.StopTrace(key) 197 | 198 | startTracingRegionOrPrintf(false, nil) 199 | } 200 | 201 | func TestPrintfStackTraceRegion(t *testing.T) { 202 | type exp struct { 203 | line string 204 | trace bool 205 | } 206 | 207 | tcs := []struct { 208 | name string 209 | fn func() 210 | exp []exp 211 | }{ 212 | { 213 | name: "don't trace if no region is active", 214 | fn: noTrace, 215 | exp: []exp{ 216 | { 217 | "no trace", false, 218 | }, 219 | }, 220 | }, 221 | { 222 | name: "trace in region", 223 | fn: traceRegion, 224 | exp: []exp{ 225 | { 226 | "trace this", true, 227 | }, 228 | }, 229 | }, 230 | { 231 | name: "trace in region until stop", 232 | fn: traceRegionUntilStop, 233 | exp: []exp{ 234 | { 235 | "trace this", true, 236 | }, 237 | { 238 | "don't trace this", false, 239 | }, 240 | }, 241 | }, 242 | { 243 | name: "trace closure", 244 | fn: traceClosure, 245 | exp: []exp{ 246 | { 247 | "trace this", true, 248 | }, 249 | { 250 | "trace this too", true, 251 | }, 252 | { 253 | "trace closure 1", true, 254 | }, 255 | { 256 | "don't trace closure 2", false, 257 | }, 258 | { 259 | "trace closure 2", true, 260 | }, 261 | { 262 | "don't trace this", false, 263 | }, 264 | }, 265 | }, 266 | { 267 | name: "trace deep stack", 268 | fn: traceDeepStack, 269 | exp: []exp{ 270 | { 271 | "trace deep stack call", true, 272 | }, 273 | }, 274 | }, 275 | { 276 | name: "ignore unnecessary calls to StopTrace", 277 | fn: stopTraceIgnoreUnnecessaryCalls, 278 | exp: []exp{ 279 | { 280 | "trace this", true, 281 | }, 282 | { 283 | "don't trace this", false, 284 | }, 285 | }, 286 | }, 287 | { 288 | name: "stop tracing if StopTrace was called from the same scope", 289 | fn: stopTraceOnlyAtScope, 290 | exp: []exp{ 291 | { 292 | "trace closure", true, 293 | }, 294 | { 295 | "trace this", true, 296 | }, 297 | { 298 | "don't trace this", false, 299 | }, 300 | }, 301 | }, 302 | { 303 | name: "stop tracing if called with correct key regardless of scope", 304 | fn: stopTraceOnKeySimple, 305 | exp: []exp{ 306 | { 307 | "trace this", true, 308 | }, 309 | { 310 | "don't trace this", false, 311 | }, 312 | }, 313 | }, 314 | { 315 | name: "accept any key to start and stop a region", 316 | fn: stopTraceOnKeyStruct, 317 | exp: []exp{ 318 | { 319 | "trace this", true, 320 | }, 321 | { 322 | "trace this too", true, 323 | }, 324 | { 325 | "don't trace this", false, 326 | }, 327 | }, 328 | }, 329 | { 330 | name: "stop tracing only on valid key", 331 | fn: stopTraceOnlyOnValidKey, 332 | exp: []exp{ 333 | { 334 | "trace this", true, 335 | }, 336 | { 337 | "trace this too", true, 338 | }, 339 | { 340 | "don't trace this", false, 341 | }, 342 | }, 343 | }, 344 | { 345 | name: "stop tracing active region with key even if no key is given", 346 | fn: stopTraceNoKeyStopsActiveRegion, 347 | exp: []exp{ 348 | { 349 | "trace this", true, 350 | }, 351 | { 352 | "don't trace this", false, 353 | }, 354 | }, 355 | }, 356 | { 357 | name: "stop tracing active region with key only when called from the same scope", 358 | fn: stopTraceNoKeyStopsOnlyScopeRegion, 359 | exp: []exp{ 360 | { 361 | "trace this", true, 362 | }, 363 | { 364 | "trace this too", true, 365 | }, 366 | { 367 | "trace this aswell", true, 368 | }, 369 | { 370 | "don't trace this", false, 371 | }, 372 | }, 373 | }, 374 | { 375 | name: "tracing region stays active across go routines", 376 | fn: tracingRegionAcrossGoroutines, 377 | exp: []exp{ 378 | {"don't trace this", false}, 379 | {"start region or printf", true}, 380 | {"start region or printf", false}, 381 | }, 382 | }, 383 | } 384 | 385 | for _, tc := range tcs { 386 | t.Run(tc.name, func(t *testing.T) { 387 | out := internal.CaptureOutput(tc.fn) 388 | lines := internal.ParseLines([]byte(out)) 389 | 390 | if len(lines) != len(tc.exp) { 391 | for _, l := range lines { 392 | // // this doesn't find the very last line if it isn't a stacktrace i believe. 393 | // // regex issue 394 | fmt.Printf("line: %v \n", l.Line()) 395 | } 396 | fmt.Printf("OUT: %v\n", out) 397 | t.Fatalf("Testcase must contain all output; expected: %v ; got: %v", len(lines), len(tc.exp)) 398 | } 399 | 400 | for i := 0; i < len(tc.exp); i++ { 401 | want := tc.exp[i] 402 | got := lines[i] 403 | 404 | if want.line != got.Line() || want.trace != got.HasTrace() { 405 | t.Errorf("Mismatch: want: %q (stacktrace: %v) ; got: %q (stacktrace: %v)", want.line, want.trace, got.Line(), got.HasTrace()) 406 | } 407 | } 408 | }) 409 | } 410 | } 411 | 412 | func BenchmarkPrintfWithRegion16(b *testing.B) { 413 | var buf bytes.Buffer 414 | dlg.SetOutput(&buf) 415 | 416 | s := internal.RandomStrings(8) 417 | 418 | dlg.StartTrace() 419 | for i := 0; i < b.N; i++ { 420 | buf.Reset() 421 | dlg.Printf(s[i%len(s)]) 422 | } 423 | dlg.StopTrace() 424 | } 425 | 426 | func BenchmarkPrintfWithRegion64(b *testing.B) { 427 | var buf bytes.Buffer 428 | dlg.SetOutput(&buf) 429 | 430 | s := internal.RandomStrings(8) 431 | 432 | dlg.StartTrace() 433 | for i := 0; i < b.N; i++ { 434 | buf.Reset() 435 | dlg.Printf(s[i%len(s)]) 436 | } 437 | dlg.StopTrace() 438 | } 439 | 440 | func BenchmarkPrintfWithRegionStartStop16(b *testing.B) { 441 | var buf bytes.Buffer 442 | dlg.SetOutput(&buf) 443 | 444 | s := internal.RandomStrings(16) 445 | 446 | for i := 0; i < b.N; i++ { 447 | buf.Reset() 448 | dlg.StartTrace() 449 | dlg.Printf(s[i%len(s)]) 450 | dlg.StopTrace() 451 | } 452 | } 453 | 454 | func BenchmarkPrintfWithRegionStartStop64(b *testing.B) { 455 | var buf bytes.Buffer 456 | dlg.SetOutput(&buf) 457 | 458 | s := internal.RandomStrings(64) 459 | 460 | for i := 0; i < b.N; i++ { 461 | buf.Reset() 462 | dlg.StartTrace() 463 | dlg.Printf(s[i%len(s)]) 464 | dlg.StopTrace() 465 | } 466 | } 467 | 468 | func BenchmarkPrintfWithRegionStartStopWithKey16(b *testing.B) { 469 | var buf bytes.Buffer 470 | dlg.SetOutput(&buf) 471 | 472 | s := internal.RandomStrings(16) 473 | key := "key" 474 | 475 | for i := 0; i < b.N; i++ { 476 | buf.Reset() 477 | dlg.StartTrace(key) 478 | dlg.Printf(s[i%len(s)]) 479 | dlg.StopTrace(key) 480 | } 481 | } 482 | 483 | func BenchmarkPrintfWithRegionStartStopWithKey64(b *testing.B) { 484 | var buf bytes.Buffer 485 | dlg.SetOutput(&buf) 486 | 487 | s := internal.RandomStrings(64) 488 | key := "key" 489 | 490 | for i := 0; i < b.N; i++ { 491 | buf.Reset() 492 | dlg.StartTrace(key) 493 | dlg.Printf(s[i%len(s)]) 494 | dlg.StopTrace(key) 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

dlg

3 |

delog - /diːˈlɑːɡ/

4 |

Printf-Style Debugging with Zero-Cost in Production Builds

5 |
6 | 7 | **dlg** provides a minimal API for printf-style debugging - a lightweight logger that completely vanishes from production builds while providing rich debugging capabilities during development. 8 | When built without the `dlg` tag, all logging calls disappear entirely from your binary, resulting in no runtime overhead. 9 | 10 | ### Why dlg? 11 | - 🚀 **True zero-cost abstraction** - Logging calls completely disappear from production binaries 12 | - ⚡️ **Near-zero overhead** - Performance-focused design for debug builds 13 | - 🔍 **Smart stack traces** - Runtime-configurable stack trace generation 14 | - 🔒 **Concurrency-safe by design** - Custom writers simply implement `sync.Locker` to be safe 15 | - ✨ **Minimalist API** - Only `Printf`, and a couple of utility functions 16 | - 🎨 **Colorize Output** - Highlight output for better visibility in noisy output 17 | 18 | ### The Magic of Zero-Cost 19 | 20 | When compiled without the `dlg` build tag: 21 | 22 | - All calls to `dlg` compile to empty functions 23 | - Go linker completely eliminates these no-ops 24 | - Final binary contains no trace of logging code 25 | - Zero memory overhead, zero CPU impact 26 | 27 | For the full technical breakdown, see [True Zero-Cost Elimination.](#true-zero-cost-elimination) 28 | 29 | ### Getting Started 30 | ```bash 31 | go get github.com/vvvvv/dlg 32 | ``` 33 | 34 | ```go 35 | package main 36 | 37 | import ( 38 | "fmt" 39 | 40 | "github.com/vvvvv/dlg" 41 | ) 42 | 43 | func risky() error { 44 | return fmt.Errorf("unexpected error") 45 | } 46 | 47 | func main() { 48 | fmt.Println("starting...") 49 | 50 | dlg.Printf("executing risky operation") 51 | err := risky() 52 | if err != nil { 53 | dlg.Printf("something failed: %v", err) 54 | } 55 | 56 | dlg.Printf("continuing") 57 | } 58 | ``` 59 | 60 | ### Activating Debug Mode 61 | 62 | Enable debug features with the `dlg` build tag: 63 | ```bash 64 | # Production build (no logging) 65 | go build -o app 66 | 67 | # Debug build 68 | go build -tags dlg -o app-debug 69 | ``` 70 | 71 | **Normal Output** 72 | 73 | ```bash 74 | $ go build -o app 75 | ./app 76 | ``` 77 | 78 | ``` 79 | starting... 80 | ``` 81 | 82 | **Debug Build Output** 83 | 84 | ```bash 85 | go build -tags dlg -o app-debug 86 | ./app-debug 87 | ``` 88 | 89 | ``` 90 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 91 | * * * * * * * * * * * * * * DEBUG BUILD * * * * * * * * * * * * * * 92 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 93 | - DLG_STACKTRACE=ERROR show stack traces on errors 94 | - DLG_STACKTRACE=ALWAYS show stack traces always 95 | - DLG_NO_WARN=1 disable this message (use at your own risk) 96 | 97 | starting... 98 | 01:28:27 [2µs] main.go:16: executing risky operation 99 | 01:28:27 [21µs] main.go:19: something failed: unexpected error 100 | 01:28:27 [23µs] main.go:22: continuing 101 | ``` 102 | 103 | **Stack Trace Output** 104 | 105 | ```bash 106 | go build -tags dlg -o app-debug 107 | DLG_STACKTRACE=ERROR ./app-debug 108 | ``` 109 | 110 | ``` 111 | # [Debug Banner omitted] 112 | starting... 113 | 01:31:34 [2µs] main.go:16: executing risky operation 114 | 01:31:34 [21µs] main.go:19: something failed: unexpected error 115 | main.main() 116 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example01/main.go:19 +0xc0 117 | 01:31:34 [38µs] main.go:22: continuing 118 | ``` 119 | 120 | ### Tracing Regions experimental 121 | Sometimes you only want stack traces for a specific area of your code while investigating an issue. 122 | Tracing regions let you define those boundaries. 123 | 124 | `dlg.StartTrace()` begins a tracing region, and `dlg.StopTrace()` ends it. 125 | When `DLG_STACKTRACE` is set to `REGION,ALWAYS`, `dlg.Printf` will print a stack trace only if the current call stack contains a function that's inside an active tracing region. 126 | Similarly, when set to `REGION,ERROR`, stack traces are printed inside tracing regions only if an error is passed to `dlg.Printf`. 127 | 128 | #### Basic Usage 129 | 130 | The simplest usage is to start and stop a trace around the code you want to inspect. 131 | Any `dlg.Printf` calls made in that tracing region will include stack traces. 132 | 133 | Let's start with the most basic example: 134 | 135 | ```go 136 | func main(){ 137 | dlg.StartTrace() 138 | dlg.Printf("foobar") 139 | dlg.StopTrace() 140 | } 141 | ``` 142 | 143 | **Output *`DLG_STACKTRACE=REGION,ALWAYS`*** 144 | 145 | ``` 146 | 16:14:39 [10µs] main.go:8: foobar 147 | main.main() 148 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example08/main.go:8 +0x4b 149 | ``` 150 | 151 | #### Tracing Across Functions 152 | 153 | Tracing isn't limited to a single function. Once a tracing region is started, it covers all functions called within that region until it's stopped. 154 | This means you can start tracing in main() and see traces for calls deeper in the stack. 155 | 156 | ```go 157 | 158 | func foo(){ 159 | dlg.Printf("hello from foo") 160 | } 161 | 162 | func main(){ 163 | dlg.Printf("outside of tracing region") 164 | 165 | dlg.StartTrace() 166 | dlg.Printf("started tracing") 167 | foo() 168 | dlg.StopTrace() 169 | } 170 | ``` 171 | 172 | **Output *`DLG_STACKTRACE=REGION,ALWAYS`*** 173 | 174 | ``` 175 | 16:19:35 [1µs] main.go:11: outside of tracing region 176 | 16:19:35 [30µs] main.go:14: started tracing 177 | main.main() 178 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example08/main.go:14 +0x67 179 | 16:19:35 [43µs] main.go:7: hello from foo 180 | main.foo() 181 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example08/main.go:7 +0x87 182 | main.main() 183 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example08/main.go:15 +0x68 184 | ``` 185 | 186 | 187 | #### Error Tracing 188 | 189 | If you only want stack traces when an error occurs inside a tracing region, set `DLG_STACKTRACE=REGION,ERROR`. 190 | In this mode, traces appear only for `dlg.Printf` calls that include an error argument. 191 | 192 | ```go 193 | 194 | func main(){ 195 | dlg.StartTrace() 196 | 197 | dlg.Printf("starting...") 198 | 199 | err := fmt.Errorf("this is an error") 200 | 201 | dlg.Printf("oh no an error: %v", err) 202 | 203 | dlg.StopTrace() 204 | } 205 | 206 | 207 | ``` 208 | 209 | **Output *`DLG_STACKTRACE=REGION,ERROR`*** 210 | 211 | ``` 212 | 16:24:20 [3µs] main.go:15: starting... 213 | 16:24:20 [29µs] main.go:19: oh no an error: this is an error 214 | main.main() 215 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example08/main.go:19 +0x97 216 | ``` 217 | 218 | #### Understanding Tracing Region Scopes 219 | 220 | A tracing region is tied to the function scope that called `StartTrace`. 221 | If you call `StopTrace()` inside a nested function, the tracing region remains active as the region was started from the outer scope. 222 | 223 | ```go 224 | func main(){ 225 | dlg.StartTrace() 226 | 227 | dlg.Printf("starting...") 228 | 229 | fn := func(){ 230 | dlg.Printf("hello from fn") 231 | 232 | dlg.StopTrace() 233 | } 234 | 235 | fn() 236 | 237 | dlg.Printf("this will still produce a stack trace") 238 | 239 | dlg.StopTrace() 240 | } 241 | 242 | ``` 243 | 244 | **Output *`DLG_STACKTRACE=REGION,ALWAYS`*** 245 | 246 | ``` 247 | 16:28:27 [12µs] main.go:15: starting... 248 | main.main() 249 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example08/main.go:15 +0x4b 250 | 16:28:27 [43µs] main.go:18: hello from fn 251 | main.main.func1() 252 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example08/main.go:18 +0x6b 253 | main.main() 254 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example08/main.go:22 +0x4c 255 | 16:28:27 [49µs] main.go:24: this will still produce a stack trace 256 | main.main() 257 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example08/main.go:24 +0x9f 258 | ``` 259 | 260 | 261 | #### Stopping a Tracing Region from Another Function 262 | 263 | To close a tracing region from a nested function, you need to start and stop it with a matching key. 264 | This allows you to end exactly the tracing region you intended, even from different scopes. 265 | 266 | 267 | ```go 268 | func main(){ 269 | dlg.StartTrace(1) 270 | 271 | dlg.Printf("starting...") 272 | 273 | fn := func(){ 274 | dlg.Printf("hello from fn") 275 | 276 | dlg.StopTrace(1) 277 | } 278 | 279 | fn() 280 | 281 | dlg.Printf("this won't trace") 282 | } 283 | 284 | ``` 285 | 286 | **Output *`DLG_STACKTRACE=REGION,ALWAYS`*** 287 | 288 | ``` 289 | 16:34:07 [9µs] main.go:15: starting... 290 | main.main() 291 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example08/main.go:15 +0x6b 292 | 16:34:07 [33µs] main.go:18: hello from fn 293 | main.main.func1() 294 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example08/main.go:18 +0x8b 295 | main.main() 296 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example08/main.go:23 +0x6c 297 | 16:34:07 [48µs] main.go:25: this won't trace 298 | ``` 299 | 300 | #### Choosing a Key 301 | 302 | You can use any type as a tracing key: integers, strings, floats, even structs. 303 | For clarity, it's best to keep keys simple, such as short strings or integers. 304 | 305 | ```go 306 | 307 | dlg.StartTrace("foo") 308 | ... 309 | dlg.StopTrace("foo") 310 | 311 | 312 | dlg.StartTrace(7.2) 313 | ... 314 | dlg.StopTrace(7.2) 315 | 316 | 317 | dlg.StartTrace(struct{name string}{name: "tracing region"}) 318 | ... 319 | 320 | dlg.StopTrace(struct{name string}{name: "tracing region"}) 321 | ``` 322 | 323 | #### Stopping Without a Key 324 | 325 | `StopTrace()` without arguments will end the most recent active tracing region, even if it was started with a key - as long as you call it from the same scope. 326 | 327 | ```go 328 | func main(){ 329 | dlg.StartTrace(1) 330 | 331 | dlg.Printf("this will trace") 332 | 333 | dlg.StopTrace() 334 | 335 | dlg.Printf("this won't trace") 336 | } 337 | ``` 338 | 339 | **Output *`DLG_STACKTRACE=REGION,ALWAYS`*** 340 | 341 | ``` 342 | 16:47:04 [10µs] main.go:14: this will trace 343 | main.main() 344 | /Users/v/src/go/src/github.com/vvvvv/dlg/examples/example08/main.go:14 +0x6b 345 | 16:47:04 [34µs] main.go:18: this won't trace 346 | ``` 347 | 348 | > 💡 All tracing regions, whether keyed or not, are closed in LIFO *(last-in, first-out)* order. 349 | 350 | #### ⚠️ Why You Should Avoid `defer StopTrace()` 351 | 352 | It might be tempting to wrap `dlg.StopTrace()` in a `defer`, but don't. 353 | The Go compiler cannot eliminate `defer` calls. Even something as trivial as `defer func(){}()` remains as a real function call in the compiled binary. 354 | If you want true zero-cost elimination, call `StopTrace` directly. 355 | 356 | *For more examples of tracing regions, see /tests/stacktraceregion/region_test.go.* 357 | 358 | 359 | ### Concurrency Safety for Custom Writers 360 | 361 | While `dlg.Printf` is safe for concurrent use, custom writers should implement [sync.Locker](https://pkg.go.dev/sync#Locker). 362 | 363 | ```go 364 | package main 365 | 366 | import ( 367 | "bytes" 368 | "fmt" 369 | "sync" 370 | 371 | "github.com/vvvvv/dlg" 372 | ) 373 | 374 | type SafeBuffer struct { 375 | bytes.Buffer 376 | sync.Mutex 377 | } 378 | 379 | func main() { 380 | sb := &SafeBuffer{} 381 | dlg.SetOutput(sb) // Now fully concurrency-safe! 382 | 383 | var wg sync.WaitGroup 384 | for i := 0; i < 10; i++ { 385 | wg.Add(1) 386 | go func() { 387 | defer wg.Done() 388 | for n := 0; n < 5; n++ { 389 | dlg.Printf("from goroutine #%v: message %v", i, n) 390 | } 391 | }() 392 | } 393 | wg.Wait() 394 | fmt.Print(sb.Buffer.String()) 395 | } 396 | ``` 397 | 398 | ### Configuration 399 | 400 | `dlg` can be configured at runtime *(environment variables)* or at compile time *(linker flags)*. 401 | Compile-time settings win over runtime. 402 | Settings configured at compile time cannot be overridden at runtime. 403 | 404 | | Variable | Runtime-configurable | Compile-time-configurable | Description | 405 | | ------------------ | -------------------- | --------------------------| --------------------------------------- | 406 | | DLG_STACKTRACE | ✔︎ | ✔︎ | Controls when stack traces are shown | 407 | | DLG_COLOR | ✘ | ✔︎ | Sets output color for file/line | 408 | | DLG_NO_WARN | ✔︎ | ✘ | Suppresses debug banner | 409 | 410 | 411 | **DLG_STACKTRACE - Controls when to generate stack traces** 412 | 413 | *Runtime:* 414 | ```bash 415 | # Errors only 416 | DLG_STACKTRACE=ERROR ./app-debug 417 | # Every call 418 | DLG_STACKTRACE=ALWAYS ./app-debug 419 | # Errors within tracing region 420 | DLG_STACKTRACE=REGION,ERROR ./app-debug 421 | # Every call within tracing region 422 | DLG_STACKTRACE=REGION,ALWAYS ./app-debug 423 | ``` 424 | 425 | 426 | *Compile-time:* 427 | ```bash 428 | go build -tags dlg -ldflags "-X 'github.com/vvvvv/dlg.DLG_STACKTRACE=ERROR'" 429 | go build -tags dlg -ldflags "-X 'github.com/vvvvv/dlg.DLG_STACKTRACE=REGION,ALWAYS'" 430 | ``` 431 | 432 | **DLG_NO_WARN - Suppress the debug startup banner** 433 | 434 | *Runtime:* 435 | ```bash 436 | DLG_NO_WARN=1 ./app-debug 437 | ``` 438 | > The debug banner cannot be disabled via linker flags. This prevents accidental deployment of debug builds to production. 439 | 440 | **DLG_COLOR - Highlight file name & line number** 441 | 442 | *Compile-time:* 443 | ```bash 444 | # Set the color to ANSI color red. 445 | go build -tags dlg -ldflags "-X 'github.com/vvvvv/dlg.DLG_COLOR=red'" 446 | # Set the color to ANSI color 4. 447 | go build -tags dlg -ldflags "-X 'github.com/vvvvv/dlg.DLG_COLOR=4'" 448 | # Set raw ANSI color. 449 | go build -tags dlg -ldflags "-X 'github.com/vvvvv/dlg.DLG_COLOR=\033[38;2;250;3;250m'" 450 | ``` 451 | > This setting respects the `NO_COLOR` convention 452 | 453 | Valid color values: 454 | - Named: *black, red, green, yellow, blue, magenta, cyan, white* 455 | - ANSI: *0 - 255* 456 | - Raw ANSI color escape sequences 457 | 458 | ### True Zero-Cost Elimination 459 | 460 | The term "zero-cost" isn't just a claim - it's a verifiable compiler behavior. When dlg is disabled, the Go toolchain performs complete dead code elimination. 461 | 462 | Consider this simple program: 463 | 464 | ```go 465 | package main 466 | 467 | import ( 468 | "fmt" 469 | "github.com/vvvvv/dlg" 470 | ) 471 | 472 | func main() { 473 | fmt.Println("hello world") 474 | dlg.Printf("hello from dlg") 475 | } 476 | ``` 477 | 478 | When built *without* the `dlg` tag: 479 | 480 | ```bash 481 | go build -o production_binary 482 | ``` 483 | 484 | The resulting disassembly (via `go tool objdump -s main.main production_binary`) shows: 485 | 486 | 487 | ```assembly 488 | ... [function prologue] ... 489 | main.go:10 0x10009c6a8 b00001a5 ADRP 217088(PC), R5 490 | main.go:10 0x10009c6ac 913480a5 ADD $3360, R5, R5 491 | main.go:10 0x10009c6b0 f9001fe5 MOVD R5, 56(RSP) 492 | main.go:10 0x10009c6b4 f0000265 ADRP 323584(PC), R5 493 | main.go:10 0x10009c6b8 9135a0a5 ADD $3432, R5, R5 494 | main.go:10 0x10009c6bc f90023e5 MOVD R5, 64(RSP) 495 | print.go:314 0x10009c6c0 b00006db ADRP 888832(PC), R27 496 | print.go:314 0x10009c6c4 f9479761 MOVD 3880(R27), R1 497 | print.go:314 0x10009c6c8 90000280 ADRP 327680(PC), R0 498 | print.go:314 0x10009c6cc 910c6000 ADD $792, R0, R0 499 | print.go:314 0x10009c6d0 9100e3e2 ADD $56, RSP, R2 500 | print.go:314 0x10009c6d4 b24003e3 ORR $1, ZR, R3 501 | print.go:314 0x10009c6d8 aa0303e4 MOVD R3, R4 502 | print.go:314 0x10009c6dc 97ffecf5 CALL fmt.Fprintln(SB) ; Only this call (fmt.Println) remains 503 | main.go:12 0x10009c6e0 f85f83fd MOVD -8(RSP), R29 504 | main.go:12 0x10009c6e4 f84507fe MOVD.P 80(RSP), R30 505 | main.go:12 0x10009c6e8 d65f03c0 RET 506 | main.go:9 0x10009c6ec aa1e03e3 MOVD R30, R3 507 | main.go:9 0x10009c6f0 97ff3bbc CALL runtime.morestack_noctxt.abi0(SB) 508 | main.go:9 0x10009c6f4 17ffffe7 JMP main.main(SB) 509 | main.go:9 0x10009c6f8 00000000 ? 510 | ``` 511 | 512 | 513 | The compiler eliminates `dlg` as if it was never imported. 514 | 515 | However, the Go compiler can only eliminate `dlg.Printf` calls if it can prove that the arguments themselves have no side effects and are not used elsewhere. 516 | This has two important implications: 517 | 1. **Referenced variables stay:** If a variable is used outside the `dlg.Printf` call, the computation remains - but the call itself is still removed. 518 | 2. **Function calls are evaluated:** Even if `dlg.Printf` is eliminated, any argument expressions with potential side effects (e.g. function calls) are still evaluated. 519 | 520 | Let's look at practical examples. 521 | 522 | **✅ Referenced Variables - Printf Eliminated, Variable Remains** 523 | ```go 524 | res := 69 * 42 // Used later -> remains 525 | dlg.Printf("res: %v", res) // Eliminated 526 | fmt.Println("result: ", res) 527 | ``` 528 | 529 | ```assembly 530 | ; res remains (used by fmt.Println) 531 | 0x10009c6c8 d2816a40 MOVD $2898, R0 ; 69*42=2898 stored 532 | ... 533 | ; fmt.Println remains 534 | 0x10009c6fc 97ffeced CALL fmt.Fprintln(SB) 535 | ``` 536 | 537 | **✅ Unused Expressions - Fully Eliminated** 538 | 539 | ```go 540 | res := 69 * 42 // Eliminated 541 | dlg.Printf("res: %v", res) // Eliminated 542 | dlg.Printf("calc: %v", 69 * 42) // Eliminated 543 | ``` 544 | 545 | ```assembly 546 | ; Entire function reduced to a single return: 547 | 0x100067b60 d65f03c0 RET 548 | ``` 549 | 550 | **⚠️ Function Calls - Still Evaluated[^1]** 551 | 552 | ```go 553 | // The call to fmt.Errorf is evaluated but the call to dlg.Printf is still eliminated 554 | dlg.Printf("call to fn: %v", fmt.Errorf("some error")) 555 | ``` 556 | 557 | 558 | ```assembly 559 | ; fmt.Errorf is still executed 560 | 0x10009f21c 913ec000 ADD $4016, R0, R0 561 | 0x10009f230 97ffd8e0 CALL fmt.Errorf(SB) 562 | ``` 563 | 564 | 565 | ### ⚡️Rule of Thumb: 566 | **Avoid placing function calls or expensive computations directly inside `dlg.Printf`.** 567 | 568 | As long as you follow this principle, `dlg` maintains its promise: 569 | ***No instructions.* 570 | *No references.* 571 | *Zero memory allocations.* 572 | *Zero CPU cycles used.* 573 | *identical binary size to code without `dlg`.* 574 | *True zero-cost.*** 575 | 576 | [^1]: There's a bit more nuance to this - if a function is side-effect free and returns a basic type (e.g., `int`, `string`), the compiler may still eliminate the function call. 577 | --------------------------------------------------------------------------------