README
¶
ctxweaver
[!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
@latestin 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]
templatecan be an inline string or an object withfilekey pointing to a template file.- CLI override behavior:
- Package patterns (CLI args): Override
packages.patternswhen provided-testflag: Overridetestconfig 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/templatesyntax 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}}-trueif method,falseif function{{.IsPointerReceiver}}-trueif pointer receiver{{.IsGenericFunc}}-trueif generic function (e.g.,func Foo[T any]()){{.IsGenericReceiver}}-trueif 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:
- Skips if the statement is up-to-date
- Updates if the function name in the statement doesn't match (e.g., after rename)
- 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
goimportsorgciafter 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 tidyto 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
gciorgoimports - Running linters with auto-fix
[!TIP] ctxweaver adds imports but does not organize them. Since
goimportsonly adds/removes imports without reordering, use tools likegciorgolangci-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
-
You want visible, reviewable code: Generated statements appear in your source files and git history. Code reviewers can see exactly what instrumentation is added.
-
You need full template control: Define exactly what gets inserted using Go templates. Not limited to predefined patterns.
-
You want vendor independence: Works with any APM (New Relic, OpenTelemetry, custom solutions). No SDK lock-in.
-
You use context-carrying frameworks: Built-in support for Echo, Gin, Fiber, Cobra, urfave/cli context types.
-
You want idempotent updates: Re-running ctxweaver updates existing statements (e.g., after function rename) without duplication.
When to Choose Orchestrion
-
You prefer zero source changes: Instrumentation happens at compile time with no visible code modifications.
-
You use Datadog: Native integration with Datadog APM and ASM.
-
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.
Related Tools
- goroutinectx - Goroutine context propagation linter
- zerologlintctx - Zerolog context propagation linter
- gormreuse - GORM instance reuse linter
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. |