logalizer 0.1.0__tar.gz

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.
Files changed (36) hide show
  1. logalizer-0.1.0/.github/workflows/ci.yml +23 -0
  2. logalizer-0.1.0/.github/workflows/publish.yml +45 -0
  3. logalizer-0.1.0/.github/workflows/release.yml +22 -0
  4. logalizer-0.1.0/.gitignore +10 -0
  5. logalizer-0.1.0/LICENSE +21 -0
  6. logalizer-0.1.0/PKG-INFO +85 -0
  7. logalizer-0.1.0/README.md +63 -0
  8. logalizer-0.1.0/logalizer/__init__.py +8 -0
  9. logalizer-0.1.0/logalizer/__main__.py +4 -0
  10. logalizer-0.1.0/logalizer/cli.py +284 -0
  11. logalizer-0.1.0/logalizer/client.py +58 -0
  12. logalizer-0.1.0/logalizer/config.py +102 -0
  13. logalizer-0.1.0/logalizer/indexpatterns.py +57 -0
  14. logalizer-0.1.0/logalizer/init.py +125 -0
  15. logalizer-0.1.0/logalizer/ping.py +78 -0
  16. logalizer-0.1.0/logalizer/reporting.py +60 -0
  17. logalizer-0.1.0/logalizer/rison.py +35 -0
  18. logalizer-0.1.0/logalizer.egg-info/PKG-INFO +85 -0
  19. logalizer-0.1.0/logalizer.egg-info/SOURCES.txt +34 -0
  20. logalizer-0.1.0/logalizer.egg-info/dependency_links.txt +1 -0
  21. logalizer-0.1.0/logalizer.egg-info/entry_points.txt +2 -0
  22. logalizer-0.1.0/logalizer.egg-info/scm_file_list.json +31 -0
  23. logalizer-0.1.0/logalizer.egg-info/scm_version.json +8 -0
  24. logalizer-0.1.0/logalizer.egg-info/top_level.txt +1 -0
  25. logalizer-0.1.0/pyproject.toml +37 -0
  26. logalizer-0.1.0/setup.cfg +4 -0
  27. logalizer-0.1.0/tests/__init__.py +0 -0
  28. logalizer-0.1.0/tests/test_cli.py +118 -0
  29. logalizer-0.1.0/tests/test_client.py +44 -0
  30. logalizer-0.1.0/tests/test_config.py +109 -0
  31. logalizer-0.1.0/tests/test_indexpatterns.py +72 -0
  32. logalizer-0.1.0/tests/test_init.py +160 -0
  33. logalizer-0.1.0/tests/test_integration.py +63 -0
  34. logalizer-0.1.0/tests/test_ping.py +105 -0
  35. logalizer-0.1.0/tests/test_reporting.py +59 -0
  36. logalizer-0.1.0/tests/test_rison.py +62 -0
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - name: Install
21
+ run: python -m pip install --upgrade pip
22
+ - name: Test
23
+ run: python -m unittest discover -s tests
@@ -0,0 +1,45 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ name: Build distribution
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ with:
15
+ fetch-depth: 0 # required for setuptools-scm to see tags
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.11"
19
+ - name: Install build
20
+ run: python -m pip install --upgrade build
21
+ - name: Build sdist + wheel
22
+ run: python -m build
23
+ - name: Store artifacts
24
+ uses: actions/upload-artifact@v4
25
+ with:
26
+ name: python-package-distributions
27
+ path: dist/
28
+
29
+ publish:
30
+ name: Publish to PyPI
31
+ needs: build
32
+ runs-on: ubuntu-latest
33
+ environment:
34
+ name: pypi
35
+ url: https://pypi.org/p/logalizer
36
+ permissions:
37
+ id-token: write # required for trusted publishing (OIDC)
38
+ steps:
39
+ - name: Download artifacts
40
+ uses: actions/download-artifact@v4
41
+ with:
42
+ name: python-package-distributions
43
+ path: dist/
44
+ - name: Publish to PyPI
45
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,22 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ version:
7
+ description: "Version to release (e.g. 0.2.0)"
8
+ required: true
9
+
10
+ jobs:
11
+ release:
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ contents: write
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - name: Create and push tag
18
+ run: |
19
+ git config user.name "github-actions[bot]"
20
+ git config user.email "github-actions[bot]@users.noreply.github.com"
21
+ git tag "v${{ github.event.inputs.version }}"
22
+ git push origin "v${{ github.event.inputs.version }}"
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+
8
+ .omo/
9
+ docs/
10
+ uv.lock
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 isdmx
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: logalizer
3
+ Version: 0.1.0
4
+ Summary: Export Kibana logs as CSV from the command line via the Kibana Reporting API.
5
+ Author: isdmx
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/isdmx/logalizer
8
+ Project-URL: Repository, https://github.com/isdmx/logalizer
9
+ Keywords: kibana,elasticsearch,csv,logs,export,cli
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: System :: Logging
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # logalizer
24
+
25
+ Export Kibana logs as CSV from the command line — via the Kibana 7.17
26
+ Reporting API. No third-party dependencies (pure Python stdlib).
27
+
28
+ `logalizer` authenticates to Kibana with basic auth, submits a CSV reporting
29
+ job, polls until it completes, and writes the result to stdout or a file. It is
30
+ built for both humans and scripts: clean exit codes and a strict stdout/stderr
31
+ separation so `logalizer ... > out.csv` always yields only CSV on stdout.
32
+
33
+ ## Install
34
+
35
+ pip install logalizer
36
+
37
+ Requires Python 3.10+.
38
+
39
+ ## Configure
40
+
41
+ logalizer --init
42
+
43
+ Interactive wizard that validates your credentials and lets you pick a space and
44
+ index pattern from live lists. Or set environment variables:
45
+
46
+ export KIBANA_URL=https://...
47
+ export KIBANA_USERNAME=...
48
+ export KIBANA_PASSWORD=...
49
+
50
+ ## Usage
51
+
52
+ # test connectivity and configuration
53
+ logalizer --ping
54
+
55
+ # discover what's available
56
+ logalizer --list-spaces
57
+ logalizer --list-indices --space <space>
58
+ logalizer --list-fields --index '<pattern>'
59
+
60
+ # export logs as CSV
61
+ logalizer -i '<index-pattern>' --query 'level:error' --last 24h \
62
+ --fields '@timestamp,level,msg' -o errors.csv
63
+
64
+ Run `logalizer --help` for the full reference and `logalizer --help-json` for a
65
+ machine-readable flag schema.
66
+
67
+ ## Release
68
+
69
+ Publishing is automated via GitHub Actions Trusted Publishing.
70
+
71
+ One-time setup on PyPI: project settings → Publishing → add a "pending
72
+ publisher" with owner `isdmx`, repository `logalizer`, workflow `publish.yml`,
73
+ environment `pypi`.
74
+
75
+ To cut a release, either use the GitHub Actions UI (Actions → "Release" → "Run
76
+ workflow", enter a version like 0.1.0) or from a local checkout:
77
+
78
+ git tag v0.1.0
79
+ git push --tags
80
+
81
+ Either way, CI builds the sdist + wheel and uploads them to PyPI automatically.
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,63 @@
1
+ # logalizer
2
+
3
+ Export Kibana logs as CSV from the command line — via the Kibana 7.17
4
+ Reporting API. No third-party dependencies (pure Python stdlib).
5
+
6
+ `logalizer` authenticates to Kibana with basic auth, submits a CSV reporting
7
+ job, polls until it completes, and writes the result to stdout or a file. It is
8
+ built for both humans and scripts: clean exit codes and a strict stdout/stderr
9
+ separation so `logalizer ... > out.csv` always yields only CSV on stdout.
10
+
11
+ ## Install
12
+
13
+ pip install logalizer
14
+
15
+ Requires Python 3.10+.
16
+
17
+ ## Configure
18
+
19
+ logalizer --init
20
+
21
+ Interactive wizard that validates your credentials and lets you pick a space and
22
+ index pattern from live lists. Or set environment variables:
23
+
24
+ export KIBANA_URL=https://...
25
+ export KIBANA_USERNAME=...
26
+ export KIBANA_PASSWORD=...
27
+
28
+ ## Usage
29
+
30
+ # test connectivity and configuration
31
+ logalizer --ping
32
+
33
+ # discover what's available
34
+ logalizer --list-spaces
35
+ logalizer --list-indices --space <space>
36
+ logalizer --list-fields --index '<pattern>'
37
+
38
+ # export logs as CSV
39
+ logalizer -i '<index-pattern>' --query 'level:error' --last 24h \
40
+ --fields '@timestamp,level,msg' -o errors.csv
41
+
42
+ Run `logalizer --help` for the full reference and `logalizer --help-json` for a
43
+ machine-readable flag schema.
44
+
45
+ ## Release
46
+
47
+ Publishing is automated via GitHub Actions Trusted Publishing.
48
+
49
+ One-time setup on PyPI: project settings → Publishing → add a "pending
50
+ publisher" with owner `isdmx`, repository `logalizer`, workflow `publish.yml`,
51
+ environment `pypi`.
52
+
53
+ To cut a release, either use the GitHub Actions UI (Actions → "Release" → "Run
54
+ workflow", enter a version like 0.1.0) or from a local checkout:
55
+
56
+ git tag v0.1.0
57
+ git push --tags
58
+
59
+ Either way, CI builds the sdist + wheel and uploads them to PyPI automatically.
60
+
61
+ ## License
62
+
63
+ MIT
@@ -0,0 +1,8 @@
1
+ """logalizer — export Kibana logs as CSV via the Reporting API."""
2
+
3
+ try:
4
+ from importlib.metadata import version as _version
5
+
6
+ __version__ = _version("logalizer")
7
+ except Exception: # pragma: no cover - fallback for uninstalled source tree
8
+ __version__ = "0.0.0"
@@ -0,0 +1,4 @@
1
+ from logalizer.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,284 @@
1
+ """Command-line interface for logalizer."""
2
+ import argparse
3
+ import json
4
+ import os
5
+ import re
6
+ import sys
7
+ from datetime import datetime, timedelta, timezone
8
+
9
+ from logalizer import reporting, indexpatterns
10
+ from logalizer import init, ping
11
+ from logalizer.client import Client, ClientError
12
+ from logalizer.config import build_settings, config_file_path, load_config
13
+
14
+
15
+ class UsageError(Exception):
16
+ pass
17
+
18
+
19
+ _DURATION_RE = re.compile(r"^(\d+)([smhd])$")
20
+ _UNIT_SECONDS = {"s": 1, "m": 60, "h": 3600, "d": 86400}
21
+
22
+
23
+ def parse_duration(text):
24
+ m = _DURATION_RE.match(text)
25
+ if not m:
26
+ raise ValueError(f"invalid duration: {text!r} (expected like 15m, 1h, 24h, 7d)")
27
+ return timedelta(seconds=int(m.group(1)) * _UNIT_SECONDS[m.group(2)])
28
+
29
+
30
+ def _iso(dt):
31
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
32
+
33
+
34
+ def build_time_filter(from_iso, to_iso, last):
35
+ if from_iso and to_iso:
36
+ gte, lte = from_iso, to_iso
37
+ elif from_iso or to_iso:
38
+ raise UsageError("--from and --to must be used together")
39
+ else:
40
+ now = datetime.now(timezone.utc)
41
+ delta = parse_duration(last or "24h")
42
+ gte, lte = _iso(now - delta), _iso(now)
43
+ return {
44
+ "meta": {"field": "@timestamp", "params": {}},
45
+ "query": {"range": {"@timestamp": {
46
+ "format": "strict_date_optional_time",
47
+ "gte": gte, "lte": lte}}},
48
+ }
49
+
50
+
51
+ HELP_TEXT = """\
52
+ logalizer — export Kibana logs as CSV (Kibana 7.17 Reporting API)
53
+
54
+ USAGE
55
+ logalizer [export] [OPTIONS] # export CSV (default action)
56
+ logalizer --list-spaces # print available spaces
57
+ logalizer --list-indices [--space SPACE] # print index patterns
58
+ logalizer --list-fields --index PATTERN # print fields (best-effort)
59
+ logalizer --help-json # machine-readable help
60
+ logalizer --init # write config.ini (wizard)
61
+ logalizer --init --url URL --username U --password P [--space S] [--index P] [--insecure]
62
+ logalizer --ping # test config + connectivity
63
+
64
+ EXAMPLES (copy-paste ready)
65
+ # Last 24h of errors from brain service, clean columns
66
+ logalizer --index 'logs-*' --query 'level:error' --last 24h \\
67
+ --fields '@timestamp,level,msg,logger' -o brain-errors.csv
68
+
69
+ # Absolute time window, match a correlation id (your real query shape)
70
+ logalizer -i 'app-logs-*' \\
71
+ --query '"00000000-0000-0000-0000-000000000000"' \\
72
+ --from 2026-08-25T10:00:00Z --to 2026-08-25T14:00:00Z
73
+
74
+ # Everything from an index, all fields, streamed to stdout for piping
75
+ logalizer -i 'agent-logs-*' --last 1h | grep 'status:500'
76
+
77
+ # Discover what's available
78
+ logalizer --list-spaces
79
+ logalizer --list-indices --space default
80
+ logalizer --list-fields --index 'logs-*'
81
+
82
+ QUERY OPTIONS
83
+ -q, --query KQL KQL query (default: "" = match all)
84
+ Examples: 'level:error'
85
+ 'status:500 AND url:*api*'
86
+ 'session_id:"01a03931-..."'
87
+ -i, --index PATTERN index pattern (REQUIRED for export), e.g. 'logs-*'
88
+ -s, --space SPACE Kibana space (default: from config, else default)
89
+ --last DURATION relative range: 30s, 15m, 1h, 24h, 7d
90
+ --from ISO --to ISO absolute range (ISO 8601). Overrides --last.
91
+ Use both, or neither.
92
+ --fields LIST comma-separated columns, in order.
93
+ Omit to export ALL fields (wide, includes internals).
94
+
95
+ OUTPUT OPTIONS
96
+ -o, --out PATH write CSV to file (default: stdout)
97
+ --timeout SECONDS max wait for the async job (default: 120)
98
+ --insecure skip TLS cert verification (self-signed servers)
99
+ -v, --verbose progress messages to stderr
100
+
101
+ DISCOVERY OPTIONS (exit 0; print to stdout, one per line)
102
+ --list-spaces list spaces
103
+ --list-indices list index patterns in --space
104
+ --list-fields list fields for --index (best-effort, may be partial)
105
+
106
+
107
+ CONFIGURATION & DIAGNOSTICS
108
+ --init write ~/.config/logalizer/config.ini (0600).
109
+ Interactive wizard when url/username/password omitted;
110
+ non-interactive when all three are given via flags.
111
+ --ping health check: config -> TLS -> auth -> space -> index.
112
+ Prints one line per check; exit 0 all good, else first
113
+ failure (2 config, 3 auth, 5 network).
114
+ --url, --username, --password used only by --init (never for export).
115
+
116
+ CREDENTIALS (never pass on command line)
117
+ KIBANA_URL, KIBANA_USERNAME, KIBANA_PASSWORD # env vars (recommended)
118
+ or ~/.config/logalizer/config.ini # 0600 perms
119
+
120
+ I/O CONTRACT
121
+ stdout = CSV data (or discovery output). stderr = all logs/errors.
122
+ Safe to run: logalizer ... > out.csv (diagnostics never pollute CSV)
123
+
124
+ EXIT CODES
125
+ 0 success
126
+ 2 usage error (bad flags / missing required)
127
+ 3 auth or permission failure (check KIBANA_USERNAME/PASSWORD, space)
128
+ 4 job failed or timed out (see stderr for Kibana's error)
129
+ 5 network / connection error
130
+
131
+ GOTCHAS
132
+ - CSV export is capped by xpack.reporting.csv.maxSizeBytes (10 MB default).
133
+ Very large exports will fail the job; narrow --query or --last.
134
+ - Omitting --fields exports EVERY field (including _id, _index, _score,
135
+ _type, @version, agent.*). Usually you want an explicit --fields list.
136
+ - --list-fields may be incomplete for read-only roles; you can still pass
137
+ any field name directly.
138
+ """
139
+
140
+
141
+ def help_json():
142
+ return {
143
+ "name": "logalizer",
144
+ "summary": "Export Kibana logs as CSV via the Kibana 7.17 Reporting API",
145
+ "flags": [
146
+ {"flag": "--query", "alias": "-q", "type": "string", "format": "KQL",
147
+ "default": "\"\"", "example": "level:error"},
148
+ {"flag": "--index", "alias": "-i", "type": "string", "format": "index-pattern",
149
+ "required": True, "example": "logs-*"},
150
+ {"flag": "--last", "type": "duration", "format": "30s|15m|1h|24h|7d",
151
+ "default": "24h", "example": "1h"},
152
+ {"flag": "--fields", "type": "csv-list", "default": "all fields",
153
+ "example": "@timestamp,level,msg"},
154
+ {"flag": "--out", "alias": "-o", "type": "path", "default": "stdout"},
155
+ {"flag": "--space", "alias": "-s", "type": "string", "default": "default"},
156
+ {"flag": "--init", "type": "bool", "default": "false"},
157
+ {"flag": "--ping", "type": "bool", "default": "false"},
158
+ {"flag": "--url", "type": "string", "used_by": "--init"},
159
+ {"flag": "--username", "type": "string", "used_by": "--init"},
160
+ {"flag": "--password", "type": "string", "used_by": "--init"},
161
+ ],
162
+ "exit_codes": {"0": "success", "2": "usage", "3": "auth/permission",
163
+ "4": "job failed/timeout", "5": "network"},
164
+ "io_contract": {"stdout": "CSV data", "stderr": "diagnostics"},
165
+ "env": ["KIBANA_URL", "KIBANA_USERNAME", "KIBANA_PASSWORD"],
166
+ }
167
+
168
+
169
+ def build_parser():
170
+ p = argparse.ArgumentParser(
171
+ prog="logalizer",
172
+ description="Export Kibana logs as CSV (Kibana 7.17 Reporting API)",
173
+ formatter_class=argparse.RawDescriptionHelpFormatter,
174
+ epilog=HELP_TEXT,
175
+ )
176
+ p.add_argument("-q", "--query", default="", help="KQL query (default: match all)")
177
+ p.add_argument("-i", "--index", help="index pattern, e.g. 'logs-*'")
178
+ p.add_argument("-s", "--space", help="Kibana space (default: from config, else default)")
179
+ p.add_argument("--last", default="24h", help="relative range: 30s/15m/1h/24h/7d (default 24h)")
180
+ p.add_argument("--from", dest="from_iso", help="absolute range start (ISO 8601)")
181
+ p.add_argument("--to", dest="to_iso", help="absolute range end (ISO 8601)")
182
+ p.add_argument("--fields", help="comma-separated columns (omit = all fields)")
183
+ p.add_argument("-o", "--out", help="write CSV to file (default: stdout)")
184
+ p.add_argument("--timeout", type=int, help="max job wait in seconds (default 120)")
185
+ p.add_argument("--insecure", action="store_true", help="skip TLS verification")
186
+ p.add_argument("-v", "--verbose", action="store_true", help="progress to stderr")
187
+ p.add_argument("--list-spaces", action="store_true", help="list spaces")
188
+ p.add_argument("--list-indices", action="store_true", help="list index patterns")
189
+ p.add_argument("--list-fields", action="store_true", help="list fields for --index")
190
+ p.add_argument("--help-json", action="store_true", help="machine-readable help")
191
+ p.add_argument("--init", action="store_true", help="configure and write config.ini")
192
+ p.add_argument("--ping", action="store_true", help="test config + connectivity")
193
+ p.add_argument("--url", help="Kibana URL (for --init)")
194
+ p.add_argument("--username", help="username (for --init)")
195
+ p.add_argument("--password", help="password (for --init)")
196
+ return p
197
+
198
+
199
+ def _err(msg, code):
200
+ print(f"logalizer: {msg}", file=sys.stderr)
201
+ return code
202
+
203
+
204
+ def main(argv=None):
205
+ args = build_parser().parse_args(argv)
206
+
207
+ if args.help_json:
208
+ print(json.dumps(help_json(), indent=2))
209
+ return 0
210
+
211
+ try:
212
+ cfg = load_config()
213
+ settings = build_settings(
214
+ os.environ, cfg, space=args.space, index=args.index, fields=args.fields,
215
+ timeout=args.timeout, insecure=(True if args.insecure else None),
216
+ )
217
+
218
+ if args.init:
219
+ return init.run_init(args, os.environ, config_path=None)
220
+
221
+ if args.ping:
222
+ return ping.run_ping(settings, config_file_path())
223
+
224
+ if not settings.url or not settings.username or not settings.password:
225
+ return _err(
226
+ "missing credentials. Set KIBANA_URL/KIBANA_USERNAME/KIBANA_PASSWORD "
227
+ "or ~/.config/logalizer/config.ini. Exit code 2.", 2)
228
+
229
+ client = Client(settings.url, settings.username, settings.password,
230
+ insecure=settings.insecure)
231
+
232
+ if args.list_spaces:
233
+ for s in indexpatterns.list_spaces(client):
234
+ print(s)
235
+ return 0
236
+ if args.list_indices:
237
+ for t in indexpatterns.list_index_patterns(client, settings.space):
238
+ print(t)
239
+ return 0
240
+ if args.list_fields:
241
+ if not settings.index:
242
+ return _err("--list-fields requires --index PATTERN", 2)
243
+ for f in indexpatterns.list_fields(client, settings.space, settings.index):
244
+ print(f)
245
+ return 0
246
+
247
+ # export
248
+ if not settings.index:
249
+ return _err("--index is required for export", 2)
250
+ index_id = indexpatterns.resolve_index_pattern(
251
+ client, settings.space, settings.index)
252
+ if not index_id:
253
+ return _err(
254
+ f"index pattern {settings.index!r} not found in space "
255
+ f"{settings.space!r}. Use --list-indices to see available patterns.", 2)
256
+
257
+ time_filter = build_time_filter(args.from_iso, args.to_iso, args.last)
258
+ columns = [c.strip() for c in settings.fields.split(",") if c.strip()] if settings.fields else None
259
+
260
+ if args.verbose:
261
+ print(f"submitting CSV job (index={settings.index}, space={settings.space})...",
262
+ file=sys.stderr)
263
+ job_id = reporting.submit(client, settings.space, index_id,
264
+ args.query, time_filter, columns)
265
+ if args.verbose:
266
+ print(f"job {job_id} submitted, waiting...", file=sys.stderr)
267
+ reporting.poll(client, job_id, timeout=settings.timeout)
268
+ if args.verbose:
269
+ print("job completed, downloading...", file=sys.stderr)
270
+ csv_text = reporting.download(client, job_id)
271
+
272
+ if args.out:
273
+ with open(args.out, "w", encoding="utf-8") as fh:
274
+ fh.write(csv_text)
275
+ else:
276
+ sys.stdout.write(csv_text)
277
+ return 0
278
+
279
+ except ClientError as e:
280
+ return _err(str(e) + f" Exit code {e.exit_code}.", e.exit_code)
281
+ except UsageError as e:
282
+ return _err(str(e) + " Exit code 2.", 2)
283
+ except (ValueError, OSError) as e:
284
+ return _err(str(e) + " Exit code 2.", 2)
@@ -0,0 +1,58 @@
1
+ """HTTP layer: basic auth + kbn-xsrf header, urllib-backed."""
2
+ import base64
3
+ import json
4
+ import ssl
5
+ import urllib.error
6
+ import urllib.request
7
+
8
+
9
+ class ClientError(Exception):
10
+ def __init__(self, message, exit_code):
11
+ super().__init__(message)
12
+ self.exit_code = exit_code
13
+
14
+
15
+ def raise_for_status(status, body, what):
16
+ if status == 401:
17
+ raise ClientError("authentication failed (401). Check KIBANA_USERNAME/KIBANA_PASSWORD.", 3)
18
+ if status == 403:
19
+ raise ClientError("permission denied (403). Role lacks access to this space.", 3)
20
+ if status >= 400:
21
+ raise ClientError(f"{what} failed ({status}): {body}", 5)
22
+
23
+
24
+ class Client:
25
+ def __init__(self, url, username, password, insecure=False, timeout=30):
26
+ self.base = (url or "").rstrip("/")
27
+ self.auth = "Basic " + base64.b64encode(
28
+ f"{username}:{password}".encode("utf-8")
29
+ ).decode("ascii")
30
+ self.insecure = insecure
31
+ self.timeout = timeout
32
+
33
+ def _context(self):
34
+ if self.insecure:
35
+ ctx = ssl.create_default_context()
36
+ ctx.check_hostname = False
37
+ ctx.verify_mode = ssl.CERT_NONE
38
+ return ctx
39
+ return None
40
+
41
+ def request(self, method, path, body=None):
42
+ data = json.dumps(body).encode("utf-8") if body is not None else None
43
+ headers = {"Authorization": self.auth}
44
+ if data is not None:
45
+ headers["Content-Type"] = "application/json"
46
+ req = urllib.request.Request(
47
+ self.base + path, data=data, method=method, headers=headers
48
+ )
49
+ req.headers["kbn-xsrf"] = "true"
50
+ try:
51
+ with urllib.request.urlopen(req, context=self._context(),
52
+ timeout=self.timeout) as resp:
53
+ raw = resp.read().decode("utf-8")
54
+ return resp.status, raw
55
+ except urllib.error.HTTPError as e:
56
+ return e.code, e.read().decode("utf-8")
57
+ except urllib.error.URLError as e:
58
+ raise ClientError(f"network error: {e.reason}", 5)