Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .idea/workspace.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

214 changes: 214 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,220 @@ async def root():
```
**This example shows how to integrate pysentinel with FastAPI, starting the scanner in the background when the application starts.**

## CLI Installation & Usage

### Install from PyPI (Recommended)

```bash


```bash
# Install PySentinel with CLI support
pip install pysentinel

# Or using Poetry
poetry add pysentinel
```

After installation, the `pysentinel` command will be available in your terminal.

## CLI Usage

PySentinel provides a command-line interface for running the scanner with configuration files.

### Basic Usage

```bash
# Run scanner synchronously (blocking)
pysentinel config.yml

# Run scanner asynchronously (non-blocking)
pysentinel config.yml --async

# Use JSON configuration
pysentinel /path/to/config.json

# Show help
pysentinel --help

# Show version
pysentinel --version
```

### Configuration File

Create a YAML or JSON configuration file:

**Example `config.yml`:**
```yaml
scanner:
interval: 30
timeout: 10

alerts:
email:
enabled: true
smtp_server: "smtp.example.com"
recipients:
- "[email protected]"

thresholds:
cpu_usage: 80
memory_usage: 85
```

### CLI Examples

```bash
# Start monitoring with 30-second intervals
pysentinel production-config.yml

# Run in background mode (async)
pysentinel monitoring.yml --async

# Use absolute path to config
pysentinel /etc/pysentinel/config.yml

# Quick help
pysentinel -h
```

### Exit Codes

- `0` - Success or user interrupted (Ctrl+C)
- `1` - Configuration or scanner error

## Docker Usage

### Running PySentinel CLI in Docker

You can run PySentinel inside a Docker container for isolated execution and easy deployment.

**Create a Dockerfile:**

```dockerfile
FROM python:3.11-slim

# Install PySentinel
RUN pip install pysentinel

# Create app directory
WORKDIR /app

# Copy configuration file
COPY config.yml /app/config.yml

# Run PySentinel CLI
CMD ["pysentinel", "config.yml"]
```
### Build and Run the Docker Container

```bash
# Build the Docker image
docker build -t pysentinel-app .

# Run synchronously
docker run --rm pysentinel-app

# Run asynchronously
docker run --rm pysentinel-app pysentinel config.yml --async

# Mount external config file
docker run --rm -v /path/to/your/config.yml:/app/config.yml pysentinel-app

# Run with environment variables for database connections
docker run --rm \
-e DB_HOST=host.docker.internal \
-e DB_PORT=5432 \
-v /path/to/config.yml:/app/config.yml \
pysentinel-app
```

### Docker Compose Example
create a `docker-compose.yml` file to run PySentinel with a PostgreSQL database:

```yaml
version: '3.8'

services:
pysentinel:
image: python:3.11-slim
command: >
sh -c "pip install pysentinel &&
pysentinel /app/config.yml --async"
volumes:
- ./config.yml:/app/config.yml
- ./logs:/app/logs
environment:
- DB_HOST=postgres
- DB_USER=sentinel_user
- DB_PASSWORD=sentinel_pass
depends_on:
- postgres
restart: unless-stopped

postgres:
image: postgres:15
environment:
POSTGRES_DB: monitoring
POSTGRES_USER: sentinel_user
POSTGRES_PASSWORD: sentinel_pass
volumes:
- postgres_data:/var/lib/postgresql/data

volumes:
postgres_data:
```
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.

### Run with Docker Compose:

```bash
# Start the monitoring stack
docker-compose up -d

# View logs
docker-compose logs pysentinel

# Stop the stack
docker-compose down
```

### Production Docker Setup
Multi-sage Dockerfile for production use:

```dockerfile
FROM python:3.11-slim as builder

# Install dependencies
RUN pip install --no-cache-dir pysentinel

FROM python:3.11-slim

# Copy installed packages
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin/pysentinel /usr/local/bin/pysentinel

# Create non-root user
RUN useradd --create-home --shell /bin/bash sentinel

# Set working directory
WORKDIR /app

# Change ownership
RUN chown -R sentinel:sentinel /app

# Switch to non-root user
USER sentinel

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD pysentinel --version || exit 1

# Default command
CMD ["pysentinel", "config.yml", "--async"]
```

## Configuration
Here’s how to use the `load_config()` function from `pysentinel.config.loader` to load your YAML config and start the scanner.
This approach works for both YAML and JSON config files.
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pysentinel"
version = "0.1.3"
version = "0.1.4"
description = "A python package for threshold based alerting using simple configuration."
authors = [
"Rakibul Haq <[email protected]>",
Expand All @@ -26,3 +26,6 @@ pytest-asyncio = "^1.0.0"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.poetry.scripts]
pysentinel = "pysentinel.cli.cli:main"
2 changes: 1 addition & 1 deletion pysentinel/channels/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .email import Email
from .telegram import Telegram
from .slack import Slack
from .webhook import Webhook
from .webhook import Webhook
22 changes: 13 additions & 9 deletions pysentinel/channels/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ class Email(AlertChannel):
async def send_alert(self, violation: Violation) -> bool:
try:
msg = MIMEMultipart()
msg['From'] = self.config['from_address']
msg['To'] = ', '.join(self.config['recipients'])
msg['Subject'] = self.config['subject_template'].format(alert_title=violation.alert_name)
msg["From"] = self.config["from_address"]
msg["To"] = ", ".join(self.config["recipients"])
msg["Subject"] = self.config["subject_template"].format(
alert_title=violation.alert_name
)

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

msg.attach(MIMEText(body, 'plain'))
msg.attach(MIMEText(body, "plain"))

password = self.config['password']
if password.startswith('${') and password.endswith('}'):
password = self.config["password"]
if password.startswith("${") and password.endswith("}"):
env_var = password[2:-1]
password = os.getenv(env_var, password)

server = smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port'])
server = smtplib.SMTP(self.config["smtp_server"], self.config["smtp_port"])
server.starttls()
server.login(self.config['username'], password)
server.login(self.config["username"], password)
text = msg.as_string()
server.sendmail(self.config['from_address'], self.config['recipients'], text)
server.sendmail(
self.config["from_address"], self.config["recipients"], text
)
server.quit()

return True
Expand Down
42 changes: 26 additions & 16 deletions pysentinel/channels/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,50 +11,60 @@ async def send_alert(self, violation: Violation) -> bool:

try:
payload = {
"channel": self.config['channel'],
"username": self.config['username'],
"icon_emoji": self.config['icon_emoji'],
"channel": self.config["channel"],
"username": self.config["username"],
"icon_emoji": self.config["icon_emoji"],
"text": f"🚨 *{violation.severity.value.upper()}* Alert: {violation.alert_name}",
"attachments": [
{
"color": "danger" if violation.severity == Severity.CRITICAL else "warning",
"color": (
"danger"
if violation.severity == Severity.CRITICAL
else "warning"
),
"fields": [
{
"title": "Message",
"value": violation.message,
"short": False
"short": False,
},
{
"title": "Current Value",
"value": str(violation.current_value),
"short": True
"short": True,
},
{
"title": "Threshold",
"value": f"{violation.operator} {violation.threshold_value}",
"short": True
"short": True,
},
{
"title": "Datasource",
"value": violation.datasource_name,
"short": True
"short": True,
},
{
"title": "Time",
"value": violation.timestamp.strftime("%Y-%m-%d %H:%M:%S UTC"),
"short": True
}
]
"value": violation.timestamp.strftime(
"%Y-%m-%d %H:%M:%S UTC"
),
"short": True,
},
],
}
]
],
}

# Add mentions if configured
if 'mention_users' in self.config:
payload['text'] = f"{' '.join(self.config['mention_users'])} {payload['text']}"
if "mention_users" in self.config:
payload["text"] = (
f"{' '.join(self.config['mention_users'])} {payload['text']}"
)

async with aiohttp.ClientSession() as session:
async with session.post(self.config['webhook_url'], json=payload) as response:
async with session.post(
self.config["webhook_url"], json=payload
) as response:
return response.status == 200
except Exception as e:
logger.error(f"Failed to send Slack alert: {e}")
Expand Down
Loading