ominfra 0.0.0.dev101__py3-none-any.whl → 0.0.0.dev103__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.
- ominfra/clouds/aws/journald2aws/cursor.py +47 -0
- ominfra/clouds/aws/journald2aws/driver.py +210 -0
- ominfra/clouds/aws/journald2aws/main.py +9 -262
- ominfra/clouds/aws/journald2aws/poster.py +89 -0
- ominfra/clouds/aws/logs.py +19 -13
- ominfra/deploy/_executor.py +25 -3
- ominfra/deploy/poly/_main.py +26 -2
- ominfra/journald/genmessages.py +14 -2
- ominfra/journald/tailer.py +39 -33
- ominfra/pyremote/_runcommands.py +25 -3
- ominfra/scripts/journald2aws.py +428 -195
- ominfra/scripts/supervisor.py +1 -1
- ominfra/threadworkers.py +139 -0
- {ominfra-0.0.0.dev101.dist-info → ominfra-0.0.0.dev103.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev101.dist-info → ominfra-0.0.0.dev103.dist-info}/RECORD +19 -16
- ominfra/threadworker.py +0 -67
- {ominfra-0.0.0.dev101.dist-info → ominfra-0.0.0.dev103.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev101.dist-info → ominfra-0.0.0.dev103.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev101.dist-info → ominfra-0.0.0.dev103.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev101.dist-info → ominfra-0.0.0.dev103.dist-info}/top_level.txt +0 -0
ominfra/scripts/journald2aws.py
CHANGED
@@ -4,38 +4,6 @@
|
|
4
4
|
# @omlish-script
|
5
5
|
# @omlish-amalg-output ../clouds/aws/journald2aws/main.py
|
6
6
|
# ruff: noqa: N802 UP006 UP007 UP036
|
7
|
-
"""
|
8
|
-
TODO:
|
9
|
-
- create log group
|
10
|
-
- log stats - chunk sizes, byte count, num calls, etc
|
11
|
-
|
12
|
-
==
|
13
|
-
|
14
|
-
https://www.freedesktop.org/software/systemd/man/latest/journalctl.html
|
15
|
-
|
16
|
-
journalctl:
|
17
|
-
-o json
|
18
|
-
--show-cursor
|
19
|
-
|
20
|
-
--since "2012-10-30 18:17:16"
|
21
|
-
--until "2012-10-30 18:17:16"
|
22
|
-
|
23
|
-
--after-cursor <cursor>
|
24
|
-
|
25
|
-
==
|
26
|
-
|
27
|
-
https://www.freedesktop.org/software/systemd/man/latest/systemd.journal-fields.html
|
28
|
-
|
29
|
-
==
|
30
|
-
|
31
|
-
@dc.dataclass(frozen=True)
|
32
|
-
class Journald2AwsConfig:
|
33
|
-
log_group_name: str
|
34
|
-
log_stream_name: str
|
35
|
-
|
36
|
-
aws_batch_size: int = 1_000
|
37
|
-
aws_flush_interval_s: float = 1.
|
38
|
-
"""
|
39
7
|
import abc
|
40
8
|
import argparse
|
41
9
|
import base64
|
@@ -81,15 +49,21 @@ if sys.version_info < (3, 8):
|
|
81
49
|
########################################
|
82
50
|
|
83
51
|
|
84
|
-
# ../../../../../omlish/lite/
|
52
|
+
# ../../../../../omlish/lite/cached.py
|
85
53
|
T = ta.TypeVar('T')
|
86
54
|
|
55
|
+
# ../../../../../omlish/lite/contextmanagers.py
|
56
|
+
ExitStackedT = ta.TypeVar('ExitStackedT', bound='ExitStacked')
|
57
|
+
|
58
|
+
# ../../../../threadworkers.py
|
59
|
+
ThreadWorkerT = ta.TypeVar('ThreadWorkerT', bound='ThreadWorker')
|
60
|
+
|
87
61
|
|
88
62
|
########################################
|
89
63
|
# ../../../../../omlish/lite/cached.py
|
90
64
|
|
91
65
|
|
92
|
-
class
|
66
|
+
class _cached_nullary: # noqa
|
93
67
|
def __init__(self, fn):
|
94
68
|
super().__init__()
|
95
69
|
self._fn = fn
|
@@ -106,6 +80,10 @@ class cached_nullary: # noqa
|
|
106
80
|
return bound
|
107
81
|
|
108
82
|
|
83
|
+
def cached_nullary(fn: ta.Callable[..., T]) -> ta.Callable[..., T]:
|
84
|
+
return _cached_nullary(fn)
|
85
|
+
|
86
|
+
|
109
87
|
########################################
|
110
88
|
# ../../../../../omlish/lite/check.py
|
111
89
|
|
@@ -222,7 +200,7 @@ class Pidfile:
|
|
222
200
|
return self
|
223
201
|
|
224
202
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
225
|
-
if self
|
203
|
+
if hasattr(self, '_f'):
|
226
204
|
self._f.close()
|
227
205
|
del self._f
|
228
206
|
|
@@ -736,6 +714,52 @@ class AwsDataclassMeta:
|
|
736
714
|
return AwsDataclassMeta.Converters(d2a, a2d)
|
737
715
|
|
738
716
|
|
717
|
+
########################################
|
718
|
+
# ../../../../../omlish/lite/contextmanagers.py
|
719
|
+
|
720
|
+
|
721
|
+
##
|
722
|
+
|
723
|
+
|
724
|
+
class ExitStacked:
|
725
|
+
_exit_stack: ta.Optional[contextlib.ExitStack] = None
|
726
|
+
|
727
|
+
def __enter__(self: ExitStackedT) -> ExitStackedT:
|
728
|
+
check_state(self._exit_stack is None)
|
729
|
+
es = self._exit_stack = contextlib.ExitStack()
|
730
|
+
es.__enter__()
|
731
|
+
return self
|
732
|
+
|
733
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
734
|
+
if (es := self._exit_stack) is None:
|
735
|
+
return None
|
736
|
+
return es.__exit__(exc_type, exc_val, exc_tb)
|
737
|
+
|
738
|
+
def _enter_context(self, cm: ta.ContextManager[T]) -> T:
|
739
|
+
es = check_not_none(self._exit_stack)
|
740
|
+
return es.enter_context(cm)
|
741
|
+
|
742
|
+
|
743
|
+
##
|
744
|
+
|
745
|
+
|
746
|
+
@contextlib.contextmanager
|
747
|
+
def attr_setting(obj, attr, val, *, default=None): # noqa
|
748
|
+
not_set = object()
|
749
|
+
orig = getattr(obj, attr, not_set)
|
750
|
+
try:
|
751
|
+
setattr(obj, attr, val)
|
752
|
+
if orig is not not_set:
|
753
|
+
yield orig
|
754
|
+
else:
|
755
|
+
yield default
|
756
|
+
finally:
|
757
|
+
if orig is not_set:
|
758
|
+
delattr(obj, attr)
|
759
|
+
else:
|
760
|
+
setattr(obj, attr, orig)
|
761
|
+
|
762
|
+
|
739
763
|
########################################
|
740
764
|
# ../../../../../omlish/lite/io.py
|
741
765
|
|
@@ -1259,7 +1283,7 @@ class DataclassObjMarshaler(ObjMarshaler):
|
|
1259
1283
|
return {k: m.marshal(getattr(o, k)) for k, m in self.fs.items()}
|
1260
1284
|
|
1261
1285
|
def unmarshal(self, o: ta.Any) -> ta.Any:
|
1262
|
-
return self.ty(**{k: self.fs[k].unmarshal(v) for k, v in o.items() if self.nonstrict or k in self.fs})
|
1286
|
+
return self.ty(**{k: self.fs[k].unmarshal(v) for k, v in o.items() if not self.nonstrict or k in self.fs})
|
1263
1287
|
|
1264
1288
|
|
1265
1289
|
@dc.dataclass(frozen=True)
|
@@ -1448,6 +1472,52 @@ def check_runtime_version() -> None:
|
|
1448
1472
|
f'Requires python {REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
|
1449
1473
|
|
1450
1474
|
|
1475
|
+
########################################
|
1476
|
+
# ../cursor.py
|
1477
|
+
|
1478
|
+
|
1479
|
+
class JournalctlToAwsCursor:
|
1480
|
+
def __init__(
|
1481
|
+
self,
|
1482
|
+
cursor_file: ta.Optional[str] = None,
|
1483
|
+
*,
|
1484
|
+
ensure_locked: ta.Optional[ta.Callable[[], None]] = None,
|
1485
|
+
) -> None:
|
1486
|
+
super().__init__()
|
1487
|
+
self._cursor_file = cursor_file
|
1488
|
+
self._ensure_locked = ensure_locked
|
1489
|
+
|
1490
|
+
#
|
1491
|
+
|
1492
|
+
def get(self) -> ta.Optional[str]:
|
1493
|
+
if self._ensure_locked is not None:
|
1494
|
+
self._ensure_locked()
|
1495
|
+
|
1496
|
+
if not (cf := self._cursor_file):
|
1497
|
+
return None
|
1498
|
+
cf = os.path.expanduser(cf)
|
1499
|
+
|
1500
|
+
try:
|
1501
|
+
with open(cf) as f:
|
1502
|
+
return f.read().strip()
|
1503
|
+
except FileNotFoundError:
|
1504
|
+
return None
|
1505
|
+
|
1506
|
+
def set(self, cursor: str) -> None:
|
1507
|
+
if self._ensure_locked is not None:
|
1508
|
+
self._ensure_locked()
|
1509
|
+
|
1510
|
+
if not (cf := self._cursor_file):
|
1511
|
+
return
|
1512
|
+
cf = os.path.expanduser(cf)
|
1513
|
+
|
1514
|
+
log.info('Writing cursor file %s : %s', cf, cursor)
|
1515
|
+
with open(ncf := cf + '.next', 'w') as f:
|
1516
|
+
f.write(cursor)
|
1517
|
+
|
1518
|
+
os.rename(ncf, cf)
|
1519
|
+
|
1520
|
+
|
1451
1521
|
########################################
|
1452
1522
|
# ../../logs.py
|
1453
1523
|
"""
|
@@ -1502,7 +1572,7 @@ class AwsPutLogEventsResponse(AwsDataclass):
|
|
1502
1572
|
##
|
1503
1573
|
|
1504
1574
|
|
1505
|
-
class
|
1575
|
+
class AwsLogMessageBuilder:
|
1506
1576
|
"""
|
1507
1577
|
TODO:
|
1508
1578
|
- max_items
|
@@ -1528,7 +1598,7 @@ class AwsLogMessagePoster:
|
|
1528
1598
|
log_group_name: str,
|
1529
1599
|
log_stream_name: str,
|
1530
1600
|
region_name: str,
|
1531
|
-
credentials: AwsSigner.Credentials,
|
1601
|
+
credentials: ta.Optional[AwsSigner.Credentials],
|
1532
1602
|
|
1533
1603
|
url: ta.Optional[str] = None,
|
1534
1604
|
service_name: str = DEFAULT_SERVICE_NAME,
|
@@ -1550,11 +1620,16 @@ class AwsLogMessagePoster:
|
|
1550
1620
|
headers = {**headers, **extra_headers}
|
1551
1621
|
self._headers = {k: [v] for k, v in headers.items()}
|
1552
1622
|
|
1553
|
-
|
1554
|
-
|
1555
|
-
|
1556
|
-
|
1557
|
-
|
1623
|
+
signer: ta.Optional[V4AwsSigner]
|
1624
|
+
if credentials is not None:
|
1625
|
+
signer = V4AwsSigner(
|
1626
|
+
credentials,
|
1627
|
+
region_name,
|
1628
|
+
service_name,
|
1629
|
+
)
|
1630
|
+
else:
|
1631
|
+
signer = None
|
1632
|
+
self._signer = signer
|
1558
1633
|
|
1559
1634
|
#
|
1560
1635
|
|
@@ -1598,13 +1673,14 @@ class AwsLogMessagePoster:
|
|
1598
1673
|
payload=body,
|
1599
1674
|
)
|
1600
1675
|
|
1601
|
-
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1605
|
-
|
1676
|
+
if (signer := self._signer) is not None:
|
1677
|
+
sig_headers = signer.sign(
|
1678
|
+
sig_req,
|
1679
|
+
sign_payload=False,
|
1680
|
+
)
|
1681
|
+
sig_req = dc.replace(sig_req, headers={**sig_req.headers, **sig_headers})
|
1606
1682
|
|
1607
|
-
post =
|
1683
|
+
post = AwsLogMessageBuilder.Post(
|
1608
1684
|
url=self._url,
|
1609
1685
|
headers={k: check_single(v) for k, v in sig_req.headers.items()},
|
1610
1686
|
data=sig_req.payload,
|
@@ -1687,15 +1763,20 @@ class JournalctlMessageBuilder:
|
|
1687
1763
|
|
1688
1764
|
|
1689
1765
|
########################################
|
1690
|
-
# ../../../../
|
1766
|
+
# ../../../../threadworkers.py
|
1691
1767
|
"""
|
1692
1768
|
TODO:
|
1693
1769
|
- implement stop lol
|
1694
1770
|
- collective heartbeat monitoring - ThreadWorkerGroups
|
1771
|
+
- group -> 'context'? :|
|
1772
|
+
- shared stop_event?
|
1695
1773
|
"""
|
1696
1774
|
|
1697
1775
|
|
1698
|
-
|
1776
|
+
##
|
1777
|
+
|
1778
|
+
|
1779
|
+
class ThreadWorker(ExitStacked, abc.ABC):
|
1699
1780
|
def __init__(
|
1700
1781
|
self,
|
1701
1782
|
*,
|
@@ -1707,46 +1788,107 @@ class ThreadWorker(abc.ABC):
|
|
1707
1788
|
stop_event = threading.Event()
|
1708
1789
|
self._stop_event = stop_event
|
1709
1790
|
|
1791
|
+
self._lock = threading.RLock()
|
1710
1792
|
self._thread: ta.Optional[threading.Thread] = None
|
1711
|
-
|
1712
1793
|
self._last_heartbeat: ta.Optional[float] = None
|
1713
1794
|
|
1714
1795
|
#
|
1715
1796
|
|
1797
|
+
def __enter__(self: ThreadWorkerT) -> ThreadWorkerT:
|
1798
|
+
with self._lock:
|
1799
|
+
return super().__enter__() # noqa
|
1800
|
+
|
1801
|
+
#
|
1802
|
+
|
1716
1803
|
def should_stop(self) -> bool:
|
1717
1804
|
return self._stop_event.is_set()
|
1718
1805
|
|
1806
|
+
class Stopping(Exception): # noqa
|
1807
|
+
pass
|
1808
|
+
|
1719
1809
|
#
|
1720
1810
|
|
1721
1811
|
@property
|
1722
1812
|
def last_heartbeat(self) -> ta.Optional[float]:
|
1723
1813
|
return self._last_heartbeat
|
1724
1814
|
|
1725
|
-
def _heartbeat(
|
1815
|
+
def _heartbeat(
|
1816
|
+
self,
|
1817
|
+
*,
|
1818
|
+
no_stop_check: bool = False,
|
1819
|
+
) -> None:
|
1726
1820
|
self._last_heartbeat = time.time()
|
1727
1821
|
|
1728
|
-
if self.should_stop():
|
1822
|
+
if not no_stop_check and self.should_stop():
|
1729
1823
|
log.info('Stopping: %s', self)
|
1730
|
-
|
1731
|
-
|
1732
|
-
return True
|
1824
|
+
raise ThreadWorker.Stopping
|
1733
1825
|
|
1734
1826
|
#
|
1735
1827
|
|
1828
|
+
def has_started(self) -> bool:
|
1829
|
+
return self._thread is not None
|
1830
|
+
|
1736
1831
|
def is_alive(self) -> bool:
|
1737
1832
|
return (thr := self._thread) is not None and thr.is_alive()
|
1738
1833
|
|
1739
1834
|
def start(self) -> None:
|
1740
|
-
|
1741
|
-
|
1742
|
-
|
1835
|
+
with self._lock:
|
1836
|
+
if self._thread is not None:
|
1837
|
+
raise RuntimeError('Thread already started: %r', self)
|
1838
|
+
|
1839
|
+
thr = threading.Thread(target=self.__run)
|
1840
|
+
self._thread = thr
|
1841
|
+
thr.start()
|
1842
|
+
|
1843
|
+
#
|
1844
|
+
|
1845
|
+
def __run(self) -> None:
|
1846
|
+
try:
|
1847
|
+
self._run()
|
1848
|
+
except ThreadWorker.Stopping:
|
1849
|
+
log.exception('Thread worker stopped: %r', self)
|
1850
|
+
except Exception: # noqa
|
1851
|
+
log.exception('Error in worker thread: %r', self)
|
1852
|
+
raise
|
1743
1853
|
|
1744
1854
|
@abc.abstractmethod
|
1745
1855
|
def _run(self) -> None:
|
1746
1856
|
raise NotImplementedError
|
1747
1857
|
|
1858
|
+
#
|
1859
|
+
|
1748
1860
|
def stop(self) -> None:
|
1749
|
-
|
1861
|
+
self._stop_event.set()
|
1862
|
+
|
1863
|
+
def join(self, timeout: ta.Optional[float] = None) -> None:
|
1864
|
+
with self._lock:
|
1865
|
+
if self._thread is None:
|
1866
|
+
raise RuntimeError('Thread not started: %r', self)
|
1867
|
+
self._thread.join(timeout)
|
1868
|
+
|
1869
|
+
|
1870
|
+
##
|
1871
|
+
|
1872
|
+
|
1873
|
+
class ThreadWorkerGroup:
|
1874
|
+
@dc.dataclass()
|
1875
|
+
class State:
|
1876
|
+
worker: ThreadWorker
|
1877
|
+
|
1878
|
+
def __init__(self) -> None:
|
1879
|
+
super().__init__()
|
1880
|
+
|
1881
|
+
self._lock = threading.RLock()
|
1882
|
+
self._states: ta.Dict[ThreadWorker, ThreadWorkerGroup.State] = {}
|
1883
|
+
|
1884
|
+
def add(self, *workers: ThreadWorker) -> 'ThreadWorkerGroup':
|
1885
|
+
with self._lock:
|
1886
|
+
for w in workers:
|
1887
|
+
if w in self._states:
|
1888
|
+
raise KeyError(w)
|
1889
|
+
self._states[w] = ThreadWorkerGroup.State(w)
|
1890
|
+
|
1891
|
+
return self
|
1750
1892
|
|
1751
1893
|
|
1752
1894
|
########################################
|
@@ -1855,6 +1997,103 @@ def subprocess_try_output_str(*args: str, **kwargs: ta.Any) -> ta.Optional[str]:
|
|
1855
1997
|
return out.decode().strip() if out is not None else None
|
1856
1998
|
|
1857
1999
|
|
2000
|
+
##
|
2001
|
+
|
2002
|
+
|
2003
|
+
def subprocess_close(
|
2004
|
+
proc: subprocess.Popen,
|
2005
|
+
timeout: ta.Optional[float] = None,
|
2006
|
+
) -> None:
|
2007
|
+
# TODO: terminate, sleep, kill
|
2008
|
+
if proc.stdout:
|
2009
|
+
proc.stdout.close()
|
2010
|
+
if proc.stderr:
|
2011
|
+
proc.stderr.close()
|
2012
|
+
if proc.stdin:
|
2013
|
+
proc.stdin.close()
|
2014
|
+
|
2015
|
+
proc.wait(timeout)
|
2016
|
+
|
2017
|
+
|
2018
|
+
########################################
|
2019
|
+
# ../poster.py
|
2020
|
+
"""
|
2021
|
+
TODO:
|
2022
|
+
- retries
|
2023
|
+
"""
|
2024
|
+
|
2025
|
+
|
2026
|
+
class JournalctlToAwsPosterWorker(ThreadWorker):
|
2027
|
+
def __init__(
|
2028
|
+
self,
|
2029
|
+
queue, # type: queue.Queue[ta.Sequence[JournalctlMessage]] # noqa
|
2030
|
+
builder: AwsLogMessageBuilder,
|
2031
|
+
cursor: JournalctlToAwsCursor,
|
2032
|
+
*,
|
2033
|
+
ensure_locked: ta.Optional[ta.Callable[[], None]] = None,
|
2034
|
+
dry_run: bool = False,
|
2035
|
+
queue_timeout_s: float = 1.,
|
2036
|
+
**kwargs: ta.Any,
|
2037
|
+
) -> None:
|
2038
|
+
super().__init__(**kwargs)
|
2039
|
+
self._queue = queue
|
2040
|
+
self._builder = builder
|
2041
|
+
self._cursor = cursor
|
2042
|
+
self._ensure_locked = ensure_locked
|
2043
|
+
self._dry_run = dry_run
|
2044
|
+
self._queue_timeout_s = queue_timeout_s
|
2045
|
+
#
|
2046
|
+
|
2047
|
+
def _run(self) -> None:
|
2048
|
+
if self._ensure_locked is not None:
|
2049
|
+
self._ensure_locked()
|
2050
|
+
|
2051
|
+
last_cursor: ta.Optional[str] = None # noqa
|
2052
|
+
while True:
|
2053
|
+
self._heartbeat()
|
2054
|
+
|
2055
|
+
try:
|
2056
|
+
msgs: ta.Sequence[JournalctlMessage] = self._queue.get(timeout=self._queue_timeout_s)
|
2057
|
+
except queue.Empty:
|
2058
|
+
msgs = []
|
2059
|
+
|
2060
|
+
if not msgs:
|
2061
|
+
log.debug('Empty queue chunk')
|
2062
|
+
continue
|
2063
|
+
|
2064
|
+
log.debug('%r', msgs)
|
2065
|
+
|
2066
|
+
cur_cursor: ta.Optional[str] = None
|
2067
|
+
for m in reversed(msgs):
|
2068
|
+
if m.cursor is not None:
|
2069
|
+
cur_cursor = m.cursor
|
2070
|
+
break
|
2071
|
+
|
2072
|
+
feed_msgs = []
|
2073
|
+
for m in msgs:
|
2074
|
+
feed_msgs.append(AwsLogMessageBuilder.Message(
|
2075
|
+
message=json.dumps(m.dct, sort_keys=True),
|
2076
|
+
ts_ms=int((m.ts_us / 1000.) if m.ts_us is not None else (time.time() * 1000.)),
|
2077
|
+
))
|
2078
|
+
|
2079
|
+
for post in self._builder.feed(feed_msgs):
|
2080
|
+
log.debug('%r', post)
|
2081
|
+
|
2082
|
+
if not self._dry_run:
|
2083
|
+
with urllib.request.urlopen(urllib.request.Request( # noqa
|
2084
|
+
post.url,
|
2085
|
+
method='POST',
|
2086
|
+
headers=dict(post.headers),
|
2087
|
+
data=post.data,
|
2088
|
+
)) as resp:
|
2089
|
+
response = AwsPutLogEventsResponse.from_aws(json.loads(resp.read().decode('utf-8')))
|
2090
|
+
log.debug('%r', response)
|
2091
|
+
|
2092
|
+
if cur_cursor is not None:
|
2093
|
+
self._cursor.set(cur_cursor)
|
2094
|
+
last_cursor = cur_cursor # noqa
|
2095
|
+
|
2096
|
+
|
1858
2097
|
########################################
|
1859
2098
|
# ../../../../journald/tailer.py
|
1860
2099
|
"""
|
@@ -2227,7 +2466,7 @@ class JournalctlTailerWorker(ThreadWorker):
|
|
2227
2466
|
self._read_size = read_size
|
2228
2467
|
self._sleep_s = sleep_s
|
2229
2468
|
|
2230
|
-
self.
|
2469
|
+
self._builder = JournalctlMessageBuilder()
|
2231
2470
|
|
2232
2471
|
self._proc: ta.Optional[subprocess.Popen] = None
|
2233
2472
|
|
@@ -2251,69 +2490,103 @@ class JournalctlTailerWorker(ThreadWorker):
|
|
2251
2490
|
|
2252
2491
|
return cmd
|
2253
2492
|
|
2493
|
+
def _read_loop(self, stdout: ta.IO) -> None:
|
2494
|
+
while stdout.readable():
|
2495
|
+
self._heartbeat()
|
2496
|
+
|
2497
|
+
buf = stdout.read(self._read_size)
|
2498
|
+
if not buf:
|
2499
|
+
log.debug('Journalctl empty read')
|
2500
|
+
break
|
2501
|
+
|
2502
|
+
log.debug('Journalctl read buffer: %r', buf)
|
2503
|
+
msgs = self._builder.feed(buf)
|
2504
|
+
if msgs:
|
2505
|
+
while True:
|
2506
|
+
try:
|
2507
|
+
self._output.put(msgs, timeout=1.)
|
2508
|
+
except queue.Full:
|
2509
|
+
self._heartbeat()
|
2510
|
+
else:
|
2511
|
+
break
|
2512
|
+
|
2254
2513
|
def _run(self) -> None:
|
2255
2514
|
with subprocess.Popen(
|
2256
2515
|
self._full_cmd(),
|
2257
2516
|
stdout=subprocess.PIPE,
|
2258
2517
|
) as self._proc:
|
2259
|
-
|
2518
|
+
try:
|
2519
|
+
stdout = check_not_none(self._proc.stdout)
|
2260
2520
|
|
2261
|
-
|
2262
|
-
|
2263
|
-
|
2521
|
+
fd = stdout.fileno()
|
2522
|
+
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
2523
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
2264
2524
|
|
2265
|
-
|
2266
|
-
|
2267
|
-
return
|
2525
|
+
while True:
|
2526
|
+
self._heartbeat()
|
2268
2527
|
|
2269
|
-
|
2270
|
-
if not self._heartbeat():
|
2271
|
-
return
|
2528
|
+
self._read_loop(stdout)
|
2272
2529
|
|
2273
|
-
|
2274
|
-
if not buf:
|
2275
|
-
log.debug('Journalctl empty read')
|
2276
|
-
break
|
2530
|
+
log.debug('Journalctl not readable')
|
2277
2531
|
|
2278
|
-
|
2279
|
-
|
2280
|
-
|
2281
|
-
while True:
|
2282
|
-
try:
|
2283
|
-
self._output.put(msgs, timeout=1.)
|
2284
|
-
except queue.Full:
|
2285
|
-
if not self._heartbeat():
|
2286
|
-
return
|
2287
|
-
else:
|
2288
|
-
break
|
2532
|
+
if self._proc.poll() is not None:
|
2533
|
+
log.critical('Journalctl process terminated')
|
2534
|
+
return
|
2289
2535
|
|
2290
|
-
|
2291
|
-
log.critical('Journalctl process terminated')
|
2292
|
-
return
|
2536
|
+
time.sleep(self._sleep_s)
|
2293
2537
|
|
2294
|
-
|
2295
|
-
|
2538
|
+
finally:
|
2539
|
+
subprocess_close(self._proc)
|
2296
2540
|
|
2297
2541
|
|
2298
2542
|
########################################
|
2299
|
-
#
|
2543
|
+
# ../driver.py
|
2544
|
+
"""
|
2545
|
+
TODO:
|
2546
|
+
- create log group
|
2547
|
+
- log stats - chunk sizes, byte count, num calls, etc
|
2548
|
+
|
2549
|
+
==
|
2300
2550
|
|
2551
|
+
https://www.freedesktop.org/software/systemd/man/latest/journalctl.html
|
2552
|
+
|
2553
|
+
journalctl:
|
2554
|
+
-o json
|
2555
|
+
--show-cursor
|
2556
|
+
|
2557
|
+
--since "2012-10-30 18:17:16"
|
2558
|
+
--until "2012-10-30 18:17:16"
|
2559
|
+
|
2560
|
+
--after-cursor <cursor>
|
2561
|
+
|
2562
|
+
==
|
2563
|
+
|
2564
|
+
https://www.freedesktop.org/software/systemd/man/latest/systemd.journal-fields.html
|
2565
|
+
|
2566
|
+
==
|
2301
2567
|
|
2302
2568
|
@dc.dataclass(frozen=True)
|
2303
|
-
class
|
2304
|
-
|
2569
|
+
class Journald2AwsConfig:
|
2570
|
+
log_group_name: str
|
2571
|
+
log_stream_name: str
|
2305
2572
|
|
2306
|
-
|
2307
|
-
|
2573
|
+
aws_batch_size: int = 1_000
|
2574
|
+
aws_flush_interval_s: float = 1.
|
2575
|
+
"""
|
2576
|
+
|
2577
|
+
|
2578
|
+
##
|
2308
2579
|
|
2309
2580
|
|
2310
|
-
class
|
2581
|
+
class JournalctlToAwsDriver(ExitStacked):
|
2311
2582
|
@dc.dataclass(frozen=True)
|
2312
2583
|
class Config:
|
2313
2584
|
pid_file: ta.Optional[str] = None
|
2314
2585
|
|
2315
2586
|
cursor_file: ta.Optional[str] = None
|
2316
2587
|
|
2588
|
+
runtime_limit: ta.Optional[float] = None
|
2589
|
+
|
2317
2590
|
#
|
2318
2591
|
|
2319
2592
|
aws_log_group_name: str = 'omlish'
|
@@ -2324,6 +2597,8 @@ class JournalctlToAws:
|
|
2324
2597
|
|
2325
2598
|
aws_region_name: str = 'us-west-1'
|
2326
2599
|
|
2600
|
+
aws_dry_run: bool = False
|
2601
|
+
|
2327
2602
|
#
|
2328
2603
|
|
2329
2604
|
journalctl_cmd: ta.Optional[ta.Sequence[str]] = None
|
@@ -2331,24 +2606,10 @@ class JournalctlToAws:
|
|
2331
2606
|
journalctl_after_cursor: ta.Optional[str] = None
|
2332
2607
|
journalctl_since: ta.Optional[str] = None
|
2333
2608
|
|
2334
|
-
#
|
2335
|
-
|
2336
|
-
dry_run: bool = False
|
2337
|
-
|
2338
2609
|
def __init__(self, config: Config) -> None:
|
2339
2610
|
super().__init__()
|
2340
|
-
self._config = config
|
2341
|
-
|
2342
|
-
#
|
2343
|
-
|
2344
|
-
_es: contextlib.ExitStack
|
2345
2611
|
|
2346
|
-
|
2347
|
-
self._es = contextlib.ExitStack().__enter__()
|
2348
|
-
return self
|
2349
|
-
|
2350
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
2351
|
-
return self._es.__exit__(exc_type, exc_val, exc_tb)
|
2612
|
+
self._config = config
|
2352
2613
|
|
2353
2614
|
#
|
2354
2615
|
|
@@ -2361,7 +2622,7 @@ class JournalctlToAws:
|
|
2361
2622
|
|
2362
2623
|
log.info('Opening pidfile %s', pfp)
|
2363
2624
|
|
2364
|
-
pf = self.
|
2625
|
+
pf = self._enter_context(Pidfile(pfp))
|
2365
2626
|
pf.write()
|
2366
2627
|
return pf
|
2367
2628
|
|
@@ -2371,48 +2632,32 @@ class JournalctlToAws:
|
|
2371
2632
|
|
2372
2633
|
#
|
2373
2634
|
|
2374
|
-
|
2375
|
-
|
2376
|
-
|
2377
|
-
|
2378
|
-
|
2379
|
-
|
2380
|
-
|
2381
|
-
try:
|
2382
|
-
with open(cf) as f:
|
2383
|
-
return f.read().strip()
|
2384
|
-
except FileNotFoundError:
|
2385
|
-
return None
|
2386
|
-
|
2387
|
-
def _write_cursor_file(self, cursor: str) -> None:
|
2388
|
-
self._ensure_locked()
|
2389
|
-
|
2390
|
-
if not (cf := self._config.cursor_file):
|
2391
|
-
return
|
2392
|
-
cf = os.path.expanduser(cf)
|
2393
|
-
|
2394
|
-
log.info('Writing cursor file %s : %s', cf, cursor)
|
2395
|
-
with open(ncf := cf + '.next', 'w') as f:
|
2396
|
-
f.write(cursor)
|
2397
|
-
|
2398
|
-
os.rename(ncf, cf)
|
2635
|
+
@cached_nullary
|
2636
|
+
def _cursor(self) -> JournalctlToAwsCursor:
|
2637
|
+
return JournalctlToAwsCursor(
|
2638
|
+
self._config.cursor_file,
|
2639
|
+
ensure_locked=self._ensure_locked,
|
2640
|
+
)
|
2399
2641
|
|
2400
2642
|
#
|
2401
2643
|
|
2402
2644
|
@cached_nullary
|
2403
|
-
def _aws_credentials(self) -> AwsSigner.Credentials:
|
2645
|
+
def _aws_credentials(self) -> ta.Optional[AwsSigner.Credentials]:
|
2646
|
+
if self._config.aws_access_key_id is None and self._config.aws_secret_access_key is None:
|
2647
|
+
return None
|
2648
|
+
|
2404
2649
|
return AwsSigner.Credentials(
|
2405
2650
|
access_key_id=check_non_empty_str(self._config.aws_access_key_id),
|
2406
2651
|
secret_access_key=check_non_empty_str(self._config.aws_secret_access_key),
|
2407
2652
|
)
|
2408
2653
|
|
2409
2654
|
@cached_nullary
|
2410
|
-
def
|
2411
|
-
return
|
2655
|
+
def _aws_log_message_builder(self) -> AwsLogMessageBuilder:
|
2656
|
+
return AwsLogMessageBuilder(
|
2412
2657
|
log_group_name=self._config.aws_log_group_name,
|
2413
2658
|
log_stream_name=check_non_empty_str(self._config.aws_log_stream_name),
|
2414
2659
|
region_name=self._config.aws_region_name,
|
2415
|
-
credentials=
|
2660
|
+
credentials=self._aws_credentials(),
|
2416
2661
|
)
|
2417
2662
|
|
2418
2663
|
#
|
@@ -2431,7 +2676,7 @@ class JournalctlToAws:
|
|
2431
2676
|
else:
|
2432
2677
|
ac = self._config.journalctl_after_cursor
|
2433
2678
|
if ac is None:
|
2434
|
-
ac = self.
|
2679
|
+
ac = self._cursor().get()
|
2435
2680
|
if ac is not None:
|
2436
2681
|
log.info('Starting from cursor %s', ac)
|
2437
2682
|
|
@@ -2447,63 +2692,49 @@ class JournalctlToAws:
|
|
2447
2692
|
|
2448
2693
|
#
|
2449
2694
|
|
2450
|
-
|
2451
|
-
|
2695
|
+
@cached_nullary
|
2696
|
+
def _aws_poster_worker(self) -> JournalctlToAwsPosterWorker:
|
2697
|
+
return JournalctlToAwsPosterWorker(
|
2698
|
+
self._journalctl_message_queue(),
|
2699
|
+
self._aws_log_message_builder(),
|
2700
|
+
self._cursor(),
|
2452
2701
|
|
2453
|
-
|
2454
|
-
|
2455
|
-
|
2702
|
+
ensure_locked=self._ensure_locked,
|
2703
|
+
dry_run=self._config.aws_dry_run,
|
2704
|
+
)
|
2456
2705
|
|
2457
|
-
|
2706
|
+
#
|
2458
2707
|
|
2459
|
-
|
2460
|
-
|
2461
|
-
|
2462
|
-
log.critical('Journalctl tailer worker died')
|
2463
|
-
break
|
2708
|
+
def run(self) -> None:
|
2709
|
+
pw: JournalctlToAwsPosterWorker = self._aws_poster_worker()
|
2710
|
+
tw: JournalctlTailerWorker = self._journalctl_tailer_worker()
|
2464
2711
|
|
2465
|
-
|
2466
|
-
msgs: ta.Sequence[JournalctlMessage] = q.get(timeout=1.)
|
2467
|
-
except queue.Empty:
|
2468
|
-
msgs = []
|
2469
|
-
if not msgs:
|
2470
|
-
continue
|
2712
|
+
ws = [pw, tw]
|
2471
2713
|
|
2472
|
-
|
2714
|
+
for w in ws:
|
2715
|
+
w.start()
|
2473
2716
|
|
2474
|
-
|
2475
|
-
|
2476
|
-
|
2477
|
-
|
2717
|
+
start = time.time()
|
2718
|
+
|
2719
|
+
while True:
|
2720
|
+
for w in ws:
|
2721
|
+
if not w.is_alive():
|
2722
|
+
log.critical('Worker died: %r', w)
|
2478
2723
|
break
|
2479
2724
|
|
2480
|
-
if not
|
2481
|
-
log.warning('
|
2482
|
-
|
2725
|
+
if (rl := self._config.runtime_limit) is not None and time.time() - start >= rl:
|
2726
|
+
log.warning('Runtime limit reached')
|
2727
|
+
break
|
2483
2728
|
|
2484
|
-
|
2485
|
-
for m in msgs:
|
2486
|
-
feed_msgs.append(mp.Message(
|
2487
|
-
message=json.dumps(m.dct, sort_keys=True),
|
2488
|
-
ts_ms=int((m.ts_us / 1000.) if m.ts_us is not None else (time.time() * 1000.)),
|
2489
|
-
))
|
2729
|
+
time.sleep(1.)
|
2490
2730
|
|
2491
|
-
|
2492
|
-
|
2731
|
+
for w in reversed(ws):
|
2732
|
+
w.stop()
|
2733
|
+
w.join()
|
2493
2734
|
|
2494
|
-
if not self._config.dry_run:
|
2495
|
-
with urllib.request.urlopen(urllib.request.Request( # noqa
|
2496
|
-
post.url,
|
2497
|
-
method='POST',
|
2498
|
-
headers=dict(post.headers),
|
2499
|
-
data=post.data,
|
2500
|
-
)) as resp:
|
2501
|
-
response = AwsPutLogEventsResponse.from_aws(json.loads(resp.read().decode('utf-8')))
|
2502
|
-
log.debug('%r', response)
|
2503
2735
|
|
2504
|
-
|
2505
|
-
|
2506
|
-
last_cursor = cur_cursor # noqa
|
2736
|
+
########################################
|
2737
|
+
# main.py
|
2507
2738
|
|
2508
2739
|
|
2509
2740
|
def _main() -> None:
|
@@ -2518,6 +2749,8 @@ def _main() -> None:
|
|
2518
2749
|
|
2519
2750
|
parser.add_argument('--message', nargs='?')
|
2520
2751
|
parser.add_argument('--real', action='store_true')
|
2752
|
+
parser.add_argument('--num-messages', type=int)
|
2753
|
+
parser.add_argument('--runtime-limit', type=float)
|
2521
2754
|
|
2522
2755
|
args = parser.parse_args()
|
2523
2756
|
|
@@ -2527,13 +2760,13 @@ def _main() -> None:
|
|
2527
2760
|
|
2528
2761
|
#
|
2529
2762
|
|
2530
|
-
config:
|
2763
|
+
config: JournalctlToAwsDriver.Config
|
2531
2764
|
if args.config_file:
|
2532
2765
|
with open(os.path.expanduser(args.config_file)) as cf:
|
2533
2766
|
config_dct = json.load(cf)
|
2534
|
-
config = unmarshal_obj(config_dct,
|
2767
|
+
config = unmarshal_obj(config_dct, JournalctlToAwsDriver.Config)
|
2535
2768
|
else:
|
2536
|
-
config =
|
2769
|
+
config = JournalctlToAwsDriver.Config()
|
2537
2770
|
|
2538
2771
|
#
|
2539
2772
|
|
@@ -2550,7 +2783,7 @@ def _main() -> None:
|
|
2550
2783
|
'--sleep-n', '2',
|
2551
2784
|
'--sleep-s', '.5',
|
2552
2785
|
*(['--message', args.message] if args.message else []),
|
2553
|
-
|
2786
|
+
str(args.num_messages or 100_000),
|
2554
2787
|
])
|
2555
2788
|
|
2556
2789
|
#
|
@@ -2558,14 +2791,14 @@ def _main() -> None:
|
|
2558
2791
|
for ca, pa in [
|
2559
2792
|
('journalctl_after_cursor', 'after_cursor'),
|
2560
2793
|
('journalctl_since', 'since'),
|
2561
|
-
('
|
2794
|
+
('aws_dry_run', 'dry_run'),
|
2562
2795
|
]:
|
2563
2796
|
if (av := getattr(args, pa)):
|
2564
2797
|
config = dc.replace(config, **{ca: av})
|
2565
2798
|
|
2566
2799
|
#
|
2567
2800
|
|
2568
|
-
with
|
2801
|
+
with JournalctlToAwsDriver(config) as jta:
|
2569
2802
|
jta.run()
|
2570
2803
|
|
2571
2804
|
|