8.x Add Micrometer Observation instrumentation for GSP view rendering#15718
8.x Add Micrometer Observation instrumentation for GSP view rendering#15718codeconsole wants to merge 13 commits into
Conversation
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.
…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 Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ 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 🚀 New features to boost your workflow:
|
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
There was a problem hiding this comment.
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.compileand recordsgsp.template.cachehit/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.
…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).
✅ All tests passed ✅🏷️ Commit: 621c74a Learn more about TestLens at testlens.app. |
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.cachecounter (taggedcache=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 nameGroovyPageObservationDocumentation— documents thegsp.*observations, witherroras the only low-cardinality (metric) key andgsp.nameas a high-cardinality (span-only) keyGroovyPageObservationConvention+DefaultGroovyPageObservationConvention— name / contextualName / KeyValues; customizable either by registering a convention on theObservationRegistryor via the per-component setterGSP_*.observation(custom, default, ctx, registry)with anObservationRegistry.isNoop()fast-path; the explicit default convention carries the per-stage nameGroovyPageViewResolver,GrailsLayoutViewResolver) resolve theObservationRegistry(andMeterRegistryfor cache counters) from the application context, falling back to NOOP, and set them on each viewGroovyPageCacheMetricsrecords hit/miss using an actual "had to build the entry" signal (theCacheEntryupdater flag /entry == null), not acontainsKeyheuristic, so reloadable/expired entries are counted as missesDependency
Adds
io.micrometer:micrometer-core—implementationingrails-gsp/coreandapiingrails-web-gsp(theMeterRegistry/Countertypes in thegsp.cachecounters and thesetMeterRegistry(...)ABI come frommicrometer-core, not frommicrometer-observation). The observation timers/spans themselves need onlymicrometer-observation, which is already transitively present via Spring. Overhead is zero when noObservationRegistry/MeterRegistrybean 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, ornone) is the only metric tag.Tests
GroovyPageViewObservationSpec—gsp.viewrecorded name/tags, NOOP no-op path, error key on a failed renderGroovyPagesTemplateRendererObservationSpec—gsp.templateEmbeddedGrailsLayoutViewObservationSpec—gsp.layoutGroovyPagesTemplateEngineObservationSpec—gsp.compileandgsp.cachehit/miss