Skip to main content

Multi-module projects

This document explains how to use whatap-go-inst in projects composed of multiple Go modules.

Terminology

TermDescriptionExample
ModuleUnit with go.modmodule mycompany/user-api
PackageDirectory-level code groupmycompany/user-api/pkg/auth
External moduleDependencies obtained via go getgithub.com/gin-gonic/gin
Local moduleSeparate module within the same projectreplace ../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 typePathInstrumentation
Go standard library$GOROOT/src/...Skip
External modules (go get)$GOMODCACHE/...Skip
My project codeLocal pathInstrumented
replace local modulesLocal 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
)

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 moduleInstrumented
replace modulesNot instrumented
External modules (GOMODCACHE)Skip
PrerequisitesNone

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.

ItemProcessing
trace.Init()Not added (normal)
sql.Open()whatapsql.Open()Transformed
gin.Default() + middlewareAdded
# 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?

StructureAdvantagesDisadvantages
MonorepoSimple instrumentation, easy dependency managementDifficult module reuse
Multi-moduleIndependent module deployment possibleComplex 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 --from=builder /app/myapp /app/myapp
CMD ["/app/myapp"]