1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336 | 1
1
1
1
1
1
1
1
1
1
1
72
72
72
72
38
38
36
36
36
36
36
36
36
29
8
1
7
18
10
10
10
1
1
1
1
1
1
1
1
1
196
7
36
34
33
32
31
30
3
3
3
3
3
1
1
1
1
1
2
1
1
3
3
3
3
1
1
2
1
1
1
1
1
1
3
1
2
2
1
1
3
1
2
2
1
1
3
3
3
3
1
1
1
2
1
1
4
1
3
3
1
2
1
2
2
1
2
1
1
1
1
1
10
10
10
2
2
1
1
1
1
1
1
1
| import request from 'request';
import fs from 'fs';
import yaml from 'js-yaml';
import merge from 'deepmerge';
import path from 'path';
import dns from 'dns';
import {series} from 'async';
import {Logger} from './Logger';
import defaultConfig from './default-config';
const noop = () => {};
/*
Eureka JS client
This module handles registration with a Eureka server, as well as heartbeats
for reporting instance health.
*/
function getYaml(file) {
let yml = {};
try {
yml = yaml.safeLoad(fs.readFileSync(file, 'utf8'));
} catch (e) {}
return yml;
}
export class Eureka {
constructor(config) {
// Allow passing in a custom logger:
this.logger = config.logger || new Logger();
this.logger.debug('initializing eureka client');
// Load up the current working directory and the environment:
const cwd = process.cwd();
const env = process.env.NODE_ENV || 'development';
// Load in the configuration files:
this.config = merge(defaultConfig, getYaml(path.join(cwd, 'eureka-client.yml')));
this.config = merge(this.config, getYaml(path.join(cwd, `eureka-client-${env}.yml`)));
// Finally, merge in the passed configuration:
this.config = merge(this.config, config);
// Validate the provided the values we need:
this.validateConfig(this.config);
this.cache = {
app: {},
vip: {}
};
}
/*
Helper method to get the instance ID. If the datacenter is AWS, this will be the
instance-id in the metadata. Else, it's the hostName.
*/
get instanceId() {
if (this.amazonDataCenter) {
return this.config.instance.dataCenterInfo.metadata['instance-id'];
}
return this.config.instance.hostName;
}
/*
Helper method to determine if this is an AWS datacenter.
*/
get amazonDataCenter() {
return this.config.instance.dataCenterInfo.name.toLowerCase() === 'amazon';
}
/*
Build the base Eureka server URL + path
*/
buildEurekaUrl(callback = noop) {
this.lookupCurrentEurekaHost(eurekaHost => {
callback(`${this.config.eureka.ssl ? 'https' : 'http'}://${eurekaHost}:${this.config.eureka.port}${this.config.eureka.servicePath}`);
});
}
/*
Registers instance with Eureka, begins heartbeats, and fetches registry.
*/
start(callback = noop) {
series([
done => {
this.register(done);
},
done => {
if (Ethis.config.eureka.fetchRegistry) {
return this.fetchRegistry(done);
}
done();
}
], callback);
}
/*
De-registers instance with Eureka, stops heartbeats / registry fetches.
*/
stop(callback = noop) {
this.deregister(callback);
clearInterval(this.heartbeat);
clearInterval(this.registryFetch);
}
/*
Validates client configuration.
*/
validateConfig(config) {
function validate(namespace, key) {
if (!config[namespace][key]) {
throw new TypeError(`Missing "${namespace}.${key}" config value.`);
}
}
validate('instance', 'app');
validate('instance', 'vipAddress');
validate('instance', 'port');
validate('instance', 'dataCenterInfo');
validate('eureka', 'host');
validate('eureka', 'port');
}
/*
Registers with the Eureka server and initializes heartbeats on registration success.
*/
register(callback = noop) {
this.config.instance.status = 'UP';
this.buildEurekaUrl(eurekaUrl => {
request.post({
url: eurekaUrl + this.config.instance.app,
json: true,
body: {instance: this.config.instance}
}, (error, response, body) => {
if (!error && response.statusCode === 204) {
this.logger.info('registered with eureka: ', `${this.config.instance.app}/${this.instanceId}`);
this.startHeartbeats();
if (Ethis.config.eureka.fetchRegistry) {
this.startRegistryFetches();
}
return callback(null);
} else if (error) {
return callback(error);
}
return callback(new Error(`eureka registration FAILED: status: ${response.statusCode} body: ${body}`));
});
});
}
/*
De-registers with the Eureka server and stops heartbeats.
*/
deregister(callback = noop) {
this.buildEurekaUrl(eurekaUrl => {
request.del({
url: `${eurekaUrl}${this.config.instance.app}/${this.instanceId}`
}, (error, response, body) => {
if (!error && response.statusCode === 200) {
this.logger.info('de-registered with eureka: ', `${this.config.instance.app}/${this.instanceId}`);
return callback(null);
} else if (error) {
return callback(error);
}
return callback(new Error(`eureka deregistration FAILED: status: ${response.statusCode} body: ${body}`));
});
});
}
/*
Sets up heartbeats on interval for the life of the application.
Heartbeat interval by setting configuration property: eureka.heartbeatInterval
*/
startHeartbeats() {
this.heartbeat = setInterval(() => {
this.renew();
}, this.config.eureka.heartbeatInterval);
}
renew() {
this.buildEurekaUrl(eurekaUrl => {
request.put({
url: `${eurekaUrl}${this.config.instance.app}/${this.instanceId}`
}, (error, response, body) => {
if (E!error && response.statusCode === 200) {
this.logger.debug('eureka heartbeat success');
} else {
if (error) {
this.logger.error('An error in the request occured.', error);
}
this.logger.warn('eureka heartbeat FAILED, will retry.', `status: ${response.statusCode} body: ${body}`);
}
});
});
}
/*
Sets up registry fetches on interval for the life of the application.
Registry fetch interval setting configuration property: eureka.registryFetchInterval
*/
startRegistryFetches() {
this.registryFetch = setInterval(() => {
this.fetchRegistry();
}, this.config.eureka.registryFetchInterval);
}
/*
Retrieves a list of instances from Eureka server given an appId
*/
getInstancesByAppId(appId) {
if (!appId) {
throw new RangeError('Unable to query instances with no appId');
}
const instances = this.cache.app[appId.toUpperCase()];
if (!instances) {
throw new Error(`Unable to retrieve instances for appId: ${appId}`);
}
return instances;
}
/*
Retrieves a list of instances from Eureka server given a vipAddress
*/
getInstancesByVipAddress(vipAddress) {
if (!vipAddress) {
throw new RangeError('Unable to query instances with no vipAddress');
}
const instances = this.cache.vip[vipAddress];
if (!instances) {
throw new Error(`Unable to retrieves instances for vipAddress: ${vipAddress}`);
}
return instances;
}
/*
Retrieves all applications registered with the Eureka server
*/
fetchRegistry(callback = noop) {
this.buildEurekaUrl(eurekaUrl => {
request.get({
url: eurekaUrl,
headers: {
Accept: 'application/json'
}
}, (error, response, body) => {
if (!error && response.statusCode === 200) {
this.logger.debug('retrieved registry successfully');
this.transformRegistry(JSON.parse(body));
return callback(null);
} else if (error) {
return callback(error);
}
callback(new Error('Unable to retrieve registry from Eureka server'));
});
});
}
/*
Transforms the given registry and caches the registry locally
*/
transformRegistry(registry) {
if (!registry) {
throw new Error('Unable to transform empty registry');
}
this.cache = {app: {}, vip: {}};
if (!registry.applications.application) {
return;
}
if (registry.applications.application.length) {
for (let i = 0; i < registry.applications.application.length; i++) {
const app = registry.applications.application[i];
this.transformApp(app);
}
} else {
this.transformApp(registry.applications.application);
}
}
/*
Transforms the given application and places in client cache. If an application
has a single instance, the instance is placed into the cache as an array of one
*/
transformApp(app) {
if (app.instance.length) {
this.cache.app[app.name.toUpperCase()] = app.instance;
this.cache.vip[app.instance[0].vipAddress] = app.instance;
} else {
const instances = [app.instance];
this.cache.vip[app.instance.vipAddress] = instances;
this.cache.app[app.name.toUpperCase()] = instances;
}
}
/*
Returns the Eureka host. This method is async because potentially we might have to
execute DNS lookups which is an async network operation.
*/
lookupCurrentEurekaHost(callback = noop) {
if (Ithis.amazonDataCenter && this.config.eureka.useDns) {
this.locateEurekaHostUsingDns(resolvedHost => {
return callback(resolvedHost);
});
} else {
return callback(this.config.eureka.host);
}
}
/*
Locates a Eureka host using DNS lookups. The DNS records are looked up by a naming
convention and TXT records must be created according to the Eureka Wiki here:
https://github.com/Netflix/eureka/wiki/Configuring-Eureka-in-AWS-Cloud
Naming convention: txt.<REGION>.<HOST>
*/
locateEurekaHostUsingDns(callback = noop) {
if (!this.config.eureka.ec2Region) {
throw new Error('EC2 region was undefined. config.eureka.ec2Region must be set to resolve Eureka using DNS records.');
}
dns.resolveTxt('txt.' + this.config.eureka.ec2Region + '.' + this.config.eureka.host, (error, addresses) => {
if (Ierror) {
throw new Error('Error resolving eureka server list for region [' + this.config.eureka.ec2Region + '] using DNS: [' + error + ']');
}
dns.resolveTxt('txt.' + addresses[0][Math.floor(Math.random() * addresses[0].length)], (error, results) => {
if (Ierror) {
throw new Error('Error locating eureka server using DNS: [' + error + ']');
}
this.logger.debug('Found Eureka Server @ ', results);
callback([].concat([], ...results).shift());
});
});
}
}
|