Files
Tim Bai 03ba5821ec actorevent: define the actor usage event and the activation epoch (#1881)
Step 3 of #1748. Vocabulary only: nothing emits the event or sets the
epoch yet.

**Event.** `ate.actor.usage_sampled` in `actorevent`, sixteen required
keys via `ateattr`: identity, pool, sandbox class, source,
`ate.stats.kind` (periodic, initial, final), the four measurements,
observation time, epoch. Measurement names follow the
`ate.actor.stats.*` instruments; units are in the registry briefs. Body
is `Actor usage sampled`, the accepted stdout shape change.

**Scope.** The usage stream has its own instrumentation scope, the
lifecycle scope with `/usage` appended, so a consumer can sample or drop
it without touching lifecycle records. `NewEmitter` takes the scope; the
package-level `Log` keeps the lifecycle one, so ateapi is unchanged.

**Epoch.** `ate.actor.epoch` and `WorkloadStatsSample.epoch_unix_nano`:
the unix-nano time an activation began. CPU time is per activation for
every source; lifetime is the sum over epochs of each epoch's highest
value. Memory usage and working set are absolute; peak is as the source
reports it. An ateom that leaves the epoch at zero still sends the raw
guest counter. The measurement comments on fields 8 to 11 say all of
this.

**Registry.** Event group in `events.yaml`, seven attributes in
`metrics.yaml`, `ate.actor.epoch` in the no-actor-identity forbidden
list. The one-record-one-place rule and the collector guide now name
both scopes.

Left for step 4: the event's code anchor moves to the ateom emit site,
atelet's poller comments and the guide's stdout usage section get
rewritten, and the controller starts passing `OTEL_LOGS_EXPORTER` to
worker pods.
2026-09-25 19:54:15 +00:00

109 lines
3.5 KiB
Go

// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package actorevent
import (
"os"
"slices"
"testing"
"sigs.k8s.io/yaml"
)
const registryPath = "../../docs/metrics/registry/events.yaml"
// registry is the part of a Weaver event group this test reads. Weaver has no
// field for a body or a severity, so those two are pinned in Go alone.
type registry struct {
Groups []struct {
Type string `json:"type"`
Name string `json:"name"`
Attributes []struct {
Ref string `json:"ref"`
// "required", or {conditionally_required: <condition>}.
RequirementLevel any `json:"requirement_level"`
} `json:"attributes"`
} `json:"groups"`
}
// TestEventsMatchTheRegistry holds the vocabulary and events.yaml in step both
// ways, so a new event has to be declared before it can ship.
//
// It covers every event group in the file. A component outside this package that
// starts emitting events needs a check of its own, and this one has to learn to
// skip what it does not own.
func TestEventsMatchTheRegistry(t *testing.T) {
t.Parallel()
raw, err := os.ReadFile(registryPath)
if err != nil {
t.Fatalf("read %s: %v", registryPath, err)
}
var reg registry
if err := yaml.Unmarshal(raw, &reg); err != nil {
t.Fatalf("parse %s: %v", registryPath, err)
}
type shape struct{ required, conditional []string }
declared := map[string]shape{}
for _, g := range reg.Groups {
if g.Type != "event" {
continue
}
var sh shape
for _, a := range g.Attributes {
switch lvl := a.RequirementLevel.(type) {
case string:
if lvl != "required" {
t.Errorf("%s declares %s as %q; an event attribute is required or conditionally required with a stated condition",
g.Name, a.Ref, lvl)
}
sh.required = append(sh.required, a.Ref)
case map[string]any:
if c, ok := lvl["conditionally_required"].(string); !ok || c == "" {
t.Errorf("%s declares %s as %v; an event attribute is required or conditionally required with a stated condition",
g.Name, a.Ref, lvl)
}
sh.conditional = append(sh.conditional, a.Ref)
default:
t.Errorf("%s declares %s with an unreadable requirement level %v", g.Name, a.Ref, lvl)
}
}
declared[g.Name] = sh
}
for _, ev := range events {
sh, ok := declared[ev.Name]
if !ok {
t.Errorf("%s has no group in %s; add one", ev.Name, registryPath)
continue
}
delete(declared, ev.Name)
got, want := slices.Sorted(slices.Values(sh.required)), slices.Sorted(slices.Values(ev.Keys))
if !slices.Equal(got, want) {
t.Errorf("%s: %s requires %v, the Event declares %v", ev.Name, registryPath, got, want)
}
got, want = slices.Sorted(slices.Values(sh.conditional)), slices.Sorted(slices.Values(ev.Conditional))
if !slices.Equal(got, want) {
t.Errorf("%s: %s conditionally requires %v, the Event declares %v", ev.Name, registryPath, got, want)
}
}
for name := range declared {
t.Errorf("%s declares %s, which no Event in this package emits", registryPath, name)
}
}