Zero-Code Instrumentation in OpenTelemetry
How you can make a running application report traces, metrics and logs without changing a single line of its source code, how that trick works under the hood, and where it helps, where it stops, and how to use it wisely.
Imagine you move into an old house and want to know how much electricity each room uses. One way is to tear open the walls and rewire every circuit with built-in meters. Another way is to plug small smart meters into the sockets that already exist. The second way takes an afternoon, needs no builder, and leaves the house untouched. The meters still tell you what you need to know, though they cannot see inside the wires themselves.
Zero-code instrumentation is the smart-meter approach for software. A company may run hundreds of programs, some written years ago by people who have left. Asking every team to edit its code and add monitoring calls would take months. With zero-code instrumentation, you attach a helper to each program when it starts, and the helper begins to report what the program is doing. No source files are opened, no new version is built, and no developer has to be convinced.
In this guide we begin from zero. We explain what instrumentation is, how the zero-code approach compares with the others, and how it works inside for each language. Then we look at Kubernetes, configuration, the data you get, the limits you must know, and the habits that keep it safe and affordable. Every new word is explained when it first appears, and each big idea comes with an everyday comparison.
1What Is Zero-Code Instrumentation?
It is a way to collect telemetry from an application by attaching something to it from outside, instead of editing the application itself.
OpenTelemetry, often shortened to OTel, is an open-source project that gives software a standard way to produce telemetry. Telemetry is the stream of information a program sends out about itself: what it did, how long things took, what went wrong. People read this stream to understand and repair their systems. The three main kinds of telemetry, called signals, are traces, metrics and logs.
Instrumentation is the work of making a program produce that telemetry. Someone, or something, must notice “a web request arrived” and “the database call took 40 milliseconds” and turn each event into a record. Zero-code instrumentation is instrumentation that needs zero changes to your application’s source code. Instead, a helper is loaded alongside your program when it starts. The helper watches popular libraries that your program already uses, such as web frameworks and database drivers, and creates the records for you.
Other names for the same idea
You will meet several names. The OpenTelemetry documentation uses zero-code instrumentation. Older articles and many tools say automatic instrumentation or auto-instrumentation. Some people say agent-based instrumentation, because in Java the helper is called an agent. They all point at the same idea: telemetry without editing the code.
What exactly is “zero”?
The word “zero” refers to your source code. You still do some work. You install or download the helper, set a few settings, and restart the program. You may change the command that starts the program, or the container image, or a deployment file. What you do not do is open the program’s own files, add monitoring calls, or recompile it. This difference is the heart of the idea, and it explains both the strength and the limits of the approach.
Think of a fitness tracker on your wrist. It tells you your steps, heart rate and sleep without you changing how you walk, run or sleep. You do not rebuild your body to be measured. The tracker sits on the outside and watches movement. Zero-code instrumentation is a tracker for software. It cannot know your private thoughts, which here means your business meaning, but it can tell you a great deal about the visible movement.
Where it fits in the OpenTelemetry family
OpenTelemetry has several parts that work together. The API is a small set of calls that code uses to create telemetry. The SDK is the engine that collects, processes and exports it. Instrumentation is the code that calls the API at the right moments. And the Collector is a separate program that receives and forwards the data. Zero-code instrumentation supplies the instrumentation part, and it brings an SDK along with it, so your program needs nothing else added.
Zero-code does not mean zero effort and does not mean zero knowledge. You still choose settings, check the output and decide what to keep. It means that the first useful data can appear in minutes rather than weeks, and that no developer has to touch the code to get it.
2The Problem It Solves
Monitoring is most useful when it covers everything, and covering everything by hand is slow, uneven and easy to forget.
The coverage problem
Suppose a company has two hundred services. To add monitoring by hand, each team must learn the tools, edit its code, test the change, release it and fix mistakes. Some teams finish in a week. Others are busy with other work and do nothing for a year. After six months you have a map with half the cities missing. Requests enter the covered services and vanish into the dark ones, and traces break at every border.
The legacy problem
Old programs are a special difficulty. The code may be fragile, the original authors may be gone, and the team that owns it may be afraid to touch it. Sometimes the source is not even available, because the program is a purchased product. You cannot edit what you do not have. A method that works from outside is the only way to see inside such a program.
The speed problem
When something is on fire, you want to see the system now. Waiting for a development cycle to add monitoring is too slow. Teams that can switch on telemetry by changing a start-up setting can get data for a troubled service within an hour.
The consistency problem
When many teams write their own instrumentation, each makes different choices. One names a field one way and another names it differently. Zero-code instrumentation applies the same well-tested rules to every program, which gives the same names, the same structure and the same quality across the company.
Slow rollout
Hand-written monitoring takes many team-months across a large company, and finishing is never certain.
Untouchable code
Legacy or purchased programs cannot be changed safely or at all, but still need to be seen.
Urgent visibility
During an incident you need data within hours, not after the next release.
Uneven quality
Different teams produce different names and different detail, so data cannot be combined.
A city that wants to track traffic has two choices. It can ask every shop owner to count passing cars by hand and report the numbers, or it can install cameras at the main junctions. The shop owners are busy and count differently. The cameras are fitted by one team in one pass and record in the same way everywhere. Zero-code instrumentation is the camera at the junction.
Cameras at junctions show where traffic flows, but they cannot tell you why a particular driver turned left. In the same way, zero-code instrumentation shows the technical movement of requests, and it cannot know your business reasons. Chapters 4 and 13 explain how to add the missing meaning on top.
Who benefits, and how
Zero-code instrumentation is not only for developers. Each group in a company gains something different. Developers get traces without a detour from their feature work, and they can read the traces to find slow parts of their own service. Operations and reliability engineers get a uniform view of every service, which shortens the time to find the cause of an outage. Security teams get a map of which services talk to which, including calls that nobody remembered. Managers and architects get evidence for decisions: which services are busiest, which dependencies are slowest and where money is being spent.
The common thread is that the cost of getting a first, useful picture falls from months of coordinated effort to days of platform work. A smaller cost of entry means that more teams start, and a system with more teams started is easier to understand as a whole.
Example: the forgotten dependency
After switching on zero-code tracing, a company discovers that an old billing service still calls a reporting service that was supposed to be retired two years ago. Nobody had a record of the link, because it lived in code written by a team that had since moved on. The traces showed it within a day, and the team could plan a safe retirement.
3A Quick Refresher: Spans, the SDK and Libraries
Five small ideas make the rest of this guide easy: span, trace, library, SDK and exporter.
Traces and spans
A span is one timed step of work, such as “handle this web request” or “run this database query”. It has a name, a start time, an end time, a status and a set of labels called attributes. A trace is the family tree of spans that belong to one request as it travels through the system. Parent spans contain child spans, and the tree shows where the time went.
Libraries: where the interesting events happen
Most programs do not talk to the network or the database directly. They use libraries, which are ready-made packages written by others: a web framework to accept requests, an HTTP client to make calls, a database driver to run queries, a message client to use a queue. The events we care about happen inside these libraries. That is the key to zero-code instrumentation. If a helper can watch the library, it does not need to touch the application code that calls the library.
The SDK and the exporter
The SDK is the part that gathers spans, applies rules such as sampling, and hands them on. An exporter is the final piece that sends the data out of the program, usually to the OpenTelemetry Collector or straight to a monitoring tool. The usual language for this journey is OTLP, the OpenTelemetry Protocol. In zero-code setups, the helper brings its own SDK and exporter, so you only need to say where to send the data.
| Term | Plain meaning | Everyday comparison |
|---|---|---|
| Span | One timed step of work | One stop on a delivery round |
| Trace | All the spans for one request | The whole delivery route |
| Library | A package your program uses for common tasks | A tool borrowed from a shared workshop |
| Instrumentation | Code that watches something and creates telemetry | A sensor on a machine |
| SDK | The engine that collects and processes telemetry | The control room that receives sensor signals |
| Exporter | The part that sends data out | The radio that sends the signals to headquarters |
A very small picture of one request
Consider a shopper pressing “Buy”. The orders service receives a web request, asks the database to save an order and calls the payment service over HTTP. With instrumentation, three spans appear: one for handling the web request, one for the database query and one for the outgoing HTTP call. The spans are linked as parent and children. This is exactly what a zero-code helper produces, with no changes to the orders service.
flowchart TD A["Span: handle web request in orders service"] --> B["Span: database query"] A --> C["Span: HTTP call to payment service"] C --> D["Span: handle request in payment service"]
A hospital monitors a patient with sensors on the body, not by asking the patient to write down their own pulse. The sensors attach to places where the signals appear: the wrist, the chest, the finger. Libraries are those places in software. The helper attaches to the web framework, the database driver and the HTTP client, because the signals appear there.
Because the helper follows the standard naming rules from OpenTelemetry’s semantic conventions, the spans and attributes it creates use agreed names such as the HTTP method and the status code. This is why data from different languages can be shown on one dashboard.
4The Spectrum: Zero-Code, Library and Manual
Instrumentation is not one choice but a range. Understanding the range tells you when zero-code is enough and when it is not.
Three levels of effort
At one end is zero-code instrumentation: you attach a helper from outside and change no source code. In the middle is library instrumentation: you add instrumentation packages to your code, and write a small amount of set-up code to turn them on. At the other end is manual instrumentation: you write calls to the OpenTelemetry API yourself, deciding which spans to create and which attributes to add. These levels are not rivals. They are tools that you can combine in one program.
flowchart LR Z["Zero-code: attach a helper, no code change"] --> L["Library: add packages and small set-up code"] L --> M["Manual: write API calls for custom spans and attributes"]
| Question | Zero-code | Library | Manual |
|---|---|---|---|
| Change source code? | No | A little set-up | Yes |
| Time to first data | Minutes to hours | Hours to days | Days to weeks |
| Coverage of common libraries | Wide and automatic | Chosen one by one | Only what you write |
| Business meaning | None by itself | Limited | Full control |
| Control over detail | Settings only | Medium | Complete |
| Best for | Quick breadth, legacy, uniform coverage | Teams that own their code | Key business operations |
What zero-code cannot know
The helper sees libraries, not intentions. It can see that a database query ran and how long it took. It cannot know that this query was “check whether the discount code is valid”. It sees an HTTP request to a payment service but not that the amount was large or that the customer was a new one. These business details live in your code, and only manual instrumentation can capture them. The mix is therefore usual: zero-code for broad coverage, then manual additions in the few places where meaning matters.
Consider a sports match. A broadcast camera that follows the ball (zero-code) tells you who has it, where it went and how fast. A commentator who knows the team’s tactics (manual) tells you why the coach changed the plan. You need the camera for everyone to see the game, and you add the commentator for the moments that need explanation.
Start with zero-code to see the whole system quickly. Look at the traces to find the important operations. Then add manual spans and attributes only for those, rather than instrumenting everything by hand.
5How the Trick Works: Four Techniques
A program does not change because you wish it to. Each language offers a different door through which a helper can slip in.
Why the technique differs by language
Languages are built differently. Some run on a virtual machine that allows code to be rewritten as it loads. Some are interpreted and let you swap functions while the program runs. Some compile to machine code with no hooks at all. Because of this, there is no single method, and OpenTelemetry uses a few. Knowing the four techniques helps you understand the strengths and failures of each.
Bytecode instrumentation
Used by Java and other virtual-machine languages. A helper called an agent changes the compiled instructions of library classes as they are loaded, adding code that records telemetry.
Monkey patching
Used by interpreted languages such as Python and Node.js. The helper replaces library functions with wrapped versions that record telemetry and then call the original.
Runtime profiler hooks
Used by .NET. A profiler interface lets a helper rewrite code as the runtime prepares it, in a similar spirit to bytecode work.
eBPF
Used for compiled languages such as Go. Small programs run safely inside the operating system’s kernel and observe the target program from outside, without changing it at all.
Bytecode in simple words
When a Java program is built, it is turned into bytecode, a compact set of instructions that the Java Virtual Machine runs. Java allows an agent to be loaded before the program starts, and that agent is allowed to look at each class as it is loaded and change its instructions. The agent finds the part of a web framework that handles requests and adds a few instructions at the start and the end: “begin a span” and “end the span”. The framework’s own file is unchanged on disk. The change happens in memory only.
Monkey patching in simple words
The name sounds silly, but the idea is easy. In Python and JavaScript, a function is just a value stored under a name, and you can replace it. A helper takes the library’s function that sends an HTTP request, stores it safely, and puts in its place a new function. The new function starts a span, calls the original function, ends the span and returns the same result. Your code still calls the same name and gets the same answer, but a span now appears.
eBPF in simple words
eBPF is a technology in the Linux kernel that lets small, verified programs run inside the operating system and watch what happens, such as network calls and function entries. A helper built on eBPF can see that a Go program accepted an HTTP request and made a database call, without being loaded into the program at all. This is useful for languages that give no friendly hooks. The price is limits: it needs a suitable Linux kernel and elevated permissions, and it may see less detail than an in-process agent.
| Technique | Where it lives | Strength | Weakness |
|---|---|---|---|
| Bytecode | Inside the program’s virtual machine | Rich detail, wide library support | Startup cost, version compatibility |
| Monkey patching | Inside the program’s interpreter | Simple, works for many libraries | Must match library versions and load order |
| Profiler hooks | Inside the .NET runtime | Good coverage without code changes | Needs environment set-up |
| eBPF | Outside the program, in the kernel | Works for compiled code, no restart of code paths | Linux only, higher privileges, less detail |
Four ways to learn what a shop sells without asking the owner. You can send a secret shopper who stands at the till and writes down each sale (monkey patching). You can fit the cash register itself with a counter, which changes it while it is being set up (bytecode and profiler hooks). Or you can watch the shop from a camera across the street (eBPF). Each method sees something different, and each has its own limits.
Because zero-code changes behaviour at the edges of your program, it can in rare cases conflict with other tools that do the same trick, such as a commercial monitoring agent. Run only one agent of this kind per program unless the documentation says that two can live together.
A first test, whatever the language
Whichever technique your language uses, the first test is the same, and it is worth doing before any rollout. Start the program with the helper attached and a console exporter turned on, so that spans are printed to the screen instead of sent over the network. Send one simple request to the program, such as opening a page. If the helper works, you will see at least one printed span with the method, route and status code of the request, and, if the page uses a database, another span for the query.
This tiny test checks three things at once. It confirms that the helper loaded, that it recognises your libraries and that spans are created. If it fails, you have narrowed the problem to the machine in front of you, without any network, Collector or backend in the way. Only after this works should you point the helper at a real Collector.
Attach and print
Start the program with the helper and the console exporter on a test machine.
Send one request
Trigger a simple request and watch for printed spans.
Connect to a Collector
Point the helper at a test Collector and confirm that the same spans arrive there.
Check the backend
Look for the service name in your tracing tool and open a trace.
6The Java Agent in Depth
Java was the first language to get full zero-code support, and its agent is the most widely used and the most complete.
What the agent is
The OpenTelemetry Java agent is a single file, a JAR, that you download from the project’s releases. It is an instrumentation agent, a special kind of Java program that the Java Virtual Machine, or JVM, can load before it runs your application. The agent bundles everything needed: a copy of the OpenTelemetry SDK, an exporter, and a large collection of instrumentation for popular libraries. It supports hundreds of libraries and frameworks, covering web servers, HTTP clients, database drivers, message systems, remote procedure calls and more.
How you attach it
You attach the agent by adding a flag to the command that starts the program, telling the JVM to load the agent file first. When the start command is hard to change, as in many containers and managed platforms, you can pass the same flag through a standard environment variable that the JVM reads on its own. Either way, the application is not edited, not rebuilt and not repackaged.
sequenceDiagram participant J as JVM participant A as OpenTelemetry agent participant L as Library classes participant O as Backend or Collector J->>A: Load the agent before the application starts A->>A: Read settings and start the SDK J->>L: Load library classes A->>L: Rewrite chosen classes to add telemetry J->>J: Run the application as normal L->>A: Report spans, metrics and logs A->>O: Export data using OTLP
What happens at startup
The JVM starts and loads the agent
Before your first line of code runs, the JVM calls the agent’s entry point and gives it a chance to register itself.
The agent reads its settings
It reads environment variables and system properties that say where to send data, how to name the service and which parts to switch on or off.
It starts the SDK and exporters
An internal SDK is created, with the chosen sampler, propagators and exporters ready for the first span.
It watches class loading
As the JVM loads classes, the agent checks each one against a list of known libraries and versions, and rewrites the ones that match.
The application runs
From then on, calls into instrumented libraries create spans and metrics without the application knowing.
What it does for you
The agent creates a span for every incoming web request and a span for each outgoing call. It records database queries, message sends and receives, and remote procedure calls. It passes the trace identity along with each outgoing request, using a propagation format such as W3C Trace Context, so that traces join across services. It records metrics about the runtime, such as memory and thread counts, and about request durations. And it can add the trace and span identifiers to log records so that logs link to traces.
Turning parts on and off
Not every instrumentation is wanted. The agent lets you disable the whole agent, disable instrumentation for one library, or turn off all instrumentation and then enable only a chosen few. Disabling noisy parts is a common way to reduce volume and cost. It also lets you avoid trouble if one library’s instrumentation misbehaves with your version.
The Java agent can be extended with your own small plug-in that adds instrumentation for a library it does not know, or changes how it handles certain data. This is still not a change to your application, and it is a good fit for company-wide rules.
Think of a new employee who arrives at a factory before the shift starts, reads the safety notes, and fits small counters to the main machines as the workers bring them online. The factory does not change its blueprint, and the workers carry on as usual. The Java agent is that employee, and the counters are the instructions it adds in memory.
7Python and Node.js
Interpreted languages let a helper replace functions while the program runs. The set-up is light, but a few details decide whether it works.
Python
Python’s route has three steps, all done outside your code. First, you install a package named opentelemetry-distro together with an OTLP exporter package. The distro bundles the SDK and a sensible default configuration. Second, you run a command called opentelemetry-bootstrap. It looks at the packages that are already installed in your environment, such as a web framework or a database driver, and installs the matching instrumentation packages, one per library found. Third, you start your program through a wrapper command named opentelemetry-instrument, placed in front of your usual start command.
The wrapper sets up the SDK, loads every instrumentation package it finds, patches the libraries, and then starts your program as normal. Your own files are not opened. This design means the set of instrumented libraries depends on what is installed, so after you add a new library you should run the bootstrap step again.
Some Python servers start several worker processes by copying a main process, a method called forking. Telemetry components that were created before the fork, such as background export threads, do not survive the copy. In such servers, the SDK may need to be started inside each worker, which usually means a small hook in the server’s own configuration file. Read the documentation for your server before you trust the first test.
Node.js
For Node.js, you install the package @opentelemetry/auto-instrumentations-node. It contains instrumentation for many popular libraries, including web frameworks, HTTP clients, database drivers and message clients. To switch it on without editing code, you tell Node to load a registration file before your application, using a start-up option. A common method is to set that option in the NODE_OPTIONS environment variable, so that every Node process in the container picks it up.
Node.js has two module systems, the older require style and the newer import style. The older one is easy to patch because the loader can be intercepted. The newer one needs a special loader hook, and the start-up flag is different. Check which style your application uses, and use the matching instructions. You can also choose which instrumentations to enable with an environment variable, which is handy when you want only a few.
| Point | Python | Node.js |
|---|---|---|
| Main package | opentelemetry-distro plus exporter | @opentelemetry/auto-instrumentations-node |
| How libraries are found | A bootstrap command installs matching packages | A bundle includes many instrumentations |
| How it is switched on | Start through the opentelemetry-instrument wrapper | Load a registration file at start-up |
| Technique | Monkey patching | Module loader interception and patching |
| Common trap | Forking servers need per-worker set-up | Bundled code cannot be patched |
The bundler trap in Node.js
Many JavaScript projects are packed into a single file by a bundler before they run. A bundler pulls the code of every library into one place and removes the module boundaries. Monkey patching works by intercepting the moment a library is loaded by name. If the libraries are already merged into one file, there is no such moment, and the patching silently does nothing. The usual cure is to leave the instrumented libraries out of the bundle, so that they are loaded normally from the folder of installed packages.
Imagine a switchboard operator who can listen in on calls only when a call is connected through the main exchange. If two neighbouring offices are wired directly to each other, the operator never sees their calls. A bundler wires libraries directly to your code, and the helper, standing at the exchange, hears nothing.
In both languages, the quickest test is to start the program with a console exporter, which prints spans to the screen. If a request produces printed spans, the patching works. If nothing prints, check the load order, the bundler and the library versions before you look anywhere else.
8.NET, Go, PHP and Other Languages
Not every language offers the same door. Support and maturity differ, so it pays to know what to expect.
.NET
The OpenTelemetry .NET automatic instrumentation uses the runtime’s profiler interface, a feature that lets a native component watch and rewrite code as the .NET runtime prepares it. It also uses a start-up hook that runs before your application’s own entry point. You install the instrumentation using a provided script or package, and then set a group of environment variables that tell the runtime to load the profiler. Once these are set, ASP.NET Core requests, outgoing HTTP calls and many database and messaging clients are covered. It runs on both Windows and Linux. As with Java, you do not recompile your application.
Go
Go compiles to native machine code and offers no easy place to attach a helper while the program runs. For this reason, zero-code support for Go is built on eBPF. A separate process, often run beside the application as a sidecar, uses eBPF to observe the Go program from the kernel. It can see incoming web requests, outgoing calls and some database and messaging activity, and it creates spans from what it sees. This needs Linux, a recent enough kernel and special privileges. The project is under active development, and a related effort contributed to OpenTelemetry brings eBPF-based instrumentation for several languages at the network level. There are also newer approaches that change the program during the build. Because this area is moving fast, check the current state before you plan around it.
PHP
PHP can be instrumented without code changes by installing the OpenTelemetry extension, a small native module loaded by the PHP engine, together with packages that provide instrumentation for common frameworks and libraries. The extension lets the packages hook into function calls. Once installed and configured, requests handled by supported frameworks produce spans.
Web servers and other places
Instrumentation also exists for web servers, such as Apache HTTP Server and Nginx, through loadable modules. These create spans for each request they handle and pass trace identity onward to the services behind them. Some other languages, such as Ruby, offer instrumentation packages that need a small amount of set-up code in the application. That is close to zero-code, but not fully. Always read the status for your language rather than assume.
| Language or server | Main mechanism | How it is attached |
|---|---|---|
| Java | Bytecode agent | A start-up flag pointing to the agent file |
| Python | Monkey patching | A wrapper command in front of the start command |
| Node.js | Module loader patching | A start-up option that loads a registration file |
| .NET | Runtime profiler hooks | Environment variables and an installed instrumentation |
| Go | eBPF from outside the process | A separate, privileged observer process |
| PHP | Native extension with packages | Install the extension and instrumentation packages |
| Apache and Nginx | Loadable server modules | Enable the module in the server configuration |
Different buildings have different doors for a delivery person. One has a ground-floor loading bay, another a staff entrance, and a third only a window on the first floor. The delivery person is skilled in every method but must be told which door each building has. Language runtimes are the buildings, and each one has its own door for instrumentation.
Maturity varies. Java and several other languages have broad, long-tested support. Newer or harder languages have narrower coverage and more caveats. Check the status labels in the documentation, and test with your real libraries before you promise coverage to anyone.
9Kubernetes, Containers and Serverless
Modern programs run in containers. The OpenTelemetry Operator can attach zero-code helpers to thousands of them from one place.
The problem at scale
Attaching a Java agent to one program by hand is easy. Attaching it to four hundred programs, in a mix of languages, each started by a different team, is not. You would have to change four hundred images or start commands. The OpenTelemetry Operator for Kubernetes solves this by making the attachment part of the platform rather than part of each program.
How the Operator does it
The Operator is a program that runs inside your Kubernetes cluster. It adds a new kind of resource called Instrumentation, in which you describe the helper: where to send data, which sampler to use, which propagators and which resource labels to add, and which language images to use. Then, you put an annotation, a small label, on the workload that you want instrumented. The annotation names the language, for example instrumentation.opentelemetry.io/inject-java for Java. Equivalent annotations exist for Node.js, Python, .NET, Go, Apache HTTP Server, Nginx and others.
When a pod is created, a component called an admission webhook in the Operator notices the annotation and changes the pod’s definition before it starts. It adds an init container, a short-lived helper that runs first and copies the agent files into a shared folder. It then adds environment variables to the application container so that the language starts with the agent attached. Your image is not rebuilt, and the developer does not change the start command.
sequenceDiagram participant U as Platform engineer participant K as Kubernetes API participant W as Operator webhook participant P as Pod U->>K: Create Instrumentation resource U->>K: Add inject annotation to a workload K->>W: New pod is being created W->>K: Add init container and environment variables K->>P: Start the init container P->>P: Copy agent files to a shared folder K->>P: Start the application with the agent attached
Install the Operator
Install it in the cluster. It normally needs a way to create certificates for its webhook, which is often provided by a certificate manager, unless you configure another method.
Deploy a Collector
Run a Collector, as an agent per node or as a gateway, so that the instrumented pods have somewhere close to send data.
Create an Instrumentation resource
Describe the endpoint, sampler and propagators once. All workloads that point to it share these settings.
Annotate workloads
Add the language annotation to a test workload first. Restart its pods, because the change applies only when pods are created.
Check and widen
Confirm traces in your tool, then roll out the annotation to more workloads, namespace by namespace.
Other places that run programs
Outside Kubernetes, the same ideas apply. On plain virtual machines, you can set the environment variables in the service definition. In Docker images, you can add the agent file and variables in the build. For serverless functions, the project publishes layers for some cloud function platforms, which are add-on packages that bring in the language helper and a lightweight Collector. Each platform has its own details, but the pattern is the same: the helper is added beside the code, never inside it.
A hotel manager wants every room fitted with a smoke detector. Visiting every room with a ladder takes weeks. Instead, the building has a standard installation step that runs whenever a room is prepared for a guest, and a detector is added automatically. The Operator is that standard step for pods.
If a pod has several containers, tell the Operator which container to instrument, using the container name annotation, so that it does not attach the helper to a side-car or tool container by mistake.
Rolling out without Kubernetes
Many companies still run programs on plain servers or in simple container setups, and zero-code works well there too. On a virtual machine, you place the helper files in a known folder, then add the environment variables and the start-up flag to the service definition, such as the file that your service manager reads. After a restart of the service, the helper is active. Because the settings are in one file per service, a configuration management tool can roll them out to many machines at once.
In a container image, you copy the helper files into the image during the build and set the variables in the image or in the deployment file. This makes the image self-contained and predictable, because every copy of the container has exactly the same helper version. The cost is that an upgrade of the helper means rebuilding images. Teams often choose a middle path: a shared base image per language that already includes the helper, which each team builds on.
Virtual machines
Install the helper in a shared folder and change the service definition. A restart activates it.
Container images
Bake the helper into the image or a shared base image, so every container has the same version.
Platform-as-a-service
Use the platform’s setting for environment variables and start options, if it exposes them.
Serverless layers
Add the published layer to the function and set the variables in the function’s configuration.
Wherever you run, keep the helper version in one place per environment, so an upgrade is one change and not hundreds. A single source of truth for the version is the best protection against fleets drifting apart.
10Configuring It with Environment Variables
Because you cannot edit the code, settings come from outside. OpenTelemetry defines a shared family of environment variables that work across languages.
Why environment variables
An environment variable is a named setting that is supplied to a program when it starts. It is available to every language, it is easy to set in containers, service definitions and deployment files, and it needs no code. OpenTelemetry has standardised a set of names that most language helpers understand, so that one knowledge carries from Java to Python to Node.js.
| Variable | What it controls |
|---|---|
| OTEL_SERVICE_NAME | The name of the service, the most important label on all data |
| OTEL_RESOURCE_ATTRIBUTES | Extra labels, such as the environment and version, as key and value pairs |
| OTEL_EXPORTER_OTLP_ENDPOINT | Where to send data, usually a Collector address |
| OTEL_EXPORTER_OTLP_PROTOCOL | Which OTLP form to use, such as gRPC or HTTP |
| OTEL_TRACES_EXPORTER, OTEL_METRICS_EXPORTER, OTEL_LOGS_EXPORTER | Which exporter each signal uses, or none to switch it off |
| OTEL_TRACES_SAMPLER and OTEL_TRACES_SAMPLER_ARG | How many traces to keep, such as a parent-based ratio sampler and its percentage |
| OTEL_PROPAGATORS | Which trace-header formats to read and write |
| OTEL_SDK_DISABLED | A switch to turn the SDK off |
The settings that matter first
If you set only three things, set the service name, the endpoint and the environment label. Without a service name, data is grouped under a placeholder such as “unknown_service”, which makes dashboards confusing. Without an endpoint, the helper tries a default address on the local machine, which usually does not exist in a container. The OTLP default ports are 4317 for gRPC and 4318 for HTTP. Without an environment label, you cannot tell production from testing.
Language-specific switches
Beyond the shared names, each helper has its own settings. The Java agent, for example, can disable individual instrumentations, turn on debug logging for the agent itself, or capture selected HTTP headers when you ask. Node.js lets you list the instrumentations to enable. Always keep a record of which settings are shared and which are specific, so you can reuse the shared ones across languages.
Name and labels
Set the service name and resource attributes so every signal says who sent it and from where.
Endpoint and protocol
Point to a Collector near the application, and choose the protocol that your network allows.
Sampler and exporters
Control how much data is kept, and which signals are sent at all.
Propagators
Choose header formats that match your neighbours, so traces connect across services.
Think of the settings label on a washing machine: a few knobs for temperature, spin and programme. You do not open the machine to change how it works, you turn the knobs. Environment variables are the knobs of the helper. A small number of well-chosen settings covers nearly every use.
Environment variables can contain secrets, such as authentication headers for a vendor. Do not paste them into shared files or logs. Supply them through your platform’s secret store, and give the least access that works.
One plan, three environments
The same settings do not suit every environment. A short planning table avoids the common mistake of copying production settings into development, or the reverse. The values below are illustrative, and you should choose your own after measuring.
| Setting | Development | Staging | Production |
|---|---|---|---|
| Sampler | Keep every trace | Keep a large share | Keep a small share, plus errors through the Collector |
| Exporter | Console, to see spans quickly | OTLP to a test Collector | OTLP to a nearby Collector |
| Debug logging of the helper | On when investigating | Off, switched on briefly if needed | Off |
| Captured headers | A few for testing | As few as needed | Only approved, safe ones |
| Environment label | development | staging | production |
Keep these differences in the deployment files, not in the application. Because zero-code is controlled entirely by settings, switching an environment’s behaviour is a matter of changing variables, and the same application build can move from one stage to the next unchanged. This is one of the most practical benefits of the approach.
11What Telemetry You Actually Get
Knowing what arrives, and what does not, saves you from both disappointment and false confidence.
Traces
The most visible output is traces. For each incoming request, the helper creates a server span with the method, route and status code. For each outgoing call, it creates a client span and adds the trace headers so that the next service continues the same trace. Database calls become spans with the database system and operation. Message sends and receives become producer and consumer spans. When all your services are instrumented, the spans join into trees that follow a request across the whole system.
Metrics
The helper also produces metrics. The common ones are request counts and durations by route and status code, plus metrics about the language runtime, such as memory use, garbage collection and thread counts. These give you the classic “rate, errors and duration” view of each service without any extra code. The exact set depends on the language and version.
Logs
For logs, the help is usually correlation rather than collection. Many helpers add the trace and span identifiers to the log lines written by popular logging libraries. That lets you click from a log line to the trace that produced it. Some helpers can also send logs through OpenTelemetry directly, if you switch that on. Check your language’s status, since log support has matured later than traces.
| Signal | What you usually get | What you usually do not get |
|---|---|---|
| Traces | Spans for web requests, outgoing calls, database and messaging operations | Spans for your own business steps |
| Metrics | Request rate and duration, runtime health | Business counts such as orders placed |
| Logs | Trace identifiers on log lines, sometimes log export | Better log messages or new log statements |
Standard names
The helper follows the OpenTelemetry semantic conventions, which are the agreed names for attributes, spans and metrics. For example, it records the HTTP method and the response status code under the agreed names, uses seconds for durations and uses low-cardinality route templates in span names. This is why data from a Java service and a Python service can appear on the same chart.
Sensible privacy defaults
Helpers are designed not to collect everything. HTTP headers and request bodies are normally not captured unless you request specific headers. Database statements are sanitised in many cases, so that literal values are replaced with placeholders. These defaults reduce the risk of leaking private data, but they are not a guarantee, so you should still check what leaves your network, as the security chapter explains.
A traffic camera records the plate number, speed and direction of each car, but not what is in the boot or who is driving. The details it records follow a fixed form, so the police in two different cities can read each other’s records. The helper’s output has the same standard form, and similar limits on what it looks at.
If you do not see a span you expected, the usual cause is that the library or its version is not on the supported list. The documentation of each helper includes a list of supported libraries and versions, and it is the first place to look.
12How It Works Inside: The Life of a Request
Follow one web request through an instrumented program to see exactly where each piece of telemetry is born.
The data flow
flowchart LR Req["Incoming request"] --> Hook1["Instrumented web framework"] Hook1 --> App["Your application code"] App --> Hook2["Instrumented database driver"] App --> Hook3["Instrumented HTTP client"] Hook1 --> SDK["Embedded SDK: sampler, processor"] Hook2 --> SDK Hook3 --> SDK SDK --> Exp["OTLP exporter"] Exp --> Col["Collector"] Col --> Back["Backend"]
The request arrives
The instrumented web framework reads any incoming trace headers, rebuilds the parent context and starts a server span.
The sampler decides
The sampler looks at the parent decision and its own rule, and chooses whether to record. A dropped span still carries the context forward.
Your code runs
The current span is stored in the context, which follows the code even as work moves between threads or callbacks.
Library calls create child spans
When your code calls the database or another service, the instrumented library starts a child span, adds trace headers to outgoing requests and ends the span on completion.
The response goes out
The framework records the status code, marks errors, and ends the server span.
Spans are batched and exported
Finished spans go to a batching processor that groups them and sends them in the background, so your request is not delayed.
Staying safe with versions
Instrumentation is written against particular versions of each library. If a library changes its internals, the instrumentation might break or, worse, break the application. The Java agent protects against this with a check often called muzzle. Before it rewrites a class, it checks that the library version has the shape the instrumentation expects. If it does not, the agent leaves that library alone instead of risking a crash. Other languages use version ranges in their packages for the same purpose. This is why the supported-versions list matters.
Context across threads
One of the harder jobs of the helper is keeping the context correct when work jumps between threads, thread pools, callbacks and asynchronous tasks. If the context is lost, a child span appears as the start of a new trace, and the tree breaks. The Java agent wraps common executors and frameworks to carry the context along. Python and Node.js use language features designed for the same purpose. Custom thread handling in your own code is the usual place where the context gets lost.
Flushing on shutdown
Because spans are batched, a few may still be waiting when the program stops. The helper normally registers a shutdown step that sends remaining data before the process ends. For programs that are killed abruptly, the last moments of data may be lost. In short-lived jobs and serverless functions, make sure the program has time to flush, or use a shorter batching delay.
A relay race follows a baton from runner to runner. The helper is a team of officials stationed at each handover who note the time and pass a coloured ribbon, the context, from hand to hand. If a runner hands the baton over in a hidden corridor where no official stands, the ribbon is dropped, and the record shows a new race. Thread hand-offs are those hidden corridors.
A troubleshooting guide
When something does not work, a methodical approach is faster than guessing. Match the symptom to the likely cause in the table below, then work through the checks.
| Symptom | Likely causes | First check |
|---|---|---|
| No spans at all | Helper not loaded, wrong start option, settings typo, SDK disabled | Look for the helper’s start-up message and try a console exporter |
| Spans for some libraries only | Unsupported library version, bundler hiding libraries, load order | Compare versions with the supported list |
| Traces broken between services | Different header formats, a proxy removing headers, lost context in threads | Compare propagator settings and inspect headers at each border |
| Everything under “unknown_service” | No service name set | Set the service name variable |
| Data never reaches the backend | Wrong endpoint or protocol, blocked network path, wrong credentials | Check the Collector’s receiver counters and the helper’s error logs |
| Too much data | No sampling, noisy instrumentation, health checks | Set a sampler and disable the noisy parts |
| Application slower to start | Many classes or modules rewritten | Allow for the extra time, and disable unneeded instrumentation |
| Duplicate spans | Two agents or two SDKs active | Remove the extra agent or the extra SDK dependency |
A good habit is to change one thing at a time and test after each change. Because zero-code involves many pieces, such as the helper, the settings, the network and the backend, changing several at once makes it impossible to know which change fixed or broke the result.
13Adding Manual Touches on Top
The best systems use zero-code for breadth and add a few manual details where business meaning matters.
One SDK, shared
The helper brings its own SDK and registers it globally. If your own code needs to add spans or attributes, you should depend only on the small API package and not on a second SDK. The API calls are routed to the SDK that the helper has already started. This means your manual spans appear in the same trace tree, with the same sampling and the same exporter, as the automatic ones. Adding a second SDK by mistake often causes duplicate or missing data.
Three small additions
Attributes on the current span
Fetch the span that the helper created for the current request and add business labels, such as the customer tier or the number of items in the cart.
Spans around key steps
Wrap an important internal step, such as “calculate discount”, in its own span so it shows in the trace tree. In Java, an annotation can do this with a single line above the method.
Custom metrics
Use the API to count business events, such as orders placed, and let the helper’s SDK export them with the rest.
flowchart TD Z["Layer 1: zero-code helper covers libraries"] --> M["Layer 2: manual API calls add business meaning"] M --> S["Shared SDK exports one consistent trace"]
Naming your own additions
Custom attributes should follow the same grammar as the standard ones: lowercase, with dots for groups and underscores for words in a group. Put them under a prefix that you own, such as your company name, and never invent names inside the standard groups. This avoids clashes with names that may be added in the future.
A short method for deciding what to add
Look at real traces
Find the slowest and most important requests, and note which steps are invisible.
Name the questions
Write the questions you could not answer, such as “which customer tier is slow?”
Add the smallest change
Add one attribute or one span that answers each question, and no more.
Check the cost
Make sure new attributes do not carry private data or an unbounded number of values.
A museum buys an audio guide that explains every famous painting in the same style. The curator then records a few extra notes for the paintings that have a special story. The standard guide gives breadth in one purchase, and the curator’s notes give the personality. Zero-code is the standard guide and manual spans are the curator’s notes.
Adding the API dependency is a code change, even if a small one, so this step is no longer zero-code. That is fine and expected. Keep the changes few and valuable, so that the benefit of fast, uniform coverage is not lost.
14Performance, Overhead and Scaling
A helper that watches everything is never completely free. Knowing where the cost appears lets you keep it small and predictable.
Where the cost comes from
Zero-code instrumentation costs something in four places. The first is start-up time. In the Java case, the agent examines and rewrites classes as they load, which can make the application take noticeably longer to start. The second is memory: the SDK, the buffers for waiting spans and the instrumentation itself all use space. The third is processor time: each span means a little extra work to create, fill and export. The fourth is network: every span, metric and log that leaves the program uses bandwidth.
The size of these costs depends on the language, the version, the number of libraries instrumented and the amount of traffic. For many typical services, the extra processor and memory use is modest, but it is not zero, and there are no universal numbers you can trust. The only reliable method is to measure your own service with and without the helper, using a realistic load.
| Cost | What drives it | How to reduce it |
|---|---|---|
| Start-up time | Number of classes or modules rewritten | Disable instrumentation you do not need; allow for it in health-check timeouts |
| Memory | SDK buffers, batch queues, instrumentation code | Set queue limits; give containers enough memory headroom |
| Processor time | Spans created per request | Sample, and switch off noisy instrumentation |
| Network | Volume of exported telemetry | Send to a nearby Collector; sample; drop low-value spans |
Sampling: the biggest lever
Most of the cost scales with the number of spans you create and export. Sampling means keeping only a share of traces. A common production choice is a parent-based sampler with a ratio, such as keeping one trace in ten, set through the environment variables. The decision is made at the start of a trace and passed along, so that all services keep or drop the same traces. Sampling at the application is cheap, because a dropped trace costs almost nothing to process. For smarter choices, such as always keeping errors, you can add tail sampling in the Collector.
Noisy instrumentation
Some instrumentation creates a very large number of spans. A program that reads from a database inside a tight loop may create thousands of tiny database spans for one request. Health-check endpoints can create a span every few seconds from every pod. These add cost and clutter without adding insight. Most helpers let you disable instrumentation for a specific library, or ignore requests to chosen paths. Review your first traces for such noise and remove it early.
Scaling to many services
Because the helper lives inside each program, its cost is paid once per instance, and it grows with your fleet. Two hundred services with five instances each means a thousand helpers sending data. A Collector close to the workloads helps. It receives data over a fast local link, batches it, and handles retries and delivery to the destination, so that the application does not need to. In Kubernetes, a Collector per node is a common design.
flowchart LR P1["Pod with helper"] --> N["Node Collector"] P2["Pod with helper"] --> N P3["Pod with helper"] --> N N --> G["Gateway Collector: sampling, redaction"] G --> B["Backend"]
A delivery company that tracks every parcel with a scanner knows that scanning has a cost: a few seconds per parcel, and a battery to charge. For a small town, this is invisible. For a huge sorting centre, it matters, so the company scans only what it must and sends all the scans to a central office through one fast link. Sampling and a nearby Collector play those roles.
When you roll out zero-code to a whole fleet at once, you may see a sudden rise in memory, start-up time and network use. Roll out in steps, watch the numbers on the first group, and keep resource requests and health-check timeouts generous enough for the new start-up behaviour.
A simple way to measure the overhead
Because no universal figure exists, run a small experiment on one representative service. First, run the service without the helper under a steady, realistic load for ten or fifteen minutes, and record start-up time, memory, processor use and the response times that matter, such as the 95th percentile. Second, run the same service, with the same load and the same machine size, with the helper attached and your chosen sampler. Third, compare the two sets of numbers.
Look at the difference rather than the absolute values. If the extra processor use is a few percent and the added start-up time fits inside your health-check timeout, the helper is a good fit. If the difference is larger, find out why: perhaps one instrumentation is very noisy, the sampler keeps everything, or the container has too little memory for the buffers. Fix the cause, repeat the test, and write down the result so that the next team can reuse the numbers as a guide, not as a promise.
Repeat the experiment after major upgrades of the helper or of your main libraries. Overheads can change from one version to the next, usually for the better, but a new instrumentation that is on by default may add spans you did not have before.
15Security, Monitoring and Cost
A helper that sits inside every program is powerful. That power calls for careful permissions, regular checks and a watch on the bill.
Security: what the helper can see and do
The helper runs with the same permissions as your application. It can read whatever the application can read. That is acceptable when you trust the source, but it also means that a compromised helper would be as dangerous as a compromised library. Treat the agent files as part of your software supply chain: download them from the official project, pin a specific version, check its integrity, and, in larger organisations, serve them from an internal mirror.
Pin versions
Use an exact version of the agent and instrumentation packages, and upgrade deliberately after testing.
Limit captured data
Leave header and body capture off unless needed, and name only the specific safe headers you want.
Redact at the Collector
Remove or mask sensitive attributes in a central place, so a single rule protects every service.
Minimal privileges
Give eBPF-based observers and Operator components only the permissions they need, and review them.
Private data in telemetry
Even with safe defaults, private data can slip through. A web address may include a token in its query part. A database statement may include a name if it was not parameterised. A span attribute that you add by hand may include an email. Treat telemetry as data that leaves your system, and review samples of it regularly. Use Collector processors to delete or mask sensitive fields, and prefer allow-lists for the most sensitive destinations.
Monitoring the helper itself
A silent failure is the main danger: the application works perfectly, but no telemetry arrives. Build simple checks. Confirm that each service appears in your tracing tool, with recent spans. Watch the Collector’s counters for data received from each source. Look at the application’s logs for messages from the helper about failed exports. Most helpers also have a debug setting that prints more detail about what they are doing, which is useful in a test place but too noisy for permanent use.
| Question | How to check |
|---|---|
| Is the helper loaded? | Look for its start-up message in the application logs |
| Is data leaving the program? | Use a console exporter in testing, or the Collector’s receiver counters |
| Is data reaching the backend? | Search for the service name and a recent trace |
| Are exports failing? | Check the helper’s logs and the Collector’s failure counters |
| Is the cost under control? | Track spans per second per service and data sent per day |
Cost: automatic means a lot
The speed of zero-code is also its financial risk. Because it instruments everything it knows, a large fleet can produce a very large stream of spans, and many monitoring tools charge by volume. Control the cost with sampling, with disabling noisy instrumentation, with dropping health-check traffic, and with removing labels that have too many different values. Track the volume per service so that a team that suddenly doubles its data is noticed early.
Ask for a cost estimate before the first fleet-wide rollout. Instrument a small, typical group of services, measure the data volume per service per day, and multiply by the number of services. This simple estimate avoids unpleasant surprises on the first invoice.
A security camera system that records every doorway generates vast video files. Without a plan, the storage fills and the bill grows. A sensible owner chooses which doors matter, records only on motion, and deletes old footage. Sampling, filtering and retention are the same habits for telemetry.
A rollout safety checklist
Before you roll the helper out beyond the pilot group, tick off a short list. It takes minutes and prevents most of the painful surprises.
- Identity: every service has a clear service name, an environment label and a version label.
- Destination: the endpoint and protocol are correct and reachable from the workloads, and a Collector is ready to receive.
- Volume: a sampler is set, noisy instrumentation is reviewed, and an estimate of data per day exists.
- Privacy: header capture is limited, sample telemetry has been inspected and redaction rules are in the Collector.
- Compatibility: supported library versions are confirmed, and no other agent is attached.
- Resources: memory requests and health-check timeouts allow for the helper’s overhead.
- Rollback: there is a quick way to switch the helper off, such as removing the annotation or setting the disable variable.
16Advantages, Disadvantages and Trade-offs
Zero-code is a strong default, not a universal answer. An honest list of both sides helps you use it well.
Advantages
- No source code changes, so no development cycle is needed.
- Fast first data, often within hours.
- Works for legacy and purchased software that you cannot edit.
- Uniform, standard naming across teams and languages.
- Wide coverage of popular libraries in one step.
- Settings can be changed from outside, per environment.
Disadvantages
- It cannot see business meaning without manual additions.
- It covers only supported libraries and versions.
- It adds start-up time, memory and processor use.
- It can produce more data than you need, raising cost.
- It can clash with other agents or with bundlers.
- Support is uneven across languages, and some need privileges.
Trade-offs you will face
| Choice | Benefit | Cost |
|---|---|---|
| Instrument everything automatically | Maximum coverage with least effort | High volume and some noise |
| Enable only chosen libraries | Lower cost and less noise | More blind spots, more work to choose |
| Use the Operator for fleet rollout | One central switch for many workloads | A new component to run, and pods must restart |
| Bake the agent into images | Predictable, versioned, offline-friendly | Images must be rebuilt for upgrades |
| Add manual spans | Business-level insight | Code changes and care with naming |
| High sampling rate | More complete data | Higher cost and load |
| Low sampling rate | Lower cost | Rare problems may be missed |
A ready-made suit fits most people well enough, costs little and is ready today. A tailored suit fits perfectly but takes weeks and costs more. Many people buy the ready-made suit and pay a tailor for a few small changes. Zero-code is the ready-made suit, and manual instrumentation is the tailor’s adjustment.
Use zero-code as the default for every service. Reach for manual instrumentation only where a question about the business cannot be answered from the automatic data.
17Patterns, Anti-patterns and Best Practices
Teams that adopt zero-code well repeat a few habits. Teams that struggle repeat a few mistakes.
Patterns that work
Pilot, then widen
Start with a few typical services, check data and overhead, then roll out by namespace or team.
Central settings
Keep endpoint, sampler and propagators in one shared place, such as the Operator’s Instrumentation resource.
Collector nearby
Send data to a Collector close to the workloads, and do sampling and redaction there.
Golden dashboards
Build a standard dashboard of rate, errors and duration that works for every instrumented service.
Targeted manual additions
Add a few spans and attributes only where traces show a business gap.
Versioned rollouts
Pin the helper’s version, test upgrades in a safe place, and upgrade on purpose.
Anti-patterns to avoid
What it looks like
The helper is switched on for the whole fleet at once, with default settings and no sampling.
Why it hurts
Memory, start-up time and data volume jump together. Health checks fail and the monitoring bill surprises everyone.
Do this instead
Pilot on a few services, measure, set sampling, and widen in steps.
What it looks like
Two instrumentation agents, such as a commercial one and the OpenTelemetry one, are attached to the same program.
Why it hurts
They may rewrite the same classes in conflicting ways, causing crashes, missing spans or double counting.
Do this instead
Use one agent per program, unless the documentation clearly supports a combination.
What it looks like
A team adds a full second SDK to its code to create manual spans, while the helper already runs one.
Why it hurts
Two SDKs can export duplicates or lose the link between automatic and manual spans.
Do this instead
Depend only on the API package in application code, and let the helper provide the SDK.
What it looks like
Nobody checks whether data arrives after the rollout. The helper may silently be disabled by a typo in a setting.
Why it hurts
The first time anyone notices is during an incident, when the data is needed most.
Do this instead
Add a check that every service has recent spans, and alert when one goes quiet.
Best practices at a glance
Set identity first
Always set the service name, environment and version before anything else.
Match the propagators
Make sure all services read and write the same trace-header formats, or traces will break at the borders.
Sample on purpose
Choose a sampler and a rate based on traffic and budget, not on the default.
Remove noise early
Disable instrumentation and ignore paths that add cost without insight.
Test with a console exporter
Confirm spans appear before you connect to the network and the backend.
Review regularly
Revisit supported versions, volumes and captured data every quarter.
Common mistakes
Setting the variables on the wrong container in a multi-container pod, so the helper is attached to a side-car and not to the application.
Annotating a Kubernetes workload and expecting running pods to change. The helper is added only when a new pod is created, so the pods must be restarted.
Assuming that a missing span means a bug in the helper, when the library version is simply not supported. Check the supported list first.
Keeping the system healthy over time
Adoption is not a one-time event. Libraries change, new services appear and old services are retired, and the helper needs attention to keep up. A light routine is enough. Each quarter, review the list of helper versions in use and move laggards forward. Check that new services were created with the helper attached, perhaps by making it part of the template that every new service starts from. Review the data volume per service and the top noisy spans. And look at a few real traces with fresh eyes, asking whether they still answer the questions the business asks.
It also helps to name an owner. A platform team that owns the Operator settings, the shared base images and the standard dashboards can answer questions and make changes once for everyone. Without an owner, the system slowly drifts, with services on many different versions and settings, and the original promise of uniform coverage fades.
Example: the quarterly review
A platform team reviews its fleet each quarter. It finds that nine services run a three-year-old agent version, two have sampling switched off and one new service was never annotated. The team upgrades the old versions in a planned change, restores sampling, and annotates the new service, all within one afternoon. Without the routine, each problem would have surfaced separately and at a worse time.
18Real-World Walkthrough and Industry Usage
Let us see zero-code in action in an imaginary company, then look at where the same ideas appear in practice.
ShopFast: from blind to visible in a week
ShopFast is an imaginary online store with sixty services in Java, Python and Node.js, running on Kubernetes. Monitoring is patchy: a few services have hand-written metrics and no tracing at all. During a sale, checkout becomes slow, and nobody can say which service is to blame. Management asks for tracing everywhere within a week, and the developers are busy with the sale.
flowchart LR Shop["Shopper"] --> GW["Gateway"] GW --> Ord["Orders: Java"] Ord --> Pay["Payment: Node.js"] Ord --> Rec["Recommendations: Python"] Ord --> DB["Orders database"] Ord -. telemetry .-> OC["Collector"] Pay -. telemetry .-> OC Rec -. telemetry .-> OC OC --> BE["Tracing backend"]
Day 1: set up the platform pieces
The platform team installs the Operator and a node-level Collector, and creates an Instrumentation resource with the endpoint, a ten percent parent-based sampler and the W3C propagators.
Day 2: pilot three services
They annotate the orders, payment and recommendations workloads, one per language, and restart them. Traces appear within minutes, with spans for web requests, outgoing calls and database queries.
Day 3: read the traces
A slow checkout trace shows that most of the time is spent in a single call from orders to recommendations, which in turn waits on an unindexed catalog query.
Day 4: remove noise
Health-check spans are dropped, and one chatty cache library is disabled, cutting data volume by about a third in their measurements.
Days 5 to 7: roll out and add meaning
They annotate the remaining namespaces step by step. Orders adds two business attributes, the customer tier and the cart size, so traces can be filtered by them.
By the end of the week, every service shows up in the tracing tool, and the team has found and fixed the real cause of the slow checkout. No team changed its service code except the small attribute addition in orders. The speed came from doing the work at the platform level.
Where the same ideas appear in industry
Vendor and cloud distributions
Several vendors and cloud providers package the OpenTelemetry agents with their own defaults. Examples include the AWS Distro for OpenTelemetry and distributions from Splunk and Elastic. They let customers use zero-code helpers with vendor-specific settings, while keeping the standard data format.
Serverless functions
For cloud functions, the project and cloud providers offer ready-made layers that add a language helper and a light Collector to a function without changing its code, which makes tracing of event-driven systems much quicker to set up.
Platform engineering teams
Teams that run internal platforms often make zero-code the default for every new workload, so that tracing exists from the first deployment. Product teams then add custom spans only where they need business insight.
Open-source and commercial backends
Backends such as Jaeger, Grafana Tempo, Honeycomb, Datadog, New Relic and Dynatrace accept OpenTelemetry data. Because the helper exports standard OTLP, a company can change backends without redoing its instrumentation.
19Frequently Asked Questions
Short answers to the questions people ask most often.
20Summary and Key Takeaways
If you remember only a handful of ideas from this guide, make it these.
Key Takeaways
- Zero-code means zero changes to source code. A helper attached from outside watches your libraries and creates traces, metrics and log links without editing the application.
- The technique depends on the language. Java uses a bytecode agent, Python and Node.js patch functions at runtime, .NET uses runtime hooks, and Go relies on eBPF from outside the process.
- Configuration lives in environment variables. Set the service name, endpoint, environment, sampler and propagators first, and pin the helper’s version.
- Kubernetes makes it scale. The OpenTelemetry Operator attaches helpers through annotations and a webhook, but pods must be restarted to pick them up.
- It sees libraries, not business meaning. Combine it with a few manual spans and attributes, using only the API so the helper’s SDK stays the single exporter.
- It has a real cost. Expect extra start-up time, memory and data volume, and control them with measurement, sampling, noise removal and a nearby Collector.
- Roll out carefully and keep watching. Pilot first, check that data arrives, redact sensitive fields at the Collector, and review supported versions and volumes regularly.