ducted 2.1__tar.gz → 2.2__tar.gz
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.
- {ducted-2.1 → ducted-2.2}/PKG-INFO +1 -1
- {ducted-2.1 → ducted-2.2}/README.md +13 -0
- {ducted-2.1 → ducted-2.2}/duct/outputs/nats.py +18 -4
- {ducted-2.1 → ducted-2.2}/duct/sources/nats.py +6 -0
- {ducted-2.1 → ducted-2.2}/duct/tests/test_outputs.py +25 -0
- {ducted-2.1 → ducted-2.2}/ducted.egg-info/PKG-INFO +1 -1
- {ducted-2.1 → ducted-2.2}/ducted.egg-info/top_level.txt +1 -0
- {ducted-2.1 → ducted-2.2}/pyproject.toml +1 -1
- {ducted-2.1 → ducted-2.2}/LICENSE +0 -0
- {ducted-2.1 → ducted-2.2}/docs/conf.py +0 -0
- {ducted-2.1 → ducted-2.2}/docs/update_modules.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/__main__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/aggregators.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/configuration.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/ihateprotobuf/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/ihateprotobuf/proto_pb2.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/interfaces.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/logs/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/logs/follower.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/logs/parsers.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/objects.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/outputs/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/outputs/bosun.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/outputs/elasticsearch.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/outputs/graphite.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/outputs/influxdb3.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/outputs/logger.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/outputs/null.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/outputs/opentsdb.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/outputs/prometheus.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/outputs/riemann.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/elasticsearch.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/i2c.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/icmp.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/influxdb3.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/opentsdb.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/riemann.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/senml.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/sflow/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/sflow/protocol/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/sflow/protocol/counters.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/sflow/protocol/flows.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/sflow/protocol/protocol.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/sflow/protocol/utils.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/sflow/protocol/xdr.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/sflow/server.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/protocol/ssh.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/service.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/apache.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/database/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/database/elasticsearch.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/database/memcache.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/database/postgresql.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/docker.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/generator.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/haproxy.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/linux/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/linux/basic.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/linux/ipsec.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/linux/process.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/linux/sensors.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/mail.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/media/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/media/libav.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/munin.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/network.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/nginx.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/postfix.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/power/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/power/nut.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/power/raritan_px.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/python/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/python/uwsgi.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/rabbitmq.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/redis.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/riak.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/riemann.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/sensors/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/sensors/environment/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/sensors/environment/ds18b20.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/sensors/environment/mpl115.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/sflow.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/snmp.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/sources/unbound.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/tests/__init__.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/tests/globs.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/tests/helpers.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/tests/test_duct.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/tests/test_logs.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/tests/test_protocol.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/tests/test_service.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/tests/test_sflow.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/tests/test_sources.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/tests/test_ssh.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/tests/test_utils.py +0 -0
- {ducted-2.1 → ducted-2.2}/duct/utils.py +0 -0
- {ducted-2.1 → ducted-2.2}/ducted.egg-info/SOURCES.txt +0 -0
- {ducted-2.1 → ducted-2.2}/ducted.egg-info/dependency_links.txt +0 -0
- {ducted-2.1 → ducted-2.2}/ducted.egg-info/entry_points.txt +0 -0
- {ducted-2.1 → ducted-2.2}/ducted.egg-info/requires.txt +0 -0
- {ducted-2.1 → ducted-2.2}/setup.cfg +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Duct
|
|
2
2
|
|
|
3
|
+
I want to talk to you about ducts. Do your ducts seem old-fashioned? Out of date?
|
|
4
|
+
|
|
3
5
|
[](https://github.com/Tamvera/ducted/actions/workflows/ci.yml)
|
|
4
6
|
[](https://tamvera.github.io/ducted/)
|
|
5
7
|
|
|
@@ -31,6 +33,17 @@ Duct is an asyncio daemon that polls *sources* on configurable intervals and rou
|
|
|
31
33
|
|
|
32
34
|
## Installation
|
|
33
35
|
|
|
36
|
+
### System wide on Linux, with a systemd script
|
|
37
|
+
```bash
|
|
38
|
+
curl -o - https://raw.githubusercontent.com/Tamvera/ducted/refs/heads/master/scripts/install.sh | sh
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### With docker
|
|
42
|
+
```bash
|
|
43
|
+
docker run -i -v ./duct.yml:/duct/duct.yml ghcr.io/tamvera/ducted:latest
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### With pip
|
|
34
47
|
```bash
|
|
35
48
|
pip install ducted
|
|
36
49
|
```
|
|
@@ -27,6 +27,11 @@ class Nats(Output):
|
|
|
27
27
|
:param prefix: Prefix added to topics (default: "")
|
|
28
28
|
:type prefix: str
|
|
29
29
|
|
|
30
|
+
:param topic: Topic template, formatted with the event's fields, e.g.
|
|
31
|
+
"{hostname}.{service}" or "metrics.{attributes[region]}.{service}"
|
|
32
|
+
(default: "{hostname}.{service}")
|
|
33
|
+
:type topic: str
|
|
34
|
+
|
|
30
35
|
:param format: Serialisation format - json, senml-json, senml-cbor (default: senml-json)
|
|
31
36
|
:type format: str
|
|
32
37
|
|
|
@@ -59,6 +64,7 @@ class Nats(Output):
|
|
|
59
64
|
|
|
60
65
|
self.servers = self.config.get("servers", ["nats://localhost:4222"])
|
|
61
66
|
self.prefix = self.config.get("prefix", "")
|
|
67
|
+
self.topic = self.config.get("topic", "{hostname}.{service}")
|
|
62
68
|
self.format = self.config.get("format", "senml-json")
|
|
63
69
|
self.inter = float(self.config.get("interval", 1.0))
|
|
64
70
|
self.use_jetstream = bool(self.config.get("jetstream", False))
|
|
@@ -117,13 +123,21 @@ class Nats(Output):
|
|
|
117
123
|
def _transform_event(self, event: Event) -> bytes:
|
|
118
124
|
return self.transformers[self.format](event)
|
|
119
125
|
|
|
126
|
+
def _build_topic(self, event: Event) -> str:
|
|
127
|
+
topic = self.topic.format_map(vars(event))
|
|
128
|
+
if self.prefix:
|
|
129
|
+
topic = f"{self.prefix}.{topic}"
|
|
130
|
+
return topic
|
|
131
|
+
|
|
120
132
|
async def sendEvents(self, events: list[Event]):
|
|
121
133
|
"Send batches of events to NATS or JetStream"
|
|
122
134
|
for ev in events:
|
|
123
|
-
|
|
124
|
-
topic =
|
|
125
|
-
|
|
126
|
-
topic
|
|
135
|
+
try:
|
|
136
|
+
topic = self._build_topic(ev)
|
|
137
|
+
except (KeyError, IndexError, AttributeError, TypeError):
|
|
138
|
+
log.exception("Failed to build topic from %r for event %s",
|
|
139
|
+
self.topic, ev.service)
|
|
140
|
+
continue
|
|
127
141
|
payload = self._transform_event(ev)
|
|
128
142
|
try:
|
|
129
143
|
if self.use_jetstream:
|
|
@@ -41,6 +41,9 @@ class Nats(Source):
|
|
|
41
41
|
:param durable: Durable consumer name for JetStream subscriptions (default: "ducted")
|
|
42
42
|
:type durable: str
|
|
43
43
|
|
|
44
|
+
:param append_topic: Append the topic to the service name for events
|
|
45
|
+
:type append_topic: bool
|
|
46
|
+
|
|
44
47
|
:param credentials_file: Path to NATS credentials (.creds) file
|
|
45
48
|
:type credentials_file: str
|
|
46
49
|
|
|
@@ -66,6 +69,7 @@ class Nats(Source):
|
|
|
66
69
|
self.format = self.config.get("format", "senml-json")
|
|
67
70
|
self.use_jetstream = bool(self.config.get("jetstream", False))
|
|
68
71
|
self.durable = self.config.get("durable", "ducted")
|
|
72
|
+
self.append_topic = self.config.get("append_topic", False)
|
|
69
73
|
|
|
70
74
|
self.credentials_file = self.config.get("credentials_file")
|
|
71
75
|
self.nkey_seed_file = self.config.get("nkey_seed_file")
|
|
@@ -147,6 +151,8 @@ class Nats(Source):
|
|
|
147
151
|
async def _get_event(self, message):
|
|
148
152
|
try:
|
|
149
153
|
ev = self.transformers[self.format](message.data)
|
|
154
|
+
if self.append_topic:
|
|
155
|
+
ev.service = f"{message.subject}.{ev.service}"
|
|
150
156
|
self.queueBack([ev])
|
|
151
157
|
except Exception:
|
|
152
158
|
log.exception(
|
|
@@ -255,6 +255,31 @@ class TestNATSOutput:
|
|
|
255
255
|
assert decoded.tags == ["prod", "eu"]
|
|
256
256
|
assert decoded.attributes["chicken"] == "little"
|
|
257
257
|
|
|
258
|
+
@pytest.mark.asyncio
|
|
259
|
+
async def test_nats_output_topic_template(self, service):
|
|
260
|
+
ev = Event('ok', 'sky', 'Chicken alert', 2.0, 30.0,
|
|
261
|
+
tags=['prod'], attributes={"region": "eu"},
|
|
262
|
+
hostname='testhost')
|
|
263
|
+
nats_mod.nats = FakeNATS()
|
|
264
|
+
out = nats_mod.Nats({"topic": "{attributes[region]}.{state}.{service}",
|
|
265
|
+
"prefix": "duct"}, service)
|
|
266
|
+
await out.createClient()
|
|
267
|
+
await out.eventsReceived([ev])
|
|
268
|
+
await out._tick()
|
|
269
|
+
|
|
270
|
+
topic, _ = out.nc.messages[0]
|
|
271
|
+
assert topic == "duct.eu.ok.sky"
|
|
272
|
+
|
|
273
|
+
@pytest.mark.asyncio
|
|
274
|
+
async def test_nats_output_topic_template_missing_key(self, service, event):
|
|
275
|
+
nats_mod.nats = FakeNATS()
|
|
276
|
+
out = nats_mod.Nats({"topic": "{attributes[nope]}.{service}"}, service)
|
|
277
|
+
await out.createClient()
|
|
278
|
+
await out.eventsReceived([event])
|
|
279
|
+
await out._tick()
|
|
280
|
+
|
|
281
|
+
assert out.nc.messages == []
|
|
282
|
+
|
|
258
283
|
@pytest.mark.asyncio
|
|
259
284
|
async def test_nats_output_cbor(self, service, event):
|
|
260
285
|
nats_mod.nats = FakeNATS()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|