{"id":50531139,"url":"https://github.com/romkey/my-log-says","last_synced_at":"2026-06-03T13:31:35.330Z","repository":{"id":358920455,"uuid":"1235782122","full_name":"romkey/my-log-says","owner":"romkey","description":"Docker log analyzer","archived":false,"fork":false,"pushed_at":"2026-06-03T00:54:47.000Z","size":135,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-03T01:06:35.434Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/romkey.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-11T16:43:52.000Z","updated_at":"2026-06-03T00:54:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/romkey/my-log-says","commit_stats":null,"previous_names":["romkey/my-log-says"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/romkey/my-log-says","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/romkey%2Fmy-log-says","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/romkey%2Fmy-log-says/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/romkey%2Fmy-log-says/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/romkey%2Fmy-log-says/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/romkey","download_url":"https://codeload.github.com/romkey/my-log-says/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/romkey%2Fmy-log-says/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33867802,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-03T13:31:34.397Z","updated_at":"2026-06-03T13:31:35.326Z","avatar_url":"https://github.com/romkey.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LogLady\n\nLogLady collects Docker container logs, stores unique log entries in PostgreSQL, and sends each unique entry to an inference server for LLM analysis. Duplicate log lines are counted on the original record and are not analyzed again.\n\n```\n                                  *:-#*##*+- +                                  \n                                :====**##**=:: *                                \n                              :-===---=-::----::-                               \n                             =*==-----:...:--+*=--*                             \n                            ***+-:..       .---**=--                            \n                           %++*-:=+=:  .=*=-=-==%@*=                            \n                           %++@=+*=*===+=+*#----@###=                           \n                           +*#*--+-- . +::--:.-:#*%--                           \n                          %+*%+-..   . :=    -.-+%%*-                           \n                         @#*+#%*:  --+-+:=-:.:--*%+%*                           \n                         @#*-=%@--.. .-:. ..:::-=%++*                           \n                          #=*#%@+:.  .::.  ...:-@%*#=-                          \n                         %#=#%#%@: .::::--: .:-@%=++=*                          \n                          %+*-*=@+:..    . .:-*@#*##*%                          \n                            ##%%%@#-.  ...:-+**@@%%#*                           \n                               #@@@@%+=====+*#%@@@#                             \n                             ##%%%*%@@%*==+#%%#@@@@%-#                          \n                    %::+###=+=#--#=-@@@@@@@@@@@-%%%%##*%#*.#                    \n                 %+--==*%#+-*#*#+%=--*@@@@@@@#-**=+-#==%#*#-::#  =---.%         \n                *+##+=*-+#*=+:%#*%-=++**@%@%++++#:..:--*#%#*-*#-+--=--:.@       \n              @ :#=#-#-+#*++# -:*-=-+-=+#@##%%#*%-..:-=%##*----=:+-:--:.-=      \n             %-=+-#-#*-+**=%+#-.:.--=+@@%@@%%%%+#+%%%#@%#-=---   ..:.-.---.     \n            *=+.-*#*%*@-#*=%-=#*++-+-#%**%@+++=*##%%  :=---:.  ::=:--...:= :    \n          *:--:--%%#%%%%%*%%#%#%++%+#+##@@*%+*#+:-=--::*-.-= .-:-. :. ..::++-   \n        %%=.==@#*#%%@@@@##%@%#%#=:#*==#-+-*#=   :.-==-:-.. :=+- ...:.:-*+**%    \n       *+.-=-==#@%%@%@@%#%%@@#%*@@@@=#--*      +*=*+*--- :-:--=*-:-=**=##       \n     %*@*--=-*-+%@%@@@@#@@%@+*%@#*##=:      :-=+=++:-@----:---=-***+%@#%#       \n    =%--.-=%*#@##%*@@@%%%*#%+-*---.        -===+*+*+=%:--..*:==#=@@%%#**@#      \n  %#@*%%*%+##--#.-::--=%#=#-.:%#==       -+-:=-:=+**+--+:-:-%%+%%%%#+#+-=%*%    \n@=@#*+@::-.-#==+#=%*%++##%@##-*-++....:*+-:-.-.*+=+#==*-#-*####+**#@@###@+*+%   \n#%%@%=%%%%@#-*%%#*+%#=%-%+*-##%%%@%-     ..=.: *++@--=#=#+*#@+@%%@@@@%=+*=---#  \n%*%@@%%%@%%%%%%*#%+#%:++:==#@##+%#+@%-    .=-.:*+*%..+==#%*#@@@@%@@@%@@:-=++-+# \n-%@+%@%%%%%%*-##*#-+*===*++=*%##...  .      :*-::-%#**-+*%%@#@%% #@#---- +.*-=%%\n```\n\n## Requirements\n\n- Ruby 3.3.11\n- Rails 8.1.3\n- PostgreSQL 18\n- Redis 8\n- Docker and Docker Compose\n\n## Setup\n\nCopy `.env.example` to `.env` and set the inference server values:\n\n```sh\nINFERENCE_URL=https://your-inference-server.example/analyze\nINFERENCE_API_KEY=your-api-key\nINFERENCE_MODEL=log-analyzer\nINFERENCE_FALLBACK_MODEL=backup-model\n```\n\n**API format:** By default LogLady POSTs a custom JSON payload to `INFERENCE_URL` (`INFERENCE_API_FORMAT=loglady`). If your server speaks the OpenAI chat completions API instead, set `INFERENCE_API_FORMAT=openai` and point `INFERENCE_URL` at the API base (e.g. `https://host/v1` or `https://host/v1/chat/completions`). A 405 response usually means the URL or format does not match your server — fix the config rather than retrying.\n\nSet `INFERENCE_FALLBACK_MODEL` to retry analysis with a second model when the server reports the primary model is unavailable (HTTP 404/410, or 400/422/503 with a model-related error).\n\nEdit the LLM prompt at **Settings** in the running app (stored in the database). On first boot, the app seeds from `config/inference_prompt.example.txt`. Override with `INFERENCE_PROMPT` (inline) or `INFERENCE_PROMPT_FILE` (path) in the environment if needed. The inference server should return JSON with `classification`, `urgency`, `needs_action`, `fixes`, and `other_suggestions` — see the example prompt for the expected schema.\n\nDo not commit `.env`; API keys and production secrets must live outside the repository.\n\n## Running Locally\n\nStart the development stack:\n\n```sh\ndocker compose -f docker-compose.dev.yml up\n```\n\nThe app listens on `http://localhost:3000`. Web runs `db:migrate` once at startup; Sidekiq waits for Web to pass its health check before starting, so migrations never run concurrently. The dev PostgreSQL and Redis containers use the `loglady-` prefix and host ports `15432` and `16379` so they do not collide with other projects.\n\nSidekiq discovers containers through the mounted Docker socket (`/var/run/docker.sock`), upserts them into the database, and imports their logs on a recurring schedule (default: every minute). LogLady skips its own containers (matching image/name patterns, compose project, or `loglady.io/skip-log-import` label) so it does not ingest Sidekiq's own job logs. Set `DOCKER_GID` in `.env` to the host docker group GID (`stat -c '%g' /var/run/docker.sock` on Linux, `stat -f '%g'` on macOS) so the Sidekiq process can access the socket.\n\nThe dev, test, and lint stacks use the official `ruby:3.3.11` image with this repository bind-mounted into the container. They run `bundle check || bundle install` against a cached Bundler volume, so Gemfile changes do not require rebuilding a tool image.\n\nIf you reset the database but keep Redis, stale Sidekiq jobs will reference old row IDs. Clear the queues once:\n\n```sh\ndocker compose -f docker-compose.dev.yml run --rm web ./bin/rails sidekiq:clear_queues\n```\n\n## Ingesting Logs\n\nLog import runs automatically in Sidekiq when `DOCKER_LOG_SYNC_ENABLED=true` (the default). You can also import manually for one container:\n\n```sh\ndocker compose -f docker-compose.dev.yml run --rm web ./bin/rails docker_logs:import CONTAINER=container-name\n```\n\nYou can also post logs directly:\n\n```sh\ncurl -X POST http://localhost:3000/log_entries \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"log_entry\":{\"source_container\":\"web\",\"stream\":\"stderr\",\"message\":\"database timeout\"}}'\n```\n\nEach log entry is fingerprinted by container, stream, and **normalized message** (timestamp, PID, and log-level prefixes stripped). Lines that differ only in those prefixes share one row, increment `occurrence_count`, and skip another LLM call.\n\nAfter upgrading, merge existing prefix-variant duplicates once:\n\n```sh\ndocker compose -f docker-compose.server.yml exec web bin/rails log_entries:merge_prefix_duplicates\n```\n\n## Testing\n\n```sh\ndocker compose -f docker-compose.test.yml run --rm test\n```\n\n## Linting\n\n```sh\ndocker compose -f docker-compose.lint.yml run --rm rubocop\n```\n\n## Architecture\n\n- `DockerContainer` tracks containers discovered from the Docker Engine API and import status.\n- `LogEntry` stores each unique log line, duplicate count, analysis status, and LLM output.\n- `LogEntries::Ingestor` creates new entries and counts duplicates.\n- `DockerContainers::Synchronizer` lists containers via the Docker socket and upserts local records.\n- `DockerLogs::Importer` reads container logs through the Docker Engine API and sends them through the ingestor.\n- `SyncDockerContainersJob` and `ImportDockerLogsJob` run in Sidekiq on the `ingestion` queue (scheduled via sidekiq-cron).\n- `DedupeLogEntryJob` merges prefix variants then enqueues `AnalyzeLogEntryJob` when needed.\n- `Inference::Client` calls the configured inference server with the API key from the environment.\n\n## CI and Images\n\nGitHub Actions runs Docker-based tests and linting. The Docker publish workflow builds and publishes an image to GitHub Container Registry on push: `latest` on `main` and `v*` release tags, `staging` on the `staging` branch, plus branch name, version tag, and commit SHA tags.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fromkey%2Fmy-log-says","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fromkey%2Fmy-log-says","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fromkey%2Fmy-log-says/lists"}