Multi-module projects
This document explains how to use whatap-go-inst in projects composed of multiple Go modules.
Terminology
| Term | Description | Example |
|---|---|---|
| Module | Unit with go.mod | module mycompany/user-api |
| Package | Directory-level code group | mycompany/user-api/pkg/auth |
| External module | Dependencies obtained via go get | github.com/gin-gonic/gin |
| Local module | Separate module within the same project | replace ../shared-lib |
Processing by package type
whatap-go-inst determines whether to instrument based on package location.
toolexec skip rules
// 1. Go standard library → Skip
if strings.HasPrefix(path, os.Getenv("GOROOT")) {
return true // No transformation
}
// 2. External packages (go get) → Skip
if strings.HasPrefix(path, os.Getenv("GOMODCACHE")) {
return true // No transformation
}
// 3. Others (my code) → Transform
return false
Summary
| Package type | Path | Instrumentation |
|---|---|---|
| Go standard library | $GOROOT/src/... | Skip |
| External modules (go get) | $GOMODCACHE/... | Skip |
| My project code | Local path | Instrumented |
| replace local modules | Local path (../) | Instrumented (fast mode only) |
Multi-module scenarios
Scenario: 3 separate modules
C:/projects/
├── db-lib/ # Module A: DB logic library
│ ├── go.mod # module mycompany/db-lib
│ ├── connection.go # Uses sql.Open()
│ └── query.go
│
├── web-lib/ # Module B: Web server library
│ ├── go.mod # module mycompany/web-lib
│ ├── server.go # Uses gin.Default()
│ └── handler.go
│
└── main-app/ # Module C: Main app
├── go.mod # module mycompany/main-app
└── main.go # Imports A, B
Module C's go.mod
module mycompany/main-app
go 1.21
require (
mycompany/db-lib v1.0.0
mycompany/web-lib v1.0.0
)
Recommended approaches
Approach 1: Separate inject per module (for deployment)
Recommended for production environment deployment.
Step 1: Inject each module
# Instrument db-lib
cd db-lib
whatap-go-inst inject -s . -o ../db-lib-instrumented
# Instrument web-lib
cd web-lib
whatap-go-inst inject -s . -o ../web-lib-instrumented
# Instrument main-app
cd main-app
whatap-go-inst inject -s . -o ../main-app-instrumented
Step 2: Replace with instrumented versions
// main-app-instrumented/go.mod
replace mycompany/db-lib => ../db-lib-instrumented
replace mycompany/web-lib => ../web-lib-instrumented
Step 3: Build
cd main-app-instrumented
go build ./...
Approach 2: Monorepo structure (new projects)
For new projects, a single module structure is simplest.
myproject/
├── go.mod # Single module mycompany/myproject
├── cmd/
│ └── main/
│ └── main.go # main package
├── internal/
│ ├── db/
│ │ └── connection.go
│ └── web/
│ └── server.go
└── pkg/
└── shared/
└── utils.go
# Build
whatap-go-inst go build ./cmd/main
Behavior comparison by mode
--wrap mode
| Item | --wrap mode |
|---|---|
| main module | Instrumented |
| replace modules | Not instrumented |
| External modules (GOMODCACHE) | Skip |
| Prerequisites | None |
replace handling
// --wrap mode only adjusts replace paths
replace mycompany/db-lib => ../db-lib
// After copying to temporary directory:
replace mycompany/db-lib => /original/path/to/db-lib // References original!
Result: replace target modules are not copied and reference the original (non-instrumented) code
To instrument replace modules: You need to inject each module separately.
# Inject each module separately
whatap-go-inst inject -s ../db-lib -o ../db-lib-instrumented
whatap-go-inst inject -s . -o ./instrumented
# Modify replace path in instrumented/go.mod
# replace mycompany/db-lib => ../db-lib-instrumented
Considerations
1. Injecting libraries without main
Libraries without a main function can also be injected.
| Item | Processing |
|---|---|
trace.Init() | Not added (normal) |
sql.Open() → whatapsql.Open() | Transformed |
gin.Default() + middleware | Added |
# Inject library module
cd db-lib
whatap-go-inst inject -s . -o ./instrumented
# Result: Transformed to whatapsql.Open()
# trace.Init() not added (should only be called in main)
2. Dependency propagation
Modules using instrumented libraries also need whatap dependencies.
// db-lib-instrumented/connection.go
import "github.com/whatap/go-api/instrumentation/.../whatapsql"
// This dependency is needed to build in main-app
cd main-app
go get github.com/whatap/go-api@latest
FAQ
Q1: Are replace modules also instrumented?
A: No, wrap mode does not instrument replace modules. It only adjusts replace paths and references the original. To instrument replace modules, you need to inject each module separately.
Inject each module separately
whatap-go-inst inject -s ../db-lib -o ../db-lib-inst
whatap-go-inst inject -s ../web-lib -o ../web-lib-inst
Q2: Can external libraries (gin, gorm, etc.) be instrumented?
A: No. External libraries are skipped because they are in $GOMODCACHE. This is intentional behavior.
- Modifying external libraries affects other projects
- Principle of immutability in Go module system
Instead, calls to external libraries in user code are transformed:
sql.Open()→whatapsql.Open()gin.Default()→ Middleware automatically added
Q3: Monorepo vs multi-module, which is better?
| Structure | Advantages | Disadvantages |
|---|---|---|
| Monorepo | Simple instrumentation, easy dependency management | Difficult module reuse |
| Multi-module | Independent module deployment possible | Complex instrumentation setup |
Recommendations:
- New projects → Monorepo
- Existing multi-module → Separate inject per module
Q4: How do I build multi-module in CI/CD?
See the CI/CD examples section below.
CI/CD examples
GitHub Actions
# .github/workflows/build.yml
name: Build with WhaTap Instrumentation
jobs:
build:
runs-on: ubuntu-latest
steps:
# 1. Checkout all modules
- uses: actions/checkout@v3
with:
path: main-app
- uses: actions/checkout@v3
with:
repository: mycompany/db-lib
path: db-lib
- uses: actions/checkout@v3
with:
repository: mycompany/web-lib
path: web-lib
# 2. Install Go
- uses: actions/setup-go@v4
with:
go-version: '1.21'
# 3. Install whatap-go-inst
- run: go install github.com/whatap/go-api-inst/cmd/whatap-go-inst@latest
# 4. Build
- run: |
cd main-app
whatap-go-inst go build -o myapp ./...
# 5. Upload artifact
- uses: actions/upload-artifact@v3
with:
name: myapp
path: main-app/myapp
GitLab CI
# .gitlab-ci.yml
stages:
- build
build:
stage: build
image: golang:1.21
before_script:
# Install whatap-go-inst
- go install github.com/whatap/go-api-inst/cmd/whatap-go-inst@latest
# Clone other modules
- git clone https://gitlab.com/mycompany/db-lib.git ../db-lib
- git clone https://gitlab.com/mycompany/web-lib.git ../web-lib
script:
- whatap-go-inst go build -o myapp ./...
artifacts:
paths:
- myapp
Docker multi-stage build
# Dockerfile
FROM golang:1.21 AS builder
WORKDIR /workspace
# Install whatap-go-inst
RUN go install github.com/whatap/go-api-inst/cmd/whatap-go-inst@latest
# Copy source (all modules)
COPY db-lib/ ./db-lib/
COPY web-lib/ ./web-lib/
COPY main-app/ ./main-app/
# Initialize and add dependencies
WORKDIR /workspace/main-app
# Build
RUN whatap-go-inst go build -o /app/myapp ./...
# Runtime image
FROM alpine:latest
COPY /app/myapp /app/myapp
CMD ["/app/myapp"]