@brimble/consul 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -24,11 +24,13 @@ See the official [HTTP API][consul-docs-api] docs for more information.
24
24
  - [Connect](#catalog-connect)
25
25
  - [Node](#catalog-node)
26
26
  - [Service](#catalog-service)
27
+ * [Config](#config)
27
28
  * [Event](#event)
28
29
  * [Health](#health)
29
30
  * [Intention](#intention)
30
31
  * [KV](#kv)
31
32
  * [Query](#query)
33
+ * [Resolver](#resolver)
32
34
  * [Session](#session)
33
35
  * [Status](#status)
34
36
  * [Transaction](#transaction)
@@ -1066,6 +1068,101 @@ Result
1066
1068
  ]
1067
1069
  ```
1068
1070
 
1071
+ <a id="config"></a>
1072
+
1073
+ ### consul.config
1074
+
1075
+ - [list](#config-list)
1076
+ - [get](#config-get)
1077
+ - [set](#config-set)
1078
+ - [destroy](#config-destroy)
1079
+
1080
+ <a id="config-list"></a>
1081
+
1082
+ ### consul.config.list(options)
1083
+
1084
+ Lists config entries for a given kind.
1085
+
1086
+ Options
1087
+
1088
+ - kind (String): config entry kind
1089
+ - dc (String, optional): datacenter
1090
+
1091
+ Usage
1092
+
1093
+ ```javascript
1094
+ await consul.config.list("service-resolver");
1095
+ ```
1096
+
1097
+ <a id="config-get"></a>
1098
+
1099
+ ### consul.config.get(options)
1100
+
1101
+ Gets a config entry.
1102
+
1103
+ Options
1104
+
1105
+ - kind (String): config entry kind
1106
+ - name (String): config entry name
1107
+ - dc (String, optional): datacenter
1108
+
1109
+ Usage
1110
+
1111
+ ```javascript
1112
+ await consul.config.get({
1113
+ kind: "service-resolver",
1114
+ name: "salary-index-ng",
1115
+ });
1116
+ ```
1117
+
1118
+ <a id="config-set"></a>
1119
+
1120
+ ### consul.config.set(entry[, options])
1121
+
1122
+ Creates or updates a config entry.
1123
+
1124
+ Options
1125
+
1126
+ - entry (Object): Consul config entry payload
1127
+ - token (String, optional): ACL token
1128
+ - dc (String, optional): datacenter
1129
+
1130
+ Usage
1131
+
1132
+ ```javascript
1133
+ await consul.config.set(
1134
+ {
1135
+ Kind: "service-resolver",
1136
+ Name: "salary-index-ng",
1137
+ Redirect: {
1138
+ Service: "6a3ea78d3d66fd3ab1dbbb12",
1139
+ },
1140
+ },
1141
+ { token: process.env.CONSUL_HTTP_TOKEN },
1142
+ );
1143
+ ```
1144
+
1145
+ <a id="config-destroy"></a>
1146
+
1147
+ ### consul.config.destroy(options)
1148
+
1149
+ Deletes a config entry. Alias: `consul.config.delete(options)`.
1150
+
1151
+ Options
1152
+
1153
+ - kind (String): config entry kind
1154
+ - name (String): config entry name
1155
+ - dc (String, optional): datacenter
1156
+
1157
+ Usage
1158
+
1159
+ ```javascript
1160
+ await consul.config.destroy({
1161
+ kind: "service-resolver",
1162
+ name: "salary-index-ng",
1163
+ });
1164
+ ```
1165
+
1069
1166
  <a id="event"></a>
1070
1167
 
1071
1168
  ### consul.event
@@ -1486,6 +1583,186 @@ Usage
1486
1583
  await consul.intention.destroy("a0f5dc05-84c3-5f5a-1d88-05b875e524e1");
1487
1584
  ```
1488
1585
 
1586
+ <a id="resolver"></a>
1587
+
1588
+ ### consul.resolver(config)
1589
+
1590
+ Create a DNS resolver instance for load balancing and service discovery with Consul.
1591
+
1592
+ The resolver provides intelligent service selection using multiple algorithms, DNS resolution, health checks, and Redis-based metrics tracking.
1593
+
1594
+ Options
1595
+
1596
+ - redis (Redis, optional): ioredis instance for caching (required if cacheEnabled is true)
1597
+ - cacheEnabled (Boolean, default: false): enable Redis caching
1598
+ - cachePrefix (String, required): prefix for Redis cache keys
1599
+ - debug (Boolean, default: false): enable debug logging
1600
+ - weights (Object, optional): custom weights for weighted round robin algorithm
1601
+ - health (Number, default: 0.25): weight for health score
1602
+ - responseTime (Number, default: 0.2): weight for response time
1603
+ - errorRate (Number, default: 0.2): weight for error rate
1604
+ - resources (Number, default: 0.15): weight for CPU/memory usage
1605
+ - connections (Number, default: 0.1): weight for active connections
1606
+ - distribution (Number, default: 0.1): weight for distribution fairness
1607
+ - metrics (Object, optional): default metrics for new services
1608
+ - responseTime (Number, default: 100): default response time in ms
1609
+ - errorRate (Number, default: 0): default error rate percentage
1610
+ - cpuUsage (Number, default: 50): default CPU usage percentage
1611
+ - memoryUsage (Number, default: 50): default memory usage percentage
1612
+ - activeConnections (Number, default: 0): default active connections
1613
+ - cacheTTL (Number, default: 60000): cache TTL in milliseconds
1614
+ - dnsEndpoints (String[], optional): custom DNS endpoints
1615
+ - dnsTimeout (Number, default: 1500): DNS query timeout in milliseconds
1616
+ - dnsRetries (Number, default: 2): number of DNS retry attempts
1617
+
1618
+ Usage
1619
+
1620
+ ```javascript
1621
+ import Consul from "@brimble/consul";
1622
+ import Redis from "ioredis";
1623
+
1624
+ const consul = new Consul({
1625
+ host: "127.0.0.1",
1626
+ port: 8500,
1627
+ });
1628
+
1629
+ const redis = new Redis({
1630
+ host: "localhost",
1631
+ port: 6379,
1632
+ });
1633
+
1634
+ const resolver = consul.resolver({
1635
+ redis,
1636
+ cacheEnabled: true,
1637
+ cachePrefix: "myapp",
1638
+ debug: false,
1639
+ });
1640
+ ```
1641
+
1642
+ <a id="resolver-select"></a>
1643
+
1644
+ ### resolver.selectOptimalService(service, algorithm)
1645
+
1646
+ Select the optimal service instance based on the specified algorithm.
1647
+
1648
+ Options
1649
+
1650
+ - service (String, required): service name to resolve
1651
+ - algorithm (String, optional): selection algorithm
1652
+ - `SelectionAlgorithm.RoundRobin` (default): round-robin selection
1653
+ - `SelectionAlgorithm.LeastConnection`: select service with fewest connections
1654
+ - `SelectionAlgorithm.WeightedRoundRobin`: weighted selection based on metrics
1655
+
1656
+ Usage
1657
+
1658
+ ```javascript
1659
+ const Consul = require("@brimble/consul");
1660
+ const { SelectionAlgorithm } = Consul.Resolver;
1661
+
1662
+ const result = await resolver.selectOptimalService(
1663
+ "my-service",
1664
+ SelectionAlgorithm.LeastConnection,
1665
+ );
1666
+
1667
+ if (result.selected) {
1668
+ console.log(`Selected: ${result.selected.ip}:${result.selected.port}`);
1669
+ }
1670
+ ```
1671
+
1672
+ Result
1673
+
1674
+ ```json
1675
+ {
1676
+ "selected": {
1677
+ "ip": "192.168.1.10",
1678
+ "port": 8080
1679
+ },
1680
+ "services": [
1681
+ {
1682
+ "ip": "192.168.1.10",
1683
+ "port": 8080
1684
+ },
1685
+ {
1686
+ "ip": "192.168.1.11",
1687
+ "port": 8080
1688
+ }
1689
+ ]
1690
+ }
1691
+ ```
1692
+
1693
+ <a id="resolver-increment"></a>
1694
+
1695
+ ### resolver.incrementConnections(serviceId)
1696
+
1697
+ Increment the active connection count for a service.
1698
+
1699
+ Options
1700
+
1701
+ - serviceId (String, required): service ID
1702
+
1703
+ Usage
1704
+
1705
+ ```javascript
1706
+ await resolver.incrementConnections("service-id-123");
1707
+ ```
1708
+
1709
+ <a id="resolver-decrement"></a>
1710
+
1711
+ ### resolver.decrementConnections(serviceId)
1712
+
1713
+ Decrement the active connection count for a service.
1714
+
1715
+ Options
1716
+
1717
+ - serviceId (String, required): service ID
1718
+
1719
+ Usage
1720
+
1721
+ ```javascript
1722
+ await resolver.decrementConnections("service-id-123");
1723
+ ```
1724
+
1725
+ <a id="resolver-metrics"></a>
1726
+
1727
+ ### resolver.getSelectionMetrics(serviceId)
1728
+
1729
+ Get current metrics for a specific service.
1730
+
1731
+ Options
1732
+
1733
+ - serviceId (String, required): service ID
1734
+
1735
+ Usage
1736
+
1737
+ ```javascript
1738
+ const metrics = await resolver.getSelectionMetrics("service-id-123");
1739
+ ```
1740
+
1741
+ Result
1742
+
1743
+ ```json
1744
+ {
1745
+ "responseTime": 150,
1746
+ "errorRate": 0.5,
1747
+ "cpuUsage": 45,
1748
+ "memoryUsage": 60,
1749
+ "activeConnections": 10,
1750
+ "lastSelectedTime": 1234567890
1751
+ }
1752
+ ```
1753
+
1754
+ <a id="resolver-refresh"></a>
1755
+
1756
+ ### resolver.refresh()
1757
+
1758
+ Clear all stored metrics from Redis cache.
1759
+
1760
+ Usage
1761
+
1762
+ ```javascript
1763
+ await resolver.refresh();
1764
+ ```
1765
+
1489
1766
  <a id="kv"></a>
1490
1767
 
1491
1768
  ### consul.kv
@@ -0,0 +1,55 @@
1
+ import { CommonOptions, Consul } from "./consul";
2
+
3
+ interface ConfigEntry {
4
+ Kind: string;
5
+ Name: string;
6
+ Namespace?: string;
7
+ Partition?: string;
8
+ Meta?: Record<string, string>;
9
+ CreateIndex?: number;
10
+ ModifyIndex?: number;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ interface ListOptions extends CommonOptions {
15
+ kind: string;
16
+ }
17
+
18
+ type ListResult = ConfigEntry[];
19
+
20
+ interface GetOptions extends CommonOptions {
21
+ kind: string;
22
+ name: string;
23
+ }
24
+
25
+ type GetResult = ConfigEntry;
26
+
27
+ type SetOptions = CommonOptions;
28
+
29
+ type SetResult = boolean;
30
+
31
+ interface DestroyOptions extends CommonOptions {
32
+ kind: string;
33
+ name: string;
34
+ }
35
+
36
+ type DestroyResult = boolean;
37
+
38
+ declare class Config {
39
+ constructor(consul: Consul);
40
+
41
+ consul: Consul;
42
+
43
+ list(options: ListOptions): Promise<ListResult>;
44
+ list(kind: string): Promise<ListResult>;
45
+
46
+ get(options: GetOptions): Promise<GetResult>;
47
+
48
+ set(entry: ConfigEntry, options?: SetOptions): Promise<SetResult>;
49
+
50
+ destroy(options: DestroyOptions): Promise<DestroyResult>;
51
+
52
+ delete(options: DestroyOptions): Promise<DestroyResult>;
53
+ }
54
+
55
+ export { Config, ConfigEntry };
package/lib/config.js ADDED
@@ -0,0 +1,117 @@
1
+ const errors = require("./errors");
2
+ const utils = require("./utils");
3
+
4
+ class Config {
5
+ constructor(consul) {
6
+ this.consul = consul;
7
+ }
8
+
9
+ /**
10
+ * Lists config entries for a kind
11
+ */
12
+ async list(opts) {
13
+ if (typeof opts === "string") {
14
+ opts = { kind: opts };
15
+ }
16
+
17
+ opts = utils.normalizeKeys(opts);
18
+ opts = utils.defaults(opts, this.consul._defaults);
19
+
20
+ const req = {
21
+ name: "config.list",
22
+ path: "/config/{kind}",
23
+ params: { kind: opts.kind },
24
+ query: {},
25
+ };
26
+
27
+ if (!opts.kind) {
28
+ throw this.consul._err(errors.Validation("kind required"), req);
29
+ }
30
+
31
+ utils.options(req, opts);
32
+
33
+ return await this.consul._get(req, utils.body);
34
+ }
35
+
36
+ /**
37
+ * Gets a config entry
38
+ */
39
+ async get(opts) {
40
+ opts = utils.normalizeKeys(opts);
41
+ opts = utils.defaults(opts, this.consul._defaults);
42
+
43
+ const req = {
44
+ name: "config.get",
45
+ path: "/config/{kind}/{name}",
46
+ params: { kind: opts.kind, name: opts.name },
47
+ query: {},
48
+ };
49
+
50
+ if (!opts.kind) {
51
+ throw this.consul._err(errors.Validation("kind required"), req);
52
+ }
53
+ if (!opts.name) {
54
+ throw this.consul._err(errors.Validation("name required"), req);
55
+ }
56
+
57
+ utils.options(req, opts);
58
+
59
+ return await this.consul._get(req, utils.body);
60
+ }
61
+
62
+ /**
63
+ * Creates or updates a config entry
64
+ */
65
+ async set(entry, opts) {
66
+ opts = utils.normalizeKeys(opts);
67
+ opts = utils.defaults(opts, this.consul._defaults);
68
+
69
+ const req = {
70
+ name: "config.set",
71
+ path: "/config",
72
+ query: {},
73
+ type: "json",
74
+ body: entry,
75
+ };
76
+
77
+ if (!entry) {
78
+ throw this.consul._err(errors.Validation("entry required"), req);
79
+ }
80
+
81
+ utils.options(req, opts);
82
+
83
+ return await this.consul._put(req, utils.body);
84
+ }
85
+
86
+ /**
87
+ * Deletes a config entry
88
+ */
89
+ async destroy(opts) {
90
+ opts = utils.normalizeKeys(opts);
91
+ opts = utils.defaults(opts, this.consul._defaults);
92
+
93
+ const req = {
94
+ name: "config.destroy",
95
+ path: "/config/{kind}/{name}",
96
+ params: { kind: opts.kind, name: opts.name },
97
+ query: {},
98
+ };
99
+
100
+ if (!opts.kind) {
101
+ throw this.consul._err(errors.Validation("kind required"), req);
102
+ }
103
+ if (!opts.name) {
104
+ throw this.consul._err(errors.Validation("name required"), req);
105
+ }
106
+
107
+ utils.options(req, opts);
108
+
109
+ return await this.consul._delete(req, utils.body);
110
+ }
111
+
112
+ delete() {
113
+ return this.destroy.apply(this, arguments);
114
+ }
115
+ }
116
+
117
+ exports.Config = Config;
package/lib/consul.d.ts CHANGED
@@ -3,11 +3,20 @@ import { Agent as httpsAgent } from "https";
3
3
  import { Acl } from "./acl";
4
4
  import { Agent } from "./agent";
5
5
  import { Catalog } from "./catalog";
6
+ import { Config } from "./config";
6
7
  import { Event } from "./event";
7
8
  import { Health } from "./health";
8
9
  import { Intention } from "./intention";
9
10
  import { Kv } from "./kv";
10
11
  import { Query } from "./query";
12
+ import {
13
+ Resolver,
14
+ ConsulResolverConfig,
15
+ SelectionAlgorithm,
16
+ ServiceInfo,
17
+ OptimalServiceResult,
18
+ ServiceMetrics,
19
+ } from "./resolver";
11
20
  import { Session } from "./session";
12
21
  import { Status } from "./status";
13
22
  import { Transaction } from "./transaction";
@@ -43,6 +52,7 @@ declare class Consul {
43
52
  acl: Acl;
44
53
  agent: Agent;
45
54
  catalog: Catalog;
55
+ config: Config;
46
56
  event: Event;
47
57
  health: Health;
48
58
  intention: Intention;
@@ -55,11 +65,13 @@ declare class Consul {
55
65
  static Acl: typeof Acl;
56
66
  static Agent: typeof Agent;
57
67
  static Catalog: typeof Catalog;
68
+ static Config: typeof Config;
58
69
  static Event: typeof Event;
59
70
  static Health: typeof Health;
60
71
  static Intention: typeof Intention;
61
72
  static Kv: typeof Kv;
62
73
  static Query: typeof Query;
74
+ static Resolver: typeof Resolver;
63
75
  static Session: typeof Session;
64
76
  static Status: typeof Status;
65
77
  static Transaction: typeof Transaction;
@@ -68,6 +80,17 @@ declare class Consul {
68
80
  destroy(): void;
69
81
 
70
82
  watch(options: WatchOptions): Watch;
83
+
84
+ resolver(config: ConsulResolverConfig): Resolver;
71
85
  }
72
86
 
87
+ export {
88
+ SelectionAlgorithm,
89
+ ConsulResolverConfig,
90
+ ServiceInfo,
91
+ OptimalServiceResult,
92
+ ServiceMetrics,
93
+ Resolver,
94
+ };
95
+
73
96
  export { Consul };
package/lib/consul.js CHANGED
@@ -3,11 +3,13 @@ const papi = require("papi");
3
3
  const Acl = require("./acl").Acl;
4
4
  const Agent = require("./agent").Agent;
5
5
  const Catalog = require("./catalog").Catalog;
6
+ const Config = require("./config").Config;
6
7
  const Event = require("./event").Event;
7
8
  const Health = require("./health").Health;
8
9
  const Intention = require("./intention").Intention;
9
10
  const Kv = require("./kv").Kv;
10
11
  const Query = require("./query").Query;
12
+ const Resolver = require("./resolver").Resolver;
11
13
  const Session = require("./session").Session;
12
14
  const Status = require("./status").Status;
13
15
  const Watch = require("./watch").Watch;
@@ -51,6 +53,7 @@ class Consul extends papi.Client {
51
53
  this.acl = new Consul.Acl(this);
52
54
  this.agent = new Consul.Agent(this);
53
55
  this.catalog = new Consul.Catalog(this);
56
+ this.config = new Consul.Config(this);
54
57
  this.event = new Consul.Event(this);
55
58
  this.health = new Consul.Health(this);
56
59
  this.intention = new Consul.Intention(this);
@@ -71,6 +74,10 @@ class Consul extends papi.Client {
71
74
  return new Consul.Watch(this, opts);
72
75
  }
73
76
 
77
+ resolver(config) {
78
+ return new Consul.Resolver(this, config);
79
+ }
80
+
74
81
  static parseQueryMeta(res) {
75
82
  return utils.parseQueryMeta(res);
76
83
  }
@@ -79,11 +86,13 @@ class Consul extends papi.Client {
79
86
  Consul.Acl = Acl;
80
87
  Consul.Agent = Agent;
81
88
  Consul.Catalog = Catalog;
89
+ Consul.Config = Config;
82
90
  Consul.Event = Event;
83
91
  Consul.Health = Health;
84
92
  Consul.Intention = Intention;
85
93
  Consul.Kv = Kv;
86
94
  Consul.Query = Query;
95
+ Consul.Resolver = Resolver;
87
96
  Consul.Session = Session;
88
97
  Consul.Status = Status;
89
98
  Consul.Transaction = Transaction;
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ function roundRobinSelection(services, currentIndex) {
4
+ const healthyServices = services.filter((service) =>
5
+ service.Checks.every((check) => check.Status === "passing"),
6
+ );
7
+
8
+ if (healthyServices.length === 0) {
9
+ throw new Error("No healthy services available");
10
+ }
11
+
12
+ const service = healthyServices[currentIndex % healthyServices.length];
13
+ const nextIndex = (currentIndex + 1) % healthyServices.length;
14
+
15
+ return {
16
+ id: service.Service.ID,
17
+ service,
18
+ nextIndex,
19
+ };
20
+ }
21
+
22
+ function leastConnectionSelection(services, metrics, defaultMetrics) {
23
+ const healthyServices = services
24
+ .filter((service) =>
25
+ service.Checks.every((check) => check.Status === "passing"),
26
+ )
27
+ .map((service) => {
28
+ const serviceMetrics = metrics.get(service.Service.ID) || defaultMetrics;
29
+ return {
30
+ service,
31
+ connections: serviceMetrics.activeConnections || 0,
32
+ };
33
+ });
34
+
35
+ if (healthyServices.length === 0) {
36
+ throw new Error("No healthy services available");
37
+ }
38
+
39
+ const selectedService = healthyServices.reduce((min, current) =>
40
+ current.connections < min.connections ? current : min,
41
+ );
42
+
43
+ return {
44
+ id: selectedService.service.Service.ID,
45
+ service: selectedService.service,
46
+ };
47
+ }
48
+
49
+ function weightedRandomSelection(rankedServices) {
50
+ if (rankedServices.length === 0) {
51
+ throw new Error("No services available for selection");
52
+ }
53
+
54
+ const totalScore = rankedServices.reduce(
55
+ (sum, service) => sum + service.score,
56
+ 0,
57
+ );
58
+
59
+ if (totalScore <= 0) {
60
+ return {
61
+ id: rankedServices[0].id,
62
+ service: rankedServices[0].service,
63
+ };
64
+ }
65
+
66
+ let random = Math.random() * totalScore;
67
+
68
+ for (const service of rankedServices) {
69
+ random -= service.score;
70
+ if (random <= 0) {
71
+ return {
72
+ id: service.id,
73
+ service: service.service,
74
+ };
75
+ }
76
+ }
77
+
78
+ return {
79
+ id: rankedServices[0].id,
80
+ service: rankedServices[0].service,
81
+ };
82
+ }
83
+
84
+ function roundRobinSrvSelection(records, currentIndex) {
85
+ if (!records || records.length === 0) {
86
+ return null;
87
+ }
88
+
89
+ const selected = records[currentIndex % records.length];
90
+ const nextIndex = (currentIndex + 1) % records.length;
91
+
92
+ return { selected, nextIndex };
93
+ }
94
+
95
+ function weightedSrvRecordSelection(records, currentIndex) {
96
+ if (!records || records.length === 0) {
97
+ return null;
98
+ }
99
+
100
+ const hasNonZeroWeights = records.some((record) => (record.weight || 0) > 0);
101
+
102
+ if (!hasNonZeroWeights) {
103
+ return roundRobinSrvSelection(records, currentIndex);
104
+ }
105
+
106
+ const totalWeight = records.reduce(
107
+ (sum, record) => sum + (record.weight || 1),
108
+ 0,
109
+ );
110
+
111
+ let random = Math.random() * totalWeight;
112
+
113
+ for (const record of records) {
114
+ random -= record.weight || 1;
115
+ if (random <= 0) {
116
+ return { selected: record, nextIndex: currentIndex };
117
+ }
118
+ }
119
+
120
+ return { selected: records[0], nextIndex: currentIndex };
121
+ }
122
+
123
+ exports.roundRobinSelection = roundRobinSelection;
124
+ exports.leastConnectionSelection = leastConnectionSelection;
125
+ exports.weightedRandomSelection = weightedRandomSelection;
126
+ exports.roundRobinSrvSelection = roundRobinSrvSelection;
127
+ exports.weightedSrvRecordSelection = weightedSrvRecordSelection;