logtap 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- logtap/__init__.py +8 -0
- logtap/__main__.py +6 -0
- logtap/api/__init__.py +5 -0
- logtap/api/app.py +45 -0
- logtap/api/dependencies.py +61 -0
- logtap/api/routes/__init__.py +1 -0
- logtap/api/routes/files.py +38 -0
- logtap/api/routes/health.py +19 -0
- logtap/api/routes/logs.py +249 -0
- logtap/api/routes/parsed.py +102 -0
- logtap/cli/__init__.py +1 -0
- logtap/cli/commands/__init__.py +1 -0
- logtap/cli/commands/files.py +86 -0
- logtap/cli/commands/query.py +127 -0
- logtap/cli/commands/serve.py +78 -0
- logtap/cli/commands/tail.py +121 -0
- logtap/cli/main.py +50 -0
- logtap/core/__init__.py +16 -0
- logtap/core/parsers/__init__.py +20 -0
- logtap/core/parsers/apache.py +165 -0
- logtap/core/parsers/auto.py +118 -0
- logtap/core/parsers/base.py +164 -0
- logtap/core/parsers/json_parser.py +119 -0
- logtap/core/parsers/nginx.py +108 -0
- logtap/core/parsers/syslog.py +80 -0
- logtap/core/reader.py +160 -0
- logtap/core/search.py +142 -0
- logtap/core/validation.py +52 -0
- logtap/models/__init__.py +11 -0
- logtap/models/config.py +39 -0
- logtap/models/responses.py +65 -0
- logtap-0.2.0.dist-info/METADATA +317 -0
- logtap-0.2.0.dist-info/RECORD +36 -0
- logtap-0.2.0.dist-info/WHEEL +4 -0
- logtap-0.2.0.dist-info/entry_points.txt +3 -0
- logtap-0.2.0.dist-info/licenses/LICENSE +674 -0
logtap/core/search.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Search and filtering functionality for logtap.
|
|
3
|
+
|
|
4
|
+
Provides substring, regex-based, and severity-based filtering of log lines.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import List, Optional, Set
|
|
9
|
+
|
|
10
|
+
from logtap.core.parsers.base import LogLevel, ParsedLogEntry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def filter_lines(
|
|
14
|
+
lines: List[str],
|
|
15
|
+
term: Optional[str] = None,
|
|
16
|
+
regex: Optional[str] = None,
|
|
17
|
+
case_sensitive: bool = True,
|
|
18
|
+
) -> List[str]:
|
|
19
|
+
"""
|
|
20
|
+
Filter lines by substring or regex pattern.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
lines: List of log lines to filter.
|
|
24
|
+
term: Substring to search for. If provided, only lines containing
|
|
25
|
+
this term will be returned.
|
|
26
|
+
regex: Regular expression pattern to match. If provided, only lines
|
|
27
|
+
matching this pattern will be returned. Takes precedence over term.
|
|
28
|
+
case_sensitive: Whether the search should be case-sensitive.
|
|
29
|
+
Defaults to True.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Filtered list of lines matching the criteria.
|
|
33
|
+
"""
|
|
34
|
+
if not term and not regex:
|
|
35
|
+
return lines
|
|
36
|
+
|
|
37
|
+
if regex:
|
|
38
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
39
|
+
try:
|
|
40
|
+
pattern = re.compile(regex, flags)
|
|
41
|
+
return [line for line in lines if pattern.search(line)]
|
|
42
|
+
except re.error:
|
|
43
|
+
# Invalid regex, return empty list
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
if term:
|
|
47
|
+
if case_sensitive:
|
|
48
|
+
return [line for line in lines if term in line]
|
|
49
|
+
return [line for line in lines if term.lower() in line.lower()]
|
|
50
|
+
|
|
51
|
+
return lines
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def filter_by_level(
|
|
55
|
+
entries: List[ParsedLogEntry],
|
|
56
|
+
min_level: Optional[LogLevel] = None,
|
|
57
|
+
levels: Optional[List[LogLevel]] = None,
|
|
58
|
+
) -> List[ParsedLogEntry]:
|
|
59
|
+
"""
|
|
60
|
+
Filter parsed log entries by severity level.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
entries: List of ParsedLogEntry objects to filter.
|
|
64
|
+
min_level: Minimum severity level to include. Entries at this level
|
|
65
|
+
or more severe will be included.
|
|
66
|
+
levels: Specific levels to include. If provided, only entries with
|
|
67
|
+
these exact levels will be returned.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Filtered list of ParsedLogEntry objects.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
# Get only ERROR and more severe
|
|
74
|
+
filter_by_level(entries, min_level=LogLevel.ERROR)
|
|
75
|
+
|
|
76
|
+
# Get only WARNING and ERROR
|
|
77
|
+
filter_by_level(entries, levels=[LogLevel.WARNING, LogLevel.ERROR])
|
|
78
|
+
"""
|
|
79
|
+
if not min_level and not levels:
|
|
80
|
+
return entries
|
|
81
|
+
|
|
82
|
+
if levels:
|
|
83
|
+
level_set: Set[LogLevel] = set(levels)
|
|
84
|
+
return [e for e in entries if e.level in level_set]
|
|
85
|
+
|
|
86
|
+
if min_level:
|
|
87
|
+
min_severity = min_level.severity
|
|
88
|
+
return [
|
|
89
|
+
e for e in entries
|
|
90
|
+
if e.level is not None and e.level.severity <= min_severity
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
return entries
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def filter_entries(
|
|
97
|
+
entries: List[ParsedLogEntry],
|
|
98
|
+
term: Optional[str] = None,
|
|
99
|
+
regex: Optional[str] = None,
|
|
100
|
+
min_level: Optional[LogLevel] = None,
|
|
101
|
+
levels: Optional[List[LogLevel]] = None,
|
|
102
|
+
case_sensitive: bool = True,
|
|
103
|
+
) -> List[ParsedLogEntry]:
|
|
104
|
+
"""
|
|
105
|
+
Filter parsed log entries by multiple criteria.
|
|
106
|
+
|
|
107
|
+
Combines text search and level filtering.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
entries: List of ParsedLogEntry objects to filter.
|
|
111
|
+
term: Substring to search for in the message.
|
|
112
|
+
regex: Regex pattern to match against the message.
|
|
113
|
+
min_level: Minimum severity level to include.
|
|
114
|
+
levels: Specific levels to include.
|
|
115
|
+
case_sensitive: Whether text search is case-sensitive.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Filtered list of ParsedLogEntry objects matching all criteria.
|
|
119
|
+
"""
|
|
120
|
+
result = entries
|
|
121
|
+
|
|
122
|
+
# Apply level filter first (usually more restrictive)
|
|
123
|
+
if min_level or levels:
|
|
124
|
+
result = filter_by_level(result, min_level=min_level, levels=levels)
|
|
125
|
+
|
|
126
|
+
# Apply text filter
|
|
127
|
+
if term or regex:
|
|
128
|
+
if regex:
|
|
129
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
130
|
+
try:
|
|
131
|
+
pattern = re.compile(regex, flags)
|
|
132
|
+
result = [e for e in result if pattern.search(e.message)]
|
|
133
|
+
except re.error:
|
|
134
|
+
result = []
|
|
135
|
+
elif term:
|
|
136
|
+
if case_sensitive:
|
|
137
|
+
result = [e for e in result if term in e.message]
|
|
138
|
+
else:
|
|
139
|
+
term_lower = term.lower()
|
|
140
|
+
result = [e for e in result if term_lower in e.message.lower()]
|
|
141
|
+
|
|
142
|
+
return result
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input validation functions for logtap.
|
|
3
|
+
|
|
4
|
+
These functions validate user input to prevent security issues
|
|
5
|
+
like path traversal attacks and DoS via overly large inputs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_filename_valid(filename: str) -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Validates a filename for path traversal and absolute paths.
|
|
12
|
+
|
|
13
|
+
The filename is considered valid if it does not contain any ".."
|
|
14
|
+
(used for path traversal) and does not start with "/" (indicating an absolute path).
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
filename: The filename to be checked.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
True if filename is valid, False otherwise.
|
|
21
|
+
"""
|
|
22
|
+
return ".." not in filename and not filename.startswith("/")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_search_term_valid(search_term: str) -> bool:
|
|
26
|
+
"""
|
|
27
|
+
Validates a search term based on its length.
|
|
28
|
+
|
|
29
|
+
The search term is considered valid if its length is less than or equal to 100.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
search_term: The search term to be checked.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True if search term is valid, False otherwise.
|
|
36
|
+
"""
|
|
37
|
+
return len(search_term) <= 100
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_limit_valid(limit: int) -> bool:
|
|
41
|
+
"""
|
|
42
|
+
Validates a limit for the number of lines to return.
|
|
43
|
+
|
|
44
|
+
The limit is considered valid if it's within the range 1 to 1000, inclusive.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
limit: The limit to be checked.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True if limit is valid, False otherwise.
|
|
51
|
+
"""
|
|
52
|
+
return 1 <= limit <= 1000
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Pydantic models for logtap API requests and responses."""
|
|
2
|
+
|
|
3
|
+
from logtap.models.config import Settings
|
|
4
|
+
from logtap.models.responses import ErrorResponse, FileListResponse, LogResponse
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"LogResponse",
|
|
8
|
+
"ErrorResponse",
|
|
9
|
+
"FileListResponse",
|
|
10
|
+
"Settings",
|
|
11
|
+
]
|
logtap/models/config.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Configuration settings for logtap."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Settings(BaseSettings):
|
|
9
|
+
"""
|
|
10
|
+
Application settings loaded from environment variables.
|
|
11
|
+
|
|
12
|
+
Environment variables are prefixed with LOGTAP_ (e.g., LOGTAP_API_KEY).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
model_config = SettingsConfigDict(
|
|
16
|
+
env_prefix="LOGTAP_",
|
|
17
|
+
env_file=".env",
|
|
18
|
+
env_file_encoding="utf-8",
|
|
19
|
+
extra="ignore",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Server settings
|
|
23
|
+
host: str = "0.0.0.0"
|
|
24
|
+
port: int = 8000
|
|
25
|
+
|
|
26
|
+
# Log directory configuration
|
|
27
|
+
log_directory: str = "/var/log"
|
|
28
|
+
testing: bool = False
|
|
29
|
+
|
|
30
|
+
# Authentication
|
|
31
|
+
api_key: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
# Query defaults
|
|
34
|
+
default_limit: int = 50
|
|
35
|
+
max_limit: int = 1000
|
|
36
|
+
|
|
37
|
+
def get_log_directory(self) -> str:
|
|
38
|
+
"""Get the log directory. Uses log_directory setting directly."""
|
|
39
|
+
return self.log_directory
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Response models for logtap API."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LogResponse(BaseModel):
|
|
9
|
+
"""Response model for log queries."""
|
|
10
|
+
|
|
11
|
+
lines: List[str] = Field(description="Log lines matching the query")
|
|
12
|
+
count: int = Field(description="Number of lines returned")
|
|
13
|
+
filename: str = Field(description="Name of the log file queried")
|
|
14
|
+
|
|
15
|
+
model_config = {
|
|
16
|
+
"json_schema_extra": {
|
|
17
|
+
"example": {
|
|
18
|
+
"lines": [
|
|
19
|
+
"Jan 8 10:23:45 server sshd[1234]: Accepted publickey",
|
|
20
|
+
"Jan 8 10:23:46 server systemd[1]: Started session",
|
|
21
|
+
],
|
|
22
|
+
"count": 2,
|
|
23
|
+
"filename": "syslog",
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ErrorResponse(BaseModel):
|
|
30
|
+
"""Response model for errors."""
|
|
31
|
+
|
|
32
|
+
error: str = Field(description="Error message")
|
|
33
|
+
detail: Optional[str] = Field(default=None, description="Additional error details")
|
|
34
|
+
|
|
35
|
+
model_config = {
|
|
36
|
+
"json_schema_extra": {
|
|
37
|
+
"example": {
|
|
38
|
+
"error": "File not found",
|
|
39
|
+
"detail": "/var/log/nonexistent does not exist",
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FileListResponse(BaseModel):
|
|
46
|
+
"""Response model for listing available log files."""
|
|
47
|
+
|
|
48
|
+
files: List[str] = Field(description="List of available log files")
|
|
49
|
+
directory: str = Field(description="Log directory path")
|
|
50
|
+
|
|
51
|
+
model_config = {
|
|
52
|
+
"json_schema_extra": {
|
|
53
|
+
"example": {
|
|
54
|
+
"files": ["syslog", "auth.log", "kern.log", "dpkg.log"],
|
|
55
|
+
"directory": "/var/log",
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class HealthResponse(BaseModel):
|
|
62
|
+
"""Response model for health check."""
|
|
63
|
+
|
|
64
|
+
status: str = Field(default="healthy", description="Service status")
|
|
65
|
+
version: str = Field(description="logtap version")
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logtap
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A CLI-first log access tool for Unix systems. Remote log file access without SSH.
|
|
5
|
+
License: GPL-3.0-or-later
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: logs,monitoring,cli,devops,sysadmin
|
|
8
|
+
Author: cainky
|
|
9
|
+
Author-email: kylecain.me@gmail.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: System :: Logging
|
|
25
|
+
Classifier: Topic :: System :: Monitoring
|
|
26
|
+
Classifier: Topic :: System :: Systems Administration
|
|
27
|
+
Requires-Dist: aiofiles (>=23.2.1)
|
|
28
|
+
Requires-Dist: fastapi (>=0.109.0)
|
|
29
|
+
Requires-Dist: httpx (>=0.26.0)
|
|
30
|
+
Requires-Dist: pydantic (>=2.5.0)
|
|
31
|
+
Requires-Dist: pydantic-settings (>=2.1.0)
|
|
32
|
+
Requires-Dist: python-dotenv (>=1.0.1)
|
|
33
|
+
Requires-Dist: rich (>=13.7.0)
|
|
34
|
+
Requires-Dist: typer[all] (>=0.9.0)
|
|
35
|
+
Requires-Dist: uvicorn[standard] (>=0.27.0)
|
|
36
|
+
Requires-Dist: websockets (>=12.0)
|
|
37
|
+
Project-URL: Homepage, https://github.com/cainky/logtap
|
|
38
|
+
Project-URL: Repository, https://github.com/cainky/logtap
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# logtap
|
|
42
|
+
|
|
43
|
+
[](https://badge.fury.io/py/logtap)
|
|
44
|
+
[](https://github.com/cainky/logtap/actions/workflows/tests.yml)
|
|
45
|
+
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
46
|
+
[](https://www.python.org/downloads/)
|
|
47
|
+
|
|
48
|
+
**A CLI-first log access tool for Unix systems. Remote log file access without SSH.**
|
|
49
|
+
|
|
50
|
+
> The simplest way to access log files remotely. No database. No complex setup.
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- **Remote Log Access** - Query log files via REST API without SSH
|
|
55
|
+
- **Beautiful CLI** - Colored output with rich formatting
|
|
56
|
+
- **Regex Search** - Powerful filtering with regex patterns
|
|
57
|
+
- **Real-time Streaming** - Follow logs like `tail -f` (WebSocket)
|
|
58
|
+
- **Lightweight** - No database required, minimal dependencies
|
|
59
|
+
- **Secure** - Optional API key authentication
|
|
60
|
+
- **Docker Ready** - One-command deployment
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
### Installation
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install logtap
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or with Docker:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
docker pull cainky/logtap
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Start the Server
|
|
77
|
+
|
|
78
|
+
On the machine with log files:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
logtap serve
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
With authentication:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
logtap serve --api-key your-secret-key
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Query Logs
|
|
91
|
+
|
|
92
|
+
From anywhere:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Basic query
|
|
96
|
+
logtap query syslog
|
|
97
|
+
|
|
98
|
+
# Search for errors
|
|
99
|
+
logtap query syslog --term "error"
|
|
100
|
+
|
|
101
|
+
# Regex search
|
|
102
|
+
logtap query auth.log --regex "Failed password.*root"
|
|
103
|
+
|
|
104
|
+
# Last 100 lines
|
|
105
|
+
logtap query syslog --limit 100
|
|
106
|
+
|
|
107
|
+
# From a remote server
|
|
108
|
+
logtap query syslog --server http://myserver:8000 --api-key secret
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### List Available Files
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
logtap files
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Real-time Streaming
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
logtap tail syslog --follow
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## CLI Commands
|
|
124
|
+
|
|
125
|
+
| Command | Description |
|
|
126
|
+
|---------|-------------|
|
|
127
|
+
| `logtap serve` | Start the API server |
|
|
128
|
+
| `logtap query <file>` | Query log files |
|
|
129
|
+
| `logtap tail <file>` | Tail logs (with `--follow` for streaming) |
|
|
130
|
+
| `logtap files` | List available log files |
|
|
131
|
+
|
|
132
|
+
### Common Options
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Server options
|
|
136
|
+
logtap serve --host 0.0.0.0 --port 8000
|
|
137
|
+
logtap serve --api-key mysecret --log-dir /var/log
|
|
138
|
+
|
|
139
|
+
# Client options
|
|
140
|
+
logtap query syslog --server http://host:8000 --api-key mysecret
|
|
141
|
+
logtap query syslog --term "error" --limit 50
|
|
142
|
+
logtap query syslog --regex "pattern" --ignore-case
|
|
143
|
+
logtap query syslog --output json # json, plain, pretty
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## API Reference
|
|
147
|
+
|
|
148
|
+
### GET /logs
|
|
149
|
+
|
|
150
|
+
Query log file contents.
|
|
151
|
+
|
|
152
|
+
**Parameters:**
|
|
153
|
+
| Parameter | Type | Default | Description |
|
|
154
|
+
|-----------|------|---------|-------------|
|
|
155
|
+
| `filename` | string | `syslog` | Log file name |
|
|
156
|
+
| `term` | string | - | Substring to search for |
|
|
157
|
+
| `regex` | string | - | Regex pattern to match |
|
|
158
|
+
| `limit` | int | `50` | Number of lines (1-1000) |
|
|
159
|
+
| `case_sensitive` | bool | `true` | Case-sensitive search |
|
|
160
|
+
|
|
161
|
+
**Example:**
|
|
162
|
+
```bash
|
|
163
|
+
curl "http://localhost:8000/logs?filename=syslog&term=error&limit=10"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Response:**
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"lines": ["Jan 8 10:23:45 server error: connection failed", "..."],
|
|
170
|
+
"count": 10,
|
|
171
|
+
"filename": "syslog"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### GET /files
|
|
176
|
+
|
|
177
|
+
List available log files.
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
curl "http://localhost:8000/files"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### GET /health
|
|
184
|
+
|
|
185
|
+
Health check endpoint.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
curl "http://localhost:8000/health"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Authentication
|
|
192
|
+
|
|
193
|
+
If `LOGTAP_API_KEY` is set, all requests require the `X-API-Key` header:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
curl -H "X-API-Key: your-secret" "http://localhost:8000/logs"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Configuration
|
|
200
|
+
|
|
201
|
+
### Environment Variables
|
|
202
|
+
|
|
203
|
+
| Variable | Default | Description |
|
|
204
|
+
|----------|---------|-------------|
|
|
205
|
+
| `LOGTAP_HOST` | `0.0.0.0` | Server bind host |
|
|
206
|
+
| `LOGTAP_PORT` | `8000` | Server bind port |
|
|
207
|
+
| `LOGTAP_LOG_DIRECTORY` | `/var/log` | Log files directory |
|
|
208
|
+
| `LOGTAP_API_KEY` | - | API key (optional) |
|
|
209
|
+
|
|
210
|
+
### Using .env File
|
|
211
|
+
|
|
212
|
+
Create a `.env` file:
|
|
213
|
+
|
|
214
|
+
```env
|
|
215
|
+
LOGTAP_LOG_DIRECTORY=/var/log
|
|
216
|
+
LOGTAP_API_KEY=your-secret-key
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Docker Deployment
|
|
220
|
+
|
|
221
|
+
### Using Docker Compose
|
|
222
|
+
|
|
223
|
+
```yaml
|
|
224
|
+
version: "3.8"
|
|
225
|
+
|
|
226
|
+
services:
|
|
227
|
+
logtap:
|
|
228
|
+
image: cainky/logtap
|
|
229
|
+
ports:
|
|
230
|
+
- "8000:8000"
|
|
231
|
+
volumes:
|
|
232
|
+
- /var/log:/var/log:ro
|
|
233
|
+
environment:
|
|
234
|
+
- LOGTAP_API_KEY=your-secret-key
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
docker-compose up -d
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Using Docker Directly
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
docker run -d \
|
|
245
|
+
-p 8000:8000 \
|
|
246
|
+
-v /var/log:/var/log:ro \
|
|
247
|
+
-e LOGTAP_API_KEY=your-secret \
|
|
248
|
+
cainky/logtap
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Development
|
|
252
|
+
|
|
253
|
+
### Setup
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# Clone the repository
|
|
257
|
+
git clone https://github.com/cainky/logtap.git
|
|
258
|
+
cd logtap
|
|
259
|
+
|
|
260
|
+
# Install dependencies
|
|
261
|
+
poetry install
|
|
262
|
+
|
|
263
|
+
# Run tests
|
|
264
|
+
poetry run pytest
|
|
265
|
+
|
|
266
|
+
# Run the server in development mode
|
|
267
|
+
poetry run logtap serve --reload
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Project Structure
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
logtap/
|
|
274
|
+
├── src/logtap/
|
|
275
|
+
│ ├── api/ # FastAPI server
|
|
276
|
+
│ ├── cli/ # Typer CLI commands
|
|
277
|
+
│ ├── core/ # Core business logic
|
|
278
|
+
│ └── models/ # Pydantic models
|
|
279
|
+
├── tests/
|
|
280
|
+
│ ├── unit/ # Unit tests
|
|
281
|
+
│ └── integration/ # API tests
|
|
282
|
+
├── Dockerfile
|
|
283
|
+
└── docker-compose.yml
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Running Tests
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
# All tests
|
|
290
|
+
poetry run pytest
|
|
291
|
+
|
|
292
|
+
# With coverage
|
|
293
|
+
poetry run pytest --cov=logtap
|
|
294
|
+
|
|
295
|
+
# Specific test file
|
|
296
|
+
poetry run pytest tests/unit/test_reader.py
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Security Considerations
|
|
300
|
+
|
|
301
|
+
- **Path Traversal Protection**: Filenames are validated to prevent `../` attacks
|
|
302
|
+
- **Input Validation**: Search terms limited to 100 chars, limits capped at 1000
|
|
303
|
+
- **Read-Only**: Log directory is mounted read-only in Docker
|
|
304
|
+
- **API Authentication**: Optional API key for production use
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
GPL v3 License - see [LICENSE](LICENSE) for details.
|
|
309
|
+
|
|
310
|
+
## Contributing
|
|
311
|
+
|
|
312
|
+
Contributions are welcome! Please open an issue to discuss potential changes before submitting a pull request.
|
|
313
|
+
|
|
314
|
+
## Author
|
|
315
|
+
|
|
316
|
+
Kyle Cain - [@cainky](https://github.com/cainky)
|
|
317
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
logtap/__init__.py,sha256=AZzOTH4EFgrz3x5g_i6HELBi5Cg8pOXqCWbJbCWnl2A,179
|
|
2
|
+
logtap/__main__.py,sha256=vqJPz3Zf-ICn_4P3B1o7U4NxcCo0qWgfAGEE_j13t-c,138
|
|
3
|
+
logtap/api/__init__.py,sha256=80bP-eIxtAzidgv5nzcfyCEdl8EI2QjVY_eyxjvvhA0,98
|
|
4
|
+
logtap/api/app.py,sha256=BBphxLKvk7yIiLyQ3tdiagqtjc7xFixd_FUzNFjlC0c,1176
|
|
5
|
+
logtap/api/dependencies.py,sha256=1cx1qrp0O6v1fHXA2JdEhC8P4caG2oUSCfMk2-8zmGs,1620
|
|
6
|
+
logtap/api/routes/__init__.py,sha256=XYvFyTP4zKywRZH0v97k0EZCYgxdL2PSUaNet20znPE,29
|
|
7
|
+
logtap/api/routes/files.py,sha256=bqZYrX6jrF5-7GzBpUIXXoPVdxUwm6o0LTcJBLtaJUE,991
|
|
8
|
+
logtap/api/routes/health.py,sha256=Ak-z2ChqZZ7FgHdu1JDo3v5aDBPR3VIICyXTLDBf75E,462
|
|
9
|
+
logtap/api/routes/logs.py,sha256=rIc9pkn7YtVjf0mAluJztVocJ_P9THR280KTEDN1zqE,8431
|
|
10
|
+
logtap/api/routes/parsed.py,sha256=XVvkKBE_hQvfJyrDBBPR_PpVxvof-y4B77xKe9Rr0Qk,3367
|
|
11
|
+
logtap/cli/__init__.py,sha256=U4zaUJ1rm0qHXqeArpzC45S5N-5SBdd8K6foe513msk,31
|
|
12
|
+
logtap/cli/commands/__init__.py,sha256=U4zaUJ1rm0qHXqeArpzC45S5N-5SBdd8K6foe513msk,31
|
|
13
|
+
logtap/cli/commands/files.py,sha256=WFr8kA0SdgQHz3ZyONTaljxHMcD-nQlndp3UIOwZATc,2455
|
|
14
|
+
logtap/cli/commands/query.py,sha256=uD9nH5E-7EqJryLf3hHkDbJSQo4kWFGmzzHgTfAKFwk,3418
|
|
15
|
+
logtap/cli/commands/serve.py,sha256=9OvfII21q6cel3zZfSsAsiERKwKFt0ZFTXmUd2Psthg,1910
|
|
16
|
+
logtap/cli/commands/tail.py,sha256=dwPRXub1dcRwKulDt_qNa2waQm1YPOxIg0QokAK6Gyw,3648
|
|
17
|
+
logtap/cli/main.py,sha256=fWSuQdin9G-RC7Oqzesfp93WZI1-v7227P-WWTsxtIQ,1045
|
|
18
|
+
logtap/core/__init__.py,sha256=tsoL0XuDrPd5xHEu975WqFHoA7EQgloxrum7CjsWHuk,450
|
|
19
|
+
logtap/core/parsers/__init__.py,sha256=5f3hFxf_DgNScRDchRT8ocFVgi7Md4xuMN-ShvlssBo,575
|
|
20
|
+
logtap/core/parsers/apache.py,sha256=JjuQ4v-b7HJvTCcjbOMgv5_dSdiNVPX_EUyplc3f5Qw,5332
|
|
21
|
+
logtap/core/parsers/auto.py,sha256=OLLuX7XIxS0Upnv9FQ-_B0sGAyZmfNxjnMDGdZtUIO4,3565
|
|
22
|
+
logtap/core/parsers/base.py,sha256=HK886mB2mOuRwsVit_U8UgSfrrQOYwMt_XQm-bgso40,4829
|
|
23
|
+
logtap/core/parsers/json_parser.py,sha256=_3uMGT0EW0wwKzXk1Uc9A7iNvey0Ijqr0uRnJuU1_XM,3728
|
|
24
|
+
logtap/core/parsers/nginx.py,sha256=j_oILELOM0azDPLc41wXrLu5o_LhnPs9fT0_iaOqqAQ,3526
|
|
25
|
+
logtap/core/parsers/syslog.py,sha256=gBNQ39QXsigOpfnq3cEdmvFa8NLp_wmiSMDlTt0SIbs,2430
|
|
26
|
+
logtap/core/reader.py,sha256=YY3wyV9NvYiskTh2nrl1tZAZNY11fr33vo85OhW1qHc,5247
|
|
27
|
+
logtap/core/search.py,sha256=-P2KLkjTWANyLQhdxqZc3I-0UpbhWjQQK_yHHxwYJaA,4350
|
|
28
|
+
logtap/core/validation.py,sha256=Nk86jHqEfI4H96fk-1rjbC5sBwfzls43hyOhnRV6rxI,1359
|
|
29
|
+
logtap/models/__init__.py,sha256=tce3Q0QjPhnlAYG8IcwxPedyh1ibBlKIF3CjXe5wwgo,280
|
|
30
|
+
logtap/models/config.py,sha256=8x6OR_y2ZB8SSoQWQGwDB7DXH30UyMNXUcRWOctjUn8,927
|
|
31
|
+
logtap/models/responses.py,sha256=45J-Xw1Gb35uP5188wxy2QZlyy3Fh18fpAFizZnpi3A,1850
|
|
32
|
+
logtap-0.2.0.dist-info/METADATA,sha256=RUTmgmA8QlKTgWT0ch5QPQ21ltJIay2j5SL44_qGBZc,7372
|
|
33
|
+
logtap-0.2.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
34
|
+
logtap-0.2.0.dist-info/entry_points.txt,sha256=NsN54PsyqB2TA7b8W9-nH4_CNl2_biG4i4FhOQAU9rQ,46
|
|
35
|
+
logtap-0.2.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
36
|
+
logtap-0.2.0.dist-info/RECORD,,
|