Skip to content

demos: add actor telemetry continuity reference demo (#503) - #750

Open
Sanskar Singh Bhardwaj (sanskar-singh-2403) wants to merge 1 commit into
agent-substrate:mainfrom
sanskar-singh-2403:demo/actor-telemetry-503
Open

Sanskar Singh Bhardwaj (sanskar-singh-2403) wants to merge 1 commit into
agent-substrate:mainfrom
sanskar-singh-2403:demo/actor-telemetry-503

Conversation

@sanskar-singh-2403

Copy link
Copy Markdown

Add a reference demo for actor telemetry continuity across suspend/resume, the
follow-up work requested in #503.

The demo is a small Go HTTP server that emits OpenTelemetry metrics and trace
spans over OTLP on every request. It reproduces the #503 loss: an OTel push
exporter batches telemetry in memory and flushes on a timer, so when the push
interval (default 60s) is longer than the active window before idle suspension,
telemetry is still buffered in the SDK when the actor is suspended. With
snapshotsConfig.onPause: Data (the mode the template uses) Substrate drops
process RAM on pause, so that buffered telemetry is lost.

The mitigation is isolated behind a flushTelemetry(ctx) seam that ForceFlushes
both the MeterProvider and TracerProvider. Today it is called from the
self-suspend path via an idle watcher; when the PreSuspend hook (#450) lands,
flushTelemetry does not change, only its call-site moves into the PreSuspend
handler. This keeps #450 a call-site move rather than a rewrite.

The ActorTemplate sets OTEL_EXPORTER_OTLP_ENDPOINT to the in-cluster
collector, since the SDK default (localhost:4317) has nothing listening inside
the actor sandbox netns.

Includes install-demo-actor-telemetry.sh wired into install-ate.sh, a
rendered-and-validated template, unit tests for the idle-flush watcher, and a
README documenting the reproduce/verify flow against Jaeger and Prometheus on
Kind.

Scope

This covers the "reference demo for actor telemetry continuity" action item in
#503 only. The docs/observability.md update is tracked separately, and the
PreSuspend/PostResume alignment is #450.

Part of #503

Testing

  • gofmt, go vet, go build, and go test ./demos/actor-telemetry/ all pass
    (4/4 unit tests on the idle-flush watcher).

  • Template renders via the install script's sed substitution and parses as
    valid Kubernetes manifests.

  • End-to-end suspend/resume verification against a live Kind cluster with
    ate-system is described in the README.

  • Tests pass

  • Appropriate changes to documentation are included in the PR

return res, nil
}

func actorName() string {

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.

ATE_ACTOR_NAME isn't set anywhere in the repo, and os.Hostname() returns "runsc" for every actor — it's hardcoded in the OCI spec (cmd/atelet/oci.go:271) and applied via the UTS namespace. So this always returns runsc.

The documented way is /run/ate/actor-id (docs/api-guide.md:108), with a working example in internal/e2e/fixtures/probe/main.go:32. api-guide.md:111 also warns specifically against the env-var approach, since env is frozen in the golden snapshot.

Choose a reason for hiding this comment

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

Good catch, confirmed. os.Hostname() returns runsc for every actor (hardcoded at cmd/atelet/oci.go:268) and ATE_ACTOR_NAME is set nowhere, so this always returned runsc. Switched to reading /run/ate/actor-id fresh per the guidance in docs/api-guide.md, mirroring internal/e2e/fixtures/probe/main.go.

ctx := context.Background()
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))

res, err := newResource(ctx)

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 resource is built in main(), so it's captured by the golden snapshot and identical for every actor of the template: even reading /run/ate/actor-id here would give you the golden actor's name.

Choose a reason for hiding this comment

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

Right, building it in main() freezes it into the golden snapshot. Moved actor identity out of the resource entirely and now read it fresh in the request handler, so it reflects the resumed actor rather than the golden one. Updated the test to cover the fresh-read and the missing-file fallback.

requests, err := meter.Int64Counter(
"demo.actor.requests",
metric.WithDescription("Requests handled by the actor-telemetry demo, by actor."),
)

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 just filed #761 : the counter might have the merge and conflate problem when actors run on different worker pods during its lifecycle.

Choose a reason for hiding this comment

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

got it that is the merge/conflate concern across worker pods and is orthogonal to this demo's fix; happy to track the counter-aggregation semantics there.

ctx, span := tracer.Start(r.Context(), "handle-request")
defer span.End()

requests.Add(ctx, 1, metric.WithAttributes(

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.

ate.dev/actor_name on the metric datapoint is unbounded cardinality. The north star of this project is to have billions of actors.

On the span below it's fine, since a span is an event rather than a series.

For a reference demo I'd keep actor identity on the span and use template name on the metric.

Choose a reason for hiding this comment

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

Agreed, per-actor labels on the metric are unbounded cardinality at billions of actors. Changed the counter to label with the bounded ate.dev/template and kept ate.dev/actor_name on the span only, since a span is an event rather than a series. README updated to match.

Comment thread demos/actor-telemetry/main.go Outdated
Comment on lines +324 to +337
func getCurrentIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "x.x.x.x"
}
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String()
}
}
}
return "y.y.y.y"
}

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 is the os.Hostname() problem again. I'd either drop it, or keep it and relabel it to something like fake-static IP.

Choose a reason for hiding this comment

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

Removed getCurrentIP entirely. Per #761 every actor egresses behind one constant interior IP inside the gVisor netns, so it could never identify the worker pod. The response now reports the actor name (read fresh from the identity file) instead.

Comment thread demos/actor-telemetry/README.md Outdated
```bash
curl -X POST -H "Host: my-telemetry-1.demo.actors.resources.substrate.ate.dev" http://localhost:8000
```
Each response reports the worker pod that served it. The metric

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.

Choose a reason for hiding this comment

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

Fixed. Reworded to "reports the actor it was handled for" now that the IP is gone.

sdktrace.WithBatcher(exp, sdktrace.WithBatchTimeout(*pushInterval)),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})

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.

You set the propagator but never extract, so tracer always starts a root span and the actor's traces never join the router's. See #427

You can also use otelhttp.NewHandler to do avoid extraction and setting attrs manually.

Choose a reason for hiding this comment

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

Good catch. Wrapped the mux in otelhttp.NewHandler(mux, "/") so the incoming W3C trace context is extracted and the per-request span joins the router's trace instead of starting a detached root. Follows the same pattern as the atenet router and glutton.

Comment thread demos/actor-telemetry/main.go Outdated
// Metric: label with the bounded template name only. Per-actor labels
// are unbounded cardinality at this project's scale (see templateName).
requests.Add(ctx, 1, metric.WithAttributes(
attribute.String("ate.dev/template", templateName),

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.

ate.dev/ is the k8s label and what we log to sdtout. Span and metric attributes use the ate.* namespace. So these should be ate.actor.template.name here and ate.actor.name below.

Please also update this in README.

Choose a reason for hiding this comment

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

Done. Switched to the dotted ate.* namespace: ate.actor.template.name on the metric and ate.actor.name on the span, matching internal/ateattr. Updated the README to match.

Comment thread demos/actor-telemetry/main.go Outdated
// OTLP every push-interval. The long interval is the whole point: it is what
// leaves telemetry buffered when a suspend arrives before the timer fires.
func initMeterProvider(ctx context.Context, res *resource.Resource) (*sdkmetric.MeterProvider, error) {
exp, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithInsecure())

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.

Please don't hardcode WithInsecure, see #741

Choose a reason for hiding this comment

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

Dropped the hardcoded WithInsecure() from both exporters. The demo endpoint is plaintext http://, which the SDK already infers from the scheme, so nothing regresses while the SDK's env-driven TLS config is now respected.

Comment thread demos/actor-telemetry/main.go Outdated
meter := mp.Meter(serviceName)
requests, err := meter.Int64Counter(
"demo.actor.requests",
metric.WithDescription("Requests handled by the actor-telemetry demo, by actor."),

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.

Suggested change
metric.WithDescription("Requests handled by the actor-telemetry demo, by actor."),
metric.WithDescription("Requests handled by the actor-telemetry demo, by template."),

Choose a reason for hiding this comment

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

done

}

meter := mp.Meter(serviceName)
requests, err := meter.Int64Counter(

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.

Nit: please add metric.WithUnit("{request}")

Choose a reason for hiding this comment

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

done

# listening, so telemetry would be flushed into the void. This mirrors the
# endpoint the ate-system components use on Kind (see
# manifests/ate-install/kind/kustomization.yaml).
- name: OTEL_EXPORTER_OTLP_ENDPOINT

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.

Da Huang (@git286), I think it's a good opportunity to set delta temporarily here as well. See my reasoning in #761 (comment)

Choose a reason for hiding this comment

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

Set OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta in the template. Agreed it fits this suspend/resume workload: with cumulative, a restore from the golden snapshot carries the golden actor's accumulated value and reset detection cannot see the restart, so delta avoids the inflate/merge. Tracking the broader identity fix in #761.

res, err := resource.New(ctx,
resource.WithTelemetrySDK(),
resource.WithSchemaURL(semconv.SchemaURL),
resource.WithAttributes(

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.

Please add service.instance.id

Choose a reason for hiding this comment

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

Added semconv.ServiceInstanceID, generated once and shared by the meter and tracer resources, mirroring internal/serverboot.

…#503)

Add a reference demo for actor telemetry continuity across suspend/resume,
the follow-up work requested in agent-substrate#503.

The demo is a small Go HTTP server that emits OpenTelemetry metrics and
trace spans over OTLP on every request. It reproduces the agent-substrate#503 loss: an
OTel push exporter batches telemetry in memory and flushes on a timer, so
when the push interval (default 60s) is longer than the active window
before idle suspension, telemetry is still buffered in the SDK when the
actor is suspended. With snapshotsConfig.onPause: Data (the mode the
template uses) Substrate drops process RAM on pause, so that buffered
telemetry is lost.

The mitigation is isolated behind a flushTelemetry(ctx) seam that
ForceFlushes both the MeterProvider and TracerProvider. Today it is called
from the self-suspend path via an idle watcher; when the PreSuspend hook
(agent-substrate#450) lands, flushTelemetry does not change, only its call-site moves into
the PreSuspend handler. This keeps agent-substrate#450 a call-site move rather than a
rewrite.

The ActorTemplate sets OTEL_EXPORTER_OTLP_ENDPOINT to the in-cluster
collector, since the SDK default (localhost:4317) has nothing listening
inside the actor sandbox netns.

Includes install-demo-actor-telemetry.sh wired into install-ate.sh, a
rendered-and-validated template, unit tests for the idle-flush watcher, and
a README documenting the reproduce/verify flow against Jaeger and
Prometheus on Kind.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/demos area/observability kind/feature An enhancement / feature request or implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants