Skip to content

Commit 5cf1ac5

Browse files
authored
Merge pull request #8 from rakibulhaq/7-add-cli-functionality
7 add cli functionality
2 parents 9ec547e + cdf730b commit 5cf1ac5

File tree

26 files changed

+872
-177
lines changed

26 files changed

+872
-177
lines changed

.idea/workspace.xml

Lines changed: 10 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,220 @@ async def root():
149149
```
150150
**This example shows how to integrate pysentinel with FastAPI, starting the scanner in the background when the application starts.**
151151

152+
## CLI Installation & Usage
153+
154+
### Install from PyPI (Recommended)
155+
156+
```bash
157+
158+
159+
```bash
160+
# Install PySentinel with CLI support
161+
pip install pysentinel
162+
163+
# Or using Poetry
164+
poetry add pysentinel
165+
```
166+
167+
After installation, the `pysentinel` command will be available in your terminal.
168+
169+
## CLI Usage
170+
171+
PySentinel provides a command-line interface for running the scanner with configuration files.
172+
173+
### Basic Usage
174+
175+
```bash
176+
# Run scanner synchronously (blocking)
177+
pysentinel config.yml
178+
179+
# Run scanner asynchronously (non-blocking)
180+
pysentinel config.yml --async
181+
182+
# Use JSON configuration
183+
pysentinel /path/to/config.json
184+
185+
# Show help
186+
pysentinel --help
187+
188+
# Show version
189+
pysentinel --version
190+
```
191+
192+
### Configuration File
193+
194+
Create a YAML or JSON configuration file:
195+
196+
**Example `config.yml`:**
197+
```yaml
198+
scanner:
199+
interval: 30
200+
timeout: 10
201+
202+
alerts:
203+
email:
204+
enabled: true
205+
smtp_server: "smtp.example.com"
206+
recipients:
207+
- "admin@example.com"
208+
209+
thresholds:
210+
cpu_usage: 80
211+
memory_usage: 85
212+
```
213+
214+
### CLI Examples
215+
216+
```bash
217+
# Start monitoring with 30-second intervals
218+
pysentinel production-config.yml
219+
220+
# Run in background mode (async)
221+
pysentinel monitoring.yml --async
222+
223+
# Use absolute path to config
224+
pysentinel /etc/pysentinel/config.yml
225+
226+
# Quick help
227+
pysentinel -h
228+
```
229+
230+
### Exit Codes
231+
232+
- `0` - Success or user interrupted (Ctrl+C)
233+
- `1` - Configuration or scanner error
234+
235+
## Docker Usage
236+
237+
### Running PySentinel CLI in Docker
238+
239+
You can run PySentinel inside a Docker container for isolated execution and easy deployment.
240+
241+
**Create a Dockerfile:**
242+
243+
```dockerfile
244+
FROM python:3.11-slim
245+
246+
# Install PySentinel
247+
RUN pip install pysentinel
248+
249+
# Create app directory
250+
WORKDIR /app
251+
252+
# Copy configuration file
253+
COPY config.yml /app/config.yml
254+
255+
# Run PySentinel CLI
256+
CMD ["pysentinel", "config.yml"]
257+
```
258+
### Build and Run the Docker Container
259+
260+
```bash
261+
# Build the Docker image
262+
docker build -t pysentinel-app .
263+
264+
# Run synchronously
265+
docker run --rm pysentinel-app
266+
267+
# Run asynchronously
268+
docker run --rm pysentinel-app pysentinel config.yml --async
269+
270+
# Mount external config file
271+
docker run --rm -v /path/to/your/config.yml:/app/config.yml pysentinel-app
272+
273+
# Run with environment variables for database connections
274+
docker run --rm \
275+
-e DB_HOST=host.docker.internal \
276+
-e DB_PORT=5432 \
277+
-v /path/to/config.yml:/app/config.yml \
278+
pysentinel-app
279+
```
280+
281+
### Docker Compose Example
282+
create a `docker-compose.yml` file to run PySentinel with a PostgreSQL database:
283+
284+
```yaml
285+
version: '3.8'
286+
287+
services:
288+
pysentinel:
289+
image: python:3.11-slim
290+
command: >
291+
sh -c "pip install pysentinel &&
292+
pysentinel /app/config.yml --async"
293+
volumes:
294+
- ./config.yml:/app/config.yml
295+
- ./logs:/app/logs
296+
environment:
297+
- DB_HOST=postgres
298+
- DB_USER=sentinel_user
299+
- DB_PASSWORD=sentinel_pass
300+
depends_on:
301+
- postgres
302+
restart: unless-stopped
303+
304+
postgres:
305+
image: postgres:15
306+
environment:
307+
POSTGRES_DB: monitoring
308+
POSTGRES_USER: sentinel_user
309+
POSTGRES_PASSWORD: sentinel_pass
310+
volumes:
311+
- postgres_data:/var/lib/postgresql/data
312+
313+
volumes:
314+
postgres_data:
315+
```
316+
This `docker-compose.yml` sets up a PySentinel service that connects to a PostgreSQL database, allowing you to run the scanner with persistent data storage.
317+
318+
### Run with Docker Compose:
319+
320+
```bash
321+
# Start the monitoring stack
322+
docker-compose up -d
323+
324+
# View logs
325+
docker-compose logs pysentinel
326+
327+
# Stop the stack
328+
docker-compose down
329+
```
330+
331+
### Production Docker Setup
332+
Multi-sage Dockerfile for production use:
333+
334+
```dockerfile
335+
FROM python:3.11-slim as builder
336+
337+
# Install dependencies
338+
RUN pip install --no-cache-dir pysentinel
339+
340+
FROM python:3.11-slim
341+
342+
# Copy installed packages
343+
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
344+
COPY --from=builder /usr/local/bin/pysentinel /usr/local/bin/pysentinel
345+
346+
# Create non-root user
347+
RUN useradd --create-home --shell /bin/bash sentinel
348+
349+
# Set working directory
350+
WORKDIR /app
351+
352+
# Change ownership
353+
RUN chown -R sentinel:sentinel /app
354+
355+
# Switch to non-root user
356+
USER sentinel
357+
358+
# Health check
359+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
360+
CMD pysentinel --version || exit 1
361+
362+
# Default command
363+
CMD ["pysentinel", "config.yml", "--async"]
364+
```
365+
152366
## Configuration
153367
Here’s how to use the `load_config()` function from `pysentinel.config.loader` to load your YAML config and start the scanner.
154368
This approach works for both YAML and JSON config files.

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pysentinel"
3-
version = "0.1.3"
3+
version = "0.1.4"
44
description = "A python package for threshold based alerting using simple configuration."
55
authors = [
66
"Rakibul Haq <haq.rakibul@gmail.com>",
@@ -26,3 +26,6 @@ pytest-asyncio = "^1.0.0"
2626
[build-system]
2727
requires = ["poetry-core>=2.0.0,<3.0.0"]
2828
build-backend = "poetry.core.masonry.api"
29+
30+
[tool.poetry.scripts]
31+
pysentinel = "pysentinel.cli.cli:main"

pysentinel/channels/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .email import Email
22
from .telegram import Telegram
33
from .slack import Slack
4-
from .webhook import Webhook
4+
from .webhook import Webhook

pysentinel/channels/email.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ class Email(AlertChannel):
1313
async def send_alert(self, violation: Violation) -> bool:
1414
try:
1515
msg = MIMEMultipart()
16-
msg['From'] = self.config['from_address']
17-
msg['To'] = ', '.join(self.config['recipients'])
18-
msg['Subject'] = self.config['subject_template'].format(alert_title=violation.alert_name)
16+
msg["From"] = self.config["from_address"]
17+
msg["To"] = ", ".join(self.config["recipients"])
18+
msg["Subject"] = self.config["subject_template"].format(
19+
alert_title=violation.alert_name
20+
)
1921

2022
body = f"""
2123
Alert: {violation.alert_name}
@@ -27,18 +29,20 @@ async def send_alert(self, violation: Violation) -> bool:
2729
Time: {violation.timestamp}
2830
"""
2931

30-
msg.attach(MIMEText(body, 'plain'))
32+
msg.attach(MIMEText(body, "plain"))
3133

32-
password = self.config['password']
33-
if password.startswith('${') and password.endswith('}'):
34+
password = self.config["password"]
35+
if password.startswith("${") and password.endswith("}"):
3436
env_var = password[2:-1]
3537
password = os.getenv(env_var, password)
3638

37-
server = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'])
39+
server = smtplib.SMTP(self.config["smtp_server"], self.config["smtp_port"])
3840
server.starttls()
39-
server.login(self.config['username'], password)
41+
server.login(self.config["username"], password)
4042
text = msg.as_string()
41-
server.sendmail(self.config['from_address'], self.config['recipients'], text)
43+
server.sendmail(
44+
self.config["from_address"], self.config["recipients"], text
45+
)
4246
server.quit()
4347

4448
return True

pysentinel/channels/slack.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,50 +11,60 @@ async def send_alert(self, violation: Violation) -> bool:
1111

1212
try:
1313
payload = {
14-
"channel": self.config['channel'],
15-
"username": self.config['username'],
16-
"icon_emoji": self.config['icon_emoji'],
14+
"channel": self.config["channel"],
15+
"username": self.config["username"],
16+
"icon_emoji": self.config["icon_emoji"],
1717
"text": f"🚨 *{violation.severity.value.upper()}* Alert: {violation.alert_name}",
1818
"attachments": [
1919
{
20-
"color": "danger" if violation.severity == Severity.CRITICAL else "warning",
20+
"color": (
21+
"danger"
22+
if violation.severity == Severity.CRITICAL
23+
else "warning"
24+
),
2125
"fields": [
2226
{
2327
"title": "Message",
2428
"value": violation.message,
25-
"short": False
29+
"short": False,
2630
},
2731
{
2832
"title": "Current Value",
2933
"value": str(violation.current_value),
30-
"short": True
34+
"short": True,
3135
},
3236
{
3337
"title": "Threshold",
3438
"value": f"{violation.operator} {violation.threshold_value}",
35-
"short": True
39+
"short": True,
3640
},
3741
{
3842
"title": "Datasource",
3943
"value": violation.datasource_name,
40-
"short": True
44+
"short": True,
4145
},
4246
{
4347
"title": "Time",
44-
"value": violation.timestamp.strftime("%Y-%m-%d %H:%M:%S UTC"),
45-
"short": True
46-
}
47-
]
48+
"value": violation.timestamp.strftime(
49+
"%Y-%m-%d %H:%M:%S UTC"
50+
),
51+
"short": True,
52+
},
53+
],
4854
}
49-
]
55+
],
5056
}
5157

5258
# Add mentions if configured
53-
if 'mention_users' in self.config:
54-
payload['text'] = f"{' '.join(self.config['mention_users'])} {payload['text']}"
59+
if "mention_users" in self.config:
60+
payload["text"] = (
61+
f"{' '.join(self.config['mention_users'])} {payload['text']}"
62+
)
5563

5664
async with aiohttp.ClientSession() as session:
57-
async with session.post(self.config['webhook_url'], json=payload) as response:
65+
async with session.post(
66+
self.config["webhook_url"], json=payload
67+
) as response:
5868
return response.status == 200
5969
except Exception as e:
6070
logger.error(f"Failed to send Slack alert: {e}")

0 commit comments

Comments
 (0)