mirror of
https://github.com/agent-substrate/substrate.git
synced 2026-10-02 03:24:42 +08:00
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.
109 lines
3.5 KiB
Go
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, ®); 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)
|
|
}
|
|
}
|