ominfra 0.0.0.dev101__py3-none-any.whl → 0.0.0.dev103__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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