qdb-cloudwatch 3.14.2__py3-none-any.whl

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.
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from .driver import main
4
+
5
+ main()
@@ -0,0 +1,177 @@
1
+ import copy
2
+ import json
3
+ import logging
4
+ import random
5
+ import re
6
+ import uuid
7
+
8
+ import quasardb
9
+ import quasardb.stats as qdbst
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def _slurp(x):
15
+ with open(x, "r") as fp:
16
+ return fp.read()
17
+
18
+
19
+ def _parse_user_security_file(x):
20
+ with open(x, "r") as fp:
21
+ parsed = json.load(fp)
22
+ return (parsed["username"], parsed["secret_key"])
23
+
24
+
25
+ def get_qdb_conn(uri, cluster_public_key=None, user_security_file=None):
26
+ logger.info("Getting qdb connection")
27
+ if cluster_public_key and user_security_file:
28
+ user, private_key = _parse_user_security_file(user_security_file)
29
+ public_key = _slurp(cluster_public_key)
30
+ return quasardb.Cluster(
31
+ uri,
32
+ user_name=user,
33
+ user_private_key=private_key,
34
+ cluster_public_key=public_key,
35
+ )
36
+ else:
37
+ return quasardb.Cluster(uri)
38
+
39
+
40
+ def _check_node_online(conn):
41
+ logger.info("Checking node online")
42
+ ret = {}
43
+
44
+ for endpoint in conn.endpoints():
45
+ ret[endpoint] = 0 # pessimistic
46
+ node = conn.node(endpoint)
47
+ entry = node.integer("$qdb.statistics.startup_epoch") # entry always exists
48
+
49
+ try:
50
+ entry.get()
51
+ ret[endpoint] = 1
52
+ except quasardb.Error as e:
53
+ logger.error(f"[{endpoint}] Failed to read sample entry: {e}")
54
+
55
+ return ret
56
+
57
+
58
+ def _check_node_writable(conn):
59
+ logger.info("Checking node writable")
60
+ key = f"_qdb_write_check_{uuid.uuid4().hex}" # almost zero chance of collision
61
+ value = random.randint(-9223372036854775808, 9223372036854775807)
62
+ ret = {}
63
+
64
+ for endpoint in conn.endpoints():
65
+ ret[endpoint] = 0 # pessimistic
66
+ node = conn.node(endpoint)
67
+ entry = node.integer(key)
68
+
69
+ try:
70
+ entry.put(value)
71
+ if entry.get() == value:
72
+ ret[endpoint] = 1
73
+ except quasardb.Error as e:
74
+ logger.error(f"[{endpoint}] Failed to put/get test entry '{key}': {e}")
75
+ finally:
76
+ try:
77
+ entry.remove()
78
+ except quasardb.AliasNotFoundError:
79
+ pass
80
+ except quasardb.Error as e:
81
+ logger.error(f"[{endpoint}] Failed to clean up test entry '{key}': {e}")
82
+
83
+ return ret
84
+
85
+
86
+ def get_critical_stats(*args, **kwargs):
87
+ """
88
+ Return the minimal set of cluster health metrics required for alerting.
89
+
90
+ These metrics (i.e., `check.online` and `node.writable`) are the ones that
91
+ feed CloudWatch alarms and signal conditions that require immediate action.
92
+ By contrast, high-volume or informational metrics (cache usage, RocksDB
93
+ internals, etc.) are non-critical because they are (usually) not part of the
94
+ alerting path.
95
+
96
+ Future extensions may allow users to define their own critical metrics.
97
+ """
98
+ logger.info("Getting critical stats")
99
+ with get_qdb_conn(*args, **kwargs) as conn:
100
+ ret = {
101
+ endpoint: {"cumulative": {}, "by_uid": {}} for endpoint in conn.endpoints()
102
+ }
103
+ online_stats = _check_node_online(conn)
104
+ writable_stats = _check_node_writable(conn)
105
+
106
+ for endpoint in conn.endpoints():
107
+ ret[endpoint]["cumulative"]["check.online"] = {
108
+ "value": online_stats.get(endpoint, 0),
109
+ "type": qdbst.Type.GAUGE,
110
+ "unit": qdbst.Unit.NONE,
111
+ }
112
+ ret[endpoint]["cumulative"]["node.writable"] = {
113
+ "value": writable_stats.get(endpoint, 0),
114
+ "type": qdbst.Type.GAUGE,
115
+ "unit": qdbst.Unit.NONE,
116
+ }
117
+
118
+ return ret
119
+
120
+
121
+ def get_all_stats(*args, **kwargs):
122
+ logger.info("Getting all the stats")
123
+ with get_qdb_conn(*args, **kwargs) as conn:
124
+ ret = qdbst.by_node(conn)
125
+ return ret
126
+
127
+
128
+ def _do_filter_metrics(metrics, fn):
129
+ return {key: metrics[key] for key in metrics if fn(key)}
130
+
131
+
132
+ def _do_filter(stats, fn):
133
+ """
134
+ Performs actual filtering of stats, keeping only those where fn(name) equals True
135
+ """
136
+
137
+ for node_id in stats:
138
+ for group_id in stats[node_id]:
139
+ if group_id == "cumulative":
140
+ stats[node_id][group_id] = _do_filter_metrics(
141
+ stats[node_id][group_id], fn
142
+ )
143
+ elif group_id == "by_uid":
144
+ for uid in stats[node_id][group_id]:
145
+ stats[node_id][group_id][uid] = _do_filter_metrics(
146
+ stats[node_id][group_id][uid], fn
147
+ )
148
+ else:
149
+ raise RuntimeError(
150
+ "Internal error: unrecognized stats group id: {}".format(group_id)
151
+ )
152
+ return stats
153
+
154
+
155
+ def filter_stats(stats, include=None, exclude=None):
156
+ logger.info("Filtering stats based on include/exclude filters")
157
+ stats_ = copy.deepcopy(stats)
158
+
159
+ if include is not None:
160
+ # Returns `true` if any of the `include` patterns is found in the metric name.
161
+ def _filter_include(metric_name):
162
+ return any(
163
+ pattern for pattern in include if re.search(pattern, metric_name)
164
+ )
165
+
166
+ stats_ = _do_filter(stats_, _filter_include)
167
+
168
+ if exclude is not None:
169
+ # Returns `false` if any of the `exclude` patterns is found in the metric name.
170
+ def _filter_exclude(metric_name):
171
+ return not any(
172
+ pattern for pattern in exclude if re.search(pattern, metric_name)
173
+ )
174
+
175
+ stats_ = _do_filter(stats_, _filter_exclude)
176
+
177
+ return stats_
@@ -0,0 +1,92 @@
1
+ import logging
2
+
3
+ import boto3
4
+ from quasardb.stats import Unit
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ _stat_unit_to_cloudwatch_unit = {
9
+ Unit.NONE: "None",
10
+ Unit.COUNT: "Count",
11
+ Unit.BYTES: "Bytes",
12
+ Unit.EPOCH: "None",
13
+ Unit.NANOSECONDS: "None",
14
+ Unit.MICROSECONDS: "Microseconds",
15
+ Unit.MILLISECONDS: "Milliseconds",
16
+ Unit.SECONDS: "Seconds",
17
+ }
18
+
19
+
20
+ def _get_client():
21
+ logger.info("Getting cloudwatch client")
22
+ return boto3.client("cloudwatch")
23
+
24
+
25
+ def _coerce_metric(k, v):
26
+ if k.startswith("cpu."):
27
+ # We don't expose CPU metrics through Cloudwatch, as this is already collected
28
+ # by the regular metrics.
29
+ return None
30
+
31
+ if v["unit"] == Unit.NANOSECONDS:
32
+ v["unit"] = Unit.MICROSECONDS
33
+ v["value"] /= 1000
34
+
35
+ return (_stat_unit_to_cloudwatch_unit.get(v["unit"], "None"), float(v["value"]))
36
+
37
+
38
+ def _to_metric(k, v):
39
+ try:
40
+ x = _coerce_metric(k, v)
41
+ if x:
42
+ (u, v_) = x
43
+ return {"MetricName": k, "Value": v_, "Unit": u}
44
+ except:
45
+ logger.debug(f"The key '{k}' cannot be sent")
46
+ return None
47
+
48
+
49
+ def _qdb_to_cloudwatch(stats):
50
+ # We want to flatten all metrics into a tuple of 3 items:
51
+ # - node_id
52
+ # - user_id
53
+ # - measurement
54
+
55
+ ret = list()
56
+
57
+ for node_id, xs in stats.items():
58
+ for user_id, xs_ in xs["by_uid"].items():
59
+ dims = [
60
+ {"Name": "UserId", "Value": str(user_id)},
61
+ {"Name": "NodeId", "Value": str(node_id)},
62
+ ]
63
+ for k, v in xs_.items():
64
+ m = _to_metric(k, v)
65
+ if m:
66
+ m["Dimensions"] = dims
67
+ ret.append(m)
68
+
69
+ dims = [{"Name": "NodeId", "Value": str(node_id)}]
70
+ for k, v in xs["cumulative"].items():
71
+ m = _to_metric(k, v)
72
+ if m:
73
+ m["Dimensions"] = dims
74
+ ret.append(m)
75
+
76
+ return ret
77
+
78
+
79
+ def push_stats(stats, namespace):
80
+ client = _get_client()
81
+ stats_ = _qdb_to_cloudwatch(stats)
82
+
83
+ metrics_per_req = 20
84
+ metrics = [
85
+ stats_[i : i + metrics_per_req] for i in range(0, len(stats_), metrics_per_req)
86
+ ]
87
+
88
+ logger.info(f"Pushing {len(stats_)} metrics")
89
+ for metric in metrics:
90
+ _ = client.put_metric_data(Namespace=namespace, MetricData=metric)
91
+
92
+ logger.info(f"Pushed {len(stats_)} metrics")
@@ -0,0 +1,106 @@
1
+ import argparse
2
+ import logging
3
+ import sys
4
+
5
+ from .check import filter_stats, get_all_stats, get_critical_stats
6
+ from .cloudwatch import push_stats
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def _parse_list(x):
12
+ """
13
+ Parses a comma-separated string into a list.
14
+ """
15
+
16
+ if x is None or not x.strip():
17
+ return None
18
+
19
+ return [token.strip() for token in x.split(",") if token.strip()]
20
+
21
+
22
+ def get_args():
23
+ parser = argparse.ArgumentParser(
24
+ description=("Fetch QuasarDB metrics for local node and export to CloudWatch.")
25
+ )
26
+ parser.add_argument(
27
+ "--cluster",
28
+ dest="cluster_uri",
29
+ help="QuasarDB cluster uri to connect to. Defaults to qdb://127.0.0.1:2836",
30
+ default="qdb://127.0.0.1:2836",
31
+ )
32
+
33
+ parser.add_argument(
34
+ "--cluster-public-key",
35
+ dest="cluster_public_key",
36
+ help="Cluster public key file",
37
+ )
38
+
39
+ parser.add_argument(
40
+ "--user-security-file",
41
+ dest="user_security_file",
42
+ help="User security file, containing both username and private access token.",
43
+ )
44
+
45
+ parser.add_argument(
46
+ "--node-id",
47
+ dest="node_id",
48
+ help="Node id to collect metrics from, e.g. 0-0-0-1",
49
+ )
50
+
51
+ parser.add_argument(
52
+ "--namespace",
53
+ dest="namespace",
54
+ help="Namespace for metrics",
55
+ default="QuasarDB",
56
+ )
57
+
58
+ parser.add_argument(
59
+ "--filter-include",
60
+ dest="filter_include",
61
+ help="Optional comma-separated list of regex patterns to filter metrics. Only metrics that match at least one of the patterns will be reported.",
62
+ )
63
+
64
+ parser.add_argument(
65
+ "--filter-exclude",
66
+ dest="filter_exclude",
67
+ help="Optional comma-separated list of regex patterns to filter metrics. Only metrics that contain none of the patterns will be reported.",
68
+ )
69
+
70
+ ret = parser.parse_args()
71
+
72
+ ret.filter_include = _parse_list(ret.filter_include)
73
+ ret.filter_exclude = _parse_list(ret.filter_exclude)
74
+
75
+ if ret.filter_include is not None:
76
+ logger.info(f"Using include filters: {ret.filter_include}")
77
+
78
+ if ret.filter_exclude is not None:
79
+ logger.info(f"Using exclude filters: {ret.filter_exclude}")
80
+
81
+ return ret
82
+
83
+
84
+ def main():
85
+ logging.basicConfig(stream=sys.stderr, level=logging.INFO)
86
+
87
+ args = get_args()
88
+
89
+ # Send critical stats first, as getting all stats is expensive when cluster is busy.
90
+ critical_stats = get_critical_stats(
91
+ args.cluster_uri,
92
+ cluster_public_key=args.cluster_public_key,
93
+ user_security_file=args.user_security_file,
94
+ )
95
+ push_stats(critical_stats, args.namespace)
96
+
97
+ stats = get_all_stats(
98
+ args.cluster_uri,
99
+ cluster_public_key=args.cluster_public_key,
100
+ user_security_file=args.user_security_file,
101
+ )
102
+ stats = filter_stats(
103
+ stats, include=args.filter_include, exclude=args.filter_exclude
104
+ )
105
+
106
+ push_stats(stats, args.namespace)
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: qdb-cloudwatch
3
+ Version: 3.14.2
4
+ Summary: Export QuasarDB statistics to AWS Cloudwatch
5
+ License-File: LICENSE.md
6
+ Requires-Dist: boto3>=1.9
7
+ Requires-Dist: quasardb==3.14.2
8
+ Provides-Extra: pandas
9
+ Requires-Dist: pandas; extra == "pandas"
10
+ Provides-Extra: tests
11
+ Requires-Dist: pytest>=6.2.5; extra == "tests"
12
+ Requires-Dist: pytest-runner>=5.3.1; extra == "tests"
13
+ Requires-Dist: teamcity-messages>=1.29; extra == "tests"
14
+ Dynamic: license-file
15
+ Dynamic: provides-extra
16
+ Dynamic: requires-dist
17
+ Dynamic: summary
@@ -0,0 +1,10 @@
1
+ qdb_cloudwatch/__main__.py,sha256=lmRCPGcwYGFyA6gjdtrSHcUSnkh_XvpkvFPQaYxQyq4,58
2
+ qdb_cloudwatch/check.py,sha256=IrOe7BRZF5WE7bwj_QFjilWsVjruk2c43BMxD5ZGy5M,5538
3
+ qdb_cloudwatch/cloudwatch.py,sha256=6sut8Ak568Cf_Nb1E-GaZL6SYHXKlzxxzCnHKtm1M4s,2376
4
+ qdb_cloudwatch/driver.py,sha256=3_tRFRARyatJBaqLmIgKe3_AmYfknO448hxUNn_yZvw,2946
5
+ qdb_cloudwatch-3.14.2.dist-info/licenses/LICENSE.md,sha256=fRy4x7atA0XNtRPb3BoZSxxkkUaGzVPmNkqKrpTXW1c,1466
6
+ qdb_cloudwatch-3.14.2.dist-info/METADATA,sha256=vyzot_NvSmoL2bwWnLNR6TyvM7ORNmWifeyu8FIICDs,526
7
+ qdb_cloudwatch-3.14.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ qdb_cloudwatch-3.14.2.dist-info/entry_points.txt,sha256=Bd9rDwsoDrlpU6o_hpRP4N5BuzY92CK7Jf7ZkVlf-fw,62
9
+ qdb_cloudwatch-3.14.2.dist-info/top_level.txt,sha256=MtJaeN-kaZ96VKDZJhPTr5bL6Kj_3pgU1-iK6mSnuGI,15
10
+ qdb_cloudwatch-3.14.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ qdb-cloudwatch = qdb_cloudwatch.driver:main
@@ -0,0 +1,11 @@
1
+ Copyright (c) 2009-2019, quasardb SAS All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ Neither the name of quasardb nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ qdb_cloudwatch