Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions grails-gsp/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ dependencies {
api project(':grails-core')
api project(':grails-taglib')
api 'org.apache.groovy:groovy-templates'
// GSP rendering/compilation Micrometer instrumentation (gsp.view/template/layout/compile + cache counters)
implementation 'io.micrometer:micrometer-core'
Comment thread
codeconsole marked this conversation as resolved.

// api project(':grails-bootstrap'), { // ConfigMap
// // API dependencies in grails-bootstrap
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.runtime.IOGroovyMethods;

import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

Expand Down Expand Up @@ -76,6 +78,10 @@
import org.grails.gsp.io.GroovyPageResourceScriptSource;
import org.grails.gsp.io.GroovyPageScriptSource;
import org.grails.gsp.jsp.TagLibraryResolver;
import org.grails.gsp.observation.DefaultGroovyPageObservationConvention;
import org.grails.gsp.observation.GroovyPageObservationContext;
import org.grails.gsp.observation.GroovyPageObservationConvention;
import org.grails.gsp.observation.GroovyPageObservationDocumentation;
import org.grails.taglib.TagLibraryLookup;

/**
Expand Down Expand Up @@ -108,6 +114,9 @@ public class GroovyPagesTemplateEngine extends ResourceAwareTemplateEngine imple
private ClassLoader classLoader;
private AtomicInteger scriptNameCount = new AtomicInteger(0);

private static final GroovyPageObservationConvention COMPILE_OBSERVATION_CONVENTION = new DefaultGroovyPageObservationConvention("gsp.compile");
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;

private GroovyPageLocator groovyPageLocator = new DefaultGroovyPageLocator();

private boolean reloadEnabled;
Expand Down Expand Up @@ -481,6 +490,18 @@ public Template createTemplate(InputStream inputStream) {
}

protected GroovyPageMetaInfo buildPageMetaInfo(Resource resource, String pageName) throws IOException {
if (this.observationRegistry.isNoop()) {
return doBuildPageMetaInfo(resource, pageName);
}
// Compilation only happens on a template cache miss, so the count of this observation is
// effectively the GSP compile (cache-miss) rate; its timer is the compile latency.
Observation observation = GroovyPageObservationDocumentation.GSP_COMPILE.observation(
null, COMPILE_OBSERVATION_CONVENTION,
() -> new GroovyPageObservationContext(pageName), this.observationRegistry);
return observation.observeChecked(() -> doBuildPageMetaInfo(resource, pageName));
}

private GroovyPageMetaInfo doBuildPageMetaInfo(Resource resource, String pageName) throws IOException {
InputStream inputStream = resource.getInputStream();
try {
return buildPageMetaInfo(inputStream, resource, pageName);
Expand Down Expand Up @@ -754,6 +775,8 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
Config config = grailsApplication.getConfig();
this.gspEncoding = config.getProperty(GroovyPageParser.CONFIG_PROPERTY_GSP_ENCODING, System.getProperty("file.encoding", GroovyPageParser.DEFAULT_ENCODING));
}
this.observationRegistry = applicationContext.getBeanProvider(ObservationRegistry.class)
.getIfAvailable(() -> ObservationRegistry.NOOP);
}

/**
Expand Down
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;
}
}
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();
}
}
}
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;
}
}
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;
}
}
Loading
Loading