Skip to content

8.x Add Micrometer Observation instrumentation for GSP view rendering#15718

Open
codeconsole wants to merge 13 commits into
apache:8.0.xfrom
codeconsole:feat/gsp-rendering-observability
Open

8.x Add Micrometer Observation instrumentation for GSP view rendering#15718
codeconsole wants to merge 13 commits into
apache:8.0.xfrom
codeconsole:feat/gsp-rendering-observability

Conversation

@codeconsole

@codeconsole codeconsole commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

What

Adds Micrometer Observation instrumentation across the GSP rendering pipeline. Each stage runs inside its own Observation, so it produces a timer metric and — under a tracing bridge — a span nested in the request trace, answering "how much of a request is spent rendering GSP, and where":

  • gsp.view — top-level GSP view render (GroovyPageView)
  • gsp.template — included <g:render template="..."/> (GroovyPagesTemplateRenderer)
  • gsp.layout — SiteMesh layout decoration (EmbeddedGrailsLayoutView)
  • gsp.compile — runtime GSP compilation (GroovyPagesTemplateEngine), which only fires on a real compile (dev / non-precompiled views)

It also adds a gsp.cache counter (tagged cache=template|view, result=hit|miss) for the caches actually consulted on a deployed request path (GroovyPagesTemplateRenderer, GroovyPageViewResolver).

Why

GSP rendering (and SiteMesh layout) is often a large, currently-invisible slice of request latency. Spring instruments the HTTP request (http.server.requests) but nothing surfaces the view-render portion. This adds it at the framework level, with enough granularity to attribute time to view vs. template vs. layout, and to distinguish compile/cache-miss cost from steady-state rendering.

How

Follows the Micrometer/Spring instrumentation pattern (mirrors ServerHttpObservation*):

  • GroovyPageObservationContext — carries the rendered resource name
  • GroovyPageObservationDocumentation — documents the gsp.* observations, with error as the only low-cardinality (metric) key and gsp.name as a high-cardinality (span-only) key
  • GroovyPageObservationConvention + DefaultGroovyPageObservationConvention — name / contextualName / KeyValues; customizable either by registering a convention on the ObservationRegistry or via the per-component setter
  • Each instrumentation site wraps rendering via GSP_*.observation(custom, default, ctx, registry) with an ObservationRegistry.isNoop() fast-path; the explicit default convention carries the per-stage name
  • Resolvers (GroovyPageViewResolver, GrailsLayoutViewResolver) resolve the ObservationRegistry (and MeterRegistry for cache counters) from the application context, falling back to NOOP, and set them on each view
  • GroovyPageCacheMetrics records hit/miss using an actual "had to build the entry" signal (the CacheEntry updater flag / entry == null), not a containsKey heuristic, so reloadable/expired entries are counted as misses

Dependency

Adds io.micrometer:micrometer-coreimplementation in grails-gsp/core and api in grails-web-gsp (the MeterRegistry/Counter types in the gsp.cache counters and the setMeterRegistry(...) ABI come from micrometer-core, not from micrometer-observation). The observation timers/spans themselves need only micrometer-observation, which is already transitively present via Spring. Overhead is zero when no ObservationRegistry/MeterRegistry bean is present (NOOP fast-path).

Cardinality

gsp.name (the rendered resource path) is high-cardinality: attached to the span for drilldown but kept off the timer, because a real app has many distinct GSPs and tagging the metric per-resource would explode the time-series count. error (exception simple name, or none) is the only metric tag.

Tests

  • GroovyPageViewObservationSpecgsp.view recorded name/tags, NOOP no-op path, error key on a failed render
  • GroovyPagesTemplateRendererObservationSpecgsp.template
  • EmbeddedGrailsLayoutViewObservationSpecgsp.layout
  • GroovyPagesTemplateEngineObservationSpecgsp.compile and gsp.cache hit/miss

Wraps GroovyPageView rendering in a 'gsp.view' Observation, so each GSP page
render becomes a timer (and, under a tracing bridge, a span nested in the request
trace) — answering how much of a request is spent rendering the GSP.

Follows the Micrometer/Spring instrumentation pattern:
- GroovyPageObservationContext carries the view URI
- GroovyPageObservationDocumentation documents the gsp.view observation and its
  low-cardinality keys (gsp.view, error)
- GroovyPageObservationConvention + DefaultGroovyPageObservationConvention build the
  name / contextualName / KeyValues, and are overridable
- GroovyPageView wraps renderTemplate via GSP_VIEW.observation(...) with a NOOP
  fast-path; GroovyPageViewResolver resolves the ObservationRegistry from the
  application context (NOOP fallback) and sets it on each view

GroovyPageViewObservationSpec verifies the recorded observation name/tags, the
NOOP no-op path, and the error key on a failed render.

No new runtime dependency — micrometer-observation is already on the classpath
(Spring 6); zero overhead when no ObservationRegistry bean is present.
@codeconsole codeconsole marked this pull request as draft June 5, 2026 08:18
…mplate)

Generalizes the observation kit so the view, included templates, and layouts share
one Context/Convention/Documentation: GroovyPageObservationContext now carries a
generic resource name; the documentation enum gains GSP_TEMPLATE and GSP_LAYOUT and
uses uniform low-cardinality keys (gsp.name, error); DefaultGroovyPageObservationConvention
is parameterized by observation name.

Instruments GroovyPagesTemplateRenderer.render with a 'gsp.template' observation
(observeChecked, since render throws IOException), tagged with the template name. The
ObservationRegistry is @Autowired (required=false), defaulting to NOOP with a fast-path.

gsp.view keeps working (now tagged gsp.name); GSP_LAYOUT is declared for the follow-up.
Wraps the SiteMesh layout decoration (EmbeddedGrailsLayoutView -> decorator.render)
in a 'gsp.layout' observation tagged with the layout page name, using the shared
GSP observation kit. Only the decoration is wrapped (not obtainContent), so it does
not double-count the inner gsp.view render — the two appear as sibling phases under
the request trace.

The ObservationRegistry is resolved in GrailsLayoutViewResolver from the refreshed
application context and set on each GrailsLayoutView (NOOP fallback / fast-path).
…on (gsp.compile)

Moves the observation kit (Context/Convention/Documentation) from grails-web-gsp down
to grails-gsp-core so the template engine — which lives in core, below the web layer,
and which core cannot import upward from web-gsp — can use it too. View/template/layout
importers are repointed to the new org.grails.gsp.observation package.

Instruments GroovyPagesTemplateEngine.buildPageMetaInfo with a 'gsp.compile' observation
(observeChecked). Compilation only happens on a template cache miss, so the observation's
count is the compile/miss rate and its timer is the compile latency; tagged with the GSP
page name. The ObservationRegistry is resolved from the application context (NOOP fallback).
@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.0000%. Comparing base (098660a) to head (36a8101).
⚠️ Report is 2 commits behind head on 8.0.x.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                @@
##                8.0.x   #15718         +/-   ##
=================================================
- Coverage     48.3729%        0   -48.3729%     
=================================================
  Files            1870        0       -1870     
  Lines           85457        0      -85457     
  Branches        14900        0      -14900     
=================================================
- Hits            41338        0      -41338     
+ Misses          37784        0      -37784     
+ Partials         6335        0       -6335     

see 1870 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Adds gsp.template.cache{result=hit|miss} counters to GroovyPagesTemplateEngine,
incremented per cacheable createTemplate() call based on whether the compiled-page
cache already held the entry — giving the cache hit ratio alongside the gsp.compile
(cache-miss) timer.

Counters are meter-only (unlike observations), so this resolves a MeterRegistry from
the application context and no-ops when absent. Adds micrometer-core as a BOM-managed
implementation dependency of grails-gsp-core; it is runtime-present in every Spring Boot
app (and runtime-transitive to consumers), so there is no class-load risk.
GroovyPagesTemplateEngineObservationSpec overrides the real GSP compilation with a stub
and wires a recording ObservationRegistry + a SimpleMeterRegistry, then asserts:
- compiling a page records a 'gsp.compile' observation tagged with the page name
- two cacheable requests for the same page produce one miss + one hit (and exactly one
  compile), exercising the gsp.template.cache counters

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces Micrometer Observation-based instrumentation around Groovy Server Pages (GSP) rendering and compilation so GSP activity can be measured (timers) and traced (spans when a tracing bridge is present), and it adds cache hit/miss counters for the compiled-template cache.

Changes:

  • Wraps GSP view rendering, template rendering, and SiteMesh layout decoration in Observations (gsp.view, gsp.template, gsp.layout).
  • Instruments GSP compilation as gsp.compile and records gsp.template.cache hit/miss counters.
  • Adds new observation support types (context, convention, documentation) and unit specs covering the behavior.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
grails-gsp/grails-web-gsp/src/test/groovy/org/grails/web/servlet/view/GroovyPageViewObservationSpec.groovy Adds unit coverage for gsp.view observation behavior (success, NOOP, error tagging).
grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/servlet/view/GroovyPageViewResolver.java Propagates an ObservationRegistry (and convention hook) into constructed GroovyPageView instances.
grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/servlet/view/GroovyPageView.java Wraps view rendering in a gsp.view observation with a NOOP fast-path.
grails-gsp/grails-web-gsp/src/main/groovy/org/grails/web/gsp/GroovyPagesTemplateRenderer.java Wraps template rendering in a gsp.template observation with a NOOP fast-path.
grails-gsp/grails-layout/src/main/groovy/org/apache/grails/web/layout/GrailsLayoutViewResolver.java Resolves and propagates an ObservationRegistry into layout views.
grails-gsp/grails-layout/src/main/groovy/org/apache/grails/web/layout/EmbeddedGrailsLayoutView.java Wraps layout decoration in a gsp.layout observation with a NOOP fast-path.
grails-gsp/core/src/test/groovy/org/grails/gsp/GroovyPagesTemplateEngineObservationSpec.groovy Adds unit coverage for gsp.compile observation and cache hit/miss counters.
grails-gsp/core/src/main/groovy/org/grails/gsp/observation/GroovyPageObservationDocumentation.java Defines the documented observation set and common low-cardinality key names.
grails-gsp/core/src/main/groovy/org/grails/gsp/observation/GroovyPageObservationConvention.java Defines the convention interface for GSP observation customization.
grails-gsp/core/src/main/groovy/org/grails/gsp/observation/GroovyPageObservationContext.java Adds a context type carrying the rendered resource name.
grails-gsp/core/src/main/groovy/org/grails/gsp/observation/DefaultGroovyPageObservationConvention.java Implements the default naming + low-cardinality tags (gsp.name, error).
grails-gsp/core/src/main/groovy/org/grails/gsp/GroovyPagesTemplateEngine.java Adds gsp.compile observation and cache hit/miss counters to compilation/cache paths.
grails-gsp/core/build.gradle Adds micrometer-core dependency to support counters/meters in core.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread grails-gsp/core/build.gradle
…miss

- DefaultGroovyPageObservationConvention gets a no-arg constructor (name 'gsp') so it
  honors ObservationDocumentation.getDefaultConvention() reflectively; instrumentation
  sites still pass an explicitly-named instance.
- GSP template cache hit/miss is now counted from whether a compile actually ran (a
  ThreadLocal flag set in buildPageMetaInfo) instead of pageCache.containsKey(), which
  mis-counted expired/reloaded entries as hits and could race on cold start.
…stable

- GrailsLayoutViewResolver now propagates a custom GroovyPageObservationConvention
  to the GrailsLayoutView it builds (previously the field on the view had no setter,
  so a configured convention was silently dropped for gsp.layout observations).
- EmbeddedGrailsLayoutView gains setObservationConvention to receive it.
- GroovyPagesTemplateRenderer.doRender is now protected so it can be overridden in
  tests, isolating the gsp.template observation wrapper from the render pipeline.
- Add GroovyPagesTemplateRendererObservationSpec covering the success, NOOP, and
  error paths (mirrors GroovyPageViewObservationSpec).
…ut test

Review follow-ups:

- GroovyPagesTemplateEngine: replace the thread-local 'compiledOnThread' flag
  (used to tell a cache hit from a recompile) with a per-call PageCompileRequest
  carried through CacheEntry's cacheRequestObject. The thread-local mis-counted
  when template creation re-entered on the same thread (an inner cache hit reset
  the flag, so the outer lookup that actually compiled was recorded as a hit), and
  it also lingered on pooled request threads (never removed). The per-call holder
  is reentrancy-safe and leak-free; updateValue (the only cacheable compile path)
  sets compiled=true. Add a regression test for the reentrant-hit case.

- EmbeddedGrailsLayoutView.renderWithLayout is now protected so the gsp.layout
  observation can be unit-tested in isolation. Add EmbeddedGrailsLayoutViewObservationSpec
  (success / NOOP / error paths), mirroring the view and template renderer specs.
  Add byte-buddy to the layout module's test runtime so Spock can mock concrete
  collaborators (consistent with grails-web-gsp/grails-sitemesh3).
…; gsp.name high-cardinality

The gsp.template.cache hit/miss counter lived on GroovyPagesTemplateEngine's runtime-compile cache, which is development-only: a precompiled production deployment serves GSPs from AOT-compiled classes and bypasses that cache, so it can never register a hit there (and the higher layers intercept any second lookup). Move hit/miss to the caches actually consulted on the request path in a deployed app -- GroovyPagesTemplateRenderer.templateCache (cache=template) and GroovyPageViewResolver.viewCache (cache=view) -- as the gsp.cache{cache,result} counter (new GroovyPageCacheMetrics helper).

Make gsp.name high-cardinality (span attribute) instead of low-cardinality (metric tag): on an app with many GSPs it exploded the render-timer series count. error remains the only low-cardinality tag.

gsp.view/gsp.template/gsp.layout render timers are unchanged. gsp.compile is kept and documented as a dev/cold-start signal -- its presence on a precompiled production deployment flags a view that is not precompiled. Adds api dependency io.micrometer:micrometer-core to grails-web-gsp (MeterRegistry now appears in its setMeterRegistry ABI).
@codeconsole codeconsole marked this pull request as ready for review June 7, 2026 15:35
@codeconsole codeconsole changed the title Add Micrometer Observation instrumentation for GSP view rendering 8.x Add Micrometer Observation instrumentation for GSP view rendering Jun 12, 2026
@testlens-app

testlens-app Bot commented Jun 16, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 621c74a
▶️ Tests: 22709 executed
⚪️ Checks: 43/43 completed


Learn more about TestLens at testlens.app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants