Skip to content

Add Micrometer Observation instrumentation for GSP view rendering#15718

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

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

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

What

Adds Micrometer Observation instrumentation for GSP view rendering. Each GSP page render runs inside a gsp.view 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 the GSP".

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.

How

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

  • GroovyPageObservationContext — carries the view URI
  • GroovyPageObservationDocumentation — documents the gsp.view observation + low-cardinality keys (gsp.view, error)
  • GroovyPageObservationConvention + DefaultGroovyPageObservationConvention — name / contextualName / KeyValues, overridable via a custom bean
  • GroovyPageView.renderTemplate wraps rendering via GSP_VIEW.observation(...) with an ObservationRegistry.isNoop() fast-path
  • GroovyPageViewResolver resolves the ObservationRegistry from the application context (NOOP fallback) and sets it on each view

Notes

  • No new runtime dependencymicrometer-observation is already on the classpath (Spring 6). Zero overhead when no ObservationRegistry bean is present (NOOP fast-path).
  • View URIs are bounded, so they are used as a low-cardinality key.
  • Covered by GroovyPageViewObservationSpec (recorded name/tags, NOOP no-op path, error key on a failed render).

Follow-ups (same pattern, not in this PR)

gsp.template (GroovyPagesTemplateRenderer), gsp.layout (GrailsLayoutView), and gsp.compile + template-cache hit/miss (GroovyPagesTemplateEngine).

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 on lines +58 to +61
@Override
public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
return DefaultGroovyPageObservationConvention.class;
}
Comment on lines +58 to +60
private static final GroovyPageObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultGroovyPageObservationConvention("gsp.layout");
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
private GroovyPageObservationConvention observationConvention;
Comment on lines +269 to +273
* Sets a custom {@link GroovyPageObservationConvention} applied to GSP view observations.
*/
public void setObservationConvention(GroovyPageObservationConvention observationConvention) {
this.observationConvention = observationConvention;
}
Comment on lines 314 to 318
if (cacheable) {
recordCacheAccess(pageCache.containsKey(pageName));
meta = CacheEntry.getValue(pageCache, pageName, -1, null,
new GroovyPagesTemplateEngineCallable(new GroovyPagesTemplateEngineCacheEntry(pageName)),
true, resource);
Comment on lines +47 to +48
// GSP rendering/compilation Micrometer instrumentation (gsp.view/template/layout/compile + cache counters)
implementation 'io.micrometer:micrometer-core'
…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).
@testlens-app

testlens-app Bot commented Jun 7, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 36a8101
▶️ Tests: 9365 executed
⚪️ Checks: 43/43 completed


Learn more about TestLens at testlens.app.

@codeconsole codeconsole marked this pull request as ready for review June 7, 2026 15:35
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