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.
- qdb_cloudwatch/__main__.py +5 -0
- qdb_cloudwatch/check.py +177 -0
- qdb_cloudwatch/cloudwatch.py +92 -0
- qdb_cloudwatch/driver.py +106 -0
- qdb_cloudwatch-3.14.2.dist-info/METADATA +17 -0
- qdb_cloudwatch-3.14.2.dist-info/RECORD +10 -0
- qdb_cloudwatch-3.14.2.dist-info/WHEEL +5 -0
- qdb_cloudwatch-3.14.2.dist-info/entry_points.txt +2 -0
- qdb_cloudwatch-3.14.2.dist-info/licenses/LICENSE.md +11 -0
- qdb_cloudwatch-3.14.2.dist-info/top_level.txt +1 -0
qdb_cloudwatch/check.py
ADDED
|
@@ -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")
|
qdb_cloudwatch/driver.py
ADDED
|
@@ -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,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
|