-
-
Notifications
You must be signed in to change notification settings - Fork 974
8.x Add Micrometer Observation instrumentation for GSP view rendering #15718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
codeconsole
wants to merge
13
commits into
apache:8.0.x
Choose a base branch
from
codeconsole:feat/gsp-rendering-observability
base: 8.0.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
f1c5ea3
Add Micrometer Observation instrumentation for GSP view rendering
codeconsole 1708db9
Generalize GSP observation kit; instrument template rendering (gsp.te…
codeconsole 497d2ed
Instrument GSP layout decoration (gsp.layout)
codeconsole 6a00f99
Relocate GSP observation kit to grails-gsp-core; instrument compilati…
codeconsole b52b856
Add GSP template-cache hit/miss metrics (gsp.template.cache)
codeconsole 82606ec
Test gsp.compile observation and gsp.template.cache hit/miss
codeconsole e911790
Address review: instantiable default convention + accurate cache hit/…
codeconsole 2f77a8e
Wire layout observation convention through; make template renderer te…
codeconsole 36f657c
Fix GSP cache hit/miss accuracy under reentrant compilation; add layo…
codeconsole eb8767d
Note the intentional benign race in GroovyPageViewResolver registry r…
codeconsole be09b48
Fix checkstyle import order and operator wrap violations in GSP obser…
codeconsole 36a8101
GSP observability: track production caches, not the dev compile cache…
codeconsole 621c74a
Merge branch '8.0.x' into feat/gsp-rendering-observability
codeconsole File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
...re/src/main/groovy/org/grails/gsp/observation/DefaultGroovyPageObservationConvention.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| package org.grails.gsp.observation; | ||
|
|
||
| import io.micrometer.common.KeyValue; | ||
| import io.micrometer.common.KeyValues; | ||
|
|
||
| import static org.grails.gsp.observation.GroovyPageObservationDocumentation.HighCardinalityKeyNames; | ||
| import static org.grails.gsp.observation.GroovyPageObservationDocumentation.LowCardinalityKeyNames; | ||
|
|
||
| /** | ||
| * Default {@link GroovyPageObservationConvention}. | ||
| * | ||
| * <p>Names the observation as configured ({@code gsp.view} / {@code gsp.template} / {@code gsp.layout}), | ||
| * attaches {@code error} (exception simple name, or {@code "none"}) as the only <em>low-cardinality</em> | ||
| * key value (the only one that becomes a metric tag), and {@code gsp.name} (the rendered resource) as a | ||
| * <em>high-cardinality</em> key value, so it appears on the span/trace but does not explode the metric's | ||
| * tag set on applications with many GSPs.</p> | ||
| * | ||
| * @author Grails | ||
| * @since 8.0 | ||
| */ | ||
| public class DefaultGroovyPageObservationConvention implements GroovyPageObservationConvention { | ||
|
|
||
| private static final KeyValue ERROR_NONE = LowCardinalityKeyNames.ERROR.withValue("none"); | ||
|
|
||
| private static final String UNKNOWN = "unknown"; | ||
|
|
||
| private static final String DEFAULT_NAME = "gsp"; | ||
|
|
||
| private final String name; | ||
|
|
||
| /** | ||
| * Creates a convention with the generic {@code "gsp"} name. Exists so this class can satisfy | ||
| * {@link io.micrometer.observation.docs.ObservationDocumentation#getDefaultConvention()} via | ||
| * reflection; instrumentation sites always pass an explicitly-named instance. | ||
| */ | ||
| public DefaultGroovyPageObservationConvention() { | ||
| this(DEFAULT_NAME); | ||
| } | ||
|
|
||
| /** | ||
| * @param name the observation name (e.g. {@code gsp.view}, {@code gsp.template}, {@code gsp.layout}) | ||
| */ | ||
| public DefaultGroovyPageObservationConvention(String name) { | ||
| this.name = name; | ||
| } | ||
|
|
||
| @Override | ||
| public String getName() { | ||
| return this.name; | ||
| } | ||
|
|
||
| @Override | ||
| public String getContextualName(GroovyPageObservationContext context) { | ||
| return this.name + " " + resource(context); | ||
| } | ||
|
|
||
| @Override | ||
| public KeyValues getLowCardinalityKeyValues(GroovyPageObservationContext context) { | ||
| return KeyValues.of(error(context)); | ||
| } | ||
|
|
||
| @Override | ||
| public KeyValues getHighCardinalityKeyValues(GroovyPageObservationContext context) { | ||
| return KeyValues.of(name(context)); | ||
| } | ||
|
|
||
| protected KeyValue name(GroovyPageObservationContext context) { | ||
| return HighCardinalityKeyNames.NAME.withValue(resource(context)); | ||
| } | ||
|
|
||
| protected KeyValue error(GroovyPageObservationContext context) { | ||
| Throwable error = context.getError(); | ||
| return (error != null) ? | ||
| LowCardinalityKeyNames.ERROR.withValue(error.getClass().getSimpleName()) : | ||
| ERROR_NONE; | ||
| } | ||
|
|
||
| private static String resource(GroovyPageObservationContext context) { | ||
| String resource = context.getResource(); | ||
| return (resource != null && !resource.isEmpty()) ? resource : UNKNOWN; | ||
| } | ||
| } |
88 changes: 88 additions & 0 deletions
88
grails-gsp/core/src/main/groovy/org/grails/gsp/observation/GroovyPageCacheMetrics.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| package org.grails.gsp.observation; | ||
|
|
||
| import io.micrometer.core.instrument.Counter; | ||
| import io.micrometer.core.instrument.MeterRegistry; | ||
|
|
||
| /** | ||
| * Records hit/miss for a GSP cache as the {@code gsp.cache} counter, tagged {@code cache} | ||
| * (which cache: {@code template} or {@code view}) and {@code result} ({@code hit} / {@code miss}). | ||
| * | ||
| * <p>This deliberately instruments the caches that are actually consulted on the request path of a | ||
| * <em>deployed</em> (non-development) application — the {@code <g:render>} template cache | ||
| * ({@code GroovyPagesTemplateRenderer}) and the view-resolver cache ({@code GroovyPageViewResolver}) | ||
| * — rather than the {@code GroovyPagesTemplateEngine}'s runtime <em>compile</em> cache. A | ||
| * production deployment serves GSPs precompiled, which bypasses the engine's compile cache entirely | ||
| * (and the layers above intercept any second lookup), so the engine cache can never register a hit | ||
| * in production and is therefore not a useful operational signal there.</p> | ||
| * | ||
| * <p>Note these caches do not expire by default ({@code cacheTimeout == -1}), so a steady-state | ||
| * <em>ratio</em> sits at ~100%; the actionable signal is the <strong>miss rate</strong>, which | ||
| * spikes on cold start / deploy and on any unexpected cache flush.</p> | ||
| * | ||
| * @author Grails | ||
| * @since 8.0 | ||
| */ | ||
| public final class GroovyPageCacheMetrics { | ||
|
|
||
| /** A no-op instance used when no {@link MeterRegistry} is available. */ | ||
| public static final GroovyPageCacheMetrics NOOP = new GroovyPageCacheMetrics(null, null); | ||
|
|
||
| private static final String METRIC_NAME = "gsp.cache"; | ||
|
|
||
| private final Counter hits; | ||
| private final Counter misses; | ||
|
|
||
| private GroovyPageCacheMetrics(Counter hits, Counter misses) { | ||
| this.hits = hits; | ||
| this.misses = misses; | ||
| } | ||
|
|
||
| /** | ||
| * Builds the hit/miss counters for the named cache, or returns {@link #NOOP} when metrics are | ||
| * not configured. | ||
| * | ||
| * @param meterRegistry the registry to register the counters with, or {@code null} to disable | ||
| * @param cache the value for the {@code cache} tag, e.g. {@code "template"} or {@code "view"} | ||
| * @return a recorder bound to {@code meterRegistry}, or {@link #NOOP} | ||
| */ | ||
| public static GroovyPageCacheMetrics forCache(MeterRegistry meterRegistry, String cache) { | ||
| if (meterRegistry == null) { | ||
| return NOOP; | ||
| } | ||
| Counter hits = Counter.builder(METRIC_NAME).tag("cache", cache).tag("result", "hit") | ||
| .description("GSP " + cache + " cache lookups served from cache").register(meterRegistry); | ||
| Counter misses = Counter.builder(METRIC_NAME).tag("cache", cache).tag("result", "miss") | ||
| .description("GSP " + cache + " cache lookups that had to build the entry").register(meterRegistry); | ||
| return new GroovyPageCacheMetrics(hits, misses); | ||
| } | ||
|
|
||
| /** | ||
| * Records a single cache access. | ||
| * | ||
| * @param hit {@code true} if the value was served from cache, {@code false} if it had to be built | ||
| */ | ||
| public void record(boolean hit) { | ||
| Counter counter = hit ? this.hits : this.misses; | ||
| if (counter != null) { | ||
| counter.increment(); | ||
| } | ||
| } | ||
| } |
47 changes: 47 additions & 0 deletions
47
grails-gsp/core/src/main/groovy/org/grails/gsp/observation/GroovyPageObservationContext.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| package org.grails.gsp.observation; | ||
|
|
||
| import io.micrometer.observation.Observation; | ||
|
|
||
| /** | ||
| * Context that holds information for {@link io.micrometer.observation.Observation} instrumentation | ||
| * of Groovy Server Pages (GSP) rendering — the view, an included template, or a layout. | ||
| * | ||
| * @author Grails | ||
| * @since 8.0 | ||
| * @see GroovyPageObservationDocumentation | ||
| */ | ||
| public class GroovyPageObservationContext extends Observation.Context { | ||
|
|
||
| private final String resource; | ||
|
|
||
| public GroovyPageObservationContext(String resource) { | ||
| this.resource = resource; | ||
| } | ||
|
|
||
| /** | ||
| * Return the name of the rendered resource — the view URI (e.g. {@code /book/show}), the | ||
| * template name, or the layout name, depending on the observation. | ||
| * @return the resource name, possibly {@code null} | ||
| */ | ||
| public String getResource() { | ||
| return this.resource; | ||
| } | ||
| } |
41 changes: 41 additions & 0 deletions
41
...-gsp/core/src/main/groovy/org/grails/gsp/observation/GroovyPageObservationConvention.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| package org.grails.gsp.observation; | ||
|
|
||
| import io.micrometer.observation.Observation; | ||
| import io.micrometer.observation.ObservationConvention; | ||
|
|
||
| /** | ||
| * {@link ObservationConvention} for GSP view rendering instrumentation. | ||
| * | ||
| * <p>Implement this interface and register it as a bean (or set it on the | ||
| * {@code GroovyPageViewResolver}) to customize the observation name and the | ||
| * {@link io.micrometer.common.KeyValues} attached to GSP view observations.</p> | ||
| * | ||
| * @author Grails | ||
| * @since 8.0 | ||
| * @see DefaultGroovyPageObservationConvention | ||
| */ | ||
| public interface GroovyPageObservationConvention extends ObservationConvention<GroovyPageObservationContext> { | ||
|
|
||
| @Override | ||
| default boolean supportsContext(Observation.Context context) { | ||
| return context instanceof GroovyPageObservationContext; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.