Skip to content

Commit 086b2fb

Browse files
Consul module (client, registration and health check)
1 parent d6b0c37 commit 086b2fb

File tree

7 files changed

+701
-0
lines changed

7 files changed

+701
-0
lines changed

coverage-report/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
<source>${project.parent.basedir}/jooby-aws/src/main/java</source>
6161
<source>${project.parent.basedir}/jooby-spymemcached/src/main/java</source>
6262
<source>${project.parent.basedir}/jooby-commons-email/src/main/java</source>
63+
<source>${project.parent.basedir}/jooby-consul/src/main/java</source>
6364
<source>${project.parent.basedir}/jooby-flyway/src/main/java</source>
6465
<source>${project.parent.basedir}/jooby-hazelcast/src/main/java</source>
6566
<source>${project.parent.basedir}/jooby-ebean/src/main/java</source>
@@ -124,6 +125,7 @@
124125
<source>${project.parent.basedir}/jooby-aws/src/test/java</source>
125126
<source>${project.parent.basedir}/jooby-spymemcached/src/test/java</source>
126127
<source>${project.parent.basedir}/jooby-commons-email/src/test/java</source>
128+
<source>${project.parent.basedir}/jooby-consul/src/test/java</source>
127129
<source>${project.parent.basedir}/jooby-flyway/src/test/java</source>
128130
<source>${project.parent.basedir}/jooby-hazelcast/src/test/java</source>
129131
<source>${project.parent.basedir}/jooby-ebean/src/test/java</source>
@@ -371,6 +373,12 @@
371373
<version>${project.version}</version>
372374
</dependency>
373375

376+
<dependency>
377+
<groupId>org.jooby</groupId>
378+
<artifactId>jooby-consul</artifactId>
379+
<version>${project.version}</version>
380+
</dependency>
381+
374382
<dependency>
375383
<groupId>org.jooby</groupId>
376384
<artifactId>jooby-caffeine</artifactId>

jooby-consul/pom.xml

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
4+
5+
<parent>
6+
<groupId>org.jooby</groupId>
7+
<artifactId>jooby-project</artifactId>
8+
<version>1.1.1-SNAPSHOT</version>
9+
</parent>
10+
11+
<modelVersion>4.0.0</modelVersion>
12+
<artifactId>jooby-consul</artifactId>
13+
14+
<name>consul module</name>
15+
16+
<build>
17+
<plugins>
18+
<!-- sure-fire -->
19+
<plugin>
20+
<groupId>org.apache.maven.plugins</groupId>
21+
<artifactId>maven-surefire-plugin</artifactId>
22+
<configuration>
23+
<includes>
24+
<include>**/*Test.java</include>
25+
<include>**/*Feature.java</include>
26+
<include>**/Issue*.java</include>
27+
</includes>
28+
</configuration>
29+
</plugin>
30+
31+
</plugins>
32+
</build>
33+
34+
<dependencies>
35+
<!-- Jooby -->
36+
<dependency>
37+
<groupId>org.jooby</groupId>
38+
<artifactId>jooby</artifactId>
39+
<version>${project.version}</version>
40+
</dependency>
41+
42+
<!-- Force consul-client to use jooby's version of jackson -->
43+
<dependency>
44+
<groupId>com.fasterxml.jackson.core</groupId>
45+
<artifactId>jackson-annotations</artifactId>
46+
<version>${jackson.version}</version>
47+
</dependency>
48+
<dependency>
49+
<groupId>com.fasterxml.jackson.core</groupId>
50+
<artifactId>jackson-core</artifactId>
51+
<version>${jackson.version}</version>
52+
</dependency>
53+
<dependency>
54+
<groupId>com.fasterxml.jackson.core</groupId>
55+
<artifactId>jackson-databind</artifactId>
56+
<version>${jackson.version}</version>
57+
</dependency>
58+
<dependency>
59+
<groupId>com.fasterxml.jackson.datatype</groupId>
60+
<artifactId>jackson-datatype-guava</artifactId>
61+
<version>${jackson.version}</version>
62+
</dependency>
63+
64+
<!-- Consul client -->
65+
<dependency>
66+
<groupId>com.orbitz.consul</groupId>
67+
<artifactId>consul-client</artifactId>
68+
</dependency>
69+
70+
<!-- Test dependencies -->
71+
<dependency>
72+
<groupId>org.jooby</groupId>
73+
<artifactId>jooby</artifactId>
74+
<version>${project.version}</version>
75+
<scope>test</scope>
76+
<classifier>tests</classifier>
77+
</dependency>
78+
79+
<dependency>
80+
<groupId>junit</groupId>
81+
<artifactId>junit</artifactId>
82+
<scope>test</scope>
83+
</dependency>
84+
85+
<dependency>
86+
<groupId>org.easymock</groupId>
87+
<artifactId>easymock</artifactId>
88+
<scope>test</scope>
89+
</dependency>
90+
91+
<dependency>
92+
<groupId>org.powermock</groupId>
93+
<artifactId>powermock-api-easymock</artifactId>
94+
<scope>test</scope>
95+
</dependency>
96+
97+
<dependency>
98+
<groupId>org.powermock</groupId>
99+
<artifactId>powermock-module-junit4</artifactId>
100+
<scope>test</scope>
101+
</dependency>
102+
103+
<dependency>
104+
<groupId>org.jacoco</groupId>
105+
<artifactId>org.jacoco.agent</artifactId>
106+
<classifier>runtime</classifier>
107+
<scope>test</scope>
108+
</dependency>
109+
110+
</dependencies>
111+
112+
</project>
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
* <p>
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
* <p>
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.jooby.consul;
20+
21+
import com.google.inject.Binder;
22+
import com.orbitz.consul.AgentClient;
23+
import com.orbitz.consul.Consul;
24+
import com.orbitz.consul.model.agent.ImmutableRegistration;
25+
import com.orbitz.consul.model.agent.Registration;
26+
import com.typesafe.config.Config;
27+
import com.typesafe.config.ConfigFactory;
28+
import org.jooby.Env;
29+
import org.jooby.Jooby;
30+
31+
import java.text.MessageFormat;
32+
import java.util.Objects;
33+
import java.util.UUID;
34+
import java.util.concurrent.TimeUnit;
35+
import java.util.function.Consumer;
36+
37+
/**
38+
* <p>Consul client module.</p>
39+
*
40+
* <p>Exports the {@link Consul} client.</p>
41+
*
42+
* <p>Also register the application as a service and setup a health check.</p>
43+
*
44+
* <h1>usage</h1>
45+
*
46+
* <pre>
47+
* {
48+
* use(new Consulby());
49+
*
50+
* get("/myservice/health", req {@literal ->} {
51+
* Consul consul = require(Consul.class);
52+
* List<ServiceHealth> serviceHealths = consul.healthClient()
53+
* .getHealthyServiceInstances("myservice")
54+
* .getResponse();
55+
* return serviceHealths;
56+
* });
57+
* }
58+
* </pre>
59+
*
60+
* <h1>configuration</h1>
61+
*
62+
* <p>Configuration is done via <code>.conf</code>.</p>
63+
*
64+
* <p>For example, one can change the consul endpoint url,
65+
* change the advertised service host, and disable registration health check:</p>
66+
*
67+
* <pre>
68+
* consul.default.url = "http://consul.internal.domain.com:8500"
69+
* consul.default.register.host = 10.0.0.2
70+
* consul.default.register.check = null
71+
* </pre>
72+
*
73+
* <p>or, disable the automatic registration feature completely:</p>
74+
*
75+
* <pre>
76+
* consul.default.register = null
77+
* </pre>
78+
*
79+
* @since 1.1.1
80+
*/
81+
public class Consulby implements Jooby.Module {
82+
83+
private final String name;
84+
85+
private Consumer<Consul.Builder> consulBuilderConsumer;
86+
private Consumer<ImmutableRegistration.Builder> registrationBuilderConsumer;
87+
88+
/**
89+
* A new {@link Consulby} instance, with the default config name: <code>default</code>.
90+
*/
91+
public Consulby() {
92+
this("default");
93+
}
94+
95+
/**
96+
* <p>A new {@link Consulby} instance, with a provided config name.</p>
97+
*
98+
* <p>The module can be instantiated more than one time to allow connecting to many Consul installations:</p>
99+
*
100+
* <pre>
101+
* {
102+
* use(new Consulby("consul1"));
103+
* use(new Consulby("consul2"));
104+
* }
105+
* </pre>
106+
*
107+
* <p>Since the module will fallback on the <code>consul.default</code> config prefix,
108+
* it is possible to only override the desired properties in the <code>.conf</code>,
109+
* for example, here, disabling health check only for `consul2`:</p>
110+
*
111+
* <pre>
112+
* consul.consul1.url = "http://consul1.internal.domain.com:8500"
113+
*
114+
* consul.consul2.url = "http://consul2.internal.domain.com:8500"
115+
* consul.consul2.register.check = null
116+
* </pre>
117+
*
118+
* @param name A config name
119+
*/
120+
public Consulby(String name) {
121+
this.name = Objects.requireNonNull(name, "A consul config name is required.");
122+
}
123+
124+
/**
125+
* <p>{@link Consul} object can be configured programmatically:</p>
126+
*
127+
* <pre>
128+
* {
129+
* use(new Consulby()
130+
* .withConsulBuilder(consulBuilder -> {
131+
* consulBuilder.withPing(false);
132+
* consulBuilder.withBasicAuth("admin", "changeme");
133+
* }));
134+
* }
135+
* </pre>
136+
*
137+
* @param consulBuilderConsumer A {@link Consumer} that accepts {@link Consul.Builder}
138+
* @return This {@link Consulby} to allow chaining
139+
*/
140+
public Consulby withConsulBuilder(Consumer<Consul.Builder> consulBuilderConsumer) {
141+
this.consulBuilderConsumer = consulBuilderConsumer;
142+
return this;
143+
}
144+
145+
/**
146+
* <p>{@link Registration} object can be configured programmatically:</p>
147+
*
148+
* <pre>
149+
* {
150+
* use(new Consulby()
151+
* .withRegistrationBuilder(registrationBuilder -> {
152+
* registrationBuilder.enableTagOverride(true);
153+
* registrationBuilder.id("custom-service-id");
154+
* }));
155+
* }
156+
* </pre>
157+
*
158+
* @param registrationBuilderConsumer A {@link Consumer} that accepts {@link ImmutableRegistration.Builder}
159+
* @return This {@link Consulby} to allow chaining
160+
*/
161+
public Consulby withRegistrationBuilder(Consumer<ImmutableRegistration.Builder> registrationBuilderConsumer) {
162+
this.registrationBuilderConsumer = registrationBuilderConsumer;
163+
return this;
164+
}
165+
166+
@Override
167+
public void configure(Env env, Config config, Binder binder) throws Throwable {
168+
169+
Config consulConfig = config.getConfig("consul.default");
170+
if (!name.equals("default") && config.hasPath("consul." + name)) {
171+
consulConfig = config.getConfig("consul." + name).withFallback(consulConfig);
172+
}
173+
174+
Consul.Builder consulBuilder = Consul.builder()
175+
.withUrl(consulConfig.getString("url"));
176+
177+
if (consulBuilderConsumer != null) {
178+
consulBuilderConsumer.accept(consulBuilder);
179+
}
180+
181+
Consul consul = consulBuilder.build();
182+
183+
env.onStop(consul::destroy);
184+
185+
env.serviceKey().generate(Consul.class, name, k -> binder.bind(k).toInstance(consul));
186+
187+
if (consulConfig.hasPath("register")) {
188+
189+
Config registerConfig = consulConfig.getConfig("register");
190+
191+
ImmutableRegistration.Builder registrationBuilder = ImmutableRegistration.builder()
192+
.name(registerConfig.getString("name"))
193+
.address(registerConfig.getString("host"))
194+
.port(registerConfig.getInt("port"))
195+
.tags(registerConfig.getStringList("tags"))
196+
.id(UUID.randomUUID().toString());
197+
198+
if (registerConfig.hasPath("check")) {
199+
200+
Config checkConfig = registerConfig.getConfig("check");
201+
202+
String http = MessageFormat.format("http://{0}:{1,number,####}{2}",
203+
registerConfig.getString("host"),
204+
registerConfig.getInt("port"),
205+
checkConfig.getString("path"));
206+
207+
Registration.RegCheck check = Registration.RegCheck.http(http,
208+
checkConfig.getDuration("interval", TimeUnit.SECONDS),
209+
checkConfig.getDuration("timeout", TimeUnit.SECONDS));
210+
211+
registrationBuilder.check(check);
212+
213+
String response = checkConfig.getString("response");
214+
env.router().get(checkConfig.getString("path"), () -> response);
215+
}
216+
217+
if (registrationBuilderConsumer != null) {
218+
registrationBuilderConsumer.accept(registrationBuilder);
219+
}
220+
221+
Registration registration = registrationBuilder.build();
222+
223+
AgentClient agentClient = consul.agentClient();
224+
env.onStarted(() -> agentClient.register(registration));
225+
env.onStop(() -> agentClient.deregister(registration.getId()));
226+
}
227+
}
228+
229+
@Override
230+
public Config config() {
231+
return ConfigFactory.parseResources(getClass(), "consul.conf");
232+
}
233+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
consul {
2+
default {
3+
url = "http://localhost:8500"
4+
register {
5+
name = ${application.name}
6+
host = ${application.host}
7+
port = ${application.port}
8+
tags = []
9+
check {
10+
path = /health
11+
response = ${application.name}-${application.version}
12+
interval = 15s
13+
timeout = 5s
14+
}
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)