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.
- logalizer-0.1.0/.github/workflows/ci.yml +23 -0
- logalizer-0.1.0/.github/workflows/publish.yml +45 -0
- logalizer-0.1.0/.github/workflows/release.yml +22 -0
- logalizer-0.1.0/.gitignore +10 -0
- logalizer-0.1.0/LICENSE +21 -0
- logalizer-0.1.0/PKG-INFO +85 -0
- logalizer-0.1.0/README.md +63 -0
- logalizer-0.1.0/logalizer/__init__.py +8 -0
- logalizer-0.1.0/logalizer/__main__.py +4 -0
- logalizer-0.1.0/logalizer/cli.py +284 -0
- logalizer-0.1.0/logalizer/client.py +58 -0
- logalizer-0.1.0/logalizer/config.py +102 -0
- logalizer-0.1.0/logalizer/indexpatterns.py +57 -0
- logalizer-0.1.0/logalizer/init.py +125 -0
- logalizer-0.1.0/logalizer/ping.py +78 -0
- logalizer-0.1.0/logalizer/reporting.py +60 -0
- logalizer-0.1.0/logalizer/rison.py +35 -0
- logalizer-0.1.0/logalizer.egg-info/PKG-INFO +85 -0
- logalizer-0.1.0/logalizer.egg-info/SOURCES.txt +34 -0
- logalizer-0.1.0/logalizer.egg-info/dependency_links.txt +1 -0
- logalizer-0.1.0/logalizer.egg-info/entry_points.txt +2 -0
- logalizer-0.1.0/logalizer.egg-info/scm_file_list.json +31 -0
- logalizer-0.1.0/logalizer.egg-info/scm_version.json +8 -0
- logalizer-0.1.0/logalizer.egg-info/top_level.txt +1 -0
- logalizer-0.1.0/pyproject.toml +37 -0
- logalizer-0.1.0/setup.cfg +4 -0
- logalizer-0.1.0/tests/__init__.py +0 -0
- logalizer-0.1.0/tests/test_cli.py +118 -0
- logalizer-0.1.0/tests/test_client.py +44 -0
- logalizer-0.1.0/tests/test_config.py +109 -0
- logalizer-0.1.0/tests/test_indexpatterns.py +72 -0
- logalizer-0.1.0/tests/test_init.py +160 -0
- logalizer-0.1.0/tests/test_integration.py +63 -0
- logalizer-0.1.0/tests/test_ping.py +105 -0
- logalizer-0.1.0/tests/test_reporting.py +59 -0
- 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 }}"
|
logalizer-0.1.0/LICENSE
ADDED
|
@@ -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.
|
logalizer-0.1.0/PKG-INFO
ADDED
|
@@ -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,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)
|