diff --git a/hosts/rx4/services/default.nix b/hosts/rx4/services/default.nix index 5201854..1014d6f 100644 --- a/hosts/rx4/services/default.nix +++ b/hosts/rx4/services/default.nix @@ -10,7 +10,7 @@ inputs.clients.nixosModules.syncthing outputs.nixosModules.tailscale - outputs.nixosModules.promtail + # outputs.nixosModules.journald-upload # FIXME ./forgejo.nix ./jirafeau.nix diff --git a/hosts/sid/services/default.nix b/hosts/sid/services/default.nix index 1be8328..b40b165 100644 --- a/hosts/sid/services/default.nix +++ b/hosts/sid/services/default.nix @@ -9,8 +9,7 @@ inputs.synix.nixosModules.openssh outputs.nixosModules.tailscale - outputs.nixosModules.loki - outputs.nixosModules.promtail + # outputs.nixosModules.journald-remote ./headscale.nix ./mailserver.nix diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix index d3511ee..2e35096 100644 --- a/modules/nixos/default.nix +++ b/modules/nixos/default.nix @@ -5,9 +5,9 @@ forgejo = import ./forgejo; forgejo-runner = import ./forgejo-runner; gnome = import ./gnome; - loki = import ./loki; + journald-remote = import ./journald-remote; + journald-upload = import ./journald-upload; monero = import ./monero; - promtail = import ./promtail; rsshub-oci = import ./rsshub-oci; tailscale = import ./tailscale; xfce = import ./xfce; diff --git a/modules/nixos/journald-remote/default.nix b/modules/nixos/journald-remote/default.nix new file mode 100644 index 0000000..797eef3 --- /dev/null +++ b/modules/nixos/journald-remote/default.nix @@ -0,0 +1,52 @@ +{ + pkgs, + lib, + ... +}: + +let + python = pkgs.python3Packages; + + mcp-log-server = python.buildPythonApplication { + pname = "mcp-log-server"; + version = "1.0.0"; + + src = ./.; + + pyproject = true; + build-system = [ python.setuptools ]; + + propagatedBuildInputs = with python; [ + fastmcp + ]; + + meta.mainProgram = "mcp-log-server"; + }; +in +{ + services.journald.remote = { + enable = true; + listen = "http"; + port = 19532; + settings.Remote.SplitMode = "host"; + }; + + systemd.services.mcp-log-server = { + description = "AI Log Access MCP Server"; + after = [ + "network.target" + "multi-user.target" + "systemd-journald.service" + ]; + wantedBy = [ "multi-user.target" ]; + + script = lib.getExe mcp-log-server; + + serviceConfig = { + User = "root"; + Group = "root"; + Environment = "PYTHONUNBUFFERED=1"; + Restart = "on-failure"; + }; + }; +} diff --git a/modules/nixos/journald-remote/pyproject.toml b/modules/nixos/journald-remote/pyproject.toml new file mode 100644 index 0000000..809656b --- /dev/null +++ b/modules/nixos/journald-remote/pyproject.toml @@ -0,0 +1,9 @@ +[build-system] +requires = ["setuptools"] + +[project] +name = "mcp-log-server" +version = "1.0.0" + +[project.scripts] +mcp-log-server = "server:main" diff --git a/modules/nixos/journald-remote/server.py b/modules/nixos/journald-remote/server.py new file mode 100644 index 0000000..39c8f28 --- /dev/null +++ b/modules/nixos/journald-remote/server.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +import json +import subprocess +from fastmcp import FastMCP + +mcp = FastMCP('ClusterLogQuery') + +@mcp.tool() +async def search_cluster_logs(keyword: str, unit: str = '*', limit: int = 500): + """ + Search centralized systemd logs collected on this server. + + Args: + keyword: The string to search for (e.g. 'oom-kill', 'failed'). + unit: The systemd unit to filter (e.g. 'sshd'). If '*', matches all. + limit: The number of log lines to return (Max 1000). + """ + + limit = min(max(limit, 10), 1000) + + cmd = [ + 'journalctl', + '-n', str(limit), + '-o', 'json', + '--no-pager', + '--directory', '/var/log/journal/remote' + ] + + if unit != '*': + cmd.append(f'UNIT={unit}') + if keyword: + cmd.append(keyword) + + try: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, _ = proc.communicate() + + logs = [] + for line in stdout.decode('utf-8').strip().split('\n'): + if not line: continue + try: + log = json.loads(line) + logs.append({ + 'timestamp': log.get('TIME_ISO8601'), + 'hostname': log.get('_HOSTNAME'), + 'unit': log.get('_COMM'), + 'message': log.get('MESSAGE') + }) + except: pass + + return json.dumps(logs) + + except Exception as e: + return f'Error querying logs: {str(e)}' + +def main(): + mcp.run(transport='sse', host='0.0.0.0', port=9500) + +if __name__ == '__main__': + main() diff --git a/modules/nixos/journald-upload/default.nix b/modules/nixos/journald-upload/default.nix new file mode 100644 index 0000000..dede56b --- /dev/null +++ b/modules/nixos/journald-upload/default.nix @@ -0,0 +1,17 @@ +{ + services.journald.upload = { + enable = true; + settings.Upload.URL = "http://100.64.0.6:19532"; + }; + + systemd.services.systemd-journal-upload = { + after = [ + "tailscaled.service" + "network-online.target" + ]; + wants = [ "network-online.target" ]; + serviceConfig = { + TimeoutStartSec = 20; # Tailscale is slow + }; + }; +} diff --git a/modules/nixos/loki/default.nix b/modules/nixos/loki/default.nix deleted file mode 100644 index a360c0d..0000000 --- a/modules/nixos/loki/default.nix +++ /dev/null @@ -1,62 +0,0 @@ -{ - services.loki = { - enable = true; - configuration = { - auth_enabled = false; - server.http_listen_port = 3100; - - common = { - ring = { - instance_addr = "127.0.0.1"; - kvstore.store = "inmemory"; - }; - replication_factor = 1; - path_prefix = "/var/lib/loki"; - }; - - schema_config = { - configs = [ - { - from = "2020-10-24"; - store = "tsdb"; - object_store = "filesystem"; - schema = "v13"; - index = { - prefix = "index_"; - period = "24h"; - }; - } - ]; - }; - - storage_config = { - filesystem = { - directory = "/var/lib/loki/chunks"; - }; - }; - }; - }; - - services.grafana = { - enable = true; - settings = { - server = { - http_addr = "0.0.0.0"; - http_port = 3003; - }; - }; - - provision = { - enable = true; - datasources.settings.datasources = [ - { - name = "Loki"; - type = "loki"; - access = "proxy"; - url = "http://127.0.0.1:3100"; - isDefault = true; - } - ]; - }; - }; -} diff --git a/modules/nixos/promtail/default.nix b/modules/nixos/promtail/default.nix deleted file mode 100644 index cdb99de..0000000 --- a/modules/nixos/promtail/default.nix +++ /dev/null @@ -1,43 +0,0 @@ -{ config, constants, ... }: - -{ - services.promtail = { - enable = true; - configuration = { - server = { - http_listen_port = 9080; - grpc_listen_port = 0; - }; - - clients = [ - { - url = "http://${constants.hosts.sid.ip}:3100/loki/api/v1/push"; - } - ]; - - scrape_configs = [ - { - job_name = "journal"; - journal = { - max_age = "12h"; - path = "/var/log/journal"; - - labels = { - job = "systemd-journal"; - host = config.networking.hostName; - }; - }; - - relabel_configs = [ - { - source_labels = [ "__journal__systemd_unit" ]; - target_label = "unit"; - } - ]; - } - ]; - }; - }; - - users.users.promtail.extraGroups = [ "systemd-journal" ]; -}