Skip to content

TechnicalPractitioner14 min read (+2 technical)

Entities, Profiles, Injector: three pieces followed through one incident

Three OpenTelemetry pieces that are often poorly explained, followed through a single incident: a way to say which object emits the data (Entities), a fourth signal that goes down to the code (Profiles) and a mechanism that installs the probes without touching the applications (Injector). For each: the problem, how it works, how mature it is.

For: engineers and SREs · architects · team managersPrerequisites: Knowing what a trace, a span and the Collector are. The article OpenTelemetry end to end introduces them.

Reading mode

Three names keep coming back in OpenTelemetry announcements: Entities, Profiles and Injector. I often see them quoted together and rarely explained simply. Rather than present them one by one in the abstract, I follow them through a single incident on a Tuesday afternoon. For each, we will see the problem it solves, how it works and where it stands.

Three pieces that do not play at the same level

Section titled “Three pieces that do not play at the same level”

You often hear that Entities is a new name for attributes that already exist, that Profiles is the code profiling people have done for years and that the Injector is one more agent to deploy. None of these three ideas is right.

Entities is a data model: a way of saying, with no ambiguity, which object the signals come from (a service, a pod, a machine) and what makes its identity. Profiles is a new signal, the fourth after traces, metrics and logs, which can be linked to the other three. The Injector is a deployment mechanism: a small library that plugs the existing agents into applications as they start, without anyone touching them.

In my view, most misunderstandings come from there: people compare things that do not sit on the same floor.

The running example: on a Tuesday, checkout is deployed

Section titled “The running example: on a Tuesday, checkout is deployed”

The scenario is illustrative. An online shop relies on several services, programs that each do one specific job. The checkout service takes the orders.

At 14:02, version 4.2.0 of checkout goes to production. It runs in a new pod (in Kubernetes, the wrapper that runs one or more containers). The tool that stores the data thinks it sees a new service: the history is cut in two. That is where Entities comes in.

At 14:10, response time climbs. The trace shows that one step in checkout is slow, without saying which piece of code slows it down. That is where Profiles comes in.

At 14:25, the lead points to pricing, the service that computes prices. It is a Java program installed on a virtual machine, outside Kubernetes. Nobody ever instrumented it. That is where the Injector comes in.

Entities: knowing which object we are talking about

Section titled “Entities: knowing which object we are talking about”

14:02. The deployment changed the version and the pod. Is it still the same checkout?

Think of an identity card. Your name and date of birth identify you: they do not change. Your address or your haircut describe you: they can change without you becoming someone else. A government office that mixed the two up would open a new file for you every time you moved house.

That is what is happening to checkout today.

The problem: the record does not say what makes the identity

Section titled “The problem: the record does not say what makes the identity”

Every OpenTelemetry signal arrives with a record that describes its source. It is called the Resource: a list of attributes, name and value pairs such as service.name = checkout. The article OpenTelemetry end to end covers it in detail.

At deployment time, three lines of the checkout record change: the version goes from 4.1.9 to 4.2.0 and the pod gets a new identifier and a new name. The service name and the machine stay the same.

The Resource works. Yet it mixes several objects (a service, a pod, a machine) in a single list. Nothing in it says which attributes make the identity and which only describe. Each storage backend guesses. If it relies on the version or the pod, it sees two different services and a plain deployment is enough to break the continuity of the charts.

For technical teams: the checkout Resource before and after the deployment
Attribute14:01, version 4.1.914:02, version 4.2.0
service.namecheckoutcheckout
service.version4.1.94.2.0 (changed)
k8s.pod.uid0c1e…9b77… (changed)
k8s.pod.namecheckout-5d9f-x2kcheckout-7c4d-m8q (changed)
host.idi-08ab…i-08ab…

The model: a type, what identifies, what describes

Section titled “The model: a type, what identifies, what describes”

The specification defines an entity as an object of interest that the produced signals attach to, whether traces, metrics, logs or profiles. An entity has three parts. First a type, which does not change during its lifetime: service, pod, machine. Then attributes that identify it and do not change either, like the name and date of birth on the identity card. Finally attributes that describe it and may evolve, like the address.

For a service, the name identifies and the version describes. For a pod, its unique identifier identifies and its name describes. At 14:02, the name of checkout has not moved: it is therefore the same service, in a new version. The pod, however, really is a new entity. Both statements are true at once and the tool no longer has to guess.

Three things change in practice. Identity stays stable: a version or a label changes, the object stays the same, so does its history. Correlation becomes explicit: traces, metrics, logs and profiles point at the same object, with no makeshift mapping rule on the backend side. The inventory comes alive: entity events can report an object even when it emits no signal, at the moment it appears, changes or disappears.

For technical teams: the same Resource split into entities
TypeIdentifies (does not change)Describes (may change)
serviceservice.nameservice.version
k8s.podk8s.pod.uidk8s.pod.name, labels
hosthost.idhost.name, host.type
processprocess identifier and start timeexecutable name

The first three rows follow the examples of the specification and of the semantic conventions. The process row is illustrative: the specification quotes that pair as an example of minimal identity, without making it a definition.

The entity data model, the entity events and the Resource model that brings them together all carry the Development status in the specification. It is the earliest stage, before any stability guarantee.

On the protocol side, OTLP (the OpenTelemetry exchange format, shared by all signals) has allowed entity references to be added to the Resource since version 1.6.0, also in Development. They say which attributes form the identity and which describe. The attributes themselves stay in the Resource: a tool that ignores entities keeps reading the record as before.

Relationships between entities, such as “this pod runs on this machine”, are the work in progress. Entity events can already carry some, though the specification states that their modelling will be refined.

The first step I recommend needs no tool at all: for each of your Resources, write down what identifies and what describes. The exercise is useful right now, whatever the specification timeline.

For technical teams: the field in the protocol

Entity references are carried by the entity_refs field of the Resource, defined in resource.proto since OTLP 1.6.0, with Development status.

14:10. The trace shows a slow step. It does not show the code that slows it down.

A trace is like parcel tracking: it tells you at which depot the parcel waited, not what was going on inside the warehouse. To find out, you would have to photograph the warehouse every few milliseconds and count how many photos show each workstation busy. That is exactly what a profile does.

After traces, metrics and logs, profiling becomes an OpenTelemetry signal in its own right. A profile says where a program spends its time, function by function. It is built from samples of the call stack, meaning the list of functions running at a given moment, each one called by the one before.

From a slow step to the function that costs

Section titled “From a slow step to the function that costs”

The trace splits the request into steps, called spans. It answers the question “where, between the steps?”:

POST /checkout ████████████████████████████████████████
validateCart ███
applyPricing ██████████████████████████████
saveOrder ██████

The applyPricing span takes almost all the time. Why? The profile of that span answers the other question, “where, in the code?”. Each line is a function called by the one above it and its width shows the share of time spent in it. This drawing is called a flame graph:

applyPricing ████████████████████████████████████████
loadRules ██████
computeDiscounts ██████████████████████████████████
readCache ████
matchPromo ██████████████████████████████
Pattern.compile ████████████████████████████

In this invented example, version 4.2.0 introduced the compilation of a regular expression (a search pattern for text) inside a loop: it is recompiled on every call.

What links the two views is that each sample of the profile can carry the identifiers of the current trace and span. That is how you go from a span to its call stack. The profile is attached to a Resource, like the other signals: the same attributes today, the same entities tomorrow, tie it to the service, the pod and the machine.

It all starts with capture. An eBPF profiler samples the call stacks of the whole Linux machine. eBPF means small programs that run inside the Linux kernel and observe without modifying the applications. This profiler comes from a donation by Elastic to the project and works as a receiver of the Collector, the relay that receives, transforms and forwards the data.

The stacks then leave in OTLP, in a model built on pprof, a widespread profile format that comes from the Go ecosystem. A pprof profile converts to OTLP and back with no loss. The Collector carries them in a dedicated pipeline, which has to be switched on explicitly. Finally, the storage backend displays the flame graphs and compares versions.

The signal brings several things. It gives the “why” behind a CPU or memory chart, at code level. It lets you jump straight from a span to its call stack. Its format is open, so the profiles stay readable by another backend. It sees the whole machine, including code you did not write.

For technical teams: links and enabling it in the Collector

The identifiers carried by each sample are the trace_id and the span_id. The Collector’s profiles pipeline is enabled with the service.profilesSupport feature gate.

The signal entered public alpha in March 2026. The announcement itself spells out what follows. It should not be used for critical production workloads. Its format can still change in incompatible ways. Support has to be enabled in the Collector, from version 0.148.0. Finally, at the date of the announcement, no production-ready backend existed, even though several vendors are working on it.

For a first try, I recommend a modest setup: the eBPF profiler on a test machine, a Collector with the profiles pipeline switched on and a backend that reads OTLP profiles. Check which format version each part supports, because it still moves.

Injector: instrumenting without touching the service

Section titled “Injector: instrumenting without touching the service”

14:25. pricing runs on a virtual machine, with no agent. To see it, you would have to modify it.

For an application to send traces, metrics and logs, you usually add an auto-instrumentation agent: a module that attaches to the program at startup and measures what it does, without changing its code. You still have to remember to plug it into every service, in every language, on every new machine, then keep all of that up to date.

Picture the front desk of an office building. Rather than asking every visitor to bring a badge, the receptionist hands one to anyone who walks through the door. The Injector plays that role. It does not replace the agents: it takes care of plugging them in automatically when each program starts, through a standard Linux feature called LD_PRELOAD.

When a program starts, Linux loads the libraries it needs. LD_PRELOAD tells it to load one before all the others. You can set it for a single program or, for the whole machine, in a system file.

So the Injector’s library comes in first. It recognises the program’s runtime, meaning the software that executes the application: the Java virtual machine, Node.js, .NET. It then adds the environment variable that this runtime knows how to read to load an agent. The runtime loads the agent and the traces, metrics and logs leave in OTLP, just as with an agent installed by hand.

The Injector itself produces no trace, no metric and no log. It only decides which agent to plug into which program and can fill in Resource attributes. For pricing, a Java service, it tells Java where the OpenTelemetry Java agent is. The service has not been touched: it gets its agent at its next restart.

For technical teams: before, after and the variables added per runtime

By hand, service by service:

# pricing.service (systemd)
ExecStart=/usr/bin/java \
-javaagent:/opt/otel/javaagent.jar \
-jar /opt/pricing/pricing.jar

With the Injector, once per host:

/etc/ld.so.preload
/usr/lib/opentelemetry/libotelinject.so
# pricing.service: unchanged

The library path is the one in the project documentation; the configuration lives by default in /etc/opentelemetry/injector/injector.conf. LD_PRELOAD is declared through an environment variable or, for the whole host, in /etc/ld.so.preload.

libotelinject.so adds the variable the runtime knows how to read: JAVA_TOOL_OPTIONS for Java, NODE_OPTIONS for Node.js, the CORECLR_* and DOTNET_* variables for .NET, RUBYOPT for Ruby. For pricing, it adds the path of the OpenTelemetry Java agent to JAVA_TOOL_OPTIONS.

It can do nothing for programs compiled into a single block, called static binaries: with no libraries loaded at startup, there is nothing to preload. Many programs written in Go are in that case.

It does not create agents for languages that have none: it plugs in an existing agent. As of 10 October 2026, the project supports Java, Node.js, .NET and Ruby; Python exists but stays disabled by default.

It does not act on programs that are already running. Everything happens at startup: the service has to be restarted to be covered.

It does not get past privileged programs. For executables that start with elevated rights (setuid executables), Linux restricts LD_PRELOAD for security reasons.

Nor does it take the security decision for you. A library loaded into every program on a machine is validated with the security team, like anything written into the system’s preload file.

Its place among the ways to instrument without changing code

Section titled “Its place among the ways to instrument without changing code”

The Injector is not alone in this space. In Kubernetes, the OpenTelemetry Operator does a similar job: it relies on an admission webhook, a check that Kubernetes calls whenever a pod is created and that can modify it to add the agent. eBPF instrumentation places probes in the Linux kernel, outside the program: it sees the network exchanges of any language while seeing less of the inside of the application.

The three approaches complement each other. The Operator covers Kubernetes, eBPF instrumentation covers every language at the level of network exchanges, the Injector covers Linux machines and virtual machines with the full agents. pricing lives on a virtual machine: the Operator can do nothing for it, so this is the Injector’s ground.

For technical teams: Injector, Operator and eBPF instrumentation
InjectorOperatoreBPF instrumentation
WhereLinux host or containerKubernetesLinux node
HowLD_PRELOAD, then the language agentadmission webhook that modifies the podprobes in the kernel, outside the process
Languagesthose the project supportsthose that have an agentall, at protocol level

The project repository shows no stability level. Its versions are numbered 0.x (0.11.0 for the latest one I noted): expect the list of languages and the configuration to keep moving. The installation packages for Debian and Red Hat (DEB and RPM) are no longer built in that repository but in the opentelemetry-packaging repository.

For a first try, I recommend a virtual machine outside Kubernetes, with a Java or Node.js service, in pre-production.

For the operations teams in charge of reliability (SREs), Entities keeps alerts and dashboards intact from one deployment to the next. Profiles gives the cause at code level during the incident, without having to reproduce it. The Injector shrinks the blind spots outside Kubernetes.

For platform and DevOps teams, Entities offers an inventory shared by all tools, with no home-made mapping rules. Profiles puts the four signals through a single pipeline and makes each service’s CPU cost visible. The Injector instruments a fleet of machines by installing a package, without reworking each deployment.

For developers, Entities lets them find their service and its versions without knowing the infrastructure. Profiles shows the function that costs, seen in production and not only on their laptop. The Injector asks for nothing in the code or in the container image.

These are expected gains, to be checked on your own scope. I quantify none of them: none of the three pieces is stable.

Let us replay the afternoon. At 14:02, checkout 4.2.0 is still checkout and the history is not cut. At 14:10, from the slow span, you open the function that costs. At 14:25, pricing is already instrumented without anyone modifying it.

TimePieceMaturity as of 10 October 2026
14:02EntitiesDevelopment
14:10Profilespublic alpha
14:25Injector0.x versions

The three complement each other: the Injector brings traces, metrics and logs in with no effort, Profiles adds the code level and Entities says which object all of this relates to. On Monday morning, the most useful task is also the cheapest: write down, for your services, what identifies them and what describes them.

Consulted on 10 October 2026.

  • OpenTelemetry, Entity Data Model: definition of an entity, type, identifying and descriptive attributes, examples, Development status.
  • OpenTelemetry, Entity Events: state and delete events, entities with no associated signal, relationships.
  • OpenTelemetry, Resource Data Model: the Resource as a composition of entities.
  • OpenTelemetry, resource.proto: entity_refs field, Development status, since version 1.6.0.
  • OpenTelemetry, service semantic conventions: service entity, identified by service.name.
  • OpenTelemetry, OpenTelemetry Profiles Enters Public Alpha: alpha status, warning about production, eBPF profiler, pprof conversion, link with traces, Collector 0.148.0, state of backends.
  • OpenTelemetry, profiles specification: data model built on pprof, link to a span, shared Resource.
  • OpenTelemetry, The State of Profiling: enabling profiles in the Collector with service.profilesSupport.
  • OpenTelemetry, opentelemetry-injector: role of the Injector, paths, variables added per runtime, supported languages, versions.
  • Linux man-pages, ld.so(8): LD_PRELOAD, /etc/ld.so.preload, restrictions in secure-execution mode.