Skip to content
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

move configuration from ToolInstallation to GlobalConfiguration #43

Merged
merged 25 commits into from
Jan 30, 2018

Conversation

mwinter69
Copy link

@mwinter69 mwinter69 commented Dec 27, 2017

The plugin used a ToolInstallation for configuration. But logstash is
not a tool that can be installed. Instead this should be a
GlobalConfiguration.
The indexers have different configuration requirements. Host and port
are common, but things like username are not required everywhere. Also
the previous implementation was totally unflexible. In order to add a
new indexer one has to modify the sources. By configuring the indexers
via an extension point we make it easily possible to have individual
configuration jelly for each type. Adding a new indexer is just adding
new classes. So it can be even moved to a separate plugin.

use the executor of the build to get the node.
builtOn is set too late for us to get the node and we end up using
master
properly get the node when BuildData is initilaized from a pipeline
pump mockito to latest version
add integration test, that use Jenkins test harness
the plugin used a ToolInstallation for configuration. But logstash is
not a tool that can be installed. Instead this should be a
GlobalConfiguration.
The indexers have different configuration requirements. Host and port
are common, but things like username are not required everywhere. Also
the previous implementation was totally unflexible. In order to add a
new indexer one has to modify the sources. By configuring the indexers
via an extension point we make it easily possible to have individual
configuration jelly for each type. Adding a new indexer is just adding
new classes. So it can be even moved to a separate plugin.

Many of the fields in the plugin were public or package private. They
have been made private or protected with corresponding getters and
setters and direct calls to the fields replaced.

Get the charset of a run and use it whenever we need to convert the
String to a byte array

Use DateFormatter from apache common lang to avoid potential threading
problems.
properly get displayname of node (jenkinsci#38)
use Jenkins 2.7.4 instead of 2.60.3
fix problem with cast that is requried with java 7
@jakub-bochenski
Copy link

Thanks, that's great.

The indexers have different configuration requirements. Host and port
are common, but things like username are not required everywhere. Also
the previous implementation was totally unflexible.

Can you check #41? We are adding a new global config field there, but hopefully this PR might supersede it?

@jakub-bochenski
Copy link

Many of the fields in the plugin were public or package private. They
have been made private or protected with corresponding getters and
setters and direct calls to the fields replaced.

Get the charset of a run and use it whenever we need to convert the
String to a byte array

Use DateFormatter from apache common lang to avoid potential threading
problems.

Fixed all findbugs warnings.

Would it be possible to send each of those as a separate PR? It will make it much easier to review and integrate (e.g. hopefully we can merge the purely refactoring changes immediatelly)

Use FastDateFormat from apache which is thread safe

Avoid possible NPE in Node detection
Avoid possible NPE in result (previous logic already avoided it but findbugs
still claimed it to be a problem)

Node detection is same for pipeline and freestyle so move it to initData
method
@mwinter69
Copy link
Author

OK will split this in several PRs

extends AbstractDescribableImpl<LogstashIndexer<?>>
implements ExtensionPoint, ReconfigurableDescribable<LogstashIndexer<?>>
{
protected String host;

Choose a reason for hiding this comment

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

I'm not sure, but maybe it would be better to extract those fields into a separate data object?
It seems cleaner to push the check in shouldRefreshInstance() into the equals() method instead of overriding shouldRefreshInstance() directly.

Copy link
Author

@mwinter69 mwinter69 Jan 1, 2018

Choose a reason for hiding this comment

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

But we're not comparing the same objects in this method so the equals() method is not the right place. Currently there is a separation between the configuration and the actual dao that does the push to the indexer. The configuration class must decide if it should create a new dao instance or return the existing one which is done in the shouldRefreshInstance().

Choose a reason for hiding this comment

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

What I don't like here is that LogstashIndexer is an abstract factory for the dao while also being the model for the configuration view (jelly). My intuition suggest extracting the model into a separate entity.

Maybe let me send you a patch to illustrate (as it might be easier than explaining) and you can revert it if you don't like it.

Choose a reason for hiding this comment

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

@mwinter69 can you check ae96695 ?
It's only a rough draft but I think it demonstrates the idea (e.g. I'd rather have the data object immutable than cloning it).

Please excuse me for the messed up formatting, I accidentally formatted the whole Syslog.java

Copy link
Author

Choose a reason for hiding this comment

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

I think this makes the whole thing with stapler and configuration more complex. Not sure if we have to make the Data object then also a describable so that stapler can auto configure it when global configuration is saved.
We could also move the comparison to the GlobalConfiguration class.
A third option would be to make the daos directly the thing that is configured.

Choose a reason for hiding this comment

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

With your explanation I think it's OK to leave the code as is. It's still better than previous one.

We could also move the comparison to the GlobalConfiguration class.

Can you explain what you mean by this?

Copy link
Author

Choose a reason for hiding this comment

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

Take a look at https://github.com/mwinter69/logstash-plugin/tree/configuration2
One effect of stapler is that when you bind a request to the configuration you always get a new instance of the logstashindexer. Thats why I had this check and implemented the reconfigure.
In this branch I replaced this by comparing (with equals) the newly configured instance with the active instance. If they are different, the active is replaced with the new one.

Copy link

@jakub-bochenski jakub-bochenski Jan 3, 2018

Choose a reason for hiding this comment

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

I like configuration2 more since we are comparing instances of LogstashIndexer instead of comparing LogstashIndexer and a LogstashDao.

update javadoc to get more information for developers that want to
extend the plugin.
Update the readme
Use the not deprecated junit runner of mockito
* @return {@link AbstractLogstashIndexerDao} instance
*/
@Nonnull
public final T getInstance()

Choose a reason for hiding this comment

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

Do we need to synchronize it? I can't find anything via a quick google

Copy link
Author

Choose a reason for hiding this comment

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

good question. Potentially after a reconfiguration or initially we might have 2 builds running into the if at the same time. Probably it will not hurt to synchronize. The worst that happens is that 2 builds have 2 different daos with the same configuration. Will this be a problem? We will already have different daos active when configuration is changed (the builds that were running when configuration changed and builds started after change).

Choose a reason for hiding this comment

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

Not at the moment, but I think it would be nice to enable a close() method on the Daos later to let them use long living connections etc.

Choose a reason for hiding this comment

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

Another problem is somebody calling setHost() concurrently with getInstance() -- those are public methods after all.

Maybe it's covered by creating a new instance

when you bind a request to the configuration you always get a new instance of the logstashindexer

but then I don't actually understand how can caching the Dao instance in the LogstashIndexer work?

Copy link
Author

Choose a reason for hiding this comment

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

it is cached because I use the reconfigure instead of the default behaviour of staper to create new instance. But this question is not relevant when we switch to the change in my configuration2 branch.

protected boolean shouldRefreshInstance()
{
return super.shouldRefreshInstance() ||
!instance.getMessageFormat().equals(messageFormat);

Choose a reason for hiding this comment

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

we are not checking the protocol -- is this intentional?

Copy link
Author

@mwinter69 mwinter69 Jan 3, 2018

Choose a reason for hiding this comment

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

The protocol is currently not used in the dao. Maybe we can remove this completely. Or we also implement the tcp protocol.

Choose a reason for hiding this comment

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

There were attempts to implement TCP in the past: #26

I'd either remove the field or use it in comparison to avoid future confusion.

Copy link
Author

Choose a reason for hiding this comment

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

will add it to comparison. We should also implement the tcp protocol I think, just for completeness

@@ -37,7 +37,7 @@
* @since 1.0.0
*/
public interface LogstashIndexerDao {
static enum IndexerType {
public static enum IndexerType {

Choose a reason for hiding this comment

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

interface members are public by default, I'd prefer to not restate default modifiers

Copy link
Author

Choose a reason for hiding this comment

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

ok

</f:entry>
</f:advanced>
</f:section>
<!-- We need to keep this file in order to prevent that in the UI we get the option to configure a Logstash Installation -->

Choose a reason for hiding this comment

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

How about leaving some explanatory text here, e.g. "Configuration moved to Global Configuration"? Bonus points for a link :)

Copy link
Author

Choose a reason for hiding this comment

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

I think it should be enough to document this on the wiki page.
btw I suggest to upgrade the version to 2.0. And maybe we find a way to show that plugin changed in a way that when you go back to previous version you have to manually adjust configuration (this is possible somehow).

Choose a reason for hiding this comment

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

I know they should check the wiki page, but this makes for less frustration.

Can we leave a short message here with a link to the wiki page then?

Copy link
Author

Choose a reason for hiding this comment

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

will add a link to global configuration page

@@ -1,4 +1,4 @@
<div>
<p>The host name or IP address of the indexer to send log data to.<br>
ELASTICSEARCH: Also specify scheme. Example: "https://myserver". </p>
Also specify scheme. Example: "https://myserver". </p>

Choose a reason for hiding this comment

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

can we add a separate scheme field instead?

Choose a reason for hiding this comment

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

actually what would be even better is to have a single field accepting a full URL instead. Do you think it's possible?

Copy link
Author

Choose a reason for hiding this comment

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

Then we could also remove the key, as this is just added to the url

Copy link
Author

Choose a reason for hiding this comment

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

done

}

@Test
public void test_redisMigration()

Choose a reason for hiding this comment

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

can we get rid of the test_ prefixes? It's not needed in JUnit4

implements ExtensionPoint, ReconfigurableDescribable<LogstashIndexer<?>>
{
protected String host;
protected int port;

Choose a reason for hiding this comment

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

On a different note: maybe we should push the host and port up?
MemoryDao is one obvious example that doesn't use those, but I think it might be cleaner code in general.
We could have another helper abstract class to avoid copying this for all the indexer types

Copy link
Author

Choose a reason for hiding this comment

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

If we replace for elastic search the host and port with a url this might make sense

Copy link
Author

Choose a reason for hiding this comment

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

done

rename junit test methods
synchronize getting the dao instance to avoid problems.
deprecate enums
// when we bind the stapler request we get a new instance of logstashIndexer
staplerRequest.bindJSON(this, json);

if (logstashIndexer != null && !logstashIndexer.equals(activeIndexer))

Choose a reason for hiding this comment

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

Objects.equals() seems shorter

@jenkinsci jenkinsci deleted a comment from mwinter69 Jan 3, 2018
introduce an abstract host based indexer that serves as base for most
indexers. Elastic search uses a uri approach where we need scheme, host,
port and url path, so we allow to configure a uri instead.
{
uri = (new URIBuilder(descriptor.getHost()))
.setPort(descriptor.getPort())
.setPath("/" + descriptor.getKey()).build();

Choose a reason for hiding this comment

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

not sure if this will work properly for keys that already start with /
does this UriBuilder collapse consecutive /?

Copy link
Author

@mwinter69 mwinter69 Jan 4, 2018

Choose a reason for hiding this comment

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

This is exactly the same behaviour as it was before in the ElasticSearchDao

Choose a reason for hiding this comment

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

OK then


@DataBoundConstructor
public ElasticSearch()
public ElasticSearch(String uri) throws URISyntaxException

Choose a reason for hiding this comment

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

Why are we changing the constructor instead of using a @DataBoundSetter?

Copy link
Author

@mwinter69 mwinter69 Jan 4, 2018

Choose a reason for hiding this comment

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

When the uri is not valid it will fail to save the configuration when it is checked in the constructor.
When using a DataBoundSetter, you can save the globalconfiguration with invalid uri

}

return FormValidation.ok();
if(uri.getPort() == -1) {

Choose a reason for hiding this comment

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

Why not let users rely on default port?
https://logstash.internal/index looks better than https://logstash.internal:443/index

}

if(StringUtils.isBlank(uri.getPath()) || uri.getPath().trim().matches("^\\/+$")) {

Choose a reason for hiding this comment

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

I have mixed feelings here.
It's obviously useful for 99% of usages, but probably somebody somewhere with some weird Apache/modrewrite in front will want to use an empty path.

Can we make this into a warning instead?

Copy link
Author

Choose a reason for hiding this comment

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

before it was also a must to have a non empty key

* @param uri
* @throws IllegalArgumentException when one of scheme, port or path
*/
public static void validateUri(URI uri) throws IllegalArgumentException

Choose a reason for hiding this comment

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

since we ignore those I'd add a check enforcing empty username and password (in authority part)

}

@DataBoundSetter
public void setKey(String key)
public void setUri(String value) throws URISyntaxException

Choose a reason for hiding this comment

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

minor; for sake of consistency can we make setter accept URI?

Copy link
Author

Choose a reason for hiding this comment

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

With URI stapler is not able to autoconvert the String to an URI. I will switch to URL as persistence here. Then I can also use a DataBoundSetter that accepts a URL

import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

public class HostConfiguration
Copy link

@jakub-bochenski jakub-bochenski Jan 4, 2018

Choose a reason for hiding this comment

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

I can't find any references to that class, am I missing something?

/**
* Abstract data access object for Logstash indexers.
*
* @author Rusty Gerard

Choose a reason for hiding this comment

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

I see some tasty copy-pasta here. Unless you are an alter ego of mr. Gerard?

Choose a reason for hiding this comment

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

Please just remove the @author tags, I think they are useless in a world with git blame

{
URI uri = (new URIBuilder("localhost:8000")).setPath("/logstash").build();

//System.out.println(uri.toURL());

Choose a reason for hiding this comment

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

commented out code

actually it's weird FB isn't flagging this

Copy link
Author

Choose a reason for hiding this comment

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

forgot to delete this main class

stapler is not able to autconvert String to URI
add more TODOs
// Clean up the the logstash log file
try {
File file = new File(logfile);

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

That was a try to get this test running in Eclipse. It always fails in Eclipse for me, thought it was due to missing folders, but didn't help. I'll remove that again.

} catch (IllegalArgumentException e) {
assertEquals("Wrong error message was thrown", "elastic index name is required", e.getMessage());
throw e;
assertEquals("Wrong error message was thrown", "java.net.MalformedURLException: unknown protocol: localhost", e.getMessage());

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

I consider using ExpectedException as a bad practice (see also https://artemzin.com/blog/test-expected-badpracticeexception/)
Why rethrow when we already catched it and checked the message is what we expect.

Choose a reason for hiding this comment

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

Yeah, but I was talking about the ExpectedException rule


     @Rule
     public ExpectedException thrown= ExpectedException.none();
// ...
     thrown.expect(NullPointerException.class);
     thrown.expectMessage("happened");
     throw new NullPointerException("What happened?");

public LogstashConfiguration()
{
load();
if (logstashIndexer != null)

Choose a reason for hiding this comment

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

Why not always assign? I don't see how activeIndexer could be non-null here

Copy link
Author

Choose a reason for hiding this comment

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

It can be null when you install the plugin. But the activeIndexer being null is not a problem

{
// when we bind the stapler request we get a new instance of logstashIndexer
staplerRequest.bindJSON(this, json);
if (!Objects.equals(logstashIndexer, activeIndexer))

Choose a reason for hiding this comment

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

can you add further comment explaining that we check for equality because Indexer instance is a holder for Dao

}
// String comparison is not optimal but comparing the urls directly is
// criticized by findbugs as being a blocking operation
else if (!url.toString().equals(other.url.toString()))

Choose a reason for hiding this comment

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

minor: I'd suggest to have an URI field instead and do an URL -> URI conversion in setter

}
}

public static void main(String[] args) throws MalformedURLException, URISyntaxException

Choose a reason for hiding this comment

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

leftover test method?

@Override
public int getDefaultPort()
{
return 9300;

Choose a reason for hiding this comment

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

umm.. isn't ES default port 9200?

Copy link
Author

Choose a reason for hiding this comment

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

yes but with URI, we don't need this at all.

@jakub-bochenski
Copy link

LGTM except for the minor issues listed above.

I think I should do another release, and then we can merge it and bump the major version.

@jakub-bochenski jakub-bochenski merged commit c014184 into jenkinsci:master Jan 30, 2018
@mwinter69 mwinter69 deleted the configuration branch January 31, 2018 01:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants