Release/0.5.2 - #200
Merged
Merged
Conversation
SQLite remains the default; PostgreSQL and MariaDB/MySQL are selected via discrete LOGSTASHUI_DB_* env vars. Django ORM only (no new CRUD layer).
TDD tasks for env-selected SQLite/PostgreSQL/MySQL, extras, Docker matrix, and BETA migrate-engine.
Record engines, extras, migrator, test matrix, and operator env in the same style as the 0.5.1 notes.
Check server version before migrate and on --skip-migrate. Strip passwords, upsert --write-env, register psycopg wait_select, seed JSONField in live dump/load, drop psql from offline docs.
If loaddata fails the target may be partial; drop/recreate and retry. A production migrator could wrap loaddata in transaction.atomic().
After testing the generated docker image it was discovered that it was not adding the pod's own IP to the ALLOWED_HOSTS. This made it so that aliveness checks were getting a 400 error. That has been addressed here. A few more edits to .gitignore for a scratch directory and relocating other lines All other changes are documentation.
Optional freeze script (wheels + docker zip + experimental PyInstaller). Default uv build unchanged. CPython 3.12 linux-x86_64, [databases] included.
Add bin/freeze_logstashui.sh for wheels, docker save, and experimental PyInstaller zips. Default uv build is unchanged. Isolated wheels install is pip --no-index on CPython 3.12 x86_64.
Show a cyan LS X.Y.Z oval for the running Logstash version on
ConnectionManager and the Policy editor Agents tab. Persist VERSION
pins as {download_dir}/logstash-X.Y.Z/bin. Agents newer than preferred
show "unreleased version" instead of a backwards Upgrade.
Migrated all database tests to `tests/integration` or `tests/unit` Now using `testcontainers[postgres]` and `testcontainers[mysql]` for integration tests. - Docker required. Tests will be skipped if docker is not present or not working Starting the migration of tests away from `src/logstashui/*/tests`
There was a hidden env var in `settings.py` that was missed with regards to disabling TLS. If DEBUG=false, it would set SECURE_SSL_REDIRECT to true. Now it sets SECURE_SSL_REDIRECT to the value of LOGSTASHUI_TLS, which defaults to true if unset.
Release prep for 0.5.2
fix: prevented windows line endings from being created when we add the notice to the top of all source code files chore: updating the fix (so I could push the refactor) resulted in adding headers to files that didn't previously have them.
…s to be green in Safari
…dy created a device, but no data had been written to Elastic
…rl with cloud id already prefilled would result in trying to use the cloud Id to create the connection
…odals that have enough input that it would be painful to lose your input by accidentally clicking off of the modal
Fix/0.5.2 bugfixes1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[0.5.2] - Multi-database + k8s - 09/06/2026
Package version is 0.5.2 (
pyproject.toml). Preferred LogstashAgent version is 0.5.2 (lockstep).SQLite does not scale under gunicorn/gevent. Operators can now run LogstashUI on SQLite (default), PostgreSQL 14+, or MariaDB 10.6+ / MySQL 8.0+ without changing the Django ORM data model.
Database engines
LOGSTASHUI_DB_ENGINE=sqlite|postgresql|mysql(MariaDB usesmysql). Aliases:sqlite3,postgres,mariadb,my.LOGSTASHUI_DB_HOST,PORT,NAME,USER,PASSWORD, plusLOGSTASHUI_DB_SSLMODE/LOGSTASHUI_DB_SSL_CA,LOGSTASHUI_DB_CONN_MAX_AGE(default 60),LOGSTASHUI_DB_CONN_HEALTH_CHECKS(default true). No YAML. NoDATABASE_URL.$LOGSTASHUI_DATA_DIR/db.sqlite3(WAL +busy_timeoutunchanged).logstashui servelogs a warning when engine is SQLite andLOGSTASHUI_WORKERS>1; it does not refuse to start.logstashui servechecks the server version beforemigrateand still checks when--skip-migrateis set.logstashui migrate-engine --toacceptspostgresql,mysql, andmariadb(mariadbis an alias ofmysql).utf8mb4/utf8mb4_binso unique names match SQLite/Postgres case-sensitivity. Create the database with that collation.LOGSTASHUI_DATA_DIRis still required when the database is remote (TLS, Django secret, logs, staticfiles).--worker-class gevent. PostgreSQL usespsycopg[binary]; MySQL/MariaDB use PyMySQL (not mysqlclient). Optional PgBouncer is documented, not required.Packaging and Docker
LogstashUI[postgres],LogstashUI[mysql],LogstashUI[databases],LogstashUI[otel]. Default wheel stays SQLite-only.bin/freeze_logstashui.sh(--wheels/--docker/--standalone). Defaultuv buildunchanged. Linux x86_64, CPython 3.12,[databases]and[otel]included, no Agent. Wheelhouse prefers manylinux2014 then manylinux_2_28; pure-Python sdists are wheeled on the builder. Standalone PyInstaller is experimental. See Air-gapped freeze.LogstashUI[databases,otel]. Tracing stays off untilLOGSTASHUI_OTEL=true. Kubernetes only sets env./etc/default/logstashuidocuments allLOGSTASHUI_DB_*keys. Set the password in the EnvironmentFile or a Secret (chmod 640).$LOGSTASHUI_DATA_DIR/gunicorn.pid.Migration off SQLite
db.sqlite3→dumpdatawhile still on SQLite → create the server database → setLOGSTASHUI_DB_*→migrate+loaddata. KeepDATA_DIR(same secret key) or encrypted keystore rows will not decrypt. Sessions are not copied; log in again.logstashui migrate-engine --to postgresql|mysql --i-have-a-backupdumps the SQLite file, loads the target, and does not restart serve. It SIGTERMs gunicorn if a pidfile is live. Prefersystemctl stopfirst soRestart=does not race. Optional--write-envappends engine/host/name/user (never the password).migrate-engineis not atomic on the target:dumpdata→migrate→loaddatais three steps. Ifloaddatafails, the target may be partially populated. Drop or recreate the target database (SQLite is only WAL-checkpointed) and re-run. A later production migrator could wraploaddataand Postgressequence_reset_sqlintransaction.atomic()aftermigrate;migrateitself applies DDL and cannot be one atomic unit on MySQL/MariaDB.Testing
pyteststays SQLite (no Docker, no extras).bin/test_databases.sh/bin/test_databases.batstart local Docker Postgres 16, MariaDB 11, and MySQL 8.0 and run the full suite on each engine, then runtests/Database/via testcontainers. CI workflow.github/workflows/test-databases.ymlcalls the same script.tests/Database/integration/usestestcontainers(postgres:16, mysql:8.0, mariadb:11 — no external Docker Compose). Parametrized over PostgreSQL and MySQL; MariaDB covered forcheck_server_versionversion-detection. Skips gracefully when Docker is unavailable. Covers DB config, migrations (clean, idempotent, no unapplied), ORM CRUD/JSON/uniqueness, and full SQLite → PG/MySQL/MariaDBmigrate-engineround-trips. Run withuv run pytest tests/Database/ -v --no-covafteruv sync --group dev --extra databases.src/logstashui/<App>/tests/to a dedicatedtests/tree at the project root. Layout:tests/<App>/unit/per Django app,tests/Database/unit|integration/for database and migration tests. Shared fixtures moved fromCommon/test_resources.pytotests/conftest.py(auto-discovered by pytest).testpathstrimmed to["tests"];pythonpath = ["src/logstashui"]unchanged so app imports still resolve. Run any app in isolation:uv run pytest tests/SNMP -v.test_case_sensitive_uniquenow asserts case-sensitive uniqueness at both layers.Network.save()callsfull_clean(), so a duplicate name raisesValidationErrorfromvalidate_unique()and theINSERTis never issued — the test previously expected anIntegrityErrorthat the database could never raise, and failed identically on both engines. The collation check (utf8mb4_binon MySQL, default on PostgreSQL) rides on thevalidate_unique()query; the database unique index is verified separately viabulk_create(), which bypassessave(), insidetransaction.atomic()so PostgreSQL can roll back the aborted statement before cleanup.testcontainers.postgres/testcontainers.mysqlshims totestcontainers.community.*. The dev-dependency floor is raised totestcontainers>=4.15.0, the first release containing that package.tests/Common/unit/test_api_token_middleware.pyandtests/Management/unit/test_api_tokens.py. The load-bearing case is the inverse one: a logged-in session POSTing without a CSRF token must still be rejected, so the exemption cannot regress into a site-wide CSRF bypass. Also covers revoked/expired/inactive-owner tokens, readonly-owner denial, agent keys passing through untouched, and that re-saving a token does not double-hash it.Kubernetes and database docs
/var/lib/logstashui, TLS kept on:8443, Ingress-nginx skip backend verify, Envoy GatewayBackendinsecureSkipVerify(enable Backend API), CloudNativePG Cluster in the app namespace.Host: logstashui(kubelet otherwise uses the pod IP and Django returns 400). Downward APIstatus.podIP→LOGSTASHUI_HOST_IPSfor the product leaf; those IPs are appended toALLOWED_HOSTSunless the list is*.docs/docs/logstashui/kubernetes/examples/{sqlite,postgresql,mysql}/.docs/docs/logstashui/kubernetes/examples/embedded-agent.yaml(compose--profile embeddedanalog). ClusterIP 9500 / 9560 / 9449. UncommentLOGSTASH_AGENT_URLandLOGSTASHUI_AGENT_CSR_SECRETon the UI examples before apply. Agent ConfigMap commentsLOGSTASH_AGENT_TLS(default true) andLOGSTASH_UI_TLS_INSECURE(default false).LOGSTASHUI_DB_*default, offline dump/load, BETAmigrate-engine, CREATE DATABASE scripts (utf8mb4_binfor MySQL/MariaDB), schema snapshots from 0.5.2migrate.Insecure HTTP (escape hatch)
LOGSTASHUI_INSECURE_HTTP=trueforces plain HTTP for the UI and every UI→agent URL, skips product CA and certificate generation, and overridesLOGSTASHUI_TLS. This is not best practice; automatic TLS remains the supported default. It is not the same asLOGSTASHUI_TLS=false(TLS-terminating ingress). See Environment.$DATA_DIR/tls/custom leaves are left intact (save_custom_ui_certificate/revert_ui_certificate_to_product_defaultraiseProductCADisabledfirst).logstashui serve(or a hand-edited EnvironmentFile). StatefulSet examples comment that probes stayscheme: HTTPS.Fixes
LOGSTASHUI_OTEL=truewithout the[otel]extra now logs ERROR (was INFO) and continues. Docker/K8s and freeze artifacts install[otel]; uncomment the OTEL keys in the Kubernetes ConfigMap examples to enable OTLP/HTTP (port 4318, not gRPC 4317).LOGSTASHUI_TLS=falsenow suppresses the Django-level HTTP→HTTPS redirect (SECURE_SSL_REDIRECT) in addition to disabling the Gunicorn TLS certificate. Previously, running the container with-e LOGSTASHUI_TLS=falsestill returned a301becauseSECURE_SSL_REDIRECTwas gated onDEBUGonly. Both knobs are now independent.ApiKey.save()no longer re-hashes an already-hashed key.make_password()ran unconditionally on every save, so any update to an existing row silently rewrote the hash and invalidated the credential. Latent until now — nothing re-saved anApiKey— but renaming or revoking a token does.list_simulation_targets()requiring a successful probe it never performed, so the sticky embedded row lost a race against the background thread and the dropdown rendered empty until a later refresh. The row is now dropped only when a probe has explicitly reported the agent offline — never-probed is treated as unknown, not offline. Target rows also carry adiscoveredflag so callers can distinguish a confirmed agent from an unconfirmed one.UI
API access
Management → API Tokens) let scripts, CI, and provisioning tools call LogstashUI's JSON endpoints without a browser session. SendAuthorization: ApiKey lsui_<prefix>_<secret>. The original motivation was registering a remote Elasticsearch cluster for Centralized Pipeline Management fromcurl, which previously failed CSRF verification; it now works against the existing/ConnectionManager/AddConnectionURL. Every other JSON endpoint accepts a token too — no per-endpoint opt-in.readonlyuser's token stays readonly. Tokens can carry an optional expiry and be revoked or deleted; revocation takes effect on the next request. Only a hash is stored, so the secret is displayed exactly once at creation.ApiTokenCsrfMiddlewareruns immediately beforeCsrfViewMiddlewareand sets_dont_enforce_csrf_checksonly after a token verifies — a forged or absent header cannot switch CSRF off, and cookie-authenticated browser requests are unaffected.ApiTokenUserMiddlewareruns just afterAuthenticationMiddleware, which would otherwise overwriterequest.user; the split is forced by that ordering. Becauserequest.userbecomes a real user,LoginRequiredMiddlewareandrequire_admin_rolepass on their own — no@csrf_exemptand noLOGIN_REQUIRED_IGNORE_PATHSentries were added.require_admin_rolenow answers API-token callers with JSON instead of anHX-Triggertoast, which is unreadable to a script.ApiKeytable and itsmake_password/check_passwordmachinery. The one addition is an unhashed, indexedprefixcolumn: agent keys are found via theconnection_idin the request body before their hash is checked, but a token presents only a header, and without a lookup key resolving it would mean a PBKDF2 comparison against every row. Existing agent keys getprefix=NULLand are never matched by the middleware.Agent version display
logstash_version_resolved, else Logstash API version). Hidden until known.{download_dir}/logstash-{version}/bin(default/opt/logstash-agent/logstash-versions/logstash-X.Y.Z/bin). Custom paths are kept. Switching back to SYSTEM restores/usr/share/logstash/binwhen the field still looks derived.__PREFERRED_LS_AGENT_VERSION__, currently 0.5.2) shows unreleased version instead of a backwards Upgrade button. Older agents still get Upgrade. Unparseable versions still get Upgrade.resolve_running_logstash_version()consulted theConnection.logstash_version_resolvedcolumn before the current check-in'sstatus_blob, and that column is only ever written on a truthy value and never cleared — so one stored version permanently shadowed every later one. The check-in handler also never readstatus_blob.logstash_api.version, the version the running instance reports through its own API, so on hosts that report it only there the column was never refreshed at all. The blob now leads and the column is the fallback, which keeps the last known version on screen while Logstash is stopped or its API is unreachable.logstash_versionto the existing agent-status SSE payload — free, since the stream already selectsstatus_blob— and the Policies → Agents table, which had no live channel at all, polls every 10s while that tab is visible and pauses when the browser tab is hidden.Logstash tarball proxy
MANAGEDorSIMULATEpolicy pinned to VERSION previously made every agent pull its own ~450 MB tarball fromartifacts.elastic.co. Tick Download the tarball from LogstashUI on the policy and LogstashUI fetches each release once, verifies its SHA-512, and serves it to every agent. Required for air-gapped sites, where the direct download is impossible. See Logstash Tarball Proxy.connection_idsegment.GET /ConnectionManager/LogstashArtifact/{filename}is nowGET /ConnectionManager/LogstashArtifact/{connection_id}/{filename}. A GET has no body, and an agent key is a bare hash with no lookup column, so the header alone cannot identify the caller — the path is what narrows the lookup to one row beforecheck_passwordruns. Agents older than the paired release cannot use the proxy; the boolean simply stays off for them and they continue downloading from Elastic.logstash_via_uiin the enrollmentpolicy_config,logstash_via_uiin the check-in response, andlogstash_runtime.via_uiin the config delta.via_uiparticipates in theruntime_changedcomparison, so flipping the checkbox alone triggers a re-materialize — no separate Deploy for binary-only changes..sha512and fails on mismatch, and writes one when none was supplied. There is no browser upload — 450 MB through a form is not viable. Progress polls over htmx only while a row is actively fetching.UPDATE, not an in-process lock.settings.pydefines noCACHES, so Django falls back to per-processLocMemCache, which does not coordinate across gunicorn workers. The first agent to ask claims the row and gets a503; everyone else gets a503until the file lands, across all workers. A fetch killed by a restart leaves a stale claim that the next request reclaims after the heartbeat window, and the orphaned.partis swept at startup — crash recovery is the normal path, not an edge case, because a 450 MB fetch will always be killed bygraceful_timeout..partfile and areos.replace()d into place only after the SHA-512 verifies. Agents get200/206when the file is ready,503while it is being fetched,429at the concurrent-serve cap,502on an upstream failure,404for an unrecognized filename, and401for a bad key or mismatchedconnection_id.503/429/502all carryRetry-After; agents honour it, backing off to a 5-minute ceiling, and never fall back toartifacts.elastic.cowhile the proxy is enabled..sha512companions — are exempt from the serve semaphore, so a burst of checksum fetches cannot consume download slots.LOGSTASHUI_LOGSTASH_DIR(default<DATA_DIR>/logstashes),LOGSTASHUI_ARTIFACT_MAX_UPSTREAM(default2, cluster-wide), andLOGSTASHUI_ARTIFACT_MAX_SERVE_PER_WORKER(default4; effective total is this ×LOGSTASHUI_WORKERS). The serve cap is deliberately per-worker — dividing a global limit by the worker count truncates to zero and makes the knob lie. The upstream base URL is Management → Settings → Logstash tarball source, blank meaninghttps://artifacts.elastic.co/downloads/logstash.<DATA_DIR>/logstashes, not understaticfiles/—STATIC_ROOTis served by WhiteNoise at/static/, which is inLOGIN_REQUIRED_IGNORE_PATHS, so every tarball would have been an unauthenticated public download, andcollectstaticwould churn over them on everyserve.LOGSTASHUI_OTEL=trueplus the newLogstashUI[otel]extra) sends traces and metrics over OTLP/HTTP. The Docker/K8s image and freeze artifacts install[otel](the wheelhouse already downloaded those wheels); tracing stays off until the env flag is set. Native pip/uv still needs the extra. IfLOGSTASHUI_OTEL=trueand the extra is missing, LogstashUI logs ERROR and the worker keeps serving. Four custom instruments answer the capacity question Django spans cannot:logstashui.gevent.hub.lag,logstashui.artifact.downloads.active,logstashui.artifact.requests, andlogstashui.artifact.serve.bytes_per_second. Only the HTTP/protobuf exporter is supported — the gRPC exporter's native threads are not gevent-patchable.PipelineManager/0029_logstash_artifacts(theLogstashArtifactmodel andPolicy.logstash_via_ui) andManagement/0004_settings_logstash_artifact_base_url.tests/PipelineManager/unit/test_logstash_artifacts.pyandtests/Management/unit/test_logstash_artifact_page.py.