Skip to content

Log a fail/warn when config metadata default values are unable to be resolved #28062

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

Closed
wants to merge 11 commits into from
Closed

Log a fail/warn when config metadata default values are unable to be resolved #28062

wants to merge 11 commits into from

Conversation

onobc
Copy link
Contributor

@onobc onobc commented Sep 18, 2021

Example

I created a simple app w/ the following config props:

@ConfigurationProperties(prefix = "foo")
public class TestConfigProps {

    /**
     * Paths of things
     */
    private List<File> paths = new ArrayList<>(Collections.singletonList(new File(".")));

    public List<File> getPaths() {
        return paths;
    }

    public void setPaths(final List<File> paths) {
        this.paths = paths;
    }
}

Warn Mode

  • I ran the config processor which output the following:

    Screen Shot 2021-09-18 at 10 42 33 AM
  • I then added the following hint:

    {
      "properties": [
        {
          "name": "foo.paths",
          "type": "java.util.List<java.io.File>",
          "defaultValue": [
            "."
          ]
        }
      ]
    }
  • I ran the config processor again - problem fixed:

    Screen Shot 2021-09-18 at 10 43 19 AM

Fail Mode

When using "fail mode" its the same as above w/ the exception of the problem output looking like the following:

Screen Shot 2021-09-18 at 10 44 21 AM

TODOS

  • add ability to disable error mode??
  • add test for configprocessor validation checks
  • docs?

Fixes gh-28039

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Sep 18, 2021
Copy link
Member

@snicoll snicoll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the initial work on this. I've added a couple comments to hopefully give you directions.

@@ -165,7 +167,11 @@ private Object getValue(VariableTree variable) throws Exception {
Class<?> wrapperType = WRAPPER_TYPES.get(variable.getType());
Object defaultValue = DEFAULT_TYPE_VALUES.get(wrapperType);
if (initializer != null) {
return getValue(initializer, defaultValue);
Object initializerResolvedValue = getValue(initializer, defaultValue);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs to be a bit more precise than that. I think the idea is that we want to fail if we have a FieldValuesParser available and ignore if we don't. We we could do is changing the contract to return a ValueWrapper and just return that here. Our fallback implementation when we couldn't find an implementation should then return null.

Note that we probably want to differentiate between null as "detected as null" and null as "couldn't figure out". This could be an attribute on ValueWrapper or something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx for the direction @snicoll . It makes sense. I will get back to this in the next couple days.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@snicoll

It needs to be a bit more precise than that. I think the idea is that we want to fail if we have a FieldValuesParser available and ignore if we don't.

So, when no FVP is available (ie the env is not an com.sun.tools.javac.processing.JavacProcessingEnvironment) the default FVP is NONE which simply returns an empty map. Because this code is in the FVP it will only apply when the FVP (itself) is available.

We we could do is changing the contract to return a ValueWrapper and just return that here. Our fallback implementation when we couldn't find an implementation should then return null.

Again, the fallback impl currently is an empty map.

I may be missing something here - please clarify.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, the fallback impl currently is an empty map.

I am aware of that and I am asking you to change the contract. You can't know what fields are available if you don't have an implementation, but you could change the contract so that, if you're being requested a field with name "foo" you can then return null or a ValueWrapper that holds additional information as I've suggested. So instead of a map a POJO that can tell you if the result contains a field with a given name and another method that gives you a ValueWrapper based on a field name.

Copy link
Contributor Author

@onobc onobc Oct 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to submit what I have so far before calling it a day as I know you are 7hr ahead of me @snicoll .

I have not moved from map to POJO and changed the contract yet as the map (w/ its ValueWrapper values) pretty much fulfills the above mentioned contract. I don't mind moving to POJO but am curious if you still think it will add value to the current proposal.

I am now marching through the JCFPV tests and updating it. I will continue that tomorrow.

@@ -353,6 +356,20 @@ private ConfigurationMetadata mergeAdditionalMetadata(ConfigurationMetadata meta
return metadata;
}

private void validateDefaultValuesResolved(ConfigurationMetadata metadata) {
// TODO do we want this? How do we configure annotation processor w/ 'levers'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally fail when we know that we're in an environment where default values can be discovered. Ignore otherwise.

@snicoll snicoll marked this pull request as draft September 21, 2021 06:54
@snicoll snicoll changed the title WIP: Logs a fail/warn when config metadata default values are unable to be resolved Log a fail/warn when config metadata default values are unable to be resolved Sep 21, 2021
@snicoll
Copy link
Member

snicoll commented Sep 30, 2021

@Bono007 do you have an update for us? If you don't, no worries, we can pick things up as they are as RC1 is our last chance to include this in 2.6.

@snicoll snicoll added the status: waiting-for-feedback We need additional information before we can continue label Sep 30, 2021
@onobc
Copy link
Contributor Author

onobc commented Sep 30, 2021

Hi @snicoll - things got busy on my end but I have carved out some time this morning to work on this.

To help me gauge if this needs to be done in "a day" or "the next few days", is 10/21 still accurate?

I will give an update (code or no code) by today @ "12:00:00 -5:00" (~5hrs from time of writing)

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Sep 30, 2021
@snicoll
Copy link
Member

snicoll commented Sep 30, 2021

Thanks for the follow-up Chris. Yeah that's the RC1 release date so we still have plenty of time. Please do not rush it and let us know if you have any question.

@onobc
Copy link
Contributor Author

onobc commented Sep 30, 2021

Thanks for the follow-up Chris. Yeah that's the RC1 release date so we still have plenty of time. Please do not rush it and let us know if you have any question.

@snicoll my plans to carve out time this morning were foiled by a Kafka Streams production outage that I am putting out. It sounds though it is ok to have something in the next 1-2 days is ok though (correct me if that is not the case). I should have time later today to get back to it.

Thanks

@@ -353,6 +356,22 @@ private ConfigurationMetadata mergeAdditionalMetadata(ConfigurationMetadata meta
return metadata;
}

private void validateDefaultValuesResolved(ConfigurationMetadata metadata) {
metadata.getItems().forEach((itemMetadata -> {
if (itemMetadata.getDefaultValue() instanceof ValueWrapper) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not propagated the change from Object -> ValueWrapper up from FVP to ItemMetadata yet but will do that shortly. Unless you can think of a reason to keep it Object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One reason to keep it Object is to prevent the ripple around all areas that are accessing ItemMetadata#defaultValue (JsonConverter, MetadataStore, etc..).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. I didn't even think of changing ItemMetadata to be honest. I was hoping we could trigger the warning/error locally to field resolution by passing the necessary bits to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately we have to wait until the additional metadata is merged to see if the user added a manual hint.

@onobc
Copy link
Contributor Author

onobc commented Oct 6, 2021

@snicoll I was looking through the build failures and our update to the processor is suggesting that we have quite a few "hints" to add ourselves...

> Task :spring-boot-project:spring-boot-autoconfigure:compileJava FAILED
error: Could not resolve default value for property 'server.tomcat.additional-tld-skip-patterns' using expression 'new ArrayList<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'server.tomcat.relaxed-path-chars' using expression 'new ArrayList<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'server.tomcat.relaxed-query-chars' using expression 'new ArrayList<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'server.undertow.accesslog.dir' using expression 'new File("logs")'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'server.undertow.max-headers' using expression 'UndertowOptions.DEFAULT_MAX_HEADERS'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'server.undertow.max-parameters' using expression 'UndertowOptions.DEFAULT_MAX_PARAMETERS'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'server.undertow.options.server' using expression 'new LinkedHashMap<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'server.undertow.options.socket' using expression 'new LinkedHashMap<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.activemq.packages.trusted' using expression 'new ArrayList<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.artemis.embedded.cluster-password' using expression 'UUID.randomUUID().toString()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.artemis.embedded.server-id' using expression 'serverIdCounter.getAndIncrement()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.cache.cache-names' using expression 'new ArrayList<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.data.elasticsearch.client.reactive.endpoints' using expression 'new ArrayList<>(Collections.singletonList("localhost:9200"))'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.datasource.xa.properties' using expression 'new LinkedHashMap<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.flyway.init-sqls' using expression 'new ArrayList<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.flyway.jdbc-properties' using expression 'new HashMap<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.flyway.placeholders' using expression 'new HashMap<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.flyway.schemas' using expression 'new ArrayList<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.freemarker.settings' using expression 'new HashMap<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.integration.channel.max-broadcast-subscribers' using expression 'Integer.MAX_VALUE'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.integration.channel.max-unicast-subscribers' using expression 'Integer.MAX_VALUE'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.integration.endpoint.no-auto-startup' using expression 'new ArrayList<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.integration.endpoint.read-only-headers' using expression 'new ArrayList<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.jersey.init' using expression 'new HashMap<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.jpa.properties' using expression 'new HashMap<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.kafka.bootstrap-servers' using expression 'new ArrayList<>(Collections.singletonList("localhost:9092"))'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.kafka.consumer.key-deserializer' using expression 'StringDeserializer.class'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.kafka.consumer.value-deserializer' using expression 'StringDeserializer.class'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.kafka.producer.key-serializer' using expression 'StringSerializer.class'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.kafka.producer.value-serializer' using expression 'StringSerializer.class'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.ldap.embedded.base-dn' using expression 'new ArrayList<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.mail.properties' using expression 'new HashMap<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.mvc.contentnegotiation.media-types' using expression 'new LinkedHashMap<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.rabbitmq.stream.port' using expression 'DEFAULT_STREAM_PORT'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.security.user.password' using expression 'UUID.randomUUID().toString()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.security.user.roles' using expression 'new ArrayList<>()'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.session.servlet.filter-order' using expression 'SessionRepositoryFilter.DEFAULT_ORDER'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.task.execution.pool.max-size' using expression 'Integer.MAX_VALUE'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.task.execution.pool.queue-capacity' using expression 'Integer.MAX_VALUE'. Add hint in additional-spring-configuration.metadata.json
error: Could not resolve default value for property 'spring.webservices.servlet.init' using expression 'new HashMap<>()'. Add hint in additional-spring-configuration.metadata.json
40 errors

I will start marching through the above list and will add these to the additional metadata.

@onobc
Copy link
Contributor Author

onobc commented Oct 6, 2021

When I encountered the above list I was a bit surprised - I did not expect to see so many entries in violation. I am thinking we should leave the default behavior as Kind.ERROR but provide the ability to switch to Kind.WARN. In most cases I bet the list of updates needed is small but I am sure there are some cases where this will be a pain to absorb at once. Thoughts?

@snicoll
Copy link
Member

snicoll commented Oct 6, 2021

Thanks for the follow-up.

I did not expect to see so many entries in violation.

There are a lot of false positive in this list. Creating an empty list or map shouldn't lead to a violation. UUID.randomUUID().toString() on the default security password is interesting. We'd probably need a way to escape that check.

@onobc
Copy link
Contributor Author

onobc commented Oct 6, 2021

Thanks for the follow-up.

Np.

There are a lot of false positive in this list. Creating an empty list or map shouldn't lead to a violation. UUID.randomUUID().toString() on the default security password is interesting. We'd probably need a way to escape that check.

I will add these exemptions and then add "hints" for the remaining entries.

@@ -56,6 +62,16 @@ Object getLiteralValue() throws Exception {
return hasLiteralValue() ? this.literalValueMethod.invoke(getInstance()) : null;
}

Object getNoArgConstructor() throws Exception {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This takes expressions of type "NEW_CLASS" and make sure they have no-args and returns the identifier.
An expression of "new ArrayList<>()" results in "ArrayList<>".

@@ -39,6 +40,10 @@
*/
public class JavaCompilerFieldValuesParser implements FieldValuesParser {

private static final Pattern COLLECTION_PATTERN = Pattern.compile("^.*(List|Set)<.*>$");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a simple pragmatic approach and is not perfect and will also detect "com.foo.SomeFooList<>".

if (noArgConstructor != null) {
String expressionStr = noArgConstructor.toString();
if (COLLECTION_PATTERN.matcher(expressionStr).matches()) {
return ValueWrapper.of("[]", expressionStr);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just skip the field altogether or does it make sense to set the default of "[]" ?

{
"name": "spring.jta.atomikos.properties.default-max-wait-time-on-shutdown",
"type": "java.lang.Long",
"defaultValue": "Long.MAX_VALUE"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we always expand these well-known constants in here? As a user I know what Long.MAX_VALUE is and it makes sense to me when I see it in auto-complete in IDE.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good question. I was tempted to do that at some point but the problem is that putting this value in a configuration file would not actually bind properly so I think it is semantically wrong to put that representation.

However, I would expect the IDE to be able to resolve that and show the hint rather than the raw, not very helpful, value.

@@ -528,6 +536,10 @@
"level": "error"
}
},
{
"name": "management.metrics.export.wavefront.sender.message-size",
"defaultValue": "2147483647B"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another interesting one DataSize.ofBytes(Integer.MAX_VALUE).

@@ -356,6 +368,14 @@
"name": "spring.artemis.broker-url",
"defaultValue": "tcp://localhost:61616"
},
{
"name": "spring.artemis.embedded.cluster-password",
"defaultValue": "UUID.randomUUID().toString()"
Copy link
Contributor Author

@onobc onobc Oct 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, like you said, this is an interesting one. It is a random string value. We could instead do something like <random-uuid-string> but "UUID.randomUUID().toString()" says that as well and also gives a hint to how its random generated.

@@ -1984,6 +2049,10 @@
"request"
]
},
{
"name": "spring.session.servlet.filter-order",
"defaultValue": -2147483598
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not convey the same hint that SessionRepositoryFilter.DEFAULT_ORDER does (which in turn is = Integer.MIN_VALUE + 50)

}

Object identifierForNewClass = expression.getIdentifierForNewClassNoArgConstructor();
Copy link
Contributor Author

@onobc onobc Oct 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newly added ExpressionTree. getIdentifierForNewClassNoArgConstructor simplifies the regex inspection needed. I could have skipped adding the methods to ExpressionTree and instead done a

if (expression.getKind().equals("NEW_CLASS")) {

but that would have complicated the regex needed to handle the prefix of new and then the suffix of ( ) which would get ugly quickly as the random spacing that can be added to the expression etc...

@onobc onobc marked this pull request as ready for review October 19, 2021 13:42
@philwebb philwebb force-pushed the main branch 2 times, most recently from 7aa1646 to 5b11628 Compare November 19, 2021 18:42
@snicoll snicoll added the for: team-meeting An issue we'd like to discuss as a team to make progress label Jul 20, 2023
@wilkinsona
Copy link
Member

Thanks, @onobc. Unfortunately, having seen what it takes to implement it we've realised that my initial idea wasn't a particularly good one. I'm afraid we're going to decline this and try some alternatives idea. Sorry for the wasted effort. We'll repurpose #28039 to do that.

@wilkinsona wilkinsona closed this Jul 20, 2023
@wilkinsona wilkinsona added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-triage An issue we've not yet triaged status: feedback-provided Feedback has been provided for: team-meeting An issue we'd like to discuss as a team to make progress labels Jul 20, 2023
@onobc
Copy link
Contributor Author

onobc commented Jul 20, 2023

@wilkinsona no worries - we try some approaches and some work and some don't. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve ways in which configuration property metadata default values can be provided
4 participants