ominfra 0.0.0.dev102__tar.gz → 0.0.0.dev103__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. {ominfra-0.0.0.dev102/ominfra.egg-info → ominfra-0.0.0.dev103}/PKG-INFO +3 -3
  2. ominfra-0.0.0.dev103/ominfra/clouds/aws/journald2aws/cursor.py +47 -0
  3. ominfra-0.0.0.dev103/ominfra/clouds/aws/journald2aws/driver.py +210 -0
  4. ominfra-0.0.0.dev103/ominfra/clouds/aws/journald2aws/main.py +81 -0
  5. ominfra-0.0.0.dev103/ominfra/clouds/aws/journald2aws/poster.py +89 -0
  6. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/clouds/aws/logs.py +19 -13
  7. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/_executor.py +25 -3
  8. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/poly/_main.py +26 -2
  9. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/journald/genmessages.py +14 -2
  10. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/journald/tailer.py +39 -33
  11. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/pyremote/_runcommands.py +25 -3
  12. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/scripts/journald2aws.py +428 -195
  13. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/scripts/supervisor.py +1 -1
  14. ominfra-0.0.0.dev103/ominfra/threadworkers.py +139 -0
  15. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103/ominfra.egg-info}/PKG-INFO +3 -3
  16. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra.egg-info/SOURCES.txt +4 -1
  17. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra.egg-info/requires.txt +2 -2
  18. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/pyproject.toml +3 -3
  19. ominfra-0.0.0.dev102/ominfra/clouds/aws/journald2aws/main.py +0 -334
  20. ominfra-0.0.0.dev102/ominfra/threadworker.py +0 -67
  21. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/LICENSE +0 -0
  22. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/MANIFEST.in +0 -0
  23. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/README.rst +0 -0
  24. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/.manifests.json +0 -0
  25. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/__about__.py +0 -0
  26. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/__init__.py +0 -0
  27. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/clouds/__init__.py +0 -0
  28. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/clouds/aws/__init__.py +0 -0
  29. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/clouds/aws/__main__.py +0 -0
  30. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/clouds/aws/auth.py +0 -0
  31. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/clouds/aws/cli.py +0 -0
  32. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/clouds/aws/dataclasses.py +0 -0
  33. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/clouds/aws/journald2aws/__init__.py +0 -0
  34. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/clouds/aws/metadata.py +0 -0
  35. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/cmds.py +0 -0
  36. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/__init__.py +0 -0
  37. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/configs.py +0 -0
  38. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/executor/__init__.py +0 -0
  39. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/executor/base.py +0 -0
  40. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/executor/concerns/__init__.py +0 -0
  41. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/executor/concerns/dirs.py +0 -0
  42. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/executor/concerns/nginx.py +0 -0
  43. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/executor/concerns/repo.py +0 -0
  44. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/executor/concerns/supervisor.py +0 -0
  45. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/executor/concerns/systemd.py +0 -0
  46. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/executor/concerns/user.py +0 -0
  47. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/executor/concerns/venv.py +0 -0
  48. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/executor/main.py +0 -0
  49. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/poly/__init__.py +0 -0
  50. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/poly/base.py +0 -0
  51. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/poly/configs.py +0 -0
  52. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/poly/deploy.py +0 -0
  53. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/poly/main.py +0 -0
  54. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/poly/nginx.py +0 -0
  55. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/poly/repo.py +0 -0
  56. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/poly/runtime.py +0 -0
  57. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/poly/site.py +0 -0
  58. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/poly/supervisor.py +0 -0
  59. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/poly/venv.py +0 -0
  60. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/deploy/remote.py +0 -0
  61. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/journald/__init__.py +0 -0
  62. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/journald/messages.py +0 -0
  63. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/manage/__init__.py +0 -0
  64. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/manage/manage.py +0 -0
  65. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/pyremote/__init__.py +0 -0
  66. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/pyremote/bootstrap.py +0 -0
  67. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/pyremote/runcommands.py +0 -0
  68. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/scripts/__init__.py +0 -0
  69. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/ssh.py +0 -0
  70. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/__init__.py +0 -0
  71. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/__main__.py +0 -0
  72. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/compat.py +0 -0
  73. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/configs.py +0 -0
  74. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/context.py +0 -0
  75. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/datatypes.py +0 -0
  76. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/dispatchers.py +0 -0
  77. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/events.py +0 -0
  78. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/exceptions.py +0 -0
  79. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/poller.py +0 -0
  80. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/process.py +0 -0
  81. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/states.py +0 -0
  82. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/supervisor.py +0 -0
  83. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/supervisor/types.py +0 -0
  84. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/tailscale/__init__.py +0 -0
  85. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/tailscale/api.py +0 -0
  86. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/tailscale/cli.py +0 -0
  87. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/tools/__init__.py +0 -0
  88. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra/tools/listresources.py +0 -0
  89. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra.egg-info/dependency_links.txt +0 -0
  90. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra.egg-info/entry_points.txt +0 -0
  91. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/ominfra.egg-info/top_level.txt +0 -0
  92. {ominfra-0.0.0.dev102 → ominfra-0.0.0.dev103}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ominfra
3
- Version: 0.0.0.dev102
3
+ Version: 0.0.0.dev103
4
4
  Summary: ominfra
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -12,8 +12,8 @@ Classifier: Operating System :: OS Independent
12
12
  Classifier: Operating System :: POSIX
13
13
  Requires-Python: >=3.12
14
14
  License-File: LICENSE
15
- Requires-Dist: omdev==0.0.0.dev102
16
- Requires-Dist: omlish==0.0.0.dev102
15
+ Requires-Dist: omdev==0.0.0.dev103
16
+ Requires-Dist: omlish==0.0.0.dev103
17
17
  Provides-Extra: all
18
18
  Requires-Dist: paramiko~=3.5; extra == "all"
19
19
  Requires-Dist: asyncssh~=2.18; extra == "all"
@@ -0,0 +1,47 @@
1
+ # ruff: noqa: UP007
2
+ import os.path
3
+ import typing as ta
4
+
5
+ from omlish.lite.logs import log
6
+
7
+
8
+ class JournalctlToAwsCursor:
9
+ def __init__(
10
+ self,
11
+ cursor_file: ta.Optional[str] = None,
12
+ *,
13
+ ensure_locked: ta.Optional[ta.Callable[[], None]] = None,
14
+ ) -> None:
15
+ super().__init__()
16
+ self._cursor_file = cursor_file
17
+ self._ensure_locked = ensure_locked
18
+
19
+ #
20
+
21
+ def get(self) -> ta.Optional[str]:
22
+ if self._ensure_locked is not None:
23
+ self._ensure_locked()
24
+
25
+ if not (cf := self._cursor_file):
26
+ return None
27
+ cf = os.path.expanduser(cf)
28
+
29
+ try:
30
+ with open(cf) as f:
31
+ return f.read().strip()
32
+ except FileNotFoundError:
33
+ return None
34
+
35
+ def set(self, cursor: str) -> None:
36
+ if self._ensure_locked is not None:
37
+ self._ensure_locked()
38
+
39
+ if not (cf := self._cursor_file):
40
+ return
41
+ cf = os.path.expanduser(cf)
42
+
43
+ log.info('Writing cursor file %s : %s', cf, cursor)
44
+ with open(ncf := cf + '.next', 'w') as f:
45
+ f.write(cursor)
46
+
47
+ os.rename(ncf, cf)
@@ -0,0 +1,210 @@
1
+ # ruff: noqa: UP007
2
+ """
3
+ TODO:
4
+ - create log group
5
+ - log stats - chunk sizes, byte count, num calls, etc
6
+
7
+ ==
8
+
9
+ https://www.freedesktop.org/software/systemd/man/latest/journalctl.html
10
+
11
+ journalctl:
12
+ -o json
13
+ --show-cursor
14
+
15
+ --since "2012-10-30 18:17:16"
16
+ --until "2012-10-30 18:17:16"
17
+
18
+ --after-cursor <cursor>
19
+
20
+ ==
21
+
22
+ https://www.freedesktop.org/software/systemd/man/latest/systemd.journal-fields.html
23
+
24
+ ==
25
+
26
+ @dc.dataclass(frozen=True)
27
+ class Journald2AwsConfig:
28
+ log_group_name: str
29
+ log_stream_name: str
30
+
31
+ aws_batch_size: int = 1_000
32
+ aws_flush_interval_s: float = 1.
33
+ """
34
+ import dataclasses as dc
35
+ import os.path
36
+ import queue
37
+ import time
38
+ import typing as ta
39
+
40
+ from omlish.lite.cached import cached_nullary
41
+ from omlish.lite.check import check_non_empty_str
42
+ from omlish.lite.contextmanagers import ExitStacked
43
+ from omlish.lite.logs import log
44
+ from omlish.lite.pidfile import Pidfile
45
+ from omlish.lite.runtime import is_debugger_attached
46
+
47
+ from ....journald.messages import JournalctlMessage # noqa
48
+ from ....journald.tailer import JournalctlTailerWorker
49
+ from ..auth import AwsSigner
50
+ from ..logs import AwsLogMessageBuilder
51
+ from .cursor import JournalctlToAwsCursor
52
+ from .poster import JournalctlToAwsPosterWorker
53
+
54
+
55
+ ##
56
+
57
+
58
+ class JournalctlToAwsDriver(ExitStacked):
59
+ @dc.dataclass(frozen=True)
60
+ class Config:
61
+ pid_file: ta.Optional[str] = None
62
+
63
+ cursor_file: ta.Optional[str] = None
64
+
65
+ runtime_limit: ta.Optional[float] = None
66
+
67
+ #
68
+
69
+ aws_log_group_name: str = 'omlish'
70
+ aws_log_stream_name: ta.Optional[str] = None
71
+
72
+ aws_access_key_id: ta.Optional[str] = None
73
+ aws_secret_access_key: ta.Optional[str] = dc.field(default=None, repr=False)
74
+
75
+ aws_region_name: str = 'us-west-1'
76
+
77
+ aws_dry_run: bool = False
78
+
79
+ #
80
+
81
+ journalctl_cmd: ta.Optional[ta.Sequence[str]] = None
82
+
83
+ journalctl_after_cursor: ta.Optional[str] = None
84
+ journalctl_since: ta.Optional[str] = None
85
+
86
+ def __init__(self, config: Config) -> None:
87
+ super().__init__()
88
+
89
+ self._config = config
90
+
91
+ #
92
+
93
+ @cached_nullary
94
+ def _pidfile(self) -> ta.Optional[Pidfile]:
95
+ if self._config.pid_file is None:
96
+ return None
97
+
98
+ pfp = os.path.expanduser(self._config.pid_file)
99
+
100
+ log.info('Opening pidfile %s', pfp)
101
+
102
+ pf = self._enter_context(Pidfile(pfp))
103
+ pf.write()
104
+ return pf
105
+
106
+ def _ensure_locked(self) -> None:
107
+ if (pf := self._pidfile()) is not None:
108
+ pf.ensure_locked()
109
+
110
+ #
111
+
112
+ @cached_nullary
113
+ def _cursor(self) -> JournalctlToAwsCursor:
114
+ return JournalctlToAwsCursor(
115
+ self._config.cursor_file,
116
+ ensure_locked=self._ensure_locked,
117
+ )
118
+
119
+ #
120
+
121
+ @cached_nullary
122
+ def _aws_credentials(self) -> ta.Optional[AwsSigner.Credentials]:
123
+ if self._config.aws_access_key_id is None and self._config.aws_secret_access_key is None:
124
+ return None
125
+
126
+ return AwsSigner.Credentials(
127
+ access_key_id=check_non_empty_str(self._config.aws_access_key_id),
128
+ secret_access_key=check_non_empty_str(self._config.aws_secret_access_key),
129
+ )
130
+
131
+ @cached_nullary
132
+ def _aws_log_message_builder(self) -> AwsLogMessageBuilder:
133
+ return AwsLogMessageBuilder(
134
+ log_group_name=self._config.aws_log_group_name,
135
+ log_stream_name=check_non_empty_str(self._config.aws_log_stream_name),
136
+ region_name=self._config.aws_region_name,
137
+ credentials=self._aws_credentials(),
138
+ )
139
+
140
+ #
141
+
142
+ @cached_nullary
143
+ def _journalctl_message_queue(self): # type: () -> queue.Queue[ta.Sequence[JournalctlMessage]]
144
+ return queue.Queue()
145
+
146
+ @cached_nullary
147
+ def _journalctl_tailer_worker(self) -> JournalctlTailerWorker:
148
+ ac: ta.Optional[str] = None
149
+
150
+ if (since := self._config.journalctl_since):
151
+ log.info('Starting since %s', since)
152
+
153
+ else:
154
+ ac = self._config.journalctl_after_cursor
155
+ if ac is None:
156
+ ac = self._cursor().get()
157
+ if ac is not None:
158
+ log.info('Starting from cursor %s', ac)
159
+
160
+ return JournalctlTailerWorker(
161
+ self._journalctl_message_queue(),
162
+
163
+ since=since,
164
+ after_cursor=ac,
165
+
166
+ cmd=self._config.journalctl_cmd,
167
+ shell_wrap=is_debugger_attached(),
168
+ )
169
+
170
+ #
171
+
172
+ @cached_nullary
173
+ def _aws_poster_worker(self) -> JournalctlToAwsPosterWorker:
174
+ return JournalctlToAwsPosterWorker(
175
+ self._journalctl_message_queue(),
176
+ self._aws_log_message_builder(),
177
+ self._cursor(),
178
+
179
+ ensure_locked=self._ensure_locked,
180
+ dry_run=self._config.aws_dry_run,
181
+ )
182
+
183
+ #
184
+
185
+ def run(self) -> None:
186
+ pw: JournalctlToAwsPosterWorker = self._aws_poster_worker()
187
+ tw: JournalctlTailerWorker = self._journalctl_tailer_worker()
188
+
189
+ ws = [pw, tw]
190
+
191
+ for w in ws:
192
+ w.start()
193
+
194
+ start = time.time()
195
+
196
+ while True:
197
+ for w in ws:
198
+ if not w.is_alive():
199
+ log.critical('Worker died: %r', w)
200
+ break
201
+
202
+ if (rl := self._config.runtime_limit) is not None and time.time() - start >= rl:
203
+ log.warning('Runtime limit reached')
204
+ break
205
+
206
+ time.sleep(1.)
207
+
208
+ for w in reversed(ws):
209
+ w.stop()
210
+ w.join()
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env python3
2
+ # @omlish-amalg ../../../scripts/journald2aws.py
3
+ import argparse
4
+ import dataclasses as dc
5
+ import json
6
+ import os.path
7
+ import sys
8
+
9
+ from omlish.lite.logs import configure_standard_logging
10
+ from omlish.lite.marshal import unmarshal_obj
11
+
12
+ from .driver import JournalctlToAwsDriver
13
+
14
+
15
+ def _main() -> None:
16
+ parser = argparse.ArgumentParser()
17
+
18
+ parser.add_argument('--config-file')
19
+ parser.add_argument('-v', '--verbose', action='store_true')
20
+
21
+ parser.add_argument('--after-cursor', nargs='?')
22
+ parser.add_argument('--since', nargs='?')
23
+ parser.add_argument('--dry-run', action='store_true')
24
+
25
+ parser.add_argument('--message', nargs='?')
26
+ parser.add_argument('--real', action='store_true')
27
+ parser.add_argument('--num-messages', type=int)
28
+ parser.add_argument('--runtime-limit', type=float)
29
+
30
+ args = parser.parse_args()
31
+
32
+ #
33
+
34
+ configure_standard_logging('DEBUG' if args.verbose else 'INFO')
35
+
36
+ #
37
+
38
+ config: JournalctlToAwsDriver.Config
39
+ if args.config_file:
40
+ with open(os.path.expanduser(args.config_file)) as cf:
41
+ config_dct = json.load(cf)
42
+ config = unmarshal_obj(config_dct, JournalctlToAwsDriver.Config)
43
+ else:
44
+ config = JournalctlToAwsDriver.Config()
45
+
46
+ #
47
+
48
+ for k in ['aws_access_key_id', 'aws_secret_access_key']:
49
+ if not getattr(config, k) and k.upper() in os.environ:
50
+ config = dc.replace(config, **{k: os.environ.get(k.upper())}) # type: ignore
51
+
52
+ #
53
+
54
+ if not args.real:
55
+ config = dc.replace(config, journalctl_cmd=[
56
+ sys.executable,
57
+ os.path.join(os.path.dirname(__file__), '..', '..', '..', 'journald', 'genmessages.py'),
58
+ '--sleep-n', '2',
59
+ '--sleep-s', '.5',
60
+ *(['--message', args.message] if args.message else []),
61
+ str(args.num_messages or 100_000),
62
+ ])
63
+
64
+ #
65
+
66
+ for ca, pa in [
67
+ ('journalctl_after_cursor', 'after_cursor'),
68
+ ('journalctl_since', 'since'),
69
+ ('aws_dry_run', 'dry_run'),
70
+ ]:
71
+ if (av := getattr(args, pa)):
72
+ config = dc.replace(config, **{ca: av})
73
+
74
+ #
75
+
76
+ with JournalctlToAwsDriver(config) as jta:
77
+ jta.run()
78
+
79
+
80
+ if __name__ == '__main__':
81
+ _main()
@@ -0,0 +1,89 @@
1
+ # ruff: noqa: UP007
2
+ """
3
+ TODO:
4
+ - retries
5
+ """
6
+ import json
7
+ import queue
8
+ import time
9
+ import typing as ta
10
+ import urllib.request
11
+
12
+ from omlish.lite.logs import log
13
+
14
+ from ....journald.messages import JournalctlMessage # noqa
15
+ from ....threadworkers import ThreadWorker
16
+ from ..logs import AwsLogMessageBuilder
17
+ from ..logs import AwsPutLogEventsResponse
18
+ from .cursor import JournalctlToAwsCursor
19
+
20
+
21
+ class JournalctlToAwsPosterWorker(ThreadWorker):
22
+ def __init__(
23
+ self,
24
+ queue, # type: queue.Queue[ta.Sequence[JournalctlMessage]] # noqa
25
+ builder: AwsLogMessageBuilder,
26
+ cursor: JournalctlToAwsCursor,
27
+ *,
28
+ ensure_locked: ta.Optional[ta.Callable[[], None]] = None,
29
+ dry_run: bool = False,
30
+ queue_timeout_s: float = 1.,
31
+ **kwargs: ta.Any,
32
+ ) -> None:
33
+ super().__init__(**kwargs)
34
+ self._queue = queue
35
+ self._builder = builder
36
+ self._cursor = cursor
37
+ self._ensure_locked = ensure_locked
38
+ self._dry_run = dry_run
39
+ self._queue_timeout_s = queue_timeout_s
40
+ #
41
+
42
+ def _run(self) -> None:
43
+ if self._ensure_locked is not None:
44
+ self._ensure_locked()
45
+
46
+ last_cursor: ta.Optional[str] = None # noqa
47
+ while True:
48
+ self._heartbeat()
49
+
50
+ try:
51
+ msgs: ta.Sequence[JournalctlMessage] = self._queue.get(timeout=self._queue_timeout_s)
52
+ except queue.Empty:
53
+ msgs = []
54
+
55
+ if not msgs:
56
+ log.debug('Empty queue chunk')
57
+ continue
58
+
59
+ log.debug('%r', msgs)
60
+
61
+ cur_cursor: ta.Optional[str] = None
62
+ for m in reversed(msgs):
63
+ if m.cursor is not None:
64
+ cur_cursor = m.cursor
65
+ break
66
+
67
+ feed_msgs = []
68
+ for m in msgs:
69
+ feed_msgs.append(AwsLogMessageBuilder.Message(
70
+ message=json.dumps(m.dct, sort_keys=True),
71
+ ts_ms=int((m.ts_us / 1000.) if m.ts_us is not None else (time.time() * 1000.)),
72
+ ))
73
+
74
+ for post in self._builder.feed(feed_msgs):
75
+ log.debug('%r', post)
76
+
77
+ if not self._dry_run:
78
+ with urllib.request.urlopen(urllib.request.Request( # noqa
79
+ post.url,
80
+ method='POST',
81
+ headers=dict(post.headers),
82
+ data=post.data,
83
+ )) as resp:
84
+ response = AwsPutLogEventsResponse.from_aws(json.loads(resp.read().decode('utf-8')))
85
+ log.debug('%r', response)
86
+
87
+ if cur_cursor is not None:
88
+ self._cursor.set(cur_cursor)
89
+ last_cursor = cur_cursor # noqa
@@ -62,7 +62,7 @@ class AwsPutLogEventsResponse(AwsDataclass):
62
62
  ##
63
63
 
64
64
 
65
- class AwsLogMessagePoster:
65
+ class AwsLogMessageBuilder:
66
66
  """
67
67
  TODO:
68
68
  - max_items
@@ -88,7 +88,7 @@ class AwsLogMessagePoster:
88
88
  log_group_name: str,
89
89
  log_stream_name: str,
90
90
  region_name: str,
91
- credentials: AwsSigner.Credentials,
91
+ credentials: ta.Optional[AwsSigner.Credentials],
92
92
 
93
93
  url: ta.Optional[str] = None,
94
94
  service_name: str = DEFAULT_SERVICE_NAME,
@@ -110,11 +110,16 @@ class AwsLogMessagePoster:
110
110
  headers = {**headers, **extra_headers}
111
111
  self._headers = {k: [v] for k, v in headers.items()}
112
112
 
113
- self._signer = V4AwsSigner(
114
- credentials,
115
- region_name,
116
- service_name,
117
- )
113
+ signer: ta.Optional[V4AwsSigner]
114
+ if credentials is not None:
115
+ signer = V4AwsSigner(
116
+ credentials,
117
+ region_name,
118
+ service_name,
119
+ )
120
+ else:
121
+ signer = None
122
+ self._signer = signer
118
123
 
119
124
  #
120
125
 
@@ -158,13 +163,14 @@ class AwsLogMessagePoster:
158
163
  payload=body,
159
164
  )
160
165
 
161
- sig_headers = self._signer.sign(
162
- sig_req,
163
- sign_payload=False,
164
- )
165
- sig_req = dc.replace(sig_req, headers={**sig_req.headers, **sig_headers})
166
+ if (signer := self._signer) is not None:
167
+ sig_headers = signer.sign(
168
+ sig_req,
169
+ sign_payload=False,
170
+ )
171
+ sig_req = dc.replace(sig_req, headers={**sig_req.headers, **sig_headers})
166
172
 
167
- post = AwsLogMessagePoster.Post(
173
+ post = AwsLogMessageBuilder.Post(
168
174
  url=self._url,
169
175
  headers={k: check_single(v) for k, v in sig_req.headers.items()},
170
176
  data=sig_req.payload,
@@ -82,7 +82,7 @@ if sys.version_info < (3, 8):
82
82
  ########################################
83
83
 
84
84
 
85
- # ../../../../omlish/lite/check.py
85
+ # ../../../../omlish/lite/cached.py
86
86
  T = ta.TypeVar('T')
87
87
 
88
88
 
@@ -112,7 +112,7 @@ class HostConfig:
112
112
  # ../../../../omlish/lite/cached.py
113
113
 
114
114
 
115
- class cached_nullary: # noqa
115
+ class _cached_nullary: # noqa
116
116
  def __init__(self, fn):
117
117
  super().__init__()
118
118
  self._fn = fn
@@ -129,6 +129,10 @@ class cached_nullary: # noqa
129
129
  return bound
130
130
 
131
131
 
132
+ def cached_nullary(fn: ta.Callable[..., T]) -> ta.Callable[..., T]:
133
+ return _cached_nullary(fn)
134
+
135
+
132
136
  ########################################
133
137
  # ../../../../omlish/lite/check.py
134
138
 
@@ -661,7 +665,7 @@ class DataclassObjMarshaler(ObjMarshaler):
661
665
  return {k: m.marshal(getattr(o, k)) for k, m in self.fs.items()}
662
666
 
663
667
  def unmarshal(self, o: ta.Any) -> ta.Any:
664
- return self.ty(**{k: self.fs[k].unmarshal(v) for k, v in o.items() if self.nonstrict or k in self.fs})
668
+ return self.ty(**{k: self.fs[k].unmarshal(v) for k, v in o.items() if not self.nonstrict or k in self.fs})
665
669
 
666
670
 
667
671
  @dc.dataclass(frozen=True)
@@ -956,6 +960,24 @@ def subprocess_try_output_str(*args: str, **kwargs: ta.Any) -> ta.Optional[str]:
956
960
  return out.decode().strip() if out is not None else None
957
961
 
958
962
 
963
+ ##
964
+
965
+
966
+ def subprocess_close(
967
+ proc: subprocess.Popen,
968
+ timeout: ta.Optional[float] = None,
969
+ ) -> None:
970
+ # TODO: terminate, sleep, kill
971
+ if proc.stdout:
972
+ proc.stdout.close()
973
+ if proc.stderr:
974
+ proc.stderr.close()
975
+ if proc.stdin:
976
+ proc.stdin.close()
977
+
978
+ proc.wait(timeout)
979
+
980
+
959
981
  ########################################
960
982
  # ../base.py
961
983
 
@@ -34,8 +34,10 @@ if sys.version_info < (3, 8):
34
34
  ########################################
35
35
 
36
36
 
37
- # ../base.py
37
+ # ../../../../omlish/lite/cached.py
38
38
  T = ta.TypeVar('T')
39
+
40
+ # ../base.py
39
41
  ConcernT = ta.TypeVar('ConcernT')
40
42
  ConfigT = ta.TypeVar('ConfigT')
41
43
  SiteConcernConfigT = ta.TypeVar('SiteConcernConfigT', bound='SiteConcernConfig')
@@ -84,7 +86,7 @@ class DeployConfig:
84
86
  # ../../../../omlish/lite/cached.py
85
87
 
86
88
 
87
- class cached_nullary: # noqa
89
+ class _cached_nullary: # noqa
88
90
  def __init__(self, fn):
89
91
  super().__init__()
90
92
  self._fn = fn
@@ -101,6 +103,10 @@ class cached_nullary: # noqa
101
103
  return bound
102
104
 
103
105
 
106
+ def cached_nullary(fn: ta.Callable[..., T]) -> ta.Callable[..., T]:
107
+ return _cached_nullary(fn)
108
+
109
+
104
110
  ########################################
105
111
  # ../../../../omlish/lite/json.py
106
112
 
@@ -809,6 +815,24 @@ def subprocess_try_output_str(*args: str, **kwargs: ta.Any) -> ta.Optional[str]:
809
815
  return out.decode().strip() if out is not None else None
810
816
 
811
817
 
818
+ ##
819
+
820
+
821
+ def subprocess_close(
822
+ proc: subprocess.Popen,
823
+ timeout: ta.Optional[float] = None,
824
+ ) -> None:
825
+ # TODO: terminate, sleep, kill
826
+ if proc.stdout:
827
+ proc.stdout.close()
828
+ if proc.stderr:
829
+ proc.stderr.close()
830
+ if proc.stdin:
831
+ proc.stdin.close()
832
+
833
+ proc.wait(timeout)
834
+
835
+
812
836
  ########################################
813
837
  # ../runtime.py
814
838
 
@@ -3,6 +3,7 @@
3
3
  import argparse
4
4
  import datetime
5
5
  import json
6
+ import os
6
7
  import re
7
8
  import sys
8
9
  import time
@@ -34,6 +35,11 @@ def _main() -> None:
34
35
  else:
35
36
  start = 0
36
37
 
38
+ stdout_fd = sys.stdout.fileno()
39
+ out_fd = os.dup(stdout_fd)
40
+ null_fd = os.open('/dev/null', os.O_WRONLY)
41
+ os.dup2(null_fd, stdout_fd)
42
+
37
43
  for i in range(start, args.n):
38
44
  if args.sleep_s:
39
45
  if not args.sleep_n or (i and i % args.sleep_n == 0):
@@ -46,8 +52,14 @@ def _main() -> None:
46
52
  '__CURSOR': f'cursor:{i}',
47
53
  '_SOURCE_REALTIME_TIMESTAMP': str(int(ts_us)),
48
54
  }
49
- print(json.dumps(dct, indent=None, separators=(',', ':')))
50
- sys.stdout.flush()
55
+
56
+ buf = json.dumps(dct, indent=None, separators=(',', ':')).encode()
57
+
58
+ try:
59
+ os.write(out_fd, buf)
60
+ os.write(out_fd, b'\n')
61
+ except BrokenPipeError:
62
+ break
51
63
 
52
64
 
53
65
  if __name__ == '__main__':