Simulator Metrics

The simulator publishes three kinds of telemetry:

  • Prometheus metrics about itself at /metrics (joulie_sim_*), for Prometheus to scrape.
  • OpenTelemetry metrics and events about the emulated nodes and the facility, in emulate mode only. Metrics are pushed as OTLP JSON over HTTP; events are written to events.jsonl and pushed when a logs endpoint is set.
  • Compatibility exporters, in emulate mode only: the Prometheus formats of real exporters (Kepler, dcgm-exporter, the ROCm device-metrics-exporter) at /exporters/<name>/metrics, opt-in.

The OpenTelemetry names are spelled once, in pkg/otel/semconv, and the controller manager reads the same names back from Prometheus. Their Prometheus spelling is computed by pkg/otel/promname.

Prometheus metrics at /metrics

Served at /metrics on the simulator’s listen address (SIM_ADDR, default 127.0.0.1:18080, and :18080 in a pod). The JSON debug endpoints are listed under HTTP API.

HTTP/request metrics

  • joulie_sim_requests_total{route,method,status} (counter)
    • total HTTP requests by route/method/status
  • joulie_sim_request_duration_seconds{route,method} (histogram)
    • request latency

Control-path metrics

  • joulie_sim_controls_total{node,action} (counter)
    • received control actions by node/action
  • joulie_sim_control_actions_total{node,action,result} (counter)
    • control action outcomes
    • result: applied|blocked|error

Per-node simulated state metrics

  • joulie_sim_node_cap_watts{node} (gauge)
    • current simulated effective cap
  • joulie_sim_node_rapl_cap_watts{node} (gauge)
    • simulated RAPL cap value
  • joulie_sim_node_throttle_pct{node} (gauge)
    • simulated DVFS throttle percent
  • joulie_sim_node_power_watts{node} (gauge)
    • simulated exported node power
  • joulie_sim_node_cpu_util{node} (gauge)
    • simulated CPU utilization
  • joulie_sim_node_freq_scale{node} (gauge)
    • simulated frequency scale
  • joulie_sim_node_running_pods{node} (gauge)
    • running pods observed on the node
  • joulie_sim_node_class_info{node,class} (gauge)
    • class assignment marker (1 on active class)

In emulate mode the per-node gauges of an emulated node are set from its snapshot at every tick.

Facility metrics

The facility model, as gauges:

  • joulie_sim_facility_ambient_temp_celsius (gauge)
    • outdoor ambient temperature, in degrees Celsius
  • joulie_sim_facility_it_power_watts (gauge)
    • total IT power of the simulated nodes
  • joulie_sim_facility_cooling_power_watts (gauge)
    • cooling power, derived from the IT power and the PUE
  • joulie_sim_facility_pue (gauge)
    • simulated Power Usage Effectiveness

Workload/job metrics

  • joulie_sim_job_submitted_total{class} (counter)
    • jobs submitted by class
  • joulie_sim_job_completed_total{class,node} (counter)
    • jobs completed by class and node
  • joulie_sim_job_completion_seconds (histogram)
    • job completion latency distribution

Emulate mode and export metrics

  • joulie_sim_emulate_nodes (gauge)
    • emulated nodes, one per cluster spec member
  • joulie_sim_emulate_tick_duration_seconds (histogram)
    • time one tick takes to step every emulated node
  • joulie_sim_emulate_tick_overruns_total (counter)
    • ticks that took longer than the tick interval; the next tick advances to wall time
  • joulie_sim_emulate_node_step_errors_total{node} (counter)
    • failed steps of one emulated node
  • joulie_sim_otlp_requests_total{signal,result} (counter)
    • OTLP exports by signal (metrics, logs) and result
    • result: success|partial|error|dropped (partial: the receiver rejected some points; dropped: the logs queue was full)
  • joulie_sim_otlp_points_total{signal} (counter)
    • data points and log records the receiver accepted
  • joulie_sim_otlp_dropped_points_total{signal} (counter)
    • values left out of the export because they were NaN, infinite or out of the int64 range
  • joulie_sim_events_total{event} (counter)
    • events emitted, by eventName

Notes

  • Prometheus metrics capture online simulator state and request/control behavior.
  • Integrated node/cluster energy totals of the label-modelled nodes are exposed through /debug/energy (JSON), not as Prometheus time series. Emulated nodes export their energy as OpenTelemetry counters (below).
  • Thermal state and averaged versus instantaneous power are not Prometheus gauges: fields such as instantPackagePowerWatts, cpu.temperatureC, cpu.thermalThrottlePct and per-device GPU averaged power are in the legacy JSON of /telemetry/{node}?format=legacy (until v0.4.0) and in /debug/nodes.

OpenTelemetry metrics (emulate mode)

The names follow the OpenTelemetry semantic conventions v1.44.0, and every resource and scope carries schemaUrl: https://opentelemetry.io/schemas/1.44.0. Every hw.* name is at Development stability upstream and may be renamed there; the pin makes such a rename a deliberate change in Joulie. Names no convention covers use the prefix joulie.. Points are cumulative, so Prometheus accepts them without otlp-deltatocumulative. Every pushed point carries the time of the export that sends it, with the values of the newest emulation step, as an OpenTelemetry SDK’s periodic reader stamps the time it collects; a step that has not changed since the last export is sent again at the later time, and counters keep their ledger’s start as startTimeUnixNano. A point stamped with the step’s time would reach Prometheus up to a step old, and rate() would read low (see the pitfalls). The per-node /otlp/telemetry/{node} route keeps the step’s time instead, because its reader derives power from the change between two reads. How they are sent is described under Simulator telemetry export.

Resources

Each emulated node is one resource:

AttributeValue
service.nameOTEL_SERVICE_NAME, default joulie-simulator
service.namespacejoulie
service.instance.id, k8s.node.name, host.namethe node name
host.idhwemu- followed by the first 32 hex characters of sha256(node name)
k8s.cluster.namethe cluster spec’s name
service.versionthe simulator’s module version

The facility is a resource of its own, with service.instance.id facility and k8s.cluster.name. OTEL_RESOURCE_ATTRIBUTES adds attributes but never overrides service.instance.id, k8s.node.name or host.name. In Prometheus a node’s series get job="joulie/joulie-simulator" and instance="<node>"; that is a convenience of the default setup, not part of the contract, so select nodes by k8s_node_name.

Metrics

Prometheus names are those of the default translation strategy (UnderscoreEscapingWithSuffixes).

OTel nameUnitInstrumentEmitted forPoint attributesPrometheus name
hw.host.energyJCounter (int)a measured hosthw.id=hosthw_host_energy_joules_total
hw.host.powerWGaugea measured hosthw.id=hosthw_host_power_watts
hw.host.ambient_temperatureCelGaugea measured host: the inlet, which is its cooling zone’s temperaturehw.id=hosthw_host_ambient_temperature_celsius
hw.energyJCountermeasured CPU packages, DRAM zones and GPUshw.id, hw.type, hw.name, hw.parent (memory only)hw_energy_joules_total
hw.powerWGaugethe samehw.id, hw.type, hw.name, hw.parent (memory only)hw_power_watts
hw.cpu.speedHzGauge (int)CPU packages with cpufreqhw.id, hw.name, hw.vendor, hw.modelhw_cpu_speed_hertz
hw.cpu.speed.limitHzGauge (int), one point per hw.limit_type: max and throttledthe samehw.id, hw.name, hw.vendor, hw.model, hw.limit_typehw_cpu_speed_limit_hertz
hw.gpu.utilization1GaugeGPUshw.id, hw.name, hw.vendor, hw.model, hw.gpu.task=generalhw_gpu_utilization_ratio
joulie.hw.power.limitWGauge, one point per joulie.hw.limit.state: requested and enforcedcomponents with a power limithw.id, hw.type, joulie.hw.limit.statejoulie_hw_power_limit_watts
hw.gpu.ioByCounter (int), one point per network.io.direction and joulie.hw.gpu.io.pathGPUs, on the peer path only: the bytes a GPU received from and sent to the other GPUs of its node. The emulator models no host or network interface bytes, so host and nic have no point. The counters come from the node’s ledger and start at its starthw.id, hw.name, hw.vendor, hw.model, network.io.direction (receive, transmit), joulie.hw.gpu.io.path (peer, host, nic)hw_gpu_io_bytes_total
joulie.hw.gpu.speedHzGauge (int), one point per joulie.hw.clock.domainGPUs with a clock model, every GPU of the builtin profiles: graphics and memory on NVIDIA, plus fabric and soc on an AMD family whose profile has fclk and socclk tableshw.id, hw.vendor, hw.model, joulie.hw.clock.domain (graphics, memory, fabric, soc)joulie_hw_gpu_speed_hertz
joulie.hw.gpu.speed.limitHzGauge (int), one point per joulie.hw.clock.domain and hw.limit_type: max and throttledthe GPUs and domains of joulie.hw.gpu.speed: max is the domain’s hardware maximum (the top of its pp_dpm_* table, NVML’s maximum clock), throttled the maximum in effect, the clock a busy GPU reaches under the present performance level, overdrive range or clock lock (on NVIDIA at most the sustained maximum)hw.id, hw.vendor, hw.model, joulie.hw.clock.domain, hw.limit_typejoulie_hw_gpu_speed_limit_hertz
joulie.hw.governor1UpDownCounter (int): 1 for the state a component is in, 0 for the one it leftCPU packages and GPUs with a governor state: 1 for the state, and 0 for the state the component was in before, sent with every export until the state changes again and dropped by a simulator restart. joulie.hw.governor.preset is the preset pkg/governor maps the native state back to, empty when no single preset does (Preset names)hw.id, hw.type, joulie.hw.governor.preset, joulie.hw.governor.nativejoulie_hw_governor
joulie.facility.ambient_temperatureCelGaugethe facilityjoulie_facility_ambient_temperature_celsius
joulie.facility.it.energyJCounterthe facilityjoulie_facility_it_energy_joules_total
joulie.facility.it.powerWGaugethe facilityjoulie_facility_it_power_watts
joulie.facility.cooling.energyJCounterthe facilityjoulie_facility_cooling_energy_joules_total
joulie.facility.cooling.powerWGaugethe facilityjoulie_facility_cooling_power_watts
joulie.facility.pue1Gaugethe facility; for dashboards, since the controller manager derives PUE itselfjoulie_facility_pue_ratio
joulie.cooling_zone.ambient_temperatureCelGaugeeach cooling zonejoulie.cooling_zone.namejoulie_cooling_zone_ambient_temperature_celsius
joulie.rack.energyJCountereach rackjoulie.rack.namejoulie_rack_energy_joules_total
joulie.rack.powerWGaugeeach rackjoulie.rack.namejoulie_rack_power_watts

The point attributes become the labels hw_id, hw_type, hw_name, hw_parent, hw_vendor, hw_model, hw_limit_type, joulie_hw_limit_state, network_io_direction, joulie_hw_gpu_io_path, joulie_hw_clock_domain, joulie_hw_governor_preset, joulie_hw_governor_native, joulie_cooling_zone_name and joulie_rack_name. The resource attributes k8s_node_name and host_name are always on target_info, and on the data series only when Prometheus promotes them (below).

Components

Componenthw.idhw.typehw.nameAlso
CPU packagecpu<socket>cpupackage-<socket>hw.vendor (Intel or AMD) and hw.model on the CPU metrics
DRAM zonememory<socket>memorydramhw.parent=cpu<socket>
GPUgpu<index>gputhe product namehw.vendor and hw.model on the GPU metrics
Hosthostnone

Producer rules

  • Whole-host energy is reported only as hw.host.energy, never as hw.energy, so a sum over hw.energy never counts the host twice. hw.host.* points carry no hw.type.
  • One cpu component per package. RAPL core, uncore and psys zones are never cpu. DRAM is memory; Joulie does not count it as node power.
  • Host power is the packages, the DRAM zones, the platform remainder and the GPUs together. Power supply losses are not included.
  • Only readings a real collector could measure are emitted: package and DRAM energy_uj, NVML total energy, amdgpu power. A node whose profile is a virtual machine gets no hw.* metric at all, and a CPU without a DRAM zone gets no memory component.
  • Exported counters never decrease, not even across a simulator restart: they continue from a ledger kept in the tree root, the GPU bytes of hw.gpu.io as well as the energies. The ledger is rewritten each tick, in place and whole up to 2 KB, by rename above, so a killed simulator leaves the last whole tick. While the simulator is down no energy accrues, so a restart shows as a gap in the rate, never as a reset. A counter’s start time is the ledger’s start.

Prometheus

A Prometheus started with --web.enable-otlp-receiver accepts the push at /api/v1/otlp/v1/metrics. Its settings (promoting k8s.node.name and host.name, the translation strategy, the out-of-order window), the spelling of every name under the other translation strategies, and the pitfalls are under Prometheus settings, Name translation strategies and Pitfalls. The full table for every name is pkg/otel/promname/testdata/prometheus-names.tsv.

Events (emulate mode)

Events are OpenTelemetry log records with an eventName. timeUnixNano is when the change happened in the emulated node, observedTimeUnixNano when the simulator emitted it. The body is only a display string; read the attributes. A record is emitted on its node’s resource, so the node is not an attribute.

They are always appended to events.jsonl in SIM_OTEL_FILE_DIR, one LogsData object per line, and pushed only when a logs endpoint is set. /debug/events and events.ndjson in SIM_DEBUG_PERSIST_DIR use the simulator’s own JSON instead.

eventNameSeverityWhenAttributes
joulie.sim.node.ready9 INFOan emulated node’s tree is in place and its first step ran, once per node per startjoulie.sim.profile, joulie.sim.pool, joulie.sim.tree.resumed, joulie.sim.tree.added
joulie.sim.node.mismatch13 WARNthe Kubernetes Node of a spec node is missing or names another profile; once per change, not once per pollerror.type (node_missing, profile_mismatch), joulie.sim.profile, joulie.sim.node.profile
joulie.sim.cap.applied9 INFOa power limit written to a node file took effect, possibly quantized or clampedhw.id, hw.type, joulie.sim.file, joulie.sim.outcome, joulie.sim.value.written, joulie.sim.value.effective
joulie.sim.setting.applied9 INFOanother setting took effect, such as a frequency bound or a governorthe same
joulie.sim.cap.reset13 WARNa GPU limit went back to its default: persistence mode off and no clientthe same, with hw.type=gpu; written is the old limit, effective the default
joulie.sim.write.rejected13 WARNa write was revertedthe same, and error.type (reverted_out_of_range, parse_error, restored, einval, eopnotsupp, ebusy, enodata)

joulie.sim.outcome is the emulator’s own name for what happened to the write, such as accepted, clamped, rejected or reset-persistence-off. restored means a file only the emulator writes was overwritten and put back. einval, eopnotsupp, ebusy and enodata mean the real driver refuses the value with that errno (outcome rejected): with plain files the write itself succeeds and the emulator reverts the value at its next step, and in FUSE mode the write fails with that errno, as on the host. Either way the event comes with the next step. Among others, einval comes from an unknown governor or EPP word and an out-of-range amdgpu power1_cap, ebusy from a non-zero EPP under the performance governor, and enodata from any write to the limit or time window of a locked or disabled RAPL constraint.

Workload run and scenario names

A v1 workload run in emulate mode writes the joulie.sim.* names below to its run directory, <SIM_RUNS_DIR>/<runId>/otlp/ (simulator.metrics.jsonl, a cumulative snapshot every SIM_OTLP_FILE_INTERVAL and one at the end; simulator.logs.jsonl, the run’s and its jobs’ events, with copies of the emulated nodes’ hardware events). The scenario runner writes the joulie.capture.* and joulie.scenario.* names to harness.metrics.jsonl and harness.logs.jsonl next to them. Both are OTLP JSON Lines, one MetricsData or LogsData object per line, in the format of the OpenTelemetry Collector’s file exporter; neither is pushed to the OTLP endpoint, and the joulie_sim_* Prometheus series above are a different set. Every resource of a run carries joulie.sim.run.id. See Workload Scenarios.

OTel nameUnitInstrumentPoint attributesPrometheus name
joulie.sim.workload.jobs.submitted{job}Counter (int)joulie.workload.classjoulie_sim_workload_jobs_submitted_total
joulie.sim.workload.jobs.finished{job}Counter (int)joulie.workload.class, joulie.sim.job.outcomejoulie_sim_workload_jobs_finished_total
joulie.sim.workload.jobs.active{job}UpDownCounter (int)joulie.workload.class, joulie.sim.job.statejoulie_sim_workload_jobs_active
joulie.sim.workload.job.durationsHistogramjoulie.workload.classjoulie_sim_workload_job_duration_seconds
joulie.sim.workload.job.queue_waitsHistogramjoulie.workload.classjoulie_sim_workload_job_queue_wait_seconds
joulie.sim.workload.job.slowdown1Histogramjoulie.workload.classjoulie_sim_workload_job_slowdown
joulie.sim.workload.work{unit}Counterjoulie.workload.class, joulie.sim.work.kindjoulie_sim_workload_work_total
joulie.sim.workload.energyJCounterjoulie.workload.class, hw.type, k8s.node.name, joulie.sim.profilejoulie_sim_workload_energy_joules_total
joulie.capture.nodetwin.measured_powerWGaugek8s.node.name, joulie.nodetwin.power_sourcejoulie_capture_nodetwin_measured_power_watts
joulie.capture.nodetwin.schedulable_class1UpDownCounter (int)k8s.node.name, joulie.node.schedulable_classjoulie_capture_nodetwin_schedulable_class
joulie.capture.nodetwin.control_result1UpDownCounter (int)k8s.node.name, hw.type, joulie.nodetwin.control.backend, joulie.nodetwin.control.resultjoulie_capture_nodetwin_control_result
joulie.scenario.assertion.value{value}Gaugejoulie.assertion.name, joulie.assertion.sourcejoulie_scenario_assertion_value
eventNameSeverity
joulie.sim.run.started, joulie.sim.run.finished, joulie.sim.run.resumed9 INFO
joulie.sim.run.stalled13 WARN
joulie.sim.job.submitted, joulie.sim.job.scheduled, joulie.sim.job.started, joulie.sim.job.resumed, joulie.sim.job.completed9 INFO
joulie.sim.job.adopted, joulie.sim.job.stalled, joulie.sim.job.incomplete13 WARN
joulie.sim.job.failed17 ERROR
joulie.scenario.started, joulie.scenario.variant.started, joulie.scenario.finished9 INFO
joulie.scenario.assertion.evaluated9 INFO, or 17 ERROR with error.type=assertion_failed for a failed check

Calibration sweep names

A calibration sweep records these two names beside the hardware metrics above, and hwcalib fit reads them back. Neither the simulator nor any Joulie component emits them. See Hardware Calibration.

OTel nameUnitInstrumentPoint attributesPrometheus name
system.cpu.utilization1Gaugecpu.mode, cpu.logical_number (opt-in)system_cpu_utilization_ratio
joulie.calibration.setpoint{setpoint}Gaugejoulie.calibration.knob, joulie.calibration.class, joulie.calibration.memory_intensityjoulie_calibration_setpoint

Compatibility exporters (emulate mode)

SIM_EXPORTERS turns on exporters that reproduce the Prometheus format of a pinned release of a real exporter, at /exporters/<name>/metrics. They are computed from the same counters the agent and the fake GPU tools read, so a query written for the real exporter reads the emulated nodes. They exist to test Joulie’s kepler and kepler-legacy input presets and Collector bridges; the OpenTelemetry export above is the default. With the chart, the ServiceMonitor scrapes each listed exporter with honorLabels: false.

NameReproducesSeriesNotes
kepler-0.7Kepler v0.7.12 (the same schema through 0.9.0)kepler_node_{package,core,uncore,dram,other,gpu,platform}_joules_total{package, instance, mode} with the constant label sourceOne series per socket, mode dynamic and idle, so each node has two platform series. instance is the node name; scraped with honor_labels: false it becomes exported_instance. GPU series for NVIDIA GPUs only, source="NVML"; the others source="rapl-sysfs". platform is the host energy, and other the platform energy not counted in package, DRAM and GPU. Idle and dynamic split as upstream: each series keeps the power of its lowest-load interval as the idle power of every later interval, and dynamic is the rest; an interval that measured less gets dynamic 0 and the full recorded idle, so under a cap that holds a package below that idle the two modes add up to more than it draws (kepler-legacy under a cap). A node without RAPL emits nothing.
kepler-0.12Kepler v0.12.0kepler_node_cpu_{joules_total,watts} and kepler_node_cpu_{active,idle}_{joules_total,watts} with zone, path and the constant label node_name; for NVIDIA GPUs kepler_node_gpu_{,active_,idle_}{joules_total,watts} with gpu, gpu_uuid, gpu_name, vendorThe zones are the RAPL zones of the node’s tree, named as Kepler names them: the socket index is stripped, so the packages of a 2-socket node are one zone package with path="aggregated-package", while a die zone package-N-die-M keeps its name. kepler_node_cpu_joules_total is the zone’s energy_uj and wraps at max_energy_range_uj, as upstream; the active and idle counters never wrap. Active energy is the node’s CPU usage share; the first read of a node counts the whole counter as idle, at 0 W. The GPU series are served only with SIM_KEPLER012_GPU=true, as Kepler serves them only with its experimental GPU option. A node whose tree has no RAPL zone emits nothing, GPUs included.
dcgm-4.8.4dcgm-exporter 4.8.4DCGM_FI_DEV_POWER_USAGE (W), DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION (mJ), DCGM_FI_DEV_GPU_UTIL (%) per NVIDIA GPULabels gpu, UUID, pci_bus_id, device=nvidia<i>, modelName, hostname (Hostname with SIM_DCGM_HOSTNAME_LABEL, as releases up to 4.8.2 spell it) and DCGM_FI_DRIVER_VERSION. The energy is the GPU’s NVML total energy counter.
amd-dme-1.5.2ROCm device-metrics-exporter v1.5.2gpu_package_power and gpu_power_usage (whole W), gpu_energy_consumed (uJ), gpu_gfx_activity (%) per AMD GPU, all typed gauge as upstream doesThe fourteen labels upstream always sets: gpu_id (the index), hostname (the node), card_model (the product name here), serial_number (synthetic, stable per node and index), gpu_partition_id="0", gpu_compute_partition_type="spx", gpu_memory_partition_type="nps1", deployment_mode="baremetal", and pod, namespace, container, job_id, job_user, job_partition, which stay empty. The energy is the GPU’s energy ledger. A power or energy reading of 0, or of a GPU without a power sensor, is left out, as upstream exports nothing for a field it reads as 0. Upstream has no power cap metric at v1.5.2, so none is served.

The exporters keep their counters in memory, so they restart from zero with the simulator, as the real exporters do. Counters that are hardware counters upstream report the emulated hardware’s own, which continue across a restart: the Kepler 0.12 totals the zones’ energy_uj, the DCGM energy the GPU’s device counter, the AMD GPU energy its ledger.