Files
panoptikon/README.md
2026-02-20 17:22:14 +01:00

8.7 KiB

Panoptikon - Website and Command Output Monitoring

A NixOS module for monitoring website content and command output changes.

Overview

Panoptikon is a generic command output and website watcher that periodically runs scripts and reports changes. It's designed to be flexible and can monitor anything from API endpoints to system metrics.

Features

  • Flexible Watchers: Monitor any command output or website content
  • Custom Frequencies: Run scripts at any interval using systemd.timer syntax
  • Multiple Reporters: Report changes to various destinations (IRC, Telegram, Prometheus, etc.)
  • Secret Support: Securely pass credentials to scripts without exposing them in the Nix store
  • Stateful Tracking: Automatically tracks previous output and reports only changes
  • Modular Design: Easy to extend with custom watchers and reporters

Installation

Add Panoptikon to your NixOS configuration:

{ config, pkgs, ... }:

{
  imports = [ 
    # Add the Panoptikon module
    (pkgs.callPackage (builtins.fetchTarball https://github.com/kfm/panoptikon-library/archive/main.tar.gz) { }).nixosModules.default
  ];

  # Enable Panoptikon service
  services.panoptikon.enable = true;

  # Configure your watchers
  services.panoptikon.watchers = {
    # Your watcher configurations go here
  };
}

Configuration

Basic Watcher Configuration

{
  services.panoptikon.enable = true;
  
  services.panoptikon.watchers = {
    # Monitor GitHub metadata
    github-meta = {
      script = pkgs.writers.writeDash "github-meta" '''
        ${pkgs.curl}/bin/curl -sSL https://api.github.com/meta | ${pkgs.jq}/bin/jq
      ''';
      frequency = "*:0/5";  # Every 5 minutes
      reporters = [
        # Report changes to Telegram
        (pkgs.writers.writeDash "telegram-reporter" '''
          ${pkgs.curl}/bin/curl -X POST https://api.telegram.org/bot''${TOKEN}/sendMessage \
            -d chat_id=123456 \
            -d text="$(cat)"
        ''')
        # Also show desktop notifications
        (pkgs.writers.writeDash "notify" '''
          ${pkgs.libnotify}/bin/notify-send "$PANOPTIKON_WATCHER has changed."
        ''')
      ];
    };

    # Monitor a website for specific content
    nixos-updates = {
      script = pkgs.panoptikon.urlSelector "#news h2" "https://nixos.org/blog/";
      frequency = "daily";
      reporters = [
        # Report to IRC
        (pkgs.panoptikon.kpaste-irc {
          target = "#nixos";
          server = "irc.libera.chat";
          messagePrefix = "New NixOS blog post: ";
        })
      ];
    };

    # Monitor a local command
    disk-space = {
      script = pkgs.writers.writeDash "disk-space" '''
        df -h / | tail -1 | awk '{print $5 " used"}'
      ''';
      frequency = "*:0/30";  # Every 30 minutes
      reporters = [
        # Log to systemd journal
        (pkgs.writers.writeDash "journal-log" '''
          journalctl -t panoptikon-disk-space --since "1 hour ago" | tail -5
        ''')
      ];
    };
  };
}

Advanced Configuration

Using Secrets

{
  services.panoptikon.watchers = {
    private-api = {
      script = pkgs.writers.writeDash "private-api" '''
        ${pkgs.curl}/bin/curl -sSL \
          -H "Authorization: Bearer $API_TOKEN" \
          https://api.example.com/data
      ''';
      frequency = "hourly";
      loadCredential = [ "API_TOKEN" ];
      reporters = [
        (pkgs.writers.writeDash "secure-reporter" '''
          ${pkgs.curl}/bin/curl -X POST https://secure.example.com/report \
            -d "$(cat)"
        ''')
      ];
    };
  };
}

JSON API Monitoring

{
  services.panoptikon.watchers = {
    crypto-prices = {
      script = pkgs.panoptikon.urlJSON {
        jqScript = ".[0] | { name: .name, price: .quote.USD.price }";
      } "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin";
      frequency = "*:0/15";  # Every 15 minutes
      reporters = [
        (pkgs.writers.writeDash "price-alert" '''
          price=$(echo "$(cat)" | ${pkgs.jq}/bin/jq -r '.price')
          if (( $(echo "$price > 60000" | bc -l) )); then
            ${pkgs.libnotify}/bin/notify-send "Bitcoin price: $$price"
          fi
        ''')
      ];
    };
  };
}

Available Helper Scripts

Panoptikon provides several helper scripts in the overlay:

URL Monitoring

  • pkgs.panoptikon.url: Fetch and convert HTML to text
  • pkgs.panoptikon.urlSelector: Extract specific HTML elements using CSS selectors
  • pkgs.panoptikon.urlJSON: Fetch JSON and apply jq transformations

Reporters

  • pkgs.panoptikon.kpaste-irc: Report changes to IRC via kpaste
    • target: IRC target (channel or user)
    • server: IRC server (default: irc.r)
    • messagePrefix: Prefix for messages (default: "change detected: ")
    • nick: Nick to use (default: watcher name + "-watcher")
    • retiolumLink: Use retiolum link (default: false)

Service Management

Systemd Integration

Each watcher gets its own systemd service and timer:

# List all Panoptikon services
systemctl list-units "panoptikon-*"

# Check a specific watcher
systemctl status panoptikon-github-meta

# View logs
journalctl -u panoptikon-github-meta -f

# Trigger a manual run
systemctl start panoptikon-github-meta

Timer Configuration

Timers use systemd.timer syntax. Common examples:

  • *:0/5 - Every 5 minutes
  • daily - Once per day
  • Mon..Fri 9:00-17:00 - Weekdays during business hours
  • *:0/15 - Every 15 minutes
  • weekly - Once per week

See systemd.time(7) for full syntax.

Security Considerations

  • Watchers run as the panoptikon system user
  • Scripts are executed in /var/lib/panoptikon
  • Use loadCredential to securely pass secrets
  • Scripts should be written defensively (use set -euo pipefail)

Troubleshooting

Common Issues

  1. No output: Check if the script runs correctly manually
  2. Permission denied: Ensure the panoptikon user can access required resources
  3. Network issues: Add network-online.target as a dependency
  4. Rate limiting: Add randomized delays using RandomizedDelaySec

Debug Mode

Enable debug logging:

services.panoptikon.watchers.github-meta = {
  # ...
  script = ''
    set -x  # Enable debug output
    ${pkgs.curl}/bin/curl -sSL https://api.github.com/meta | ${pkgs.jq}/bin/jq
  '';
};

Examples

Monitor Cryptocurrency Prices

{
  services.panoptikon.enable = true;
  
  services.panoptikon.watchers = {
    bitcoin-price = {
      script = pkgs.panoptikon.urlJSON {
        jqScript = ".[0] | { name: .name, price: .quote.USD.price, change: .quote.USD.percent_change_24h }";
      } "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin";
      frequency = "*:0/30";
      reporters = [
        (pkgs.writers.writeDash "btc-alert" '''
          price=$(echo "$(cat)" | ${pkgs.jq}/bin/jq -r '.price')
          change=$(echo "$(cat)" | ${pkgs.jq}/bin/jq -r '.change')
          
          ${pkgs.libnotify}/bin/notify-send \
            "Bitcoin: $$price ($$change%)"
        ''')
      ];
    };
  };
}

Monitor System Metrics

{
  services.panoptikon.enable = true;
  
  services.panoptikon.watchers = {
    load-average = {
      script = pkgs.writers.writeDash "load-average" '''
        uptime | awk -F'load average:' '{print $2}' | awk '{print $1 " (1m), " $2 " (5m), " $3 " (15m)"}'
      ''';
      frequency = "*:0/2";  # Every 2 minutes
      reporters = [
        (pkgs.writers.writeDash "load-alert" '''
          load=$(echo "$(cat)" | awk '{print $1}' | tr -d ',')
          if (( $(echo "$load > 2.0" | bc -l) )); then
            ${pkgs.libnotify}/bin/notify-send "High load: $$load"
          fi
        ''')
      ];
    };
  };
}

Monitor NixOS Updates

{
  services.panoptikon.enable = true;
  
  services.panoptikon.watchers = {
    nixos-updates = {
      script = pkgs.panoptikon.url "https://nixos.org/blog/";
      frequency = "daily";
      reporters = [
        (pkgs.writers.writeDash "update-notify" '''
          ${pkgs.libnotify}/bin/notify-send "NixOS blog updated: $(cat | head -5)"
        ''')
      ];
    };
  };
}

Development

Adding Custom Reporters

Create a new reporter in the overlay:

final: prev: {
  panoptikon = prev.panoptikon // {
    my-custom-reporter = {
      endpoint,
      authToken ? "",
      ...
    }:
      prev.writers.writeDash "my-reporter" ''
        ${prev.curl}/bin/curl -X POST ''${endpoint} \
          -H "Authorization: Bearer ''${authToken} \
          -d "$(cat)"
      '';
  };
};

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests if applicable
  4. Submit a pull request

License

MIT License