Skip to content

Redesign of metric labels - secondary queries (lazy|query) now just u…#3779

Merged
rbygrave merged 4 commits into
masterfrom
feature/metric-labels-redesign
Jun 8, 2026
Merged

Redesign of metric labels - secondary queries (lazy|query) now just u…#3779
rbygrave merged 4 commits into
masterfrom
feature/metric-labels-redesign

Conversation

@rob-bygrave

@rob-bygrave rob-bygrave commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

…se parent + relativePath + type

Query metric/plan label change — comparison

Improves the metric/plan name generated for secondary (_lazy / _query) loads so
they relate to their parent/root query, and unifies separators on ..

The secondary lazy name is now always orm.<parent's full name>.<path>.<loadMode>,
so it always prefixes the real parent metric.

Root query name

Root query source Original New
setLabel("custMain") orm.Customer_custMain orm.Customer.custMain
ProfileLocation CustomerFinder.byName orm.CustomerFinder.byName orm.CustomerFinder.byName (same)
ProfileLocation DataLoader.loadAll (Customer query) orm.Customer_DataLoader.loadAll orm.DataLoader.loadAll
Unlabeled, no location orm.Customer.findList orm.Customer.findList (same)

An explicit setLabel(..) is a short, ambiguous token so it is prefixed with the bean
type (Customer.custMain) for disambiguation. A profile location is already a unique,
type-independent identifier (class + method) so it is used as-is — no bean type prefix.
The bean/entity type is still available downstream as the avaje-metrics type tag.

Secondary lazy (contacts) name

Original prefixes the loaded type (Contact) + the call-site location and uses __
between path and load mode. New prefixes the parent's full name.

Root query source Original lazy name New lazy name
setLabel("custMain") (profile location also present) orm.Contact_CustomerFinder.findActive_contacts__lazy — explicit label lost orm.Customer.custMain.contacts.lazy
setLabel("custMain") (no profile location) orm.Contact_custMain_contacts__lazy orm.Customer.custMain.contacts.lazy
ProfileLocation CustomerFinder.byName orm.Contact_CustomerFinder.byName_contacts__lazy orm.CustomerFinder.byName.contacts.lazy
ProfileLocation DataLoader.loadAll orm.Contact_DataLoader.loadAll_contacts__lazy orm.DataLoader.loadAll.contacts.lazy
Unlabeled, no location orm.Contact.findList (own name; no parent path) orm.Contact.findList (same)

Problems fixed

  • (A) Secondary load didn't relate to its parent — original prefixed the loaded type
    (Contact) + the call-site location, never the parent query's name. New name literally
    starts with the parent's full name.
  • Explicit setLabel was silently dropped for secondary queries when a profile location
    existed.
  • __ path/loadMode separator and mixed _/. replaced by uniform ..
  • A profile-location root and its secondary loads now use a consistent name (and inline
    SQL comment) — neither is bean-type prefixed, so the secondary is an exact extension of the
    root (e.g. root QOrderTest.run → secondary QOrderTest.run.customer.query).

Nested and .query secondary loads

Each secondary query name is <immediate parent's full name>.<immediate path>.<loadMode>,
so every hop literally prefixes its parent metric. Example with root setLabel("custMain")
on Customer, chain Customer -> orders -> details:

Nested lazy:

orm.Customer.custMain
orm.Customer.custMain.orders.lazy
orm.Customer.custMain.orders.lazy.details.lazy

Secondary eager .query fetch:

orm.Customer.custMain
orm.Customer.custMain.orders.query
orm.Customer.custMain.orders.query.details.query

The intermediate load mode (.lazy. / .query.) is retained so each name is an exact
extension of its immediate parent's name.

avaje-metrics: bean type moved to a tag

Because the new ebean names no longer prefix the queried bean type, the entity is now
surfaced as an avaje-metrics tag instead. ebean.query metrics now carry:

ebean.query{kind=orm|dto|sql, type=<BeanSimpleName>, label=<ebean label>}

The previous type tag (which held the category orm/dto/sql) was renamed to kind,
freeing type to carry the entity name. type is omitted when the bean type is unknown.

This tag is what disambiguates two different-entity queries that happen to share a
profile-location label (e.g. two DB.find(..) calls of different types in one method with
a fixed ProfileLocation). It does so independently of source line numbers, which are not
always present (fetch-group secondary loads and ProfileLocation.create("fixed") have none).

DatabaseMetricSupplier caches the derived Metric.ID by name + bean type (not name
alone), so two plans that share a name but differ by type map to distinct ids/tags rather
than the first one's cached id being reused for the second.

The same applies to a ProfileLocation root (no explicit setLabel). Example with the
finder CustomerFinder.byNameStatus on Customer, chain Customer -> orders -> details:

Nested lazy:

orm.CustomerFinder.byNameStatus
orm.CustomerFinder.byNameStatus.orders.lazy
orm.CustomerFinder.byNameStatus.orders.lazy.details.lazy

Secondary eager .query fetch:

orm.CustomerFinder.byNameStatusQueryFetch
orm.CustomerFinder.byNameStatusQueryFetch.orders.query
orm.CustomerFinder.byNameStatusQueryFetch.orders.query.details.query

…se parent + relativePath + type

# Query metric/plan label change — comparison

Improves the metric/plan name generated for **secondary** (`_lazy` / `_query`) loads so
they relate to their parent/root query, and unifies separators on `.`.

The secondary lazy name is now **always** `orm.<parent's full name>.<path>.<loadMode>`,
so it always prefixes the real parent metric.

## Root query name

| Root query source | Original | New |
|---|---|---|
| `setLabel("custMain")` | `orm.Customer_custMain` | `orm.Customer.custMain` |
| ProfileLocation `CustomerFinder.byName` | `orm.CustomerFinder.byName` | `orm.CustomerFinder.byName` *(same)* |
| ProfileLocation `DataLoader.loadAll` (Customer query) | `orm.Customer_DataLoader.loadAll` | `orm.Customer.DataLoader.loadAll` |
| Unlabeled, no location | `orm.Customer.findList` | `orm.Customer.findList` *(same)* |

## Secondary lazy (`contacts`) name

Original prefixes the **loaded** type (`Contact`) + the call-site location and uses `__`
between path and load mode. New prefixes the **parent's full name**.

| Root query source | Original lazy name | New lazy name |
|---|---|---|
| `setLabel("custMain")` *(profile location also present)* | `orm.Contact_CustomerFinder.findActive_contacts__lazy` — explicit label **lost** | `orm.Customer.custMain.contacts.lazy` |
| `setLabel("custMain")` *(no profile location)* | `orm.Contact_custMain_contacts__lazy` | `orm.Customer.custMain.contacts.lazy` |
| ProfileLocation `CustomerFinder.byName` | `orm.Contact_CustomerFinder.byName_contacts__lazy` | `orm.CustomerFinder.byName.contacts.lazy` |
| ProfileLocation `DataLoader.loadAll` | `orm.Contact_DataLoader.loadAll_contacts__lazy` | `orm.Customer.DataLoader.loadAll.contacts.lazy` |
| Unlabeled, no location | `orm.Contact.findList` *(own name; no parent path)* | `orm.Contact.findList` *(same)* |

## Problems fixed
- **(A) Secondary load didn't relate to its parent** — original prefixed the *loaded* type
  (`Contact`) + the *call-site* location, never the parent query's name. New name literally
  starts with the parent's full name.
- Explicit `setLabel` was **silently dropped** for secondary queries when a profile location
  existed.
- `__` path/loadMode separator and mixed `_`/`.` replaced by uniform `.`.

## Nested and `.query` secondary loads

Each secondary query name is `<immediate parent's full name>.<immediate path>.<loadMode>`,
so every hop literally prefixes its parent metric. Example with root `setLabel("custMain")`
on `Customer`, chain `Customer -> orders -> details`:

Nested lazy:
```
orm.Customer.custMain
orm.Customer.custMain.orders.lazy
orm.Customer.custMain.orders.lazy.details.lazy
```

Secondary eager `.query` fetch:
```
orm.Customer.custMain
orm.Customer.custMain.orders.query
orm.Customer.custMain.orders.query.details.query
```

The intermediate load mode (`.lazy.` / `.query.`) is retained so each name is an exact
extension of its immediate parent's name.
@rob-bygrave rob-bygrave self-assigned this Jun 8, 2026
@rob-bygrave

Copy link
Copy Markdown
Contributor Author

Hi @rPraml ... this is a breaking change wrt "metric names" that ebean is using ... but, I really think we need to make this change.

The main downside is that for secondary queries (lazy/query) the query type of that is no longer in the metric name. However, the type is still part of the MetaQueryMetric API. I am also making changes in the avaje metrics lib that is targeting reportering that uses "Tags". That is, StatsD Tags and Open Telemetry Attributes ... so in this world this we get the bean type from that "type" tag or attribute and the name is more clear as to how it relates to parent metrics.

Hopefully this is ok, but yeah I can also understand that this change could also be a pain wrt a change with lots of existing production metrics etc.

@rbygrave rbygrave added this to the 16.9.0 milestone Jun 8, 2026
@rbygrave rbygrave merged commit 1408696 into master Jun 8, 2026
1 check passed
@rbygrave rbygrave deleted the feature/metric-labels-redesign branch June 8, 2026 07:03
rob-bygrave added a commit that referenced this pull request Jun 18, 2026
…ry-comment

Regression introduced by #3779 in sql inline comment for label (missing bean type prefix)
pull Bot pushed a commit to Mu-L/ebean that referenced this pull request Jun 18, 2026
…el (missing bean type prefix)

So when we used to get an inline comment like:
```sql
select /* Customer.hiLabel */ ...
```
We started to instead have (missing bean type):
```sql
select /* hiLabel */ ...
```
This fixes that regression that was introduced in ebean-orm#3779
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants