Add application startup metrics support

This commit adds a new `StartupStep` interface and its factory
`ApplicationStartup`. Such steps are created, tagged with metadata and
thir execution time can be recorded - in order to collect metrics about
the application startup.

The default implementation is a "no-op" variant and has no side-effect.
Other implementations can record and collect events in a dedicated
metrics system or profiling tools. We provide here an implementation for
recording and storing steps with Java Flight Recorder.

This commit also instruments the Spring application context to gather
metrics about various phases of the application context, such as:

* context refresh phase
* bean definition registry post-processing
* bean factory post-processing
* beans instantiation and post-processing

Third part libraries involved in the Spring application context can
reuse the same infrastructure to record similar metrics.

Closes gh-24878
This commit is contained in:
Brian Clozel
2020-07-27 15:08:01 +02:00
parent 4252b7fd7d
commit 9301d7a294
22 changed files with 890 additions and 16 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@ import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.metrics.ApplicationStartup;
import org.springframework.core.convert.ConversionService;
import org.springframework.lang.Nullable;
import org.springframework.util.StringValueResolver;
@@ -276,6 +277,20 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single
@Nullable
Scope getRegisteredScope(String scopeName);
/**
* Set the {@code ApplicationStartup} for this bean factory.
* <p>This allows the application context to record metrics during application startup.
* @param applicationStartup the new application startup
* @since 5.3.0
*/
void setApplicationStartup(ApplicationStartup applicationStartup);
/**
* Return the {@code ApplicationStartup} for this bean factory.
* @since 5.3.0
*/
ApplicationStartup getApplicationStartup();
/**
* Provides a security access control context relevant to this factory.
* @return the applicable AccessControlContext (never {@code null})

View File

@@ -69,6 +69,8 @@ import org.springframework.beans.factory.config.DestructionAwareBeanPostProcesso
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
import org.springframework.beans.metrics.ApplicationStartup;
import org.springframework.beans.metrics.StartupStep;
import org.springframework.core.AttributeAccessor;
import org.springframework.core.DecoratingClassLoader;
import org.springframework.core.NamedThreadLocal;
@@ -178,6 +180,8 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
private final ThreadLocal<Object> prototypesCurrentlyInCreation =
new NamedThreadLocal<>("Prototype beans currently in creation");
/** Application startup metrics. **/
private ApplicationStartup applicationStartup = ApplicationStartup.getDefault();
/**
* Create a new AbstractBeanFactory.
@@ -297,6 +301,11 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
}
try {
StartupStep beanCreation = this.applicationStartup.start("spring.beans.instantiate")
.tag("beanName", name);
if (requiredType != null) {
beanCreation.tag("beanType", requiredType::toString);
}
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);
@@ -374,6 +383,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
throw new ScopeNotActiveException(beanName, scopeName, ex);
}
}
beanCreation.end();
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
@@ -1044,6 +1054,17 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
this.securityContextProvider = securityProvider;
}
@Override
public void setApplicationStartup(ApplicationStartup applicationStartup) {
Assert.notNull(applicationStartup, "applicationStartup should not be null");
this.applicationStartup = applicationStartup;
}
@Override
public ApplicationStartup getApplicationStartup() {
return this.applicationStartup;
}
/**
* Delegate the creation of the access control context to the
* {@link #setSecurityContextProvider SecurityContextProvider}.
@@ -1380,7 +1401,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
else {
throw new NoSuchBeanDefinitionException(parentBeanName,
"Parent name '" + parentBeanName + "' is equal to bean name '" + beanName +
"': cannot be resolved without a ConfigurableBeanFactory parent");
"': cannot be resolved without a ConfigurableBeanFactory parent");
}
}
}
@@ -2068,7 +2089,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
super.replaceAll(operator);
beanPostProcessorCache = null;
}
};
}
/**

View File

@@ -71,6 +71,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.config.NamedBeanHolder;
import org.springframework.beans.metrics.StartupStep;
import org.springframework.core.OrderComparator;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotation;
@@ -564,7 +565,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);
}
}
else {
else {
if (includeNonSingletons || isNonLazyDecorated ||
(allowFactoryBeanInit && isSingleton(beanName, mbd, dbd))) {
matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);
@@ -937,6 +938,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton) {
StartupStep smartInitialize = this.getApplicationStartup().start("spring.beans.smart-initialize")
.tag("beanName", beanName);
SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
@@ -947,6 +950,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
else {
smartSingleton.afterSingletonsInstantiated();
}
smartInitialize.end();
}
}
}
@@ -1672,7 +1676,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
if (candidatePriority.equals(highestPriority)) {
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
"Multiple beans found with the same priority ('" + highestPriority +
"') among candidates: " + candidates.keySet());
"') among candidates: " + candidates.keySet());
}
else if (candidatePriority < highestPriority) {
highestPriorityBeanName = candidateBeanName;
@@ -1758,7 +1762,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
throw new NoSuchBeanDefinitionException(resolvableType,
"expected at least 1 bean which qualifies as autowire candidate. " +
"Dependency annotations: " + ObjectUtils.nullSafeToString(descriptor.getAnnotations()));
"Dependency annotations: " + ObjectUtils.nullSafeToString(descriptor.getAnnotations()));
}
/**
@@ -1803,6 +1807,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
public boolean isRequired() {
return false;
}
@Override
public Object resolveCandidate(String beanName, Class<?> requiredType, BeanFactory beanFactory) {
return (!ObjectUtils.isEmpty(args) ? beanFactory.getBean(beanName, args) :
@@ -2019,6 +2024,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
public boolean isRequired() {
return false;
}
@Override
@Nullable
public Object resolveNotUnique(ResolvableType type, Map<String, Object> matchingBeans) {

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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.springframework.beans.metrics;
/**
* Instruments the application startup phase using {@link StartupStep steps}.
* <p>The core container and its infrastructure components can use the {@code ApplicationStartup}
* to mark steps during the application startup and collect data about the execution context
* or their processing time.
*
* @author Brian Clozel
* @since 5.3.0
*/
public interface ApplicationStartup {
/**
* Return a default "no op" {@code ApplicationStartup} implementation.
* <p>This variant is designed for minimal overhead and does not record data.
*/
static ApplicationStartup getDefault() {
return new DefaultApplicationStartup();
}
/**
* Create a new step and marks its beginning.
* <p>A step name describes the current action or phase. This technical
* name should be "." namespaced and can be reused to describe other instances of
* the same step during application startup.
* @param name the step name
*/
StartupStep start(String name);
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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.springframework.beans.metrics;
import java.util.Collections;
import java.util.Iterator;
import java.util.function.Supplier;
/**
* Default "no op" {@code ApplicationStartup} implementation.
* <p>This variant is designed for minimal overhead and does not record events.
*
* @author Brian Clozel
*/
class DefaultApplicationStartup implements ApplicationStartup {
@Override
public DefaultStartupStep start(String name) {
return new DefaultStartupStep();
}
static class DefaultStartupStep implements StartupStep {
boolean recorded = false;
private final DefaultTags TAGS = new DefaultTags();
@Override
public String getName() {
return "default";
}
@Override
public long getId() {
return 0L;
}
@Override
public Long getParentId() {
return null;
}
@Override
public Tags tags() {
return this.TAGS;
}
@Override
public StartupStep tag(String key, String value) {
if (this.recorded) {
throw new IllegalArgumentException();
}
return this;
}
@Override
public StartupStep tag(String key, Supplier<String> value) {
if (this.recorded) {
throw new IllegalArgumentException();
}
return this;
}
@Override
public void end() {
this.recorded = true;
}
static class DefaultTags implements StartupStep.Tags {
@Override
public Iterator<StartupStep.Tag> iterator() {
return Collections.emptyIterator();
}
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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.springframework.beans.metrics;
import java.util.function.Supplier;
import org.springframework.lang.Nullable;
/**
* Step recording metrics about a particular phase or action happening during the {@link ApplicationStartup}.
* <p>The lifecycle of a {@code StartupStep} goes as follows:
* <ol>
* <li>the step is created and starts by calling {@link ApplicationStartup#start(String) the application startup}
* and is assigned a unique {@link StartupStep#getId() id}.
* <li>we can then attach information with {@link Tags} during processing
* <li>we then need to mark the {@link #end()} of the step
* </ol>
* <p>Implementations can track the "execution time" or other metrics for steps.
*
* @author Brian Clozel
* @since 5.3.0
*/
public interface StartupStep {
/**
* Return the name of the startup step.
* <p>A step name describes the current action or phase. This technical
* name should be "." namespaced and can be reused to describe other instances of
* similar steps during application startup.
*/
String getName();
/**
* Return the unique id for this step within the application startup.
*/
long getId();
/**
* Return, if available, the id of the parent step.
* <p>The parent step is the step that was started the most recently when the current step was created.
*/
@Nullable
Long getParentId();
/**
* Add a {@link Tag} to the step.
* @param key tag key
* @param value tag value
*/
StartupStep tag(String key, String value);
/**
* Add a {@link Tag} to the step.
* @param key tag key
* @param value {@link Supplier} for the tag value
*/
StartupStep tag(String key, Supplier<String> value);
/**
* Return the {@link Tag} collection for this step.
*/
Tags tags();
/**
* Record the state of the step and possibly other metrics like execution time.
* <p>Once ended, changes on the step state are not allowed.
*/
void end();
/**
* Mutable collection of {@link Tag}.
*/
interface Tags extends Iterable<Tag> {
}
/**
* Simple key/value association for storing step metadata.
*/
interface Tag {
/**
* Return the {@code Tag} name.
*/
String getKey();
/**
* Return the {@code Tag} value.
*/
String getValue();
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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.springframework.beans.metrics.jfr;
import java.util.ArrayDeque;
import java.util.Deque;
import org.springframework.beans.metrics.ApplicationStartup;
import org.springframework.beans.metrics.StartupStep;
/**
* {@link ApplicationStartup} implementation for the Java Flight Recorder.
* <p>This variant records {@link StartupStep} as Flight Recorder events; because such events
* only support base types, the {@link StartupStep.Tags} are serialized as a single String attribute.
* <p>Once this is configured on the application context, you can record data by launching the application
* with recording enabled: {@code java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar app.jar}.
*
* @author Brian Clozel
* @since 5.3
*/
public class FlightRecorderApplicationStartup implements ApplicationStartup {
private long currentSequenceId;
private final Deque<Long> currentSteps;
public FlightRecorderApplicationStartup() {
this.currentSequenceId = 0;
this.currentSteps = new ArrayDeque<>();
this.currentSteps.offerFirst(0L);
}
@Override
public StartupStep start(String name) {
FlightRecorderStartupStep step = new FlightRecorderStartupStep(++this.currentSequenceId, name,
this.currentSteps.peekFirst(), committedStep -> this.currentSteps.removeFirst());
this.currentSteps.offerFirst(this.currentSequenceId);
return step;
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed 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.springframework.beans.metrics.jfr;
import jdk.jfr.Category;
import jdk.jfr.Description;
import jdk.jfr.Event;
import jdk.jfr.Label;
/**
* {@link Event} extension for recording {@link FlightRecorderStartupStep}
* in Java Flight Recorder.
* <p>{@link org.springframework.beans.metrics.StartupStep.Tags} are serialized as a single {@code String},
* since Flight Recorder events do not support complex types.
*
* @author Brian Clozel
*/
@Category("Spring Application")
@Label("Startup Step")
@Description("Spring Application Startup")
class FlightRecorderStartupEvent extends Event {
public final long eventId;
public final long parentId;
@Label("Name")
public final String name;
@Label("Tags")
String tags = "";
public FlightRecorderStartupEvent(long eventId, String name, long parentId) {
this.name = name;
this.eventId = eventId;
this.parentId = parentId;
}
public void setTags(String tags) {
this.tags = tags;
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed 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.springframework.beans.metrics.jfr;
import java.util.Iterator;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.metrics.StartupStep;
/**
* {@link StartupStep} implementation for the Java Flight Recorder.
* <p>This variant delegates to a {@link FlightRecorderStartupEvent JFR event extension}
* to collect and record data in Java Flight Recorder.
*
* @author Brian Clozel
*/
class FlightRecorderStartupStep implements StartupStep {
private final FlightRecorderStartupEvent event;
private final FlightRecorderTags tags = new FlightRecorderTags();
private final Consumer<FlightRecorderStartupStep> recordingCallback;
public FlightRecorderStartupStep(long id, String name, long parentId,
Consumer<FlightRecorderStartupStep> recordingCallback) {
this.event = new FlightRecorderStartupEvent(id, name, parentId);
this.event.begin();
this.recordingCallback = recordingCallback;
}
@Override
public String getName() {
return this.event.name;
}
@Override
public long getId() {
return this.event.eventId;
}
@Override
public Long getParentId() {
return this.event.parentId;
}
@Override
public StartupStep tag(String key, String value) {
this.tags.add(key, value);
return this;
}
@Override
public StartupStep tag(String key, Supplier<String> value) {
this.tags.add(key, value.get());
return this;
}
@Override
public Tags tags() {
return this.tags;
}
@Override
public void end() {
this.event.end();
if (this.event.shouldCommit()) {
StringBuilder builder = new StringBuilder();
this.tags.forEach(tag ->
builder.append(tag.getKey()).append('=').append(tag.getValue()).append(',')
);
this.event.setTags(builder.toString());
}
this.event.commit();
this.recordingCallback.accept(this);
}
protected FlightRecorderStartupEvent getEvent() {
return this.event;
}
static class FlightRecorderTags implements Tags {
private Tag[] tags = new Tag[0];
public void add(String key, String value) {
Tag[] newTags = new Tag[this.tags.length + 1];
System.arraycopy(this.tags, 0, newTags, 0, this.tags.length);
newTags[newTags.length - 1] = new FlightRecorderTag(key, value);
this.tags = newTags;
}
public void add(String key, Supplier<String> value) {
add(key, value.get());
}
@NotNull
@Override
public Iterator<Tag> iterator() {
return new TagsIterator();
}
private class TagsIterator implements Iterator<Tag> {
private int idx = 0;
@Override
public boolean hasNext() {
return this.idx < tags.length;
}
@Override
public Tag next() {
return tags[this.idx++];
}
@Override
public void remove() {
throw new UnsupportedOperationException("tags are append only");
}
}
}
static class FlightRecorderTag implements Tag {
private final String key;
private final String value;
public FlightRecorderTag(String key, String value) {
this.key = key;
this.value = value;
}
@Override
public String getKey() {
return this.key;
}
@Override
public String getValue() {
return this.value;
}
}
}

View File

@@ -0,0 +1,9 @@
/**
* Support package for recording startup metrics using Java Flight Recorder.
*/
@NonNullApi
@NonNullFields
package org.springframework.beans.metrics.jfr;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -0,0 +1,9 @@
/**
* Support package for recording metrics during application startup.
*/
@NonNullApi
@NonNullFields
package org.springframework.beans.metrics;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;