netflume 0.1.0__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.
netflume/__init__.py ADDED
@@ -0,0 +1,109 @@
1
+ """NetFlow v5, NetFlow v9 and IPFIX collection and parsing, as a library.
2
+
3
+ Built for programs rather than for terminals: a daemon writing flows to a
4
+ database, a process forwarding a subset of fields over MQTT, an analyser
5
+ replaying a capture. Standard library only, so it installs anywhere Python
6
+ does and adds nothing to a deployment's dependency surface.
7
+
8
+ Two layers, and either can be used without the other:
9
+
10
+ **Parsing.** :func:`~netflume.parse.parse_message` and friends turn
11
+ bytes into records. No socket, no clock the caller cannot control, no state
12
+ beyond the :class:`~netflume.parse.TemplateStore` you hand in. Feed it
13
+ from a pcap, a queue or a test fixture.
14
+
15
+ **Collection.** :class:`Collector` binds a UDP socket and yields what arrives::
16
+
17
+ from netflume import Collector
18
+
19
+ with Collector(port=2055) as collector:
20
+ for flow in collector.flows():
21
+ print(flow.src_addr, flow.dst_addr, flow.octets)
22
+
23
+ A flow is a plain dict of normalised keys, the same keys whichever version the
24
+ exporter speaks, and :class:`~netflume.flow.Flow` is an opt-in typed view over
25
+ that dict which resolves the field aliases and keeps the dict attached. The
26
+ README documents both shapes.
27
+
28
+ Nothing here prints. Sampling rates, export gaps and undecodable datagrams are
29
+ :mod:`~netflume.events` objects from
30
+ :meth:`~netflume.decoder.Decoder.take_events`, and are also written to
31
+ the ``netflume`` logger for callers who want no more than that. As a
32
+ library should, this package adds nothing but a NullHandler; configure a real
33
+ handler, or records go nowhere.
34
+
35
+ Hostname resolution is not here. It was an optional module that nothing else
36
+ in this package imported, and it now lives on its own as ``lanname``: reverse
37
+ DNS, mDNS and NetBIOS behind a cache, non-blocking, and off until a mode is
38
+ chosen. Install it alongside if a consumer wants names. Nothing here needs it.
39
+ """
40
+
41
+ import logging
42
+
43
+ from .collector import DEFAULT_PORT, DEFAULT_RCVBUF, Collector
44
+ from .decoder import Decoder, Message
45
+ from .events import DecodeError, ExportGap, SamplingChange
46
+ from .flow import METADATA_KEYS, MODELLED_FIELDS, Flow
47
+ from .ie import FLOW_END_REASON, IE, PROTO_NAMES, TCP_FLAG_BITS
48
+ from .parse import (
49
+ IPFIX_HDR,
50
+ SUPPORTED_VERSIONS,
51
+ V5_HDR,
52
+ V5_REC,
53
+ V9_HDR,
54
+ TemplateStore,
55
+ decode_value,
56
+ flow_duration,
57
+ flow_endpoints,
58
+ flow_timestamp,
59
+ parse_data_record,
60
+ parse_message,
61
+ parse_v5,
62
+ parse_v9_or_ipfix,
63
+ read_template_fields,
64
+ record_min_length,
65
+ )
66
+ from .sampling import SamplingWatch, sampling_rate
67
+ from .sequence import (
68
+ MAX_PLAUSIBLE_GAP,
69
+ MAX_REORDER,
70
+ RESYNC_AFTER,
71
+ SEQ_MODULUS,
72
+ SequenceWatch,
73
+ )
74
+ from .values import (
75
+ ADDR_KINDS,
76
+ addr_kind,
77
+ flow_end_reason_name,
78
+ proto_name,
79
+ service_name,
80
+ tcp_flags_str,
81
+ )
82
+
83
+ __version__ = "0.1.0"
84
+
85
+ # A library that logs to an unconfigured root logger prints to stderr, which is
86
+ # exactly what this package exists not to do.
87
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
88
+
89
+ __all__ = [
90
+ # collection
91
+ "Collector", "DEFAULT_PORT", "DEFAULT_RCVBUF", "Decoder", "Message",
92
+ # the flow shape
93
+ "Flow", "MODELLED_FIELDS", "METADATA_KEYS",
94
+ # parsing
95
+ "TemplateStore", "parse_message", "parse_v5", "parse_v9_or_ipfix",
96
+ "parse_data_record", "read_template_fields", "record_min_length",
97
+ "decode_value", "flow_endpoints", "flow_timestamp", "flow_duration",
98
+ "SUPPORTED_VERSIONS", "V5_HDR", "V5_REC", "V9_HDR", "IPFIX_HDR",
99
+ # what the exporter says about itself
100
+ "SamplingWatch", "sampling_rate", "SequenceWatch", "SEQ_MODULUS",
101
+ "MAX_PLAUSIBLE_GAP", "MAX_REORDER", "RESYNC_AFTER",
102
+ # events
103
+ "ExportGap", "SamplingChange", "DecodeError",
104
+ # field level helpers
105
+ "addr_kind", "ADDR_KINDS", "service_name", "proto_name", "tcp_flags_str",
106
+ "flow_end_reason_name", "IE", "PROTO_NAMES", "FLOW_END_REASON",
107
+ "TCP_FLAG_BITS",
108
+ "__version__",
109
+ ]
netflume/collector.py ADDED
@@ -0,0 +1,285 @@
1
+ """Binding the socket and handing back flows.
2
+
3
+ :class:`Collector` is a UDP socket, a :class:`~netflume.decoder.Decoder`
4
+ and three ways to get at what arrives:
5
+
6
+ * ``for rec, hdr in collector:`` gives plain dicts, the cheapest form
7
+ * ``for flow in collector.flows():`` gives typed objects, built on demand
8
+ * ``message = collector.poll(timeout)`` gives one datagram, or None, for a
9
+ caller who owns the event loop
10
+
11
+ It prints nothing, exits nothing, and reads no arguments. Everything it would
12
+ otherwise have to say arrives as an event from
13
+ :meth:`~netflume.decoder.Decoder.take_events` or on the
14
+ ``netflume`` logger.
15
+ """
16
+
17
+ import errno
18
+ import logging
19
+ import selectors
20
+ import socket
21
+ import time
22
+
23
+ from .decoder import Decoder
24
+
25
+ __all__ = ["Collector", "DEFAULT_PORT", "DEFAULT_RCVBUF"]
26
+
27
+ log = logging.getLogger(__name__)
28
+
29
+ #: The port every NetFlow exporter defaults to. Not registered to anything and
30
+ #: not privileged, so a daemon need not run as root.
31
+ DEFAULT_PORT = 2055
32
+
33
+ #: 4 MB of kernel receive buffer. UDP has no backpressure: a buffer that fills
34
+ #: while this process is busy drops datagrams silently and the only trace is a
35
+ #: sequence gap. Ask for more than the system default, and do not fail if the
36
+ #: system says no.
37
+ DEFAULT_RCVBUF = 4 * 1024 * 1024
38
+
39
+ #: The largest datagram to accept. A NetFlow export never approaches this;
40
+ #: it is simply the largest a UDP payload can be.
41
+ MAX_DATAGRAM = 65535
42
+
43
+
44
+ class Collector:
45
+ """A bound UDP socket that yields decoded NetFlow.
46
+
47
+ ::
48
+
49
+ with Collector(port=2055) as collector:
50
+ for rec, hdr in collector:
51
+ store(rec, hdr)
52
+
53
+ The socket is bound in the constructor, so a port already in use raises
54
+ OSError there rather than at first read. Use ``with``, or call
55
+ :meth:`close`, so the socket is released promptly; the alternative is a
56
+ port left held until the garbage collector gets round to it.
57
+
58
+ Iteration is endless by design: a collector with nothing to report is a
59
+ quiet network, not a finished job. Break out of the loop, or call
60
+ :meth:`stop` from a signal handler or another thread.
61
+ """
62
+
63
+ def __init__(self, port=DEFAULT_PORT, bind="0.0.0.0", decoder=None,
64
+ timeout=1.0, rcvbuf=DEFAULT_RCVBUF, reuse_address=True,
65
+ sock=None):
66
+ """Bind and get ready to receive.
67
+
68
+ port, bind where to listen. "0.0.0.0" is every IPv4 interface; name
69
+ one interface if the machine is multi-homed and only one
70
+ of them faces the exporters.
71
+ decoder an existing Decoder, if you want to share template state
72
+ with something else or to have turned tracking off.
73
+ timeout how long the blocking reads inside iteration wait before
74
+ coming up for air. It bounds how long :meth:`stop` takes
75
+ to be noticed; it is not a deadline on receiving.
76
+ rcvbuf kernel receive buffer to request, or None to leave the
77
+ system default alone.
78
+ reuse_address set SO_REUSEADDR before binding. Default True, which lets
79
+ a restart of this process take the port back immediately
80
+ instead of waiting for the old socket to clear. The cost
81
+ is that on UDP it also lets a *second* process bind the
82
+ same port, and then only one of them receives: a
83
+ collector that looks bound and healthy while another
84
+ process quietly takes its traffic. Pass False to make
85
+ that clash raise OSError here instead of going silent.
86
+ sock an already-bound socket to use instead of making one,
87
+ which is how a test drives this without a real exporter.
88
+ """
89
+ self.decoder = decoder if decoder is not None else Decoder()
90
+ self.timeout = timeout
91
+ self._closed = False
92
+ self._stopping = False
93
+
94
+ if sock is None:
95
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
96
+ if reuse_address:
97
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
98
+ if rcvbuf:
99
+ try:
100
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, rcvbuf)
101
+ except OSError as exc:
102
+ log.debug("could not set SO_RCVBUF to %d: %s", rcvbuf, exc)
103
+ try:
104
+ sock.bind((bind, port))
105
+ except OSError:
106
+ sock.close()
107
+ raise
108
+ self.socket = sock
109
+ self.socket.setblocking(False)
110
+
111
+ # A socketpair rather than a pipe: select() on Windows takes sockets
112
+ # and nothing else, and stop() has to be able to interrupt a wait from
113
+ # another thread or a signal handler.
114
+ self._waker_r, self._waker_w = socket.socketpair()
115
+ self._waker_r.setblocking(False)
116
+ self._selector = selectors.DefaultSelector()
117
+ self._selector.register(self.socket, selectors.EVENT_READ)
118
+ self._selector.register(self._waker_r, selectors.EVENT_READ)
119
+
120
+ log.info("listening for NetFlow/IPFIX on %s", self.address)
121
+
122
+ # == properties ==========================================================
123
+
124
+ @property
125
+ def address(self):
126
+ """The (host, port) actually bound, which matters when port was 0."""
127
+ return self.socket.getsockname()
128
+
129
+ @property
130
+ def stats(self):
131
+ """The decoder's running counters. See the README for the keys."""
132
+ return self.decoder.stats
133
+
134
+ def fileno(self):
135
+ """The socket's descriptor, so this can go straight into a caller's
136
+ own ``selectors`` or ``asyncio`` loop. Pair it with
137
+ ``poll(timeout=0)``, which never blocks."""
138
+ return self.socket.fileno()
139
+
140
+ # == receiving ===========================================================
141
+
142
+ def poll(self, timeout=0):
143
+ """Wait up to `timeout` seconds for one datagram and decode it.
144
+
145
+ Returns a :class:`~netflume.decoder.Message`, or None if
146
+ nothing arrived in time or what arrived would not decode. The two are
147
+ deliberately not distinguished here: neither is an error, and a caller
148
+ who cares can read :meth:`~netflume.decoder.Decoder.take_events`.
149
+
150
+ ``timeout=0`` polls without blocking, ``timeout=None`` waits
151
+ indefinitely.
152
+
153
+ Raises ValueError once :meth:`close` has been called. Iteration and
154
+ :meth:`flows` stop quietly instead, because a loop that has been shut
155
+ down is finishing normally, whereas asking a closed collector for one
156
+ more datagram is a mistake worth hearing about.
157
+ """
158
+ if self._closed:
159
+ raise ValueError("collector is closed")
160
+ data, addr = self._receive(timeout, honour_stop=False)
161
+ if data is None:
162
+ return None
163
+ return self.decoder.decode(data, addr[0])
164
+
165
+ def messages(self, timeout=None):
166
+ """Yield one :class:`~netflume.decoder.Message` per datagram.
167
+
168
+ Endless, and skips datagrams that do not decode. Use this rather than
169
+ :meth:`__iter__` when the sequence number, the export time or the
170
+ option records matter, since those belong to a message, not a flow.
171
+ """
172
+ if timeout is None:
173
+ timeout = self.timeout
174
+ while not self._stopping and not self._closed:
175
+ data, addr = self._receive(timeout)
176
+ if data is None:
177
+ continue
178
+ message = self.decoder.decode(data, addr[0])
179
+ if message is not None:
180
+ yield message
181
+
182
+ def __iter__(self):
183
+ """Yield (record, header) for every flow, as plain dicts.
184
+
185
+ The header comes along because a v5 or v9 record cannot be timestamped
186
+ without it. This is the cheapest shape: no objects are built beyond
187
+ what the parser already made.
188
+ """
189
+ for message in self.messages():
190
+ header = message.header
191
+ for rec in message.flows:
192
+ yield rec, header
193
+
194
+ def flows(self, now=None):
195
+ """Yield a :class:`~netflume.flow.Flow` for every flow.
196
+
197
+ The typed shape: aliases resolved, timestamp derived, sampling rate
198
+ attached, and the original dict still on ``flow.raw``.
199
+ """
200
+ for message in self.messages():
201
+ for flow in message.typed_flows(now=now):
202
+ yield flow
203
+
204
+ # == shutting down =======================================================
205
+
206
+ def stop(self):
207
+ """Ask the iterators to finish. Safe from a signal handler or thread.
208
+
209
+ Returns immediately; the loop stops after at most one `timeout`. The
210
+ socket is left open, so a stopped collector can be read again with
211
+ :meth:`poll`. Call :meth:`close` to release it.
212
+ """
213
+ self._stopping = True
214
+ try:
215
+ self._waker_w.send(b"\x00")
216
+ except OSError:
217
+ pass
218
+
219
+ def close(self):
220
+ """Release the socket. Idempotent, and implied by leaving a ``with``."""
221
+ if self._closed:
222
+ return
223
+ self._closed = True
224
+ self._stopping = True
225
+ self._selector.close()
226
+ for handle in (self.socket, self._waker_r, self._waker_w):
227
+ try:
228
+ handle.close()
229
+ except OSError:
230
+ pass
231
+
232
+ def __enter__(self):
233
+ return self
234
+
235
+ def __exit__(self, *exc):
236
+ self.close()
237
+ return False
238
+
239
+ # == the one place that touches the socket ===============================
240
+
241
+ def _receive(self, timeout, honour_stop=True):
242
+ """(data, addr), or (None, None) if nothing was ready in time.
243
+
244
+ `honour_stop` is what separates the two callers. An iterator wants a
245
+ stop() to end its wait immediately, however long the timeout was.
246
+ poll() is a single explicit read and answers the question it was
247
+ asked, stopped or not.
248
+ """
249
+ deadline = None if timeout is None else time.monotonic() + timeout
250
+ while True:
251
+ remaining = (None if deadline is None
252
+ else max(0.0, deadline - time.monotonic()))
253
+ ready = self._selector.select(remaining)
254
+ if not ready:
255
+ return None, None
256
+
257
+ woken = False
258
+ for key, _events in ready:
259
+ if key.fileobj is self._waker_r:
260
+ woken = True
261
+ try:
262
+ self._waker_r.recv(4096) # drain, it carries no data
263
+ except OSError:
264
+ pass
265
+ continue
266
+ try:
267
+ return self.socket.recvfrom(MAX_DATAGRAM)
268
+ except OSError as exc:
269
+ if exc.errno in (errno.EAGAIN, errno.EWOULDBLOCK):
270
+ return None, None
271
+ # WSAECONNRESET: Windows reports an ICMP port-unreachable
272
+ # caused by an earlier send as an error on the *next* read
273
+ # of a UDP socket. Nothing is wrong with this socket.
274
+ if getattr(exc, "winerror", None) == 10054:
275
+ log.debug("ignoring a stale ICMP error on the socket")
276
+ return None, None
277
+ raise
278
+
279
+ # Only the wakeup fired. Go round again with whatever time is left,
280
+ # rather than reporting nothing: stop() must not cost the caller a
281
+ # datagram that was already queued behind it.
282
+ if not woken or (honour_stop and self._stopping):
283
+ return None, None
284
+ if deadline is not None and time.monotonic() >= deadline:
285
+ return None, None
netflume/decoder.py ADDED
@@ -0,0 +1,238 @@
1
+ """Turning datagrams into messages, and remembering what that implies.
2
+
3
+ :class:`Decoder` is the parsing layer with its state attached: the template
4
+ store that has to outlive a datagram, the sequence watch that can only find a
5
+ gap by comparing one message with the last, and the sampling watch that has to
6
+ remember what an exporter said an hour ago. It has no socket in it, so a caller
7
+ holding bytes from a pcap, a queue or a test fixture gets the same answers a
8
+ live collector would.
9
+ """
10
+
11
+ import logging
12
+ import struct
13
+ from collections import Counter, deque
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, Dict, List, Mapping
16
+
17
+ from .events import DecodeError
18
+ from .flow import Flow
19
+ from .parse import SUPPORTED_VERSIONS, TemplateStore, parse_v5, parse_v9_or_ipfix
20
+ from .sampling import SamplingWatch
21
+ from .sequence import SequenceWatch
22
+
23
+ __all__ = ["Decoder", "MAX_PENDING_EVENTS", "Message"]
24
+
25
+ log = logging.getLogger(__name__)
26
+
27
+ #: Events held for :meth:`Decoder.take_events` before the oldest are dropped.
28
+ #: A collector on an open UDP port receives things that are not NetFlow, such
29
+ #: as scanners, sFlow on the same port, or a misconfigured exporter, and each
30
+ #: one raises a DecodeError. A caller that never drains would otherwise
31
+ #: accumulate one per bad datagram for as long as the process runs. Drops are
32
+ #: counted in ``stats["events_dropped"]`` so that the loss is visible rather
33
+ #: than silent.
34
+ MAX_PENDING_EVENTS = 10000
35
+
36
+
37
+ @dataclass
38
+ class Message:
39
+ """One export datagram, decoded.
40
+
41
+ The message is the unit that arrives on the wire, and some things are only
42
+ true of a message rather than of a flow: the sequence number, the export
43
+ time, and whether anything was lost getting here.
44
+ """
45
+
46
+ #: The header dict: version, exporter, domain, sequence, unix_secs,
47
+ #: sys_uptime. Needed to timestamp a v5 or v9 flow, so keep it with the
48
+ #: records rather than discarding it.
49
+ header: Mapping[str, Any]
50
+ #: Flow records, as plain dicts of normalised keys.
51
+ flows: List[Dict[str, Any]] = field(default_factory=list)
52
+ #: Option records: what the exporter says about itself, not about traffic.
53
+ #: Sampling rates are read out of these automatically; interface names and
54
+ #: the like are left for the caller to use or ignore.
55
+ options: List[Dict[str, Any]] = field(default_factory=list)
56
+ #: Exports the sequence counter says never arrived before this message. 0
57
+ #: is the normal answer, and is also the answer while the stream is still
58
+ #: being learned. See :class:`~netflume.sequence.SequenceWatch`.
59
+ gap: int = 0
60
+ #: The 1-in-N rate in force for this exporter, 1 if unsampled or unstated.
61
+ sampling_rate: int = 1
62
+
63
+ @property
64
+ def exporter(self):
65
+ return self.header.get("exporter")
66
+
67
+ @property
68
+ def version(self):
69
+ return self.header.get("version")
70
+
71
+ @property
72
+ def sequence(self):
73
+ return self.header.get("sequence")
74
+
75
+ def typed_flows(self, now=None):
76
+ """The flow records as :class:`~netflume.flow.Flow` objects.
77
+
78
+ Built on demand, not up front: a consumer that wants two fields out of
79
+ a dict should not pay to construct thirty.
80
+ """
81
+ return [Flow.from_record(rec, self.header, self.sampling_rate, now=now)
82
+ for rec in self.flows]
83
+
84
+ def __len__(self):
85
+ return len(self.flows)
86
+
87
+ def __bool__(self):
88
+ """Always True. A Message exists or it is None; there is no empty one.
89
+
90
+ Without this, ``__len__`` makes a message carrying only template sets,
91
+ only option records, or only a sequence gap evaluate false, and the
92
+ natural ``if message:`` after :meth:`~netflume.collector.Collector.poll`
93
+ silently discards all three. Ask ``if message is not None`` to test for
94
+ a decode, and ``len(message)`` or ``message.flows`` for traffic.
95
+ """
96
+ return True
97
+
98
+
99
+ class Decoder:
100
+ """Stateful decoding of one exporter's datagrams, or of many.
101
+
102
+ Templates, sequence tracking and sampling rates are all keyed by
103
+ (exporter, observation domain) inside, so one Decoder can serve every device
104
+ sending to a collector, and a device running several domains keeps them
105
+ apart. It is not thread safe; give each thread its own, or put one behind a
106
+ queue.
107
+
108
+ The per-exporter tables have ceilings, namely MAX_TEMPLATES, MAX_STREAMS
109
+ and MAX_SAMPLING_STREAMS, because a UDP source address is whatever the
110
+ sender typed, and anything keyed by one and never evicted is a memory leak
111
+ that anyone able to reach the socket can pull on.
112
+ """
113
+
114
+ def __init__(self, track_sequence=True, track_sampling=True):
115
+ self.templates = TemplateStore()
116
+ self.stats = Counter()
117
+ self.sequence = SequenceWatch() if track_sequence else None
118
+ self.sampling = SamplingWatch() if track_sampling else None
119
+ self._events = deque()
120
+
121
+ def decode(self, data, exporter):
122
+ """Decode one datagram. Returns a :class:`Message`, or None.
123
+
124
+ None means nothing usable was in it: too short, a version this package
125
+ does not decode, or a body that would not parse. Every one of those is
126
+ counted in :attr:`stats` and queued as a
127
+ :class:`~netflume.events.DecodeError`; none of them raises,
128
+ because a decoder that dies on one bad packet is useless on a real
129
+ network.
130
+
131
+ A Message with no flows in it is not a failure. Template sets, option
132
+ records and data sets whose template has not arrived yet all produce
133
+ one, and an exporter's first few datagrams are routinely all template.
134
+ """
135
+ self.stats["packets"] += 1
136
+ self.stats["bytes_rx"] += len(data)
137
+
138
+ if len(data) < 2:
139
+ return self._fail(exporter, "short", "fewer than two bytes")
140
+
141
+ version = struct.unpack_from("!H", data, 0)[0]
142
+ if version not in SUPPORTED_VERSIONS:
143
+ self.stats["unsupported_version"] += 1
144
+ return self._fail(exporter, "unsupported",
145
+ f"version {version}", counted=True)
146
+
147
+ try:
148
+ if version == 5:
149
+ hdr, records, opts = parse_v5(data, exporter)
150
+ else:
151
+ hdr, records, opts = parse_v9_or_ipfix(
152
+ data, exporter, self.templates, self.stats)
153
+ except Exception as exc: # keep listening even on a bad datagram
154
+ self.stats["parse_errors"] += 1
155
+ return self._fail(exporter, "malformed",
156
+ f"{type(exc).__name__}: {exc}", counted=True)
157
+
158
+ if hdr is None:
159
+ return self._fail(exporter, "short", "truncated header")
160
+
161
+ self.stats[f"v{version}_msgs"] += 1
162
+ self.stats["flows"] += len(records)
163
+ self.stats["option_records"] += len(opts)
164
+
165
+ gap = 0
166
+ if self.sequence is not None:
167
+ gap = self.sequence.observe(exporter, hdr.get("domain"), version,
168
+ hdr.get("sequence"), len(records),
169
+ len(opts))
170
+ if gap:
171
+ self.stats["missed_exports"] += gap
172
+ self._queue(self.sequence.take_events())
173
+
174
+ rate = 1
175
+ if self.sampling is not None:
176
+ domain = hdr.get("domain")
177
+ for opt in opts:
178
+ self.sampling.note(exporter, domain, opt)
179
+ self._queue(self.sampling.take_events())
180
+ rate = self.sampling.rate_for(exporter, domain)
181
+
182
+ return Message(header=hdr, flows=records, options=opts, gap=gap,
183
+ sampling_rate=rate)
184
+
185
+ def flows(self, data, exporter, typed=False, now=None):
186
+ """Decode a datagram straight to its flows, discarding the rest.
187
+
188
+ The convenience form for a caller who has bytes and wants records. The
189
+ header is still needed to timestamp them, so `typed=True` is the shape
190
+ to use if you want start times; the dict form leaves that to you.
191
+ """
192
+ message = self.decode(data, exporter)
193
+ if message is None:
194
+ return []
195
+ return message.typed_flows(now=now) if typed else message.flows
196
+
197
+ # == what the caller may want to know ====================================
198
+
199
+ def _queue(self, events):
200
+ """Add events to the pending queue, oldest dropped past the ceiling."""
201
+ for event in events:
202
+ if len(self._events) >= MAX_PENDING_EVENTS:
203
+ self._events.popleft()
204
+ self.stats["events_dropped"] += 1
205
+ self._events.append(event)
206
+
207
+ def take_events(self):
208
+ """Hand over everything raised since the last call, and forget it.
209
+
210
+ A list of :class:`~netflume.events.ExportGap`,
211
+ :class:`~netflume.events.SamplingChange` and
212
+ :class:`~netflume.events.DecodeError`. Empty is the normal
213
+ answer on a healthy network, so this is cheap to call in a loop.
214
+ """
215
+ events = list(self._events)
216
+ self._events.clear()
217
+ return events
218
+
219
+ def sampling_rate(self, exporter, domain=None):
220
+ """The 1-in-N rate last advertised, or 1.
221
+
222
+ Rates are scoped to an observation domain, so name one. Omitting it
223
+ answers for the exporter as a whole, and only when its domains agree.
224
+ See :meth:`~netflume.sampling.SamplingWatch.rate_for`.
225
+ """
226
+ return self.sampling.rate_for(exporter, domain) if self.sampling else 1
227
+
228
+ def export_gaps(self):
229
+ """Cumulative loss per stream, as ExportGap records, worst first."""
230
+ return self.sequence.gaps() if self.sequence else []
231
+
232
+ def _fail(self, exporter, reason, detail, counted=False):
233
+ if not counted:
234
+ self.stats["malformed"] += 1
235
+ error = DecodeError(exporter, reason, detail)
236
+ self._queue((error,))
237
+ log.debug("%s", error)
238
+ return None