Skip to content

feat: Metrics service unifying model-performance stats across v11 and v12#1411

Merged
nicolasbisurgi merged 7 commits into
masterfrom
feature/metrics-service-v11-v12
Jun 4, 2026
Merged

feat: Metrics service unifying model-performance stats across v11 and v12#1411
nicolasbisurgi merged 7 commits into
masterfrom
feature/metrics-service-v11-v12

Conversation

@nicolasbisurgi

@nicolasbisurgi nicolasbisurgi commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Adds tm1.metrics, exposing TM1 model-performance statistics with one method per Stats Category that returns the same shape regardless of the underlying TM1 version:

  • by_cube, by_server — gauge categories (long Metric/Value/Unit rows)
  • by_rule — per-rule-line stats (both versions; same }StatsByRule read path)
  • by_client, by_cube_by_client, by_chore, by_process — entity categories (v11 only; raise a clear version error on v12)

How it works

  • v11 (TM1 Server < 12): reads the }Stats* control cubes via MDX/cellset, with LATEST as the default snapshot and an opt-in time_interval for the rolling window.
  • v12 (Planning Analytics Database): reads the Metrics() OData endpoint, plus the cube-bound rule-stats actions start_collecting_rule_stats / stop_collecting_rule_stats / flush_collected_rule_stats.

The v11/v12 branch is hidden inside each method. Canonical metric names follow IBM's v12 vocabulary verbatim; v11 measures map onto them. Units pass through unconverted, with the source NativeName retained for traceability.

flowchart TD
    User(["User code"])

    subgraph API["tm1.metrics — unified by_* methods"]
        Gauge["Gauge: by_cube, by_server"]
        Entity["Entity: by_rule, by_client, by_cube_by_client, by_chore, by_process"]
        Lifecycle["Rule-stats lifecycle: start / stop / flush (v12)"]
        PerfMon["Performance Monitor: start / stop / get state (v11)"]
    end

    Dispatch{"version dispatch (self._is_v12)"}

    subgraph Helpers["Pure helpers in MetricService (server-free)"]
        Vocabulary["vocabulary — v11 to canonical map"]
        MDX["MDX builders — v11 query builder"]
        ODataFilter["filter builder — v12 $filter builder"]
        Shapers["shapers — raw payload to records"]
    end

    V11[("TM1 v11: Stats control cubes (MDX / cellset)")]
    V12[("TM1 v12: Metrics() OData + rule-stats actions")]

    Records["Unified records: list of dict / DataFrame"]

    User --> API
    API --> Dispatch
    Dispatch -->|v11| MDX
    Dispatch -->|v12| ODataFilter
    MDX --> V11
    ODataFilter --> V12
    V11 -->|raw cellset| Shapers
    V12 -->|raw JSON| Shapers
    Vocabulary -.-> Shapers
    Shapers --> Records
    Records --> User
Loading

Usage

Per-cube metrics (gauge-long), unified across versions:

rows = tm1.metrics.by_cube(cube="plan_BudgetPlan")
# [
#   {"Category": "by_cube", "CubeName": "plan_BudgetPlan",
#    "Metric": "cube_memory_used", "NativeName": "Total Memory Used",
#    "Value": 8385536, "Unit": "B", "ReplicaID": 0,
#    "TimeInterval": "LATEST", "Timestamp": None},
#   ...
# ]

The canonical Metric name is stable across versions; NativeName carries the source's own name. Values pass through unconverted — read Unit to interpret them (cube_memory_used is bytes on v11, KB on v12). On v12 the same call adds DatabaseName/DatabaseID and populates Timestamp.

As a filtered DataFrame:

df = tm1.metrics.by_cube_as_dataframe(metrics=["cube_memory_used", "cube_num_fed_cells"])

Server/replica-level metrics — v12 (highly-available) yields one row per replica; v11 is a single replica (ReplicaID=0):

tm1.metrics.by_server(metrics=["replica_memory_used"])

Per-rule timing — unified across versions. }StatsByRule is structurally identical on v11 and v12 (}Cubes × }LineNumber × }RuleStats), so the same read/shape path serves both; only how the cube gets populated differs.

On v11 the Performance Monitor populates it, so ensure it is running, then read:

if not tm1.metrics.get_performance_monitor_state():
    tm1.metrics.start_performance_monitor()
rules = tm1.metrics.by_rule(cube="plan_BudgetPlan")  # v11

On v12 you collect on demand — start, exercise the cube's rules, wait for the ~60s sampling interval, flush — then read the same way (verified live on 12.5.9: this creates/populates }StatsByRule with v11-identical dimensionality):

tm1.metrics.start_collecting_rule_stats("plan_BudgetPlan")
# ... exercise the cube's rules, wait ~60s for the sampling interval ...
tm1.metrics.flush_collected_rule_stats("plan_BudgetPlan")
tm1.metrics.stop_collecting_rule_stats("plan_BudgetPlan")
rules = tm1.metrics.by_rule(cube="plan_BudgetPlan")

Performance Monitor controls (v11 onlyPerformanceMonitorOn is a v11 configuration parameter, so these raise TM1pyVersionDeprecationException on v12). They mirror the existing ServerService controls and delegate to them, so the whole v11 stats workflow is reachable from tm1.metrics:

tm1.metrics.get_performance_monitor_state()   # -> bool
tm1.metrics.start_performance_monitor()        # populates the }Stats* cubes
tm1.metrics.stop_performance_monitor()

v11-only categories (the cubes were removed in v12; these raise TM1pyVersionException on a v12 database):

tm1.metrics.by_process()
tm1.metrics.by_chore()
tm1.metrics.by_client()
tm1.metrics.by_cube_by_client(cube="plan_BudgetPlan")

Every read method has a parallel *_as_dataframe(...) variant. These examples also live in the MetricService docstring, so they render in the auto-generated mkdocs API Reference.

Note on extensibility

New v12 gauges surface automatically: by_cube/by_server keep every Metrics() row matching the cube_*/replica_* prefix and v12 names are already canonical, so no code change is needed when IBM adds a metric. v11 is the asymmetric case — a new }Stats* measure must be added to the vocabulary tables (the MDX only selects known measures, and the normalizer raises on unknowns), because v11 native names are not canonical and must be mapped.

Design

MetricService follows TM1py's single-flat-file service convention (like CellService, CubeService, etc.): a thin service over pure, server-free, module-level helpers in the same file —

  • vocabulary: v11 native measure → canonical Metric/Unit mapping (single source of truth)
  • MDX builders: v11 }Stats* query builders (gauge + entity)
  • $filter builder: v12 Metrics() URL builder (extracted from Introduce Metric service (for v12) #1396's inline logic)
  • shapers: raw v12 JSON / v11 cellset → unified records

The Performance Monitor controls live on ServerService (where the existing start/stop already were, alongside static-config management) and are mirrored as thin delegates on MetricService for workflow ergonomics. The pre-existing ServerService.start/stop_performance_monitor are now version-guarded too (previously unguarded, they would silently attempt the config write on v12).

Testing

44 unit tests (pure helpers, fixture-driven) + integration tests, in a single Tests/MetricService_test.py per TM1py's one-test-file-per-service convention; passing against live v11 (11.8) and v12 (12.5.9) servers. Integration coverage includes the Performance Monitor controls: a v11 on/off round-trip (restoring original state), metrics↔server-service agreement, and the v12 deprecation guard.

Relationship to #1396

Supersedes #1396. That PR's Metrics() $filter logic and service wiring are preserved and extended here; this branch is cut from current master (which #1396 had drifted 10 commits behind). #1396 can be closed once this is reviewed.

🤖 Generated with Claude Code

nicolasbisurgi and others added 2 commits June 3, 2026 11:55
… and v12

Add `tm1.metrics`, exposing TM1 model-performance statistics with one method
per Stats Category (by_cube, by_server, by_rule, by_client, by_cube_by_client,
by_chore, by_process) that returns the same shape regardless of TM1 version.

- v11: reads the }Stats* control cubes via MDX/cellset.
- v12: reads the Metrics() OData endpoint plus the cube-bound rule-stats
  actions (start/stop/flush_collecting_rule_stats).

The v11/v12 branch is hidden inside each method. Gauge categories (by_cube,
by_server) return a long Metric/Value/Unit shape; entity categories return a
wide one-row-per-entity shape. Canonical metric names follow IBM's v12
vocabulary; units pass through unconverted with the source NativeName retained.

Logic lives in deep, server-free modules behind the service: vocabulary
(v11->canonical map), mdx (v11 query builder), odata_filter (v12 $filter,
extracted from #1396), shapers (raw payload -> records). 44 unit + 12
integration tests.

Supersedes #1396 — its Metrics() $filter logic and service wiring are
preserved here. Also adds Utils.datetime_to_iso and a max_version bound on
TM1pyVersionException (for v11-only categories invoked on v12).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the TM1py/Metrics/ sub-package (vocabulary, mdx, odata_filter,
shapers) into TM1py/Services/MetricService.py as module-level helpers, matching
the single-flat-file pattern every other service follows. Consolidate the five
Metric*_test.py files into one Tests/MetricService_test.py per the
one-test-file-per-service convention. Fold usage examples into the service
docstring so they render in the auto-generated mkdocs API Reference. Remove ADR/
PRD references from docstrings and comments.

No behavior change: 44 unit tests pass; black and ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nicolasbisurgi nicolasbisurgi reopened this Jun 3, 2026
@nicolasbisurgi nicolasbisurgi self-assigned this Jun 3, 2026
@nicolasbisurgi nicolasbisurgi added the release:minor Triggers major version bump (e.g.: 1.4.1 → 1.5.0) label Jun 3, 2026
nicolasbisurgi and others added 4 commits June 3, 2026 14:53
Verified live on PA Database 12.5.9: start/stop/flush_collecting_rule_stats
return 204 but flush creates no }StatsByRule cube and exposes no rule-stats
OData entity ($metadata defines only the three lifecycle actions; Metrics()
carries no rule_* names). The previous claim that flush 'creates }StatsByRule
on demand' was false, and by_rule on v12 silently returned [] forever.

by_rule now raises NotImplementedError on v12 with a clear message; the v11
Performance-Monitor path is unchanged. Corrected the flush/by_rule docstrings
and the class usage example; split the lifecycle/by_rule integration tests to
assert the real behavior (lifecycle 204s, no cube created, by_rule raises).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Corrects the prior commit. On PA Database 12.5.9, flush_collected_rule_stats
DOES create/populate }StatsByRule — verified live: dims }Cubes x }LineNumber x
}RuleStats (identical to v11), and the existing v11 cellset read/shape path
returns correct records on v12. The cube appears only after the full sequence:
start_collecting_rule_stats -> change/exercise the rules -> wait ~60s sampling
interval -> flush_collected_rule_stats. My earlier probes skipped the rule
activity and the wait, so the cube hadn't materialized.

Restores the unified by_rule (read on both versions; [] + warning when the cube
is absent, with a v12-specific hint describing the collection sequence) and the
accurate flush docstring. Tests assert by_rule returns the list/shape on both
versions and the lifecycle returns 204s (cleanup only deletes }StatsByRule if
the test itself created it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the Performance Monitor on/off/read controls onto MetricService,
delegating to ServerService, since the v11 }Stats* read paths depend on
the monitor running.

- ServerService: add get_performance_monitor_state(); guard start/stop/get
  with @deprecated_in_version("12.0.0") since PerformanceMonitorOn is a
  v11-only config parameter (previously start/stop silently ran on v12)
- MetricService: thin start_/stop_/get_performance_monitor_state delegates,
  same v12 guard; update by_rule hint + class docstring example
- Tests: v11 toggle round-trip, metrics<->server agreement, v12 raises

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s, tests)

- datetime_to_iso: convert tz-aware datetimes to UTC and preserve sub-second
  precision to milliseconds (was hard-coded .000Z, ignored tzinfo)
- MetricService: wire v12-only `since` Timestamp filter into by_cube/by_server
  (rejected on v11, symmetric to time_interval on v12)
- drop misleading required_version=V12 from v11-only version errors
  (max_version carries the meaning); document the deliberate split between
  version-gated reads (TM1pyVersionException) and deprecated perfmon mutators
- warn on truncated v11 cellsets (len(Cells) < axis product) instead of
  silently reading missing cells as None
- add context to bare KeyErrors (unknown measure / missing measure dimension)
- tests: mocked-REST dispatch + metrics= filtering + by_rule missing-cube
  warning (G1-G3), entity shapers for by_process/by_chore/by_client (G4),
  and datetime_to_iso tz/precision coverage

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Tests completed for environment: tm1-12-cloud. Check artifacts for details.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Tests completed for environment: tm1-11-cloud. Check artifacts for details.

@nicolasbisurgi nicolasbisurgi merged commit a067e31 into master Jun 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:minor Triggers major version bump (e.g.: 1.4.1 → 1.5.0)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant