Skip to content

Commit

Permalink
Add application startup metrics support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bclozel committed Jul 27, 2020
1 parent 4252b7f commit 9301d7a
Show file tree
Hide file tree
Showing 22 changed files with 890 additions and 16 deletions.
@@ -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.
Expand All @@ -26,6 +26,7 @@
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;
Expand Down Expand Up @@ -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})
Expand Down
Expand Up @@ -69,6 +69,8 @@
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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -297,6 +301,11 @@ else if (requiredType != null) {
}

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);

Expand Down Expand Up @@ -374,6 +383,7 @@ else if (mbd.isPrototype()) {
throw new ScopeNotActiveException(beanName, scopeName, ex);
}
}
beanCreation.end();
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
Expand Down Expand Up @@ -1044,6 +1054,17 @@ public void setSecurityContextProvider(SecurityContextProvider securityProvider)
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}.
Expand Down Expand Up @@ -1380,7 +1401,7 @@ protected RootBeanDefinition getMergedBeanDefinition(
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");
}
}
}
Expand Down Expand Up @@ -2068,7 +2089,7 @@ public void replaceAll(UnaryOperator<BeanPostProcessor> operator) {
super.replaceAll(operator);
beanPostProcessorCache = null;
}
};
}


/**
Expand Down
Expand Up @@ -71,6 +71,7 @@
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;
Expand Down Expand Up @@ -564,7 +565,7 @@ private String[] doGetBeanNamesForType(ResolvableType type, boolean includeNonSi
matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);
}
}
else {
else {
if (includeNonSingletons || isNonLazyDecorated ||
(allowFactoryBeanInit && isSingleton(beanName, mbd, dbd))) {
matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);
Expand Down Expand Up @@ -937,6 +938,8 @@ public void preInstantiateSingletons() throws BeansException {
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>) () -> {
Expand All @@ -947,6 +950,7 @@ public void preInstantiateSingletons() throws BeansException {
else {
smartSingleton.afterSingletonsInstantiated();
}
smartInitialize.end();
}
}
}
Expand Down Expand Up @@ -1672,7 +1676,7 @@ protected String determineHighestPriorityCandidate(Map<String, Object> candidate
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;
Expand Down Expand Up @@ -1758,7 +1762,7 @@ private void raiseNoMatchingBeanFound(

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()));
}

/**
Expand Down Expand Up @@ -1803,6 +1807,7 @@ private Optional<?> createOptionalDependency(
public boolean isRequired() {
return false;
}

@Override
public Object resolveCandidate(String beanName, Class<?> requiredType, BeanFactory beanFactory) {
return (!ObjectUtils.isEmpty(args) ? beanFactory.getBean(beanName, args) :
Expand Down Expand Up @@ -2019,6 +2024,7 @@ public Object getIfUnique() throws BeansException {
public boolean isRequired() {
return false;
}

@Override
@Nullable
public Object resolveNotUnique(ResolvableType type, Map<String, Object> matchingBeans) {
Expand Down
@@ -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);

}
@@ -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();
}
}
}

}

0 comments on commit 9301d7a

Please sign in to comment.