Skip to content

benchmarking: add observability benchmarking for the OTel collector - #749

Merged
Da Huang (git286) merged 2 commits into
agent-substrate:mainfrom
JeffLuoo:obs-bench
Aug 17, 2026
Merged

Da Huang (git286) merged 2 commits into
agent-substrate:mainfrom
JeffLuoo:obs-bench

Conversation

@JeffLuoo

@JeffLuoo Jeff Luo (JeffLuoo) commented Aug 4, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #563

Substrate had no way to measure how much telemetry it sends. This PR adds one, and builds it from the parts the benchmark suite already has.

The managed collector sends its self-metrics to Cloud Monitoring only, and gives no data for each source. Thus you cannot see which component makes the volume. A second collector, the telemetry meter, counts the spans and the datapoints by service.name.

Contents

  • benchmarking/telemetry/meter.yaml — the meter. A tee: it counts the telemetry by service, then sends it on to the managed collector. Thus one run gives the volume for each service and the cost of the load together.
  • benchmarking/telemetry/README.md — the method: install, measure, read, remove.
  • hack/install-ate.sh --otlp-endpoint — sends all control plane telemetry to the meter. Each component reads the endpoint from the ate-otel-config ConfigMap through envFrom, and ate-controller copies the value to the ateom worker pods, thus one patch is sufficient for all of them.
  • benchmarking/workloads/deploy.sh --otlp-endpoint — the same for actor containers, which substrate configures separately.
  • benchmarking/monitoring.yaml — scrapes the meter and cAdvisor.
  • benchmarking/observability.md — prerequisites and the scenario ladder.
  • docs/dev/best-practices/otel-collector.md — the volume model and the size guidance, corrected for the ParentBased defaults from Define a production-ready default tracing policy #711.

Correction

The glutton ActorTemplate had no env. Substrate puts no OTLP configuration in an actor container, thus the exporter used the SDK default localhost:4317, no process listens on that address in the sandbox, and substrate discarded each actor span with no error message. The template now sets OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_RESOURCE_ATTRIBUTES.

ActorTemplate.spec is immutable, thus deploy.sh --deploy deletes the templates before it applies them.

How I verified this

On a GKE cluster with managed OTel. The sequence:

# 1. Start from a clean state.
./hack/install-ate.sh --delete-benchmarks
./hack/install-ate.sh --delete-ate-system

# 2. Install substrate from this branch.
./hack/install-ate.sh --deploy-ate-system

# 3. Install the meter and Prometheus.
kubectl apply -f benchmarking/telemetry/meter.yaml
kubectl apply -f benchmarking/monitoring.yaml

# 4. Send the control plane to the meter.
METER=http://telemetry-meter.benchmarking.svc.cluster.local:4317
./hack/install-ate.sh --deploy-ate-system --otlp-endpoint "${METER}"

# 5. Install the workloads and locust, with the actors on the meter also.
./benchmarking/deploy_locust.sh --deploy --worker-count 10 \
    --otlp-endpoint "${METER}"

# 6. Read the volume.
kubectl port-forward -n benchmarking svc/prometheus 9090:9090

Result

The idle floor, from one measurement on 10 workers:

Service datapoints/min
ate-controller 234.8
ateapi 54.3
atelet 44.9
atenet-router 10.4
ateom-gvisor 5.2
glutton-actor 4.2
Total 353.7

The glutton-actor row is the correction above. Before it, actor telemetry could not arrive at all.

Positive control for the same window: otelcol_receiver_accepted_metric_points 10.5/s, and each otelcol_receiver_refused_* counter at zero. Thus the zero values in the table are true zeros, and not data that did not arrive.

Two values differ from the earlier manual measurement. ate-controller is now the largest source, and ateom-gvisor is no longer zero. The new imagecache counters from #831 are a probable cause. This difference is the reason to keep results out of the repository.

Traces

Trace volume follows the root sample decision, not the rate of each component. atelet and ateom-gvisor sent zero spans in each run, because they inherit the decision that ateapi makes and start no trace of their own. At idle each component is near zero: a trace needs an operation, but a metric pushes on a timer.

This run gives no span volume for a usual load. The actors crashed on suspend, and the failed retries made an artificial rate of 13.7k spans/s. That rate is not a measurement of substrate. It does show that the meter accepted the spans with no refusals.

Follow-up

A separate PR adds the ladder to automation/tests.yaml and writes the results as artifacts of each run. The open items are at the end of observability.md.

  • Tests pass
  • Appropriate changes to documentation are included in the PR

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some comments! Do we want to make this repeatable locally (and in CI?) via a script?

Comment thread benchmarking/observability.md Resolved
Comment thread benchmarking/telemetry/meter.yaml Resolved
Comment thread benchmarking/observability.md Resolved
Comment thread benchmarking/observability.md Resolved
Comment thread benchmarking/observability.md Resolved
Comment thread benchmarking/telemetry-meter.yaml Resolved
Comment thread benchmarking/telemetry/meter.yaml Resolved
@JeffLuoo

Copy link
Copy Markdown
Collaborator Author

Added some comments! Do we want to make this repeatable locally (and in CI?) via a script?

Might do it in a follow-up and figure out the best way to trigger it. Running it against each CL might be expensive, can consider running it as a daily job..? Can connect with people who work on benchmarking and CI/CD to discuss about it.

@JeffLuoo
Jeff Luo (JeffLuoo) marked this pull request as draft August 12, 2026 15:04
@JeffLuoo
Jeff Luo (JeffLuoo) force-pushed the obs-bench branch 10 times, most recently from 85fc01e to 0011ab3 Compare August 12, 2026 18:30
@JeffLuoo
Jeff Luo (JeffLuoo) marked this pull request as ready for review August 12, 2026 18:35
@JeffLuoo

Copy link
Copy Markdown
Collaborator Author

Hi Max, can you please review it? It doesn't generate any report yet, just set up things we need to benchmark observability. Max Smythe (@maxsmythe)

@maxsmythe Max Smythe (maxsmythe) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks awesome! Couple questions that may just need clarificatoin.

Comment thread benchmarking/telemetry/README.md Resolved
Comment thread benchmarking/telemetry/README.md Resolved
Comment thread benchmarking/telemetry/README.md Resolved
Comment thread benchmarking/telemetry/meter.yaml Resolved
Comment thread benchmarking/telemetry/README.md Resolved
Comment thread benchmarking/monitoring.yaml Resolved
`cycles/sec ≈ users / (1.0s + avg_wait_time)`. Change the number of users. Do
not change the wait time.

| # | Scenario | Duration | Setup |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that Python doesn't play well with gRPC when the number of users gets high enough, causing seconds worth of latency and throttling QPS. This is why the switch to boomer occurred.

Sleep currently uses Python worker. Might want to switch to glutton user to avoid this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I can update it to glutton in a follow-up that adds the ladder to the config.


## 2. Open items

- Add the ladder to [automation/tests.yaml](automation/tests.yaml). Write the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ladder seems like something that locust/runner.py should execute in some way.

The non-linear traffic pattern is something that is ideal to provide as a custom shape (locust/shapes)

Changing trace probability dynamically is an interesting wrinkle.

The Boomer worker does dynamically fetch config from master:

// Fetch GETs `url` and merges any returned fields into `current`. Returns
// the merged Config (or current unchanged on a soft no-op response).
func Fetch(ctx context.Context, url string, current Config) (Config, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return current, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return current, fmt.Errorf("GET %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return current, fmt.Errorf("GET %s: status %d", url, resp.StatusCode)
}
var p payload
if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
return current, fmt.Errorf("decode %s: %w", url, err)
}
return p.merge(current), nil
}

So it is possible. Reloads occur whenever worker receives a spawn event from locust, though that may not always be an intuitive trigger.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, and this is already the planned follow-up. This PR focus on getting manifest ready.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, should have said this was an informational comment rather than a blocker.

Comment thread benchmarking/telemetry/README.md Resolved
Comment on lines +121 to +138
traces/count:
receivers: [otlp]
processors: [transform/svc]
exporters: [count]
traces/forward:
receivers: [otlp]
exporters: [otlp/managed]
metrics/count:
receivers: [otlp]
processors: [transform/svc]
exporters: [count]
metrics/forward:
receivers: [otlp]
exporters: [otlp/managed]
metrics/counts:
receivers: [count]
processors: [deltatocumulative]
exporters: [prometheus]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add memory_limiter, and configure GOMEMLIMIT?

@JeffLuoo Jeff Luo (JeffLuoo) Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a tee needed by GKE Managed OTel collector.

This tee collector is not needed for kind cluster and it should only process telemetry data and stream it to the GKE Managed collector, which means this is not a "real" collector. I am open to add memory_limiter and configure GOMEMLIMIT but just think it's not necessary.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I these are still beneficial for any perf/loadtesting setups to ensure reliability, but we can add them as a follow-up.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will keep this comment open to allow me track it.

Comment thread benchmarking/workloads/deploy.sh Resolved
# long run out of Cloud Monitoring, but it makes the resource values of
# the managed collector those of an idle collector.
otlp/managed:
endpoint: opentelemetry-collector.gke-managed-otel.svc.cluster.local:4317

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in deploy.sh, can we make this work with local clusters as well?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should also make this use the centralized OTel config introduced in #746?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need the meter.yaml for kind cluster because the otel collector is customized. Instead, I think I can have a follow-up to update the manifest of the otel collector deployed to the kind cluster and integrate it with the benchmark.

I can do it in a follow-up.

@JeffLuoo
Jeff Luo (JeffLuoo) force-pushed the obs-bench branch 3 times, most recently from ea2deda to 8b172ad Compare August 13, 2026 19:24
Part of agent-substrate#563.

The suite measured how fast substrate is. Nothing measured how much
telemetry it produces, or what that telemetry costs the OTel collector.
This change adds the measurement, and builds it from the parts that the
suite already has.

The managed collector sends its self-metrics to Cloud Monitoring only,
and gives no data for each source. Thus benchmarking/telemetry/meter.yaml
adds a second collector that counts the spans and the datapoints by
service.name. The meter is a tee: it counts the telemetry and then sends
it on to the managed collector, thus one run gives the volume for each
service and the cost of the load together.

Contents:

  - telemetry/meter.yaml and telemetry/README.md: the instrument and the
    method. Install, measure, read, remove.
  - hack/install-ate.sh --otlp-endpoint: sends all control plane
    telemetry to the meter. Each component reads the endpoint from the
    ate-otel-config ConfigMap through envFrom, and ate-controller copies
    the value to the ateom worker pods, thus one patch is sufficient for
    all of them. The override runs after the bundle apply, because the
    bundle has its own copy of the ConfigMap and would replace an earlier
    patch with no error message. deploy_locust.sh forwards the flag.
  - workloads/deploy.sh --otlp-endpoint: the same for actor containers,
    which substrate configures separately.
  - monitoring.yaml: scrapes the meter and cAdvisor.
  - observability.md: the prerequisites and the scenario ladder.
  - docs/dev/best-practices/otel-collector.md: the volume model and the
    size guidance for Deployment or DaemonSet. This file already had a
    shorter form of the same model, which was incorrect after the
    ParentBased defaults from agent-substrate#711.

Correction: the glutton ActorTemplate had no env. Substrate puts no OTLP
configuration in an actor container, thus the exporter used the SDK
default localhost:4317, no process listens on that address in the
sandbox, and substrate discarded each actor span with no error message.
The template now sets OTEL_EXPORTER_OTLP_ENDPOINT and
OTEL_RESOURCE_ATTRIBUTES. ActorTemplate.spec is immutable, thus the
deploy function deletes the templates before it applies them.

Findings:

  - With ParentBased, the caller makes the root decision. Locust always
    sends a traceparent, thus its --trace-probability is the root rate
    for benchmark traffic and the default in ateapi does not apply.
  - The metrics increase with the number of components, not with the
    load. The spans increase with the rate of the operations.
  - An idle worker pod sends zero datapoints. Lifecycle traffic starts
    the telemetry of ateom, not the number of actors.
  - atelet and ateom-gvisor send no spans of their own. They inherit the
    decision that ateapi makes.
  - Actor throughput becomes constant at the limit of the snapshot
    suspend and restore operations, not at a limit of the telemetry. At
    that load the collector used a small part of its memory limit but
    stayed at maxReplicas, thus throughput is the wrong axis for the size
    calculation. The number of pods in the cluster sets the limit.

The guidance is qualitative. It gives the shape of each curve and tells
the reader to measure the values for their own cluster. No result tables
go in the repository, because a result becomes incorrect after a change
to the sample rates or to the instruments.

All prose is in Simplified Technical English (ASD-STE100).
Comment thread benchmarking/telemetry/README.md Resolved
Co-authored-by: Krisztian F <103492698+krisztianfekete@users.noreply.github.com>

@maxsmythe Max Smythe (maxsmythe) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@git286
Da Huang (git286) merged commit 9bfc67e into agent-substrate:main Aug 17, 2026
11 checks passed
Jeff Luo (JeffLuoo) added a commit to JeffLuoo/substrate that referenced this pull request Aug 19, 2026
The scenario ladder was a table in observability.md that an operator
followed by hand. Each step was a separate run, thus each step also
carried a deploy, a set of cold caches, and a new baseline for the reader
to align. The sample-rate sweep needed a new run for each rate, because
the rate reached the boomer worker one time, at its start.

Add shapes/ladder_shape.py and put the ladder in tests.yaml.

`--ladder users:duration[:trace_probability],...` makes the steps of a
sweep one run. The shape holds each step for its duration and then goes to
the next one, thus it owns the end of the run and runner.py forwards no
`-t` for a ladder. A step that names a probability changes the sample rate
as it starts: the value goes to the parsed options of the master, thus a
Python user class reads it from the sampler of common.trace and a boomer
worker reads it from /boomer-config at the spawn message that the step
change makes.

A headless master has no Flask app, thus it had no /boomer-config and
runner.py gave boomer the values one time through --config-json. That is
sufficient for a run that holds one set of values, and not for a ladder.
Serve the same payload from a plain HTTP server on the loopback interface
when --boomer-config-port is set, which runner.py sets for a ladder along
with the same port as boomer's --master-web-port.

The four tests run on glutton, and thus on boomer. maxsmythe called this
out on agent-substrate#749: Python and gRPC hold each other at a high number of users, so
a ladder on SleepUser would put the latency of the load generator in the
measurement. The user sweep stops at 15 users, and not at the 30 of the
earlier table, because the pool of the run is 10 workers; a step above
that measures the queue of the scheduler.

The idle floor sends no request on purpose, thus locust writes no
measurement row for it and runner.py counted the run as a failure. Add
--allow-empty-stats for that one case, rather than accepting an empty
result for every test.

Follow-up to agent-substrate#749.
Jeff Luo (JeffLuoo) added a commit to JeffLuoo/substrate that referenced this pull request Aug 21, 2026
The scenario ladder was a table in observability.md that an operator
followed by hand. Each step was a separate run, thus each step also
carried a deploy, a set of cold caches, and a new baseline for the reader
to align. The sample-rate sweep needed a new run for each rate, because
the rate reached the boomer worker one time, at its start.

Add shapes/ladder_shape.py and put the ladder in tests.yaml.

`--ladder users:duration[:trace_probability],...` makes the steps of a
sweep one run. The shape holds each step for its duration and then goes to
the next one, thus it owns the end of the run and runner.py forwards no
`-t` for a ladder. A step that names a probability changes the sample rate
as it starts: the value goes to the parsed options of the master, thus a
Python user class reads it from the sampler of common.trace and a boomer
worker reads it from /boomer-config at the spawn message that the step
change makes.

A headless master has no Flask app, thus it had no /boomer-config and
runner.py gave boomer the values one time through --config-json. That is
sufficient for a run that holds one set of values, and not for a ladder.
Serve the same payload from a plain HTTP server on the loopback interface
when --boomer-config-port is set, which runner.py sets for a ladder along
with the same port as boomer's --master-web-port.

The four tests run on glutton, and thus on boomer. maxsmythe called this
out on agent-substrate#749: Python and gRPC hold each other at a high number of users, so
a ladder on SleepUser would put the latency of the load generator in the
measurement. The user sweep stops at 15 users, and not at the 30 of the
earlier table, because the pool of the run is 10 workers; a step above
that measures the queue of the scheduler.

The idle floor sends no request on purpose, thus locust writes no
measurement row for it and runner.py counted the run as a failure. Add
--allow-empty-stats for that one case, rather than accepting an empty
result for every test.

Follow-up to agent-substrate#749.
Da Huang (git286) pushed a commit that referenced this pull request Aug 21, 2026
Follow-up to #749. One of three; the other two are independent of this
one.

This closes the review thread that stayed open on that PR:
krisztianfekete asked
for `memory_limiter` and `GOMEMLIMIT` on the meter, and I kept it open
to track.
Jeff Luo (JeffLuoo) added a commit to JeffLuoo/substrate that referenced this pull request Aug 21, 2026
The scenario ladder was a table in observability.md that an operator
followed by hand. Each step was a separate run, thus each step also
carried a deploy, a set of cold caches, and a new baseline for the reader
to align. The sample-rate sweep needed a new run for each rate, because
the rate reached the boomer worker one time, at its start.

Add shapes/ladder_shape.py and put the ladder in tests.yaml.

`--ladder users:duration[:trace_probability],...` makes the steps of a
sweep one run. The shape holds each step for its duration and then goes to
the next one, thus it owns the end of the run and runner.py forwards no
`-t` for a ladder. A step that names a probability changes the sample rate
as it starts: the value goes to the parsed options of the master, thus a
Python user class reads it from the sampler of common.trace and a boomer
worker reads it from /boomer-config at the spawn message that the step
change makes.

A headless master has no Flask app, thus it had no /boomer-config and
runner.py gave boomer the values one time through --config-json. That is
sufficient for a run that holds one set of values, and not for a ladder.
Serve the same payload from a plain HTTP server on the loopback interface
when --boomer-config-port is set, which runner.py sets for a ladder along
with the same port as boomer's --master-web-port.

The four tests run on glutton, and thus on boomer. maxsmythe called this
out on agent-substrate#749: Python and gRPC hold each other at a high number of users, so
a ladder on SleepUser would put the latency of the load generator in the
measurement. The user sweep stops at 15 users, and not at the 30 of the
earlier table, because the pool of the run is 10 workers; a step above
that measures the queue of the scheduler.

The idle floor sends no request on purpose, thus locust writes no
measurement row for it and runner.py counted the run as a failure. Add
--allow-empty-stats for that one case, rather than accepting an empty
result for every test.

Follow-up to agent-substrate#749.
Max Smythe (maxsmythe) pushed a commit that referenced this pull request Aug 22, 2026
Follow-up to #749, for comment of adding the ladder design in
benchmarking for observability.
Jeff Luo (JeffLuoo) added a commit to JeffLuoo/substrate that referenced this pull request Aug 24, 2026
The volume measurement worked on GKE only. There the managed collector
cannot hold a connector, thus benchmarking/telemetry/meter.yaml puts a
second collector in front of it and counts the data as it goes through.
On kind the collector is ours, thus it can do the count itself and the
cluster needs no meter and no extra hop.

Add the count connector to the kind collector. It now gives the same two
counts as the meter, substrate_spans_total{service_name} and
substrate_datapoints_total{service_name}, on the Prometheus exporter port
that it already opens. The connector resolves each key from the attributes
of the item, then the scope, then the resource, thus service.name resolves
with no transform in front of it. Drop the transform from the meter also.

Give the collector its otelcol_* counters also. The default reader binds
to localhost, thus nothing outside the pod could read it. Bind it to each
interface and open port 8888. The counters are the positive control of a
measurement: without them a service that reports zero volume and data that
never arrived look the same.

One scrape job reads both ports. The prometheus.io/scrape annotation can
name one port only, and port 8889 re-exports each metric that the pipelines
push. The pods of ateapi, atelet, and atenet-router already carry that
annotation on 9090, thus an annotation on the collector would bring each of
their series in a second time and sum() would give double. The job keeps
the substrate_* and otelcol_* names only.

The actors need no change. benchmarking/workloads/deploy.sh reads the
endpoint from the ate-otel-config ConfigMap, and the kind overlay of that
ConfigMap already names this collector.

Follow-up to agent-substrate#749.
Jeff Luo (JeffLuoo) added a commit to JeffLuoo/substrate that referenced this pull request Aug 24, 2026
The volume measurement worked on GKE only. There the managed collector
cannot hold a connector, thus benchmarking/telemetry/meter.yaml puts a
second collector in front of it and counts the data as it goes through.
On kind the collector is ours, thus it can do the count itself and the
cluster needs no meter and no extra hop.

Add the count connector to the kind collector. It now gives the same two
counts as the meter, substrate_spans_total{service_name} and
substrate_datapoints_total{service_name}, on the Prometheus exporter port
that it already opens. The connector resolves each key from the attributes
of the item, then the scope, then the resource, thus service.name resolves
with no transform in front of it. Drop the transform from the meter also.

Give the collector its otelcol_* counters also. The default reader binds
to localhost, thus nothing outside the pod could read it. Bind it to each
interface and open port 8888. The counters are the positive control of a
measurement: without them a service that reports zero volume and data that
never arrived look the same.

One scrape job reads both ports. The prometheus.io/scrape annotation can
name one port only, and port 8889 re-exports each metric that the pipelines
push. The pods of ateapi, atelet, and atenet-router already carry that
annotation on 9090, thus an annotation on the collector would bring each of
their series in a second time and sum() would give double. The job keeps
the substrate_* and otelcol_* names only.

The actors need no change. benchmarking/workloads/deploy.sh reads the
endpoint from the ate-otel-config ConfigMap, and the kind overlay of that
ConfigMap already names this collector.

Follow-up to agent-substrate#749.
Jeff Luo (JeffLuoo) added a commit to JeffLuoo/substrate that referenced this pull request Aug 24, 2026
The volume measurement worked on GKE only. There the managed collector
cannot hold a connector, thus benchmarking/telemetry/meter.yaml puts a
second collector in front of it and counts the data as it goes through.
On kind the collector is ours, thus it can do the count itself and the
cluster needs no meter and no extra hop.

Add the count connector to the kind collector. It now gives the same two
counts as the meter, substrate_spans_total{service_name} and
substrate_datapoints_total{service_name}, on the Prometheus exporter port
that it already opens. The connector resolves each key from the attributes
of the item, then the scope, then the resource, thus service.name resolves
with no transform in front of it. Drop the transform from the meter also.

Give the collector its otelcol_* counters also. The default reader binds
to localhost, thus nothing outside the pod could read it. Bind it to each
interface and open port 8888. The counters are the positive control of a
measurement: without them a service that reports zero volume and data that
never arrived look the same.

One scrape job reads both ports. The prometheus.io/scrape annotation can
name one port only, and port 8889 re-exports each metric that the pipelines
push. The pods of ateapi, atelet, and atenet-router already carry that
annotation on 9090, thus an annotation on the collector would bring each of
their series in a second time and sum() would give double. The job keeps
the substrate_* and otelcol_* names only.

The actors need no change. benchmarking/workloads/deploy.sh reads the
endpoint from the ate-otel-config ConfigMap, and the kind overlay of that
ConfigMap already names this collector.

Follow-up to agent-substrate#749.
Jeff Luo (JeffLuoo) added a commit to JeffLuoo/substrate that referenced this pull request Aug 24, 2026
The volume measurement worked on GKE only. There the managed collector
cannot hold a connector, thus benchmarking/telemetry/meter.yaml puts a
second collector in front of it and counts the data as it goes through.
On kind the collector is ours, thus it can do the count itself and the
cluster needs no meter and no extra hop.

Add the count connector to the kind collector. It now gives the same two
counts as the meter, substrate_spans_total{service_name} and
substrate_datapoints_total{service_name}, on the Prometheus exporter port
that it already opens. The connector resolves each key from the attributes
of the item, then the scope, then the resource, thus service.name resolves
with no transform in front of it. Drop the transform from the meter also.

Give the collector its otelcol_* counters also. The default reader binds
to localhost, thus nothing outside the pod could read it. Bind it to each
interface and open port 8888. The counters are the positive control of a
measurement: without them a service that reports zero volume and data that
never arrived look the same.

One scrape job reads both ports. The prometheus.io/scrape annotation can
name one port only, and port 8889 re-exports each metric that the pipelines
push. The pods of ateapi, atelet, and atenet-router already carry that
annotation on 9090, thus an annotation on the collector would bring each of
their series in a second time and sum() would give double. The job keeps
the substrate_* and otelcol_* names only.

The actors need no change. benchmarking/workloads/deploy.sh reads the
endpoint from the ate-otel-config ConfigMap, and the kind overlay of that
ConfigMap already names this collector.

Follow-up to agent-substrate#749.
Jeff Luo (JeffLuoo) added a commit to JeffLuoo/substrate that referenced this pull request Aug 25, 2026
The volume measurement worked on GKE only. There the managed collector
cannot hold a connector, thus benchmarking/telemetry/meter.yaml puts a
second collector in front of it and counts the data as it goes through.
On kind the collector is ours, thus it can do the count itself and the
cluster needs no meter and no extra hop.

Add the count connector to the kind collector. It now gives the same two
counts as the meter, substrate_spans_total{service_name} and
substrate_datapoints_total{service_name}, on the Prometheus exporter port
that it already opens. The connector resolves each key from the attributes
of the item, then the scope, then the resource, thus service.name resolves
with no transform in front of it. Drop the transform from the meter also.

Give the collector its otelcol_* counters also. The default reader binds
to localhost, thus nothing outside the pod could read it. Bind it to each
interface and open port 8888. The counters are the positive control of a
measurement: without them a service that reports zero volume and data that
never arrived look the same.

One scrape job reads both ports. The prometheus.io/scrape annotation can
name one port only, and port 8889 re-exports each metric that the pipelines
push. The pods of ateapi, atelet, and atenet-router already carry that
annotation on 9090, thus an annotation on the collector would bring each of
their series in a second time and sum() would give double. The job keeps
the substrate_* and otelcol_* names only.

The actors need no change. benchmarking/workloads/deploy.sh reads the
endpoint from the ate-otel-config ConfigMap, and the kind overlay of that
ConfigMap already names this collector.

Follow-up to agent-substrate#749.
Da Huang (git286) pushed a commit that referenced this pull request Aug 25, 2026
The volume measurement in current benchmarking repo worked on GKE only.
There the managed collector cannot hold a connector, thus
benchmarking/telemetry/meter.yaml puts a second collector in front of it
and counts the data as it goes through.

This PR adds the support to kind cluster to count the telemetry data.

Follow-up to #749.

> It's a good idea to open an issue first for discussion.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants