Source code for duct.outputs.nats
"""
.. module:: nats
:synopsis: Output which sends events to NATS topics
.. moduleauthor:: Colin Alston <colin@tamvera.com>
"""
import logging
import asyncio
import ssl
import nats
from nats.aio.client import Client as NATS
from duct.objects import Output, Event
from duct.protocol.senml import event_to_senml, event_to_senml_cbor, event_to_json
log = logging.getLogger(__name__)
[docs]
class Nats(Output):
"""NATS output
:param servers: List of NATS URIs (default: ["nats://localhost:4222"])
:type servers: list
:param prefix: Prefix added to topics (default: "")
:type prefix: str
:param format: Serialisation format - json, senml-json, senml-cbor (default: senml-json)
:type format: str
:param interval: Queue drain interval in seconds (default: 1.0)
:type interval: float
:param jetstream: Publish via JetStream instead of core NATS (default: false)
:type jetstream: bool
:param credentials_file: Path to NATS credentials (.creds) file
:type credentials_file: str
:param nkey_seed_file: Path to NKey seed file
:type nkey_seed_file: str
:param tls_ca_file: Path to CA certificate file for TLS
:type tls_ca_file: str
:param tls_cert_file: Path to client certificate file for mTLS
:type tls_cert_file: str
:param tls_key_file: Path to client private key file for mTLS
:type tls_key_file: str
"""
def __init__(self, *a, **kw):
super().__init__(*a, **kw)
self._tick_task = None
self.servers = self.config.get("servers", ["nats://localhost:4222"])
self.prefix = self.config.get("prefix", "")
self.format = self.config.get("format", "senml-json")
self.inter = float(self.config.get("interval", 1.0))
self.use_jetstream = bool(self.config.get("jetstream", False))
self.credentials_file = self.config.get("credentials_file")
self.nkey_seed_file = self.config.get("nkey_seed_file")
self.tls_ca_file = self.config.get("tls_ca_file")
self.tls_cert_file = self.config.get("tls_cert_file")
self.tls_key_file = self.config.get("tls_key_file")
self.transformers = {
"senml-json": event_to_senml,
"senml-cbor": event_to_senml_cbor,
"json": event_to_json,
}
self.nc = None
self.js = None
def _build_tls_context(self):
if not (self.tls_ca_file or self.tls_cert_file):
return None
ctx = ssl.create_default_context()
if self.tls_ca_file:
ctx.load_verify_locations(cafile=self.tls_ca_file)
if self.tls_cert_file and self.tls_key_file:
ctx.load_cert_chain(certfile=self.tls_cert_file, keyfile=self.tls_key_file)
return ctx
async def _on_disconnect(self):
log.warning("NATS disconnected")
async def _on_reconnect(self):
log.info("NATS reconnected")
[docs]
async def stop(self):
if self._tick_task:
self._tick_task.cancel()
try:
await self._tick_task
except asyncio.CancelledError:
pass
if self.nc:
log.info("Disconnecting NATS")
await self.nc.drain()
await self.nc.close()
async def _tick(self):
if not self.events:
return
events = self.events
self.events = []
await self.sendEvents(events)
def _transform_event(self, event: Event) -> bytes:
return self.transformers[self.format](event)
[docs]
async def sendEvents(self, events: list[Event]):
"Send batches of events to NATS or JetStream"
for ev in events:
if self.prefix:
topic = f"{self.prefix}.{ev.hostname}.{ev.service}"
else:
topic = f"{ev.hostname}.{ev.service}"
payload = self._transform_event(ev)
try:
if self.use_jetstream:
await self.js.publish(topic, payload)
else:
await self.nc.publish(topic, payload)
except Exception:
log.exception("Failed to publish event to topic %s", topic)
async def _drain_loop(self):
try:
while True:
await asyncio.sleep(self.inter)
await self._tick()
except asyncio.CancelledError:
pass
[docs]
async def createClient(self) -> NATS:
log.info("Connecting to NATS: %s", self.servers)
connect_kwargs = dict(
servers=self.servers,
allow_reconnect=True,
max_reconnect_attempts=-1,
reconnect_time_wait=1,
disconnected_cb=self._on_disconnect,
reconnected_cb=self._on_reconnect,
)
tls_ctx = self._build_tls_context()
if tls_ctx:
connect_kwargs["tls"] = tls_ctx
if self.credentials_file:
connect_kwargs["user_credentials"] = self.credentials_file
elif self.nkey_seed_file:
connect_kwargs["nkeys_seed"] = self.nkey_seed_file
self.nc = await nats.connect(**connect_kwargs)
if self.use_jetstream:
self.js = self.nc.jetstream()
log.info("Connected to NATS (JetStream mode)")
else:
log.info("Connected to NATS")
self._tick_task = asyncio.create_task(self._drain_loop())