Why Does SeaLights Use Static Instrumentation for Go?
SeaLights uses static instrumentation for Go applications, how it works, and how to deploy it safely without impacting production environments.
TL;DR
Go does not support dynamic runtime instrumentation, so SeaLights uses build-time static instrumentation.
This approach adds a registration call at the start of each method to track coverage during test execution.
The functional method code remains unchanged, and no repository files are modified.
SeaLights’ instrumentation in Go is equivalent in effect to dynamic agents in Java/.NET — it just happens earlier in the lifecycle.
Safe deployment options ensure instrumentation is only active in testing environments and never impacts production.
Why doesn't Go support runtime instrumentation like Java or .NET?
Java and .NET rely on intermediate runtime environments (JVM and CLR), which allow dynamic instrumentation using agents or profilers. These tools can modify bytecode in memory at runtime to track method execution and collect coverage data.
Go, by contrast, compiles directly to native machine code and does not expose hooks or an intermediate representation during execution. This means runtime instrumentation is not possible, and coverage instrumentation must happen earlier — at build time.
What does SeaLights do instead?
To support coverage collection in Go, SeaLights performs static instrumentation during the build process:
A lightweight method-entry call is prepended to the beginning of each method, just before the functional code.
These instructions record that a method was executed during test runs, enabling method-level coverage reporting.
The method’s logic and control flow remain unchanged.
Most importantly, the source code in your repository is never touched — instrumentation is applied only to a temporary local copy before compilation.
What SeaLights does during static instrumentation in Go is conceptually the same as what happens "behind the scenes" during dynamic instrumentation in Java/.NET. The key difference is when it happens — at build time instead of runtime.
When instrumentation is not explicitly enabled (e.g., in production), these registration calls are inert and introduce no runtime overhead.
How can we deploy the instrumented binaries safely?
SeaLights suggests three deployment strategies to ensure instrumentation is active only in test environments, with no risk of affecting production behavior:
Single Binary
One binary built with instrumentation, deployed across all environments
Controlled via environment variable (e.g., SEALIGHTS_DISABLE_ON_INIT=false
)
Simplest setup; instrumentation is inactive unless explicitly enabled
Separate Images
Two distinct container images: one for test (instrumented), one for production (original)
CI/CD selects the appropriate image
Strongest separation; instrumented image can be deleted after testing to reduce storage (e.g., after 48 hours)
Dual Binary in One Container
Both original and instrumented binaries are included in one image, with a symbolic link used to select the active binary
Pre-deployment script modifies the symlink based on the environment
Maintains clear separation of binaries; requires slight deployment logic
Examples for Deployment Options
The examples below demonstrate how to activate or switch SeaLights instrumentation in different environments using Kubernetes manifests, as Kubernetes is a widely adopted standard for containerized applications. These examples can be easily adapted to fit your existing Continuous Deployment setup, regardless of the specific tools or orchestration platform you use.
Single Binary – Enabling SeaLights via Environment Variable
After scanning the application with the --disable-on-init true
parameter, you can use the same container image across all environments and selectively enable SeaLights only in test environments by adding an environment variable:
apiVersion: v1
kind: Pod
metadata:
name: myapp-test
spec:
containers:
- name: myapp
image: myorg/myapp:instrumented
env:
- name: SEALIGHTS_DISABLE_ON_INIT
value: "false"
In production, either omit the SEALIGHTS_DISABLE_ON_INIT
variable or set it to "true"
to ensure instrumentation remains disabled.
If you scan the application with the --disable-token-save
parameter, the agent token must be provided explicitly via the SEALIGHTS_AGENT_TOKEN
environment variable. This approach is considered a best practice, as it avoids embedding the token in the binary and reduces the risk of exposure through reverse engineering, for example.
This approach avoids maintaining multiple images and activates coverage only where needed.
Separate Images – Switching via Image Tag and Environment Variables
This approach uses two distinct container images:
myorg/myapp:latest
– uninstrumented (regular) version for production or performance environmentsmyorg/myapp-sealights:latest
– SeaLights-instrumented version for test environments
Base Deployment (prod/default environments):
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 1
template:
spec:
containers:
- name: myapp
image: myorg/myapp:latest
Test Deployment (SeaLights-enabled):
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 1
template:
spec:
containers:
- name: myapp
image: myorg/myapp-sealights:latest
env:
- name: SEALIGHTS_LAB_ID
value: "test-lab"
In environments where SeaLights instrumentation is not required, simply deploy the standard image (myorg/myapp:latest
) without any need for declaring the SEALIGHTS_LAB_ID
variable.
This option ensures strong separation of concerns by isolating instrumented and production builds, making it easy to control where coverage is active and minimizing the impact on production environments.
Dual Binary in Single Container – Symlink Switch in InitContainer
In this approach, the container image includes two versions of the application binary:
/app/myapp-original
– the uninstrumented (regular) version/app/sl-myapp-executable
– the SeaLights-instrumented version
At runtime, an InitContainer replaces the default binary with the instrumented version by updating the symbolic link:
apiVersion: v1
kind: Pod
metadata:
name: myapp-test
spec:
initContainers:
- name: switch-to-sealights
image: busybox
command: ["sh", "-c"]
args:
- |
mv /app/myapp-executable /app/myapp-original
ln -s /app/sl-myapp-executable /app/myapp-executable
volumeMounts:
- name: app-bin
mountPath: /app
containers:
- name: myapp
image: myorg/myapp:combined
volumeMounts:
- name: app-bin
mountPath: /app
volumes:
- name: app-bin
emptyDir: {}
In environments where SeaLights instrumentation is not required, you can simply omit the InitContainer or adjust its logic to preserve the default binary (/app/myapp-executable
pointing to the original version).
This option keeps binaries physically separated within the same image while enabling flexible runtime switching based on the environment.
Last updated
Was this helpful?