@hazeljs/discovery 0.2.0-beta.8 → 0.2.0-beta.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +192 -21
- package/README.md +190 -21
- package/dist/__tests__/consul-backend.test.d.ts +6 -0
- package/dist/__tests__/consul-backend.test.d.ts.map +1 -0
- package/dist/__tests__/consul-backend.test.js +300 -0
- package/dist/__tests__/kubernetes-backend.test.d.ts +6 -0
- package/dist/__tests__/kubernetes-backend.test.d.ts.map +1 -0
- package/dist/__tests__/kubernetes-backend.test.js +261 -0
- package/dist/__tests__/redis-backend.test.d.ts +6 -0
- package/dist/__tests__/redis-backend.test.d.ts.map +1 -0
- package/dist/__tests__/redis-backend.test.js +280 -0
- package/dist/__tests__/service-client.test.js +2 -1
- package/dist/backends/consul-backend.d.ts +46 -7
- package/dist/backends/consul-backend.d.ts.map +1 -1
- package/dist/backends/consul-backend.js +23 -39
- package/dist/backends/kubernetes-backend.d.ts +44 -6
- package/dist/backends/kubernetes-backend.d.ts.map +1 -1
- package/dist/backends/kubernetes-backend.js +11 -32
- package/dist/backends/memory-backend.d.ts +0 -1
- package/dist/backends/memory-backend.d.ts.map +1 -1
- package/dist/backends/memory-backend.js +3 -32
- package/dist/backends/redis-backend.d.ts +11 -6
- package/dist/backends/redis-backend.d.ts.map +1 -1
- package/dist/backends/redis-backend.js +66 -46
- package/dist/client/discovery-client.d.ts +6 -4
- package/dist/client/discovery-client.d.ts.map +1 -1
- package/dist/client/discovery-client.js +30 -30
- package/dist/client/service-client.d.ts +4 -8
- package/dist/client/service-client.d.ts.map +1 -1
- package/dist/client/service-client.js +94 -34
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/registry/service-registry.d.ts.map +1 -1
- package/dist/registry/service-registry.js +13 -2
- package/dist/utils/filter.d.ts +10 -0
- package/dist/utils/filter.d.ts.map +1 -0
- package/dist/utils/filter.js +39 -0
- package/dist/utils/logger.d.ts +21 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +34 -0
- package/dist/utils/validation.d.ts +36 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +109 -0
- package/package.json +7 -5
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Redis Backend Tests
|
|
4
|
+
* Uses mocked ioredis client
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const redis_backend_1 = require("../backends/redis-backend");
|
|
8
|
+
const types_1 = require("../types");
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
// Suppress console logs during tests
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
logger_1.DiscoveryLogger.setLogger({
|
|
13
|
+
debug: jest.fn(),
|
|
14
|
+
info: jest.fn(),
|
|
15
|
+
warn: jest.fn(),
|
|
16
|
+
error: jest.fn(),
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
logger_1.DiscoveryLogger.resetLogger();
|
|
21
|
+
});
|
|
22
|
+
/** Helper to create a mock Redis instance */
|
|
23
|
+
function createMockRedis() {
|
|
24
|
+
const store = new Map();
|
|
25
|
+
const sets = new Map();
|
|
26
|
+
const ttls = new Map();
|
|
27
|
+
const listeners = new Map();
|
|
28
|
+
const mock = {
|
|
29
|
+
on: jest.fn((event, handler) => {
|
|
30
|
+
if (!listeners.has(event))
|
|
31
|
+
listeners.set(event, []);
|
|
32
|
+
listeners.get(event).push(handler);
|
|
33
|
+
}),
|
|
34
|
+
setex: jest.fn(async (key, ttl, value) => {
|
|
35
|
+
store.set(key, value);
|
|
36
|
+
ttls.set(key, ttl);
|
|
37
|
+
}),
|
|
38
|
+
get: jest.fn(async (key) => store.get(key) || null),
|
|
39
|
+
del: jest.fn(async (key) => {
|
|
40
|
+
store.delete(key);
|
|
41
|
+
ttls.delete(key);
|
|
42
|
+
return 1;
|
|
43
|
+
}),
|
|
44
|
+
mget: jest.fn(async (...keys) => {
|
|
45
|
+
// mget can receive keys as individual args or a single array
|
|
46
|
+
const flatKeys = keys.flat();
|
|
47
|
+
return flatKeys.map((k) => store.get(k) || null);
|
|
48
|
+
}),
|
|
49
|
+
sadd: jest.fn(async (key, member) => {
|
|
50
|
+
if (!sets.has(key))
|
|
51
|
+
sets.set(key, new Set());
|
|
52
|
+
sets.get(key).add(member);
|
|
53
|
+
return 1;
|
|
54
|
+
}),
|
|
55
|
+
srem: jest.fn(async (key, member) => {
|
|
56
|
+
sets.get(key)?.delete(member);
|
|
57
|
+
return 1;
|
|
58
|
+
}),
|
|
59
|
+
smembers: jest.fn(async (key) => {
|
|
60
|
+
return Array.from(sets.get(key) || []);
|
|
61
|
+
}),
|
|
62
|
+
scard: jest.fn(async (key) => {
|
|
63
|
+
return sets.get(key)?.size || 0;
|
|
64
|
+
}),
|
|
65
|
+
exists: jest.fn(async (key) => {
|
|
66
|
+
return store.has(key) ? 1 : 0;
|
|
67
|
+
}),
|
|
68
|
+
expire: jest.fn(async () => 1),
|
|
69
|
+
scan: jest.fn(async (_cursor, _match, pattern) => {
|
|
70
|
+
const keys = Array.from(sets.keys()).filter((k) => {
|
|
71
|
+
// Simple glob match: convert "prefix*" to regex
|
|
72
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
73
|
+
return regex.test(k);
|
|
74
|
+
});
|
|
75
|
+
return ['0', keys];
|
|
76
|
+
}),
|
|
77
|
+
quit: jest.fn(async () => 'OK'),
|
|
78
|
+
// Helper to emit events during tests
|
|
79
|
+
_emit: (event, ...args) => {
|
|
80
|
+
const handlers = listeners.get(event) || [];
|
|
81
|
+
handlers.forEach((h) => h(...args));
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
return mock;
|
|
85
|
+
}
|
|
86
|
+
function createInstance(id, name = 'test-service', overrides = {}) {
|
|
87
|
+
return {
|
|
88
|
+
id,
|
|
89
|
+
name,
|
|
90
|
+
host: 'localhost',
|
|
91
|
+
port: 3000,
|
|
92
|
+
status: types_1.ServiceStatus.UP,
|
|
93
|
+
lastHeartbeat: new Date('2025-01-01T00:00:00Z'),
|
|
94
|
+
registeredAt: new Date('2025-01-01T00:00:00Z'),
|
|
95
|
+
...overrides,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
describe('RedisRegistryBackend', () => {
|
|
99
|
+
let redis;
|
|
100
|
+
let backend;
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
redis = createMockRedis();
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
backend = new redis_backend_1.RedisRegistryBackend(redis);
|
|
105
|
+
});
|
|
106
|
+
describe('constructor', () => {
|
|
107
|
+
it('should use default config values', () => {
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
+
const b = new redis_backend_1.RedisRegistryBackend(redis);
|
|
110
|
+
expect(b).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
it('should accept custom config', () => {
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
+
const b = new redis_backend_1.RedisRegistryBackend(redis, {
|
|
115
|
+
keyPrefix: 'custom:',
|
|
116
|
+
ttl: 120,
|
|
117
|
+
});
|
|
118
|
+
expect(b).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
it('should throw on invalid TTL', () => {
|
|
121
|
+
expect(() => {
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
123
|
+
new redis_backend_1.RedisRegistryBackend(redis, { ttl: -5 });
|
|
124
|
+
}).toThrow();
|
|
125
|
+
});
|
|
126
|
+
it('should register connection event handlers', () => {
|
|
127
|
+
expect(redis.on).toHaveBeenCalledWith('error', expect.any(Function));
|
|
128
|
+
expect(redis.on).toHaveBeenCalledWith('connect', expect.any(Function));
|
|
129
|
+
expect(redis.on).toHaveBeenCalledWith('reconnecting', expect.any(Function));
|
|
130
|
+
expect(redis.on).toHaveBeenCalledWith('close', expect.any(Function));
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('register', () => {
|
|
134
|
+
it('should store the instance with TTL', async () => {
|
|
135
|
+
const instance = createInstance('1');
|
|
136
|
+
await backend.register(instance);
|
|
137
|
+
expect(redis.setex).toHaveBeenCalledWith('hazeljs:discovery:instance:1', 90, JSON.stringify(instance));
|
|
138
|
+
});
|
|
139
|
+
it('should add instance to service set', async () => {
|
|
140
|
+
const instance = createInstance('1', 'my-service');
|
|
141
|
+
await backend.register(instance);
|
|
142
|
+
expect(redis.sadd).toHaveBeenCalledWith('hazeljs:discovery:service:my-service', '1');
|
|
143
|
+
});
|
|
144
|
+
it('should set expiration on service set', async () => {
|
|
145
|
+
const instance = createInstance('1');
|
|
146
|
+
await backend.register(instance);
|
|
147
|
+
expect(redis.expire).toHaveBeenCalledWith('hazeljs:discovery:service:test-service', 180);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('deregister', () => {
|
|
151
|
+
it('should remove instance from Redis', async () => {
|
|
152
|
+
const instance = createInstance('1');
|
|
153
|
+
await backend.register(instance);
|
|
154
|
+
await backend.deregister('1');
|
|
155
|
+
expect(redis.srem).toHaveBeenCalledWith('hazeljs:discovery:service:test-service', '1');
|
|
156
|
+
expect(redis.del).toHaveBeenCalledWith('hazeljs:discovery:instance:1');
|
|
157
|
+
});
|
|
158
|
+
it('should do nothing if instance does not exist', async () => {
|
|
159
|
+
await backend.deregister('non-existent');
|
|
160
|
+
expect(redis.del).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe('heartbeat', () => {
|
|
164
|
+
it('should update heartbeat timestamp and status', async () => {
|
|
165
|
+
const instance = createInstance('1');
|
|
166
|
+
instance.status = types_1.ServiceStatus.DOWN;
|
|
167
|
+
await backend.register(instance);
|
|
168
|
+
await backend.heartbeat('1');
|
|
169
|
+
// Should have been called again with updated data
|
|
170
|
+
expect(redis.setex).toHaveBeenCalledTimes(2);
|
|
171
|
+
const lastCall = redis.setex.mock.calls[1];
|
|
172
|
+
const storedInstance = JSON.parse(lastCall[2]);
|
|
173
|
+
expect(storedInstance.status).toBe(types_1.ServiceStatus.UP);
|
|
174
|
+
});
|
|
175
|
+
it('should do nothing if instance does not exist', async () => {
|
|
176
|
+
await backend.heartbeat('non-existent');
|
|
177
|
+
// setex should not be called
|
|
178
|
+
expect(redis.setex).not.toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('getInstances', () => {
|
|
182
|
+
it('should return all instances for a service using MGET', async () => {
|
|
183
|
+
const i1 = createInstance('1', 'svc');
|
|
184
|
+
const i2 = createInstance('2', 'svc');
|
|
185
|
+
await backend.register(i1);
|
|
186
|
+
await backend.register(i2);
|
|
187
|
+
const instances = await backend.getInstances('svc');
|
|
188
|
+
expect(instances).toHaveLength(2);
|
|
189
|
+
expect(redis.mget).toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
it('should return empty array for non-existent service', async () => {
|
|
192
|
+
const instances = await backend.getInstances('non-existent');
|
|
193
|
+
expect(instances).toEqual([]);
|
|
194
|
+
});
|
|
195
|
+
it('should apply filter', async () => {
|
|
196
|
+
const i1 = createInstance('1', 'svc', { zone: 'us-east-1' });
|
|
197
|
+
const i2 = createInstance('2', 'svc', { zone: 'us-west-1' });
|
|
198
|
+
await backend.register(i1);
|
|
199
|
+
await backend.register(i2);
|
|
200
|
+
const instances = await backend.getInstances('svc', { zone: 'us-east-1' });
|
|
201
|
+
expect(instances).toHaveLength(1);
|
|
202
|
+
expect(instances[0].zone).toBe('us-east-1');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
describe('getInstance', () => {
|
|
206
|
+
it('should return a specific instance', async () => {
|
|
207
|
+
const instance = createInstance('1');
|
|
208
|
+
await backend.register(instance);
|
|
209
|
+
const retrieved = await backend.getInstance('1');
|
|
210
|
+
expect(retrieved).toBeDefined();
|
|
211
|
+
expect(retrieved.id).toBe('1');
|
|
212
|
+
});
|
|
213
|
+
it('should return null for non-existent instance', async () => {
|
|
214
|
+
const result = await backend.getInstance('non-existent');
|
|
215
|
+
expect(result).toBeNull();
|
|
216
|
+
});
|
|
217
|
+
it('should convert dates back from strings', async () => {
|
|
218
|
+
const instance = createInstance('1');
|
|
219
|
+
await backend.register(instance);
|
|
220
|
+
const retrieved = await backend.getInstance('1');
|
|
221
|
+
expect(retrieved.lastHeartbeat).toBeInstanceOf(Date);
|
|
222
|
+
expect(retrieved.registeredAt).toBeInstanceOf(Date);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe('getAllServices', () => {
|
|
226
|
+
it('should use SCAN instead of KEYS', async () => {
|
|
227
|
+
const i1 = createInstance('1', 'svc-a');
|
|
228
|
+
const i2 = createInstance('2', 'svc-b');
|
|
229
|
+
await backend.register(i1);
|
|
230
|
+
await backend.register(i2);
|
|
231
|
+
const services = await backend.getAllServices();
|
|
232
|
+
expect(redis.scan).toHaveBeenCalled();
|
|
233
|
+
expect(services).toHaveLength(2);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
describe('updateStatus', () => {
|
|
237
|
+
it('should update the status of an instance', async () => {
|
|
238
|
+
const instance = createInstance('1');
|
|
239
|
+
await backend.register(instance);
|
|
240
|
+
await backend.updateStatus('1', types_1.ServiceStatus.DOWN);
|
|
241
|
+
const retrieved = await backend.getInstance('1');
|
|
242
|
+
expect(retrieved.status).toBe(types_1.ServiceStatus.DOWN);
|
|
243
|
+
});
|
|
244
|
+
it('should do nothing for non-existent instance', async () => {
|
|
245
|
+
await backend.updateStatus('non-existent', types_1.ServiceStatus.DOWN);
|
|
246
|
+
expect(redis.setex).not.toHaveBeenCalled();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe('cleanup', () => {
|
|
250
|
+
it('should remove stale entries from service sets', async () => {
|
|
251
|
+
const instance = createInstance('1', 'svc');
|
|
252
|
+
await backend.register(instance);
|
|
253
|
+
// Simulate the instance key expiring in Redis
|
|
254
|
+
redis.exists.mockResolvedValueOnce(0);
|
|
255
|
+
await backend.cleanup();
|
|
256
|
+
expect(redis.srem).toHaveBeenCalledWith('hazeljs:discovery:service:svc', '1');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
describe('connection error handling', () => {
|
|
260
|
+
it('should track connection state via events', () => {
|
|
261
|
+
redis._emit('close');
|
|
262
|
+
// Backend should mark as disconnected
|
|
263
|
+
// Next operation should throw
|
|
264
|
+
expect(backend.getInstances('svc')).rejects.toThrow('Redis backend is not connected');
|
|
265
|
+
});
|
|
266
|
+
it('should recover on connect event', async () => {
|
|
267
|
+
redis._emit('close');
|
|
268
|
+
redis._emit('connect');
|
|
269
|
+
// Should work again
|
|
270
|
+
const result = await backend.getInstances('svc');
|
|
271
|
+
expect(result).toEqual([]);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
describe('close', () => {
|
|
275
|
+
it('should call redis.quit()', async () => {
|
|
276
|
+
await backend.close();
|
|
277
|
+
expect(redis.quit).toHaveBeenCalled();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -139,7 +139,8 @@ describe('ServiceClient', () => {
|
|
|
139
139
|
const mockRequest = jest.fn().mockRejectedValue(new Error('Network error'));
|
|
140
140
|
serviceClient.axiosInstance = { request: mockRequest };
|
|
141
141
|
await expect(serviceClient.get('/api/users')).rejects.toThrow();
|
|
142
|
-
|
|
142
|
+
// retries: 3 → RetryPolicy maxAttempts: 3 → 1 initial + 3 retries = 4 total
|
|
143
|
+
expect(mockRequest).toHaveBeenCalledTimes(4);
|
|
143
144
|
});
|
|
144
145
|
it('should respect retry delay', async () => {
|
|
145
146
|
const client = new service_client_1.ServiceClient(discoveryClient, {
|
|
@@ -4,7 +4,51 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { RegistryBackend } from './registry-backend';
|
|
6
6
|
import { ServiceInstance, ServiceFilter } from '../types';
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Minimal type definitions for the Consul client API surface we use.
|
|
9
|
+
* These mirror the shapes exposed by the `consul` npm package.
|
|
10
|
+
*/
|
|
11
|
+
export interface ConsulClient {
|
|
12
|
+
agent: {
|
|
13
|
+
service: {
|
|
14
|
+
register(opts: Record<string, unknown>): Promise<void>;
|
|
15
|
+
deregister(serviceId: string): Promise<void>;
|
|
16
|
+
list(): Promise<Record<string, ConsulServiceEntry>>;
|
|
17
|
+
};
|
|
18
|
+
check: {
|
|
19
|
+
pass(checkId: string): Promise<void>;
|
|
20
|
+
fail(checkId: string): Promise<void>;
|
|
21
|
+
warn(checkId: string): Promise<void>;
|
|
22
|
+
list(): Promise<Record<string, ConsulCheckEntry>>;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
health: {
|
|
26
|
+
service(opts: {
|
|
27
|
+
service: string;
|
|
28
|
+
passing?: boolean;
|
|
29
|
+
}): Promise<ConsulHealthEntry[]>;
|
|
30
|
+
};
|
|
31
|
+
catalog: {
|
|
32
|
+
service: {
|
|
33
|
+
list(): Promise<Record<string, string[]>>;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export interface ConsulServiceEntry {
|
|
38
|
+
ID: string;
|
|
39
|
+
Service: string;
|
|
40
|
+
Address: string;
|
|
41
|
+
Port: number;
|
|
42
|
+
Meta?: Record<string, string>;
|
|
43
|
+
Tags?: string[];
|
|
44
|
+
}
|
|
45
|
+
export interface ConsulCheckEntry {
|
|
46
|
+
Status: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ConsulHealthEntry {
|
|
49
|
+
Service: ConsulServiceEntry;
|
|
50
|
+
Checks?: ConsulCheckEntry[];
|
|
51
|
+
}
|
|
8
52
|
export interface ConsulBackendConfig {
|
|
9
53
|
host?: string;
|
|
10
54
|
port?: number;
|
|
@@ -17,7 +61,7 @@ export declare class ConsulRegistryBackend implements RegistryBackend {
|
|
|
17
61
|
private consul;
|
|
18
62
|
private readonly ttl;
|
|
19
63
|
private checkIntervals;
|
|
20
|
-
constructor(consul:
|
|
64
|
+
constructor(consul: ConsulClient, config?: ConsulBackendConfig);
|
|
21
65
|
/**
|
|
22
66
|
* Register a service instance with Consul
|
|
23
67
|
*/
|
|
@@ -67,10 +111,5 @@ export declare class ConsulRegistryBackend implements RegistryBackend {
|
|
|
67
111
|
* Parse TTL string to seconds
|
|
68
112
|
*/
|
|
69
113
|
private parseTTL;
|
|
70
|
-
/**
|
|
71
|
-
* Apply filter to instances
|
|
72
|
-
*/
|
|
73
|
-
private applyFilter;
|
|
74
114
|
}
|
|
75
|
-
export {};
|
|
76
115
|
//# sourceMappingURL=consul-backend.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consul-backend.d.ts","sourceRoot":"","sources":["../../src/backends/consul-backend.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"consul-backend.d.ts","sourceRoot":"","sources":["../../src/backends/consul-backend.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.ConsulRegistryBackend = void 0;
|
|
8
8
|
const types_1 = require("../types");
|
|
9
|
+
const filter_1 = require("../utils/filter");
|
|
10
|
+
const logger_1 = require("../utils/logger");
|
|
11
|
+
const validation_1 = require("../utils/validation");
|
|
9
12
|
class ConsulRegistryBackend {
|
|
10
13
|
constructor(consul, config = {}) {
|
|
11
14
|
this.checkIntervals = new Map();
|
|
15
|
+
(0, validation_1.validateConsulBackendConfig)(config);
|
|
12
16
|
this.consul = consul;
|
|
13
17
|
this.ttl = config.ttl || '30s';
|
|
14
18
|
}
|
|
@@ -51,19 +55,21 @@ class ConsulRegistryBackend {
|
|
|
51
55
|
* Update service instance heartbeat
|
|
52
56
|
*/
|
|
53
57
|
async heartbeat(instanceId) {
|
|
58
|
+
const logger = logger_1.DiscoveryLogger.getLogger();
|
|
54
59
|
const checkId = `service:${instanceId}`;
|
|
55
60
|
try {
|
|
56
61
|
// Pass TTL check
|
|
57
62
|
await this.consul.agent.check.pass(checkId);
|
|
58
63
|
}
|
|
59
|
-
catch {
|
|
60
|
-
|
|
64
|
+
catch (error) {
|
|
65
|
+
logger.warn(`Consul heartbeat failed for ${instanceId}, will retry on next heartbeat`, error);
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
/**
|
|
64
69
|
* Get all instances of a service
|
|
65
70
|
*/
|
|
66
71
|
async getInstances(serviceName, filter) {
|
|
72
|
+
const logger = logger_1.DiscoveryLogger.getLogger();
|
|
67
73
|
try {
|
|
68
74
|
const result = await this.consul.health.service({
|
|
69
75
|
service: serviceName,
|
|
@@ -99,12 +105,10 @@ class ConsulRegistryBackend {
|
|
|
99
105
|
};
|
|
100
106
|
});
|
|
101
107
|
// Apply additional filters
|
|
102
|
-
|
|
103
|
-
return this.applyFilter(instances, filter);
|
|
104
|
-
}
|
|
105
|
-
return instances;
|
|
108
|
+
return (0, filter_1.applyServiceFilter)(instances, filter);
|
|
106
109
|
}
|
|
107
|
-
catch {
|
|
110
|
+
catch (error) {
|
|
111
|
+
logger.error(`Failed to get instances for service "${serviceName}" from Consul`, error);
|
|
108
112
|
return [];
|
|
109
113
|
}
|
|
110
114
|
}
|
|
@@ -112,6 +116,7 @@ class ConsulRegistryBackend {
|
|
|
112
116
|
* Get a specific service instance
|
|
113
117
|
*/
|
|
114
118
|
async getInstance(instanceId) {
|
|
119
|
+
const logger = logger_1.DiscoveryLogger.getLogger();
|
|
115
120
|
try {
|
|
116
121
|
const services = await this.consul.agent.service.list();
|
|
117
122
|
const service = services[instanceId];
|
|
@@ -144,7 +149,8 @@ class ConsulRegistryBackend {
|
|
|
144
149
|
registeredAt: service.Meta?.registeredAt ? new Date(service.Meta.registeredAt) : new Date(),
|
|
145
150
|
};
|
|
146
151
|
}
|
|
147
|
-
catch {
|
|
152
|
+
catch (error) {
|
|
153
|
+
logger.error(`Failed to get instance "${instanceId}" from Consul`, error);
|
|
148
154
|
return null;
|
|
149
155
|
}
|
|
150
156
|
}
|
|
@@ -152,11 +158,13 @@ class ConsulRegistryBackend {
|
|
|
152
158
|
* Get all registered services
|
|
153
159
|
*/
|
|
154
160
|
async getAllServices() {
|
|
161
|
+
const logger = logger_1.DiscoveryLogger.getLogger();
|
|
155
162
|
try {
|
|
156
163
|
const services = await this.consul.catalog.service.list();
|
|
157
164
|
return Object.keys(services);
|
|
158
165
|
}
|
|
159
|
-
catch {
|
|
166
|
+
catch (error) {
|
|
167
|
+
logger.error('Failed to list services from Consul', error);
|
|
160
168
|
return [];
|
|
161
169
|
}
|
|
162
170
|
}
|
|
@@ -164,6 +172,7 @@ class ConsulRegistryBackend {
|
|
|
164
172
|
* Update service instance status
|
|
165
173
|
*/
|
|
166
174
|
async updateStatus(instanceId, status) {
|
|
175
|
+
const logger = logger_1.DiscoveryLogger.getLogger();
|
|
167
176
|
const checkId = `service:${instanceId}`;
|
|
168
177
|
try {
|
|
169
178
|
if (status === types_1.ServiceStatus.UP) {
|
|
@@ -176,8 +185,8 @@ class ConsulRegistryBackend {
|
|
|
176
185
|
await this.consul.agent.check.warn(checkId);
|
|
177
186
|
}
|
|
178
187
|
}
|
|
179
|
-
catch {
|
|
180
|
-
|
|
188
|
+
catch (error) {
|
|
189
|
+
logger.warn(`Failed to update status for ${instanceId} in Consul`, error);
|
|
181
190
|
}
|
|
182
191
|
}
|
|
183
192
|
/**
|
|
@@ -201,6 +210,7 @@ class ConsulRegistryBackend {
|
|
|
201
210
|
* Start TTL check updates for a service
|
|
202
211
|
*/
|
|
203
212
|
startTTLCheck(instanceId, checkId) {
|
|
213
|
+
const logger = logger_1.DiscoveryLogger.getLogger();
|
|
204
214
|
// Parse TTL to get interval (update at 2/3 of TTL)
|
|
205
215
|
const ttlSeconds = this.parseTTL(this.ttl);
|
|
206
216
|
const intervalMs = (ttlSeconds * 1000 * 2) / 3;
|
|
@@ -208,8 +218,8 @@ class ConsulRegistryBackend {
|
|
|
208
218
|
try {
|
|
209
219
|
await this.consul.agent.check.pass(checkId);
|
|
210
220
|
}
|
|
211
|
-
catch {
|
|
212
|
-
|
|
221
|
+
catch (error) {
|
|
222
|
+
logger.warn(`TTL check pass failed for ${instanceId}`, error);
|
|
213
223
|
}
|
|
214
224
|
}, intervalMs);
|
|
215
225
|
this.checkIntervals.set(instanceId, interval);
|
|
@@ -245,31 +255,5 @@ class ConsulRegistryBackend {
|
|
|
245
255
|
return 30;
|
|
246
256
|
}
|
|
247
257
|
}
|
|
248
|
-
/**
|
|
249
|
-
* Apply filter to instances
|
|
250
|
-
*/
|
|
251
|
-
applyFilter(instances, filter) {
|
|
252
|
-
return instances.filter((instance) => {
|
|
253
|
-
if (filter.zone && instance.zone !== filter.zone) {
|
|
254
|
-
return false;
|
|
255
|
-
}
|
|
256
|
-
if (filter.status && instance.status !== filter.status) {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
260
|
-
if (!instance.tags || !filter.tags.every((tag) => instance.tags.includes(tag))) {
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (filter.metadata) {
|
|
265
|
-
for (const [key, value] of Object.entries(filter.metadata)) {
|
|
266
|
-
if (!instance.metadata || instance.metadata[key] !== value) {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
return true;
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
258
|
}
|
|
275
259
|
exports.ConsulRegistryBackend = ConsulRegistryBackend;
|
|
@@ -4,7 +4,50 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { RegistryBackend } from './registry-backend';
|
|
6
6
|
import { ServiceInstance, ServiceFilter } from '../types';
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Minimal type definitions for the Kubernetes client API surface we use.
|
|
9
|
+
* These mirror the shapes exposed by `@kubernetes/client-node`.
|
|
10
|
+
*/
|
|
11
|
+
export interface KubeConfig {
|
|
12
|
+
makeApiClient(apiClass: new () => any): any;
|
|
13
|
+
}
|
|
14
|
+
export interface K8sEndpointAddress {
|
|
15
|
+
ip: string;
|
|
16
|
+
targetRef?: {
|
|
17
|
+
name?: string;
|
|
18
|
+
};
|
|
19
|
+
nodeName?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface K8sEndpointPort {
|
|
22
|
+
port: number;
|
|
23
|
+
}
|
|
24
|
+
export interface K8sEndpointSubset {
|
|
25
|
+
ports?: K8sEndpointPort[];
|
|
26
|
+
addresses?: K8sEndpointAddress[];
|
|
27
|
+
notReadyAddresses?: K8sEndpointAddress[];
|
|
28
|
+
}
|
|
29
|
+
export interface K8sObjectMeta {
|
|
30
|
+
name?: string;
|
|
31
|
+
annotations?: Record<string, string>;
|
|
32
|
+
labels?: Record<string, string>;
|
|
33
|
+
creationTimestamp?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface K8sEndpoints {
|
|
36
|
+
metadata?: K8sObjectMeta;
|
|
37
|
+
subsets?: K8sEndpointSubset[];
|
|
38
|
+
}
|
|
39
|
+
export interface K8sService {
|
|
40
|
+
metadata?: K8sObjectMeta;
|
|
41
|
+
}
|
|
42
|
+
export interface K8sApiResponse<T> {
|
|
43
|
+
body: T;
|
|
44
|
+
}
|
|
45
|
+
export interface CoreV1ApiLike {
|
|
46
|
+
readNamespacedEndpoints(name: string, namespace: string): Promise<K8sApiResponse<K8sEndpoints>>;
|
|
47
|
+
listNamespacedService(namespace: string, pretty?: string, allowWatchBookmarks?: boolean, _continue?: string, fieldSelector?: string, labelSelector?: string): Promise<K8sApiResponse<{
|
|
48
|
+
items: K8sService[];
|
|
49
|
+
}>>;
|
|
50
|
+
}
|
|
8
51
|
export interface KubernetesBackendConfig {
|
|
9
52
|
namespace?: string;
|
|
10
53
|
labelSelector?: string;
|
|
@@ -56,10 +99,5 @@ export declare class KubernetesRegistryBackend implements RegistryBackend {
|
|
|
56
99
|
* Create a ServiceInstance from Kubernetes endpoint data
|
|
57
100
|
*/
|
|
58
101
|
private createServiceInstance;
|
|
59
|
-
/**
|
|
60
|
-
* Apply filter to instances
|
|
61
|
-
*/
|
|
62
|
-
private applyFilter;
|
|
63
102
|
}
|
|
64
|
-
export {};
|
|
65
103
|
//# sourceMappingURL=kubernetes-backend.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kubernetes-backend.d.ts","sourceRoot":"","sources":["../../src/backends/kubernetes-backend.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"kubernetes-backend.d.ts","sourceRoot":"","sources":["../../src/backends/kubernetes-backend.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAKzE;;;GAGG;AACH,MAAM,WAAW,UAAU;IAEzB,aAAa,CAAC,QAAQ,EAAE,UAAU,GAAG,GAAG,GAAG,CAAC;CAC7C;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;IAC1B,SAAS,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACjC,iBAAiB,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,OAAO,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,IAAI,EAAE,CAAC,CAAC;CACT;AAED,MAAM,WAAW,aAAa;IAC5B,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC,CAAC;IAChG,qBAAqB,CACnB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,EACf,mBAAmB,CAAC,EAAE,OAAO,EAC7B,SAAS,CAAC,EAAE,MAAM,EAClB,aAAa,CAAC,EAAE,MAAM,EACtB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,cAAc,CAAC;QAAE,KAAK,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,yBAA0B,YAAW,eAAe;IAC/D,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,UAAU,EAAE,UAAU,EAAE,MAAM,GAAE,uBAA4B;IAWxE;;;;OAIG;IACG,QAAQ,CAAC,SAAS,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAMzD;;;OAGG;IACG,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpD;;;OAGG;IACG,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnD;;OAEG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IA4D3F;;OAEG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAQtE;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAoBzC;;;OAGG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvE;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B;;OAEG;IACH,OAAO,CAAC,qBAAqB;CAiC9B"}
|