ctxweaver

module
v0.6.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 24, 2025 License: MIT

README

ctxweaver

Go Reference Go Report Card Codecov

[!NOTE] This project was written by AI (Claude Code).

A Go code generator that weaves statements into functions receiving context-like parameters.

Overview

ctxweaver automatically inserts or updates statements at the beginning of functions that receive context.Context or other context-carrying types. This is useful for:

  • APM instrumentation: Automatically add tracing segments to all context-aware functions
  • Logging setup: Insert structured logging initialization
  • Metrics collection: Add timing or counting instrumentation
  • Custom middleware: Any pattern that needs to run at function entry with context

How It Works

// Before: A function receiving context
func (s *Service) ProcessOrder(ctx context.Context, orderID string) error {
    // business logic...
}

// After: ctxweaver inserts your template at the top
func (s *Service) ProcessOrder(ctx context.Context, orderID string) error {
    defer newrelic.FromContext(ctx).StartSegment("myapp.(*Service).ProcessOrder").End()

    // business logic...
}

The inserted statement is fully customizable via Go templates.

Installation & Usage

Using go install
go install github.com/mpyw/ctxweaver/cmd/ctxweaver@latest
ctxweaver ./...
Using go tool (Go 1.24+)
# Add to go.mod as a tool dependency
go get -tool github.com/mpyw/ctxweaver/cmd/ctxweaver@latest

# Run via go tool
go tool ctxweaver ./...
Using go run
go run github.com/mpyw/ctxweaver/cmd/ctxweaver@latest ./...

[!CAUTION] To prevent supply chain attacks, pin to a specific version tag instead of @latest in CI/CD pipelines (e.g., @v0.6.3).

Configuration

ctxweaver uses a YAML configuration file. Create ctxweaver.yaml in your project root:

# ctxweaver.yaml
template: |
  defer newrelic.FromContext({{.Ctx}}).StartSegment({{.FuncName | quote}}).End()

imports:
  - github.com/newrelic/go-agent/v3/newrelic

packages:
  patterns:
    - ./...

test: false

See ctxweaver.example.yaml for a complete example with all options.

Configuration Options
Option Type Required Default Description
template string | {file: string} Go template for the statement to insert (inline or file path)
imports []string [] Import paths to add when statement is inserted
packages.patterns []string Package patterns to process (overridden by CLI args)
packages.regexps.only []string [] Only process packages matching these regex patterns
packages.regexps.omit []string [] Skip packages matching these regex patterns
functions.types []FuncType ["function", "method"] Enum: "function" | "method"
functions.scopes []FuncScope ["exported", "unexported"] Enum: "exported" | "unexported"
functions.regexps.only []string [] Only process functions matching these regex patterns
functions.regexps.omit []string [] Skip functions matching these regex patterns
test bool false Whether to process test files (overridden by -test flag)
carriers []Carrier | CarriersConfig [] Context carrier configuration (see Custom Carriers)
hooks.pre []string [] Shell commands to run before processing
hooks.post []string [] Shell commands to run after processing

[!NOTE]

  • template can be an inline string or an object with file key pointing to a template file.
  • CLI override behavior:
    • Package patterns (CLI args): Override packages.patterns when provided
    • -test flag: Override test config when explicitly passed
Package Filtering

Control which packages are processed using packages.patterns and regex filters:

packages:
  patterns:
    - ./...
  regexps:
    # Only process packages matching these patterns (empty = all)
    only:
      - /handler/
      - /service/
    # Skip packages matching these patterns
    omit:
      - /internal/
      - /mock/
      - _test$

Regex patterns are matched against the full import path (e.g., github.com/user/repo/internal/util).

Function Filtering

Control which functions are processed using type, scope, and regex filters:

functions:
  # Filter by function type (enum, default: both)
  types:
    - function  # "function": Top-level functions without receivers
    - method    # "method": Methods with receivers

  # Filter by export scope (enum, default: both)
  scopes:
    - exported    # "exported": Public functions (e.g., GetUser)
    - unexported  # "unexported": Private functions (e.g., parseInput)

  # Regex filters on function names
  regexps:
    only:
      - ^Handle   # Only functions starting with "Handle"
    omit:
      - Helper$   # Skip functions ending with "Helper"

Example: Only instrument exported handlers

functions:
  types: [function]
  scopes: [exported]
  regexps:
    only: [^Handle]

Example: Skip test helpers and mocks

functions:
  regexps:
    omit:
      - Mock
      - Helper$
      - ^setup

Flags

Flag Default Description
-config ctxweaver.yaml Path to configuration file
-dry-run false Print changes without writing files
-verbose false Print processed files
-silent false Suppress all output except errors
-test false Process test files (*_test.go)
-remove false Remove generated statements instead of adding them
-no-hooks false Skip pre/post hooks defined in config
Examples
# Use default config file (ctxweaver.yaml)
ctxweaver ./...

# Use custom config file
ctxweaver -config=.ctxweaver.yaml ./...

# Dry run - preview changes
ctxweaver -dry-run -verbose ./...

# Include test files
ctxweaver -test ./...

# Remove previously inserted statements
ctxweaver -remove ./...

# Skip hooks (useful in CI)
ctxweaver -no-hooks ./...

[!TIP] Refreshing statements after template changes: When you modify your template, ctxweaver detects existing statements via skeleton matching. If the template structure changes significantly, old statements may not be recognized and will remain alongside newly inserted ones.

To cleanly refresh all statements after a template change:

# Step 1: Remove with the OLD template still in config
ctxweaver -remove ./...

# Step 2: Update your template in ctxweaver.yaml

# Step 3: Re-run to insert with the NEW template
ctxweaver ./...

Template System

[!TIP] For Go text/template syntax guide, see: https://docs.gomplate.ca/syntax/

Available Variables
Variable Type Description
{{.Ctx}} string Expression to access context.Context
{{.CtxVar}} string Name of the context parameter variable
{{.FuncName}} string Fully qualified function name
{{.PackageName}} string Package name
{{.PackagePath}} string Full import path of the package
{{.FuncBaseName}} string Function name without package/receiver
{{.ReceiverType}} string Receiver type name (empty if not a method)
{{.ReceiverVar}} string Receiver variable name (empty if not a method)
{{.IsMethod}} bool Whether this is a method
{{.IsPointerReceiver}} bool Whether the receiver is a pointer
{{.IsGenericFunc}} bool Whether the function has type parameters
{{.IsGenericReceiver}} bool Whether the receiver type has type parameters
FuncName Format

{{.FuncName}} provides a fully qualified function name in the following format:

Type Format Example
Function pkg.Func service.CreateUser
Method (pointer receiver) pkg.(*Type).Method service.(*UserService).GetByID
Method (value receiver) pkg.Type.Method service.UserService.String
Generic function pkg.Func[...] service.Process[...]
Generic method (pointer) pkg.(*Type[...]).Method service.(*Container[...]).Get
Generic method (value) pkg.Type[...].Method service.Wrapper[...].Unwrap
Built-in Functions
Function Description
quote Wraps string in double quotes
backtick Wraps string in backticks
Basic Example

New Relic

template: |
  defer newrelic.FromContext({{.Ctx}}).StartSegment({{.FuncName | quote}}).End()
imports:
  - github.com/newrelic/go-agent/v3/newrelic

OpenTelemetry

template: |
  {{.CtxVar}}, span := otel.Tracer({{.PackageName | quote}}).Start({{.Ctx}}, {{.FuncName | quote}}); defer span.End()
imports:
  - go.opentelemetry.io/otel
Custom Function Name Format

If you need a different naming format, you can build it yourself using template variables. The following example replicates the default {{.FuncName}} behavior:

template: |
  {{- $receiver := .ReceiverType -}}
  {{- if .IsGenericReceiver -}}
    {{- $receiver = printf "%s[...]" .ReceiverType -}}
  {{- end -}}
  {{- $name := "" -}}
  {{- if .IsMethod -}}
    {{- if .IsPointerReceiver -}}
      {{- $name = printf "%s.(*%s).%s" .PackageName $receiver .FuncBaseName -}}
    {{- else -}}
      {{- $name = printf "%s.%s.%s" .PackageName $receiver .FuncBaseName -}}
    {{- end -}}
  {{- else -}}
    {{- if .IsGenericFunc -}}
      {{- $name = printf "%s.%s[...]" .PackageName .FuncBaseName -}}
    {{- else -}}
      {{- $name = printf "%s.%s" .PackageName .FuncBaseName -}}
    {{- end -}}
  {{- end -}}
  defer newrelic.FromContext({{.Ctx}}).StartSegment({{$name | quote}}).End()
imports:
  - github.com/newrelic/go-agent/v3/newrelic

This gives you full control over the naming format. Available variables for building custom names:

  • {{.PackageName}} - Package name (e.g., service)
  • {{.PackagePath}} - Full import path (e.g., github.com/example/myapp/pkg/service)
  • {{.ReceiverType}} - Receiver type name without generics (e.g., UserService, Container)
  • {{.FuncBaseName}} - Function/method name (e.g., GetByID)
  • {{.IsMethod}} - true if method, false if function
  • {{.IsPointerReceiver}} - true if pointer receiver
  • {{.IsGenericFunc}} - true if generic function (e.g., func Foo[T any]())
  • {{.IsGenericReceiver}} - true if generic receiver type (e.g., func (c *Container[T]) Method())

Built-in Context Carriers

ctxweaver recognizes the following types as context carriers (checks the first parameter only):

Type Accessor Notes
context.Context (none) Standard library
*http.Request .Context() Standard library
echo.Context .Request().Context() Echo framework
*cli.Context .Context urfave/cli
*cobra.Command .Context() Cobra
*gin.Context .Request.Context() Gin
*fiber.Ctx .Context() Fiber
Custom Carriers

Add custom carriers in your config file:

# Simple form: array of carriers (default carriers remain enabled)
carriers:
  - package: github.com/example/myapp/pkg/web
    type: Context
    accessor: .Ctx()

To disable default carriers and use only custom ones:

# Extended form: object with custom carriers and default toggle
carriers:
  custom:
    - package: github.com/example/myapp/pkg/web
      type: Context
      accessor: .Ctx()
  default: false  # Disable built-in carriers
Carrier Schema
Field Type Required Description
package string Import path of the package containing the type
type string Name of the type
accessor string Expression to extract context.Context (e.g., .Context())
CarriersConfig Schema (Extended Form)
Field Type Default Description
custom []Carrier [] Custom carrier definitions
default bool true Whether to include built-in default carriers

Directives

//ctxweaver:skip

Skip processing for a specific function or entire file:

//ctxweaver:skip
func legacyHandler(ctx context.Context) {
    // This function will not be modified
}

File-level skip (place at the top of the file):

//ctxweaver:skip

package legacy

// All functions in this file will be skipped

Existing Statement Detection

ctxweaver detects if a matching statement already exists and:

  1. Skips if the statement is up-to-date
  2. Updates if the function name in the statement doesn't match (e.g., after rename)
  3. Inserts if no matching statement exists

Currently, detection is specific to the defer XXX.StartSegment(ctx, "name").End() pattern.

Performance

ctxweaver uses golang.org/x/tools/go/packages to load type information efficiently:

  • Single load: All target packages are loaded in one pass
  • Accurate type resolution: Import paths are resolved correctly via type information
  • Comment preservation: Uses DST (Decorated Syntax Tree) to preserve comments

Import Management

ctxweaver automatically adds imports specified in the config file when statements are inserted.

[!NOTE] ctxweaver does not reorder or reformat existing imports. Use goimports or gci after ctxweaver if you need consistent import formatting.

Hooks

ctxweaver supports pre and post hooks to run shell commands before and after processing.

hooks:
  pre:
    - go mod tidy
  post:
    - gci write .
    - gofmt -w .
Pre Hooks

Commands run sequentially before processing. If any command fails (non-zero exit), processing is aborted and no files are modified. Useful for:

  • Running go mod tidy to ensure dependencies are up to date
  • Validating preconditions
Post Hooks

Commands run sequentially after processing. If any command fails, an error is reported but files have already been modified. Useful for:

  • Formatting code with gofmt
  • Organizing imports with gci or goimports
  • Running linters with auto-fix

[!TIP] ctxweaver adds imports but does not organize them. Since goimports only adds/removes imports without reordering, use tools like gci or golangci-lint run --fix (with gci enabled) to enforce consistent import ordering.

Recommended post hooks:

hooks:
  post:
    - gci write .
    - gofmt -w .

Or with golangci-lint:

hooks:
  post:
    - golangci-lint run --fix ./...

Use the -no-hooks flag to skip hooks (useful for CI or when running ctxweaver as part of a larger pipeline).

Documentation

  • Architecture - Technical specification and design decisions
  • CLAUDE.md - AI assistant guidance for development

Development

# Run tests
go test ./...

# Build CLI
go build -o bin/ctxweaver ./cmd/ctxweaver

# Run on a project
./bin/ctxweaver -config=ctxweaver.yaml ./...

Why ctxweaver?

For Go instrumentation, there are two main approaches: compile-time instrumentation (like Datadog Orchestrion) and code generation (like ctxweaver). Here's how they compare:

Feature ctxweaver Orchestrion
Approach Explicit code generation Compile-time AST injection
Output visibility Generated code in source files Hidden in build process
Comment preservation Yes (DST) N/A (no source modification)
Vendor lock-in None (template-based) Datadog by default
Custom templates Full control via Go templates Limited (//dd:span directive)
Framework support Built-in (Echo, Gin, Fiber, etc.) Via integrations
Reversibility ctxweaver -remove Remove toolchain config
Git diff Visible changes No source changes
When to Choose ctxweaver
  1. You want visible, reviewable code: Generated statements appear in your source files and git history. Code reviewers can see exactly what instrumentation is added.

  2. You need full template control: Define exactly what gets inserted using Go templates. Not limited to predefined patterns.

  3. You want vendor independence: Works with any APM (New Relic, OpenTelemetry, custom solutions). No SDK lock-in.

  4. You use context-carrying frameworks: Built-in support for Echo, Gin, Fiber, Cobra, urfave/cli context types.

  5. You want idempotent updates: Re-running ctxweaver updates existing statements (e.g., after function rename) without duplication.

When to Choose Orchestrion
  1. You prefer zero source changes: Instrumentation happens at compile time with no visible code modifications.

  2. You use Datadog: Native integration with Datadog APM and ASM.

  3. You want automatic library instrumentation: Orchestrion can instrument third-party library calls automatically.

[!NOTE] Traditional AOP libraries (gogap/aop, AspectGo) exist but are largely unmaintained. Go's culture favors explicit code over implicit magic, which is why ctxweaver generates visible source code rather than hiding instrumentation in the build process.

License

MIT License

Directories

Path Synopsis
cmd
ctxweaver command
Command ctxweaver weaves statements into context-aware functions.
Command ctxweaver weaves statements into context-aware functions.
Package internal provides shared utilities for ctxweaver.
Package internal provides shared utilities for ctxweaver.
directive
Package directive provides utilities for processing ctxweaver directives in comments.
Package directive provides utilities for processing ctxweaver directives in comments.
dstutil
Package dstutil provides utilities for DST (Decorated Syntax Tree) manipulation.
Package dstutil provides utilities for DST (Decorated Syntax Tree) manipulation.
pkg
carrier
Package carrier provides carrier type matching for context propagation.
Package carrier provides carrier type matching for context propagation.
config
Package config provides configuration loading for ctxweaver.
Package config provides configuration loading for ctxweaver.
processor
Package processor provides DST-based code transformation.
Package processor provides DST-based code transformation.
template
Package template provides template rendering for ctxweaver.
Package template provides template rendering for ctxweaver.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL