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.
@@ -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/check.py
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 cached_nullary: # noqa
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._f is not None:
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 AwsLogMessagePoster:
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
- self._signer = V4AwsSigner(
1554
- credentials,
1555
- region_name,
1556
- service_name,
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
- sig_headers = self._signer.sign(
1602
- sig_req,
1603
- sign_payload=False,
1604
- )
1605
- sig_req = dc.replace(sig_req, headers={**sig_req.headers, **sig_headers})
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 = AwsLogMessagePoster.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
- # ../../../../threadworker.py
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
- class ThreadWorker(abc.ABC):
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(self) -> bool:
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
- return False
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
- thr = threading.Thread(target=self._run)
1741
- self._thread = thr
1742
- thr.start()
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
- raise NotImplementedError
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._mb = JournalctlMessageBuilder()
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
- stdout = check_not_none(self._proc.stdout)
2518
+ try:
2519
+ stdout = check_not_none(self._proc.stdout)
2260
2520
 
2261
- fd = stdout.fileno()
2262
- fl = fcntl.fcntl(fd, fcntl.F_GETFL)
2263
- fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
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
- while True:
2266
- if not self._heartbeat():
2267
- return
2525
+ while True:
2526
+ self._heartbeat()
2268
2527
 
2269
- while stdout.readable():
2270
- if not self._heartbeat():
2271
- return
2528
+ self._read_loop(stdout)
2272
2529
 
2273
- buf = stdout.read(self._read_size)
2274
- if not buf:
2275
- log.debug('Journalctl empty read')
2276
- break
2530
+ log.debug('Journalctl not readable')
2277
2531
 
2278
- log.debug('Journalctl read buffer: %r', buf)
2279
- msgs = self._mb.feed(buf)
2280
- if msgs:
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
- if self._proc.poll() is not None:
2291
- log.critical('Journalctl process terminated')
2292
- return
2536
+ time.sleep(self._sleep_s)
2293
2537
 
2294
- log.debug('Journalctl readable')
2295
- time.sleep(self._sleep_s)
2538
+ finally:
2539
+ subprocess_close(self._proc)
2296
2540
 
2297
2541
 
2298
2542
  ########################################
2299
- # main.py
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 JournalctlOpts:
2304
- after_cursor: ta.Optional[str] = None
2569
+ class Journald2AwsConfig:
2570
+ log_group_name: str
2571
+ log_stream_name: str
2305
2572
 
2306
- since: ta.Optional[str] = None
2307
- until: ta.Optional[str] = None
2573
+ aws_batch_size: int = 1_000
2574
+ aws_flush_interval_s: float = 1.
2575
+ """
2576
+
2577
+
2578
+ ##
2308
2579
 
2309
2580
 
2310
- class JournalctlToAws:
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
- def __enter__(self) -> 'JournalctlToAws':
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._es.enter_context(Pidfile(pfp))
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
- def _read_cursor_file(self) -> ta.Optional[str]:
2375
- self._ensure_locked()
2376
-
2377
- if not (cf := self._config.cursor_file):
2378
- return None
2379
- cf = os.path.expanduser(cf)
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 _aws_log_message_poster(self) -> AwsLogMessagePoster:
2411
- return AwsLogMessagePoster(
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=check_not_none(self._aws_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._read_cursor_file()
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
- def run(self) -> None:
2451
- self._ensure_locked()
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
- q = self._journalctl_message_queue() # type: queue.Queue[ta.Sequence[JournalctlMessage]]
2454
- jtw = self._journalctl_tailer_worker() # type: JournalctlTailerWorker
2455
- mp = self._aws_log_message_poster() # type: AwsLogMessagePoster
2702
+ ensure_locked=self._ensure_locked,
2703
+ dry_run=self._config.aws_dry_run,
2704
+ )
2456
2705
 
2457
- jtw.start()
2706
+ #
2458
2707
 
2459
- last_cursor: ta.Optional[str] = None # noqa
2460
- while True:
2461
- if not jtw.is_alive():
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
- try:
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
- log.debug('%r', msgs)
2714
+ for w in ws:
2715
+ w.start()
2473
2716
 
2474
- cur_cursor: ta.Optional[str] = None
2475
- for m in reversed(msgs):
2476
- if m.cursor is not None:
2477
- cur_cursor = m.cursor
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 msgs:
2481
- log.warning('Empty queue chunk')
2482
- continue
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
- feed_msgs = []
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
- [post] = mp.feed(feed_msgs)
2492
- log.debug('%r', post)
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
- if cur_cursor is not None:
2505
- self._write_cursor_file(cur_cursor)
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: JournalctlToAws.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, JournalctlToAws.Config)
2767
+ config = unmarshal_obj(config_dct, JournalctlToAwsDriver.Config)
2535
2768
  else:
2536
- config = JournalctlToAws.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
- '100000',
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
- ('dry_run', 'dry_run'),
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 JournalctlToAws(config) as jta:
2801
+ with JournalctlToAwsDriver(config) as jta:
2569
2802
  jta.run()
2570
2803
 
2571
2804