diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..f51e305 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,38 @@ +# Build and push the web application image (PHP + baked src + samplelogs) to GHCR. +# Tags: semver from git tag ( v* ), plus :latest . +name: Docker publish + +on: + push: + tags: + - "v*" + +permissions: + contents: read + packages: write + +jobs: + push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - id: lc + name: GHCR image name (lowercase) + run: | + echo "image=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + push: true + tags: | + ${{ steps.lc.outputs.image }}:${{ github.ref_name }} + ${{ steps.lc.outputs.image }}:latest diff --git a/.gitignore b/.gitignore index 3e2c1db..6098f37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/.env + src/config.php *.bak /vendor/ diff --git a/AGENTS.md b/AGENTS.md index c88c8c0..48be32d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,47 +1,78 @@ # LogAnalyzer agent / contributor notes -## Docker development +## Docker stacks -From the repository root: +| Compose file | Use case | +|----------------|----------| +| [`docker/docker-compose.yml`](docker/docker-compose.yml) | **Default/local install.** Application code is baked into the image. By default (**`LOGANALYZER_SEED_SAMPLE_SOURCES=0`**) MySQL seed creates **schema + admin only** — no bundled disk sources unless you set **`LOGANALYZER_DISK_*`** (first seed) or add sources in Administration. Fixtures remain under **`/samplelogs`** in the image so you can still opt into demo sources. MySQL (`loganalyzer_mysql_data`) and `config.php` (`loganalyzer_config`) use volumes. **`LOGANALYZER_OVERWRITE_CONFIG=0`**. | +| [`docker/docker-compose.dev.yml`](docker/docker-compose.dev.yml) | **Development.** Bind-mount [`src`](src) and [`tests/fixtures/samplelogs`](tests/fixtures/samplelogs); `LOGANALYZER_OVERWRITE_CONFIG=1` regenerates `src/config.php` on each container start so editor + DB drift is minimized. Host **MySQL** port `3306` is exposed like before. Separate volume label `loganalyzer_dev_mysql_data`. | + +Build context for images is **repository root**; see [`docker/Dockerfile`](docker/Dockerfile) (multi-stage: runtime + baked `src`; optional fixture tree at `/samplelogs`). Install walkthrough (**`.env`**, volumes, **`LOGANALYZER_DISK_*`**, reset): [`docker/README.md`](docker/README.md). + +Credentials and bootstrap flags normally come **at runtime**, not **`docker build`**: copy [`docker/env.example`](docker/env.example) to **`.env` in the repository root** (same folder as `README.md`) and adjust `LOGANALYZER_ADMIN_*`, `MYSQL_*`, `LOGANALYZER_SEED_SAMPLE_SOURCES`, and optional **`LOGANALYZER_DISK_*`** before **`docker compose up`**. Compose injects `${VARIABLE:-defaults}` automatically; **`.gitignore` excludes `.env`**. + +**Real disk sources at first seed:** set **`LOGANALYZER_DISK_SOURCE_PATHS`** (comma-separated file paths *inside the web container*) and/or **`LOGANALYZER_DISK_SOURCES`** (see comments in [`docker/env.example`](docker/env.example)). Bind-mount host directories on the **`web`** service (commented example in [`docker/docker-compose.yml`](docker/docker-compose.yml)). `docker/write-config.php` extends **`DiskAllowed`** from those paths plus **`LOGANALYZER_DISK_ALLOWED_EXTRA`**. After the first boot, updating disk paths in `.env` requires either **`LOGANALYZER_OVERWRITE_CONFIG=1`** once or wiping the **`loganalyzer_config`** volume so **`config.php`** regenerates with the new prefixes (and usually **`docker compose … down -v`** if you must re-run the seed for new rows). ```bash docker compose -f docker/docker-compose.yml up --build ``` -**Clean rebuild** (drops MySQL data and re-seeds; `src/config.php` is regenerated on web container start): +**Clean reset** (wipe DB + persisted config): + +- Linux/macOS/Git Bash: `sh docker/rebuild-consumer.sh` +- Windows (cmd): `docker\rebuild-consumer.bat` + +Those scripts invoke **`docker/init-env-interactive.sh`** (POSIX path requires **bash** on `PATH`; if none, they fall back to copying **`docker/env.example`**) or **`docker/init-env-interactive.ps1`** (called from **`.bat`**) whenever **`.env`** is absent. Each **`KEY=value`** line from **`docker/env.example`** is prompted with **Enter = default**. **`*PASSWORD*`** prompts are masked in Bash and PowerShell **`7.1+`**, echoed in **`5.x`** / **7.0**). Afterwards: **`docker compose down -v`** then **`up --build`** for [`docker-compose.yml`](docker/docker-compose.yml). + +### Developer stack + +```bash +docker compose -f docker/docker-compose.dev.yml up --build +``` + +**Clean rebuild** (`src/config.php` + MySQL wiped for this stack): - Linux/macOS/Git Bash: `sh docker/rebuild-dev.sh` - Windows (cmd): `docker\rebuild-dev.bat` -**E2E clean run** (fresh E2E DB + Playwright): +### E2E clean run (Playwright) - `sh docker/rebuild-e2e.sh` or `docker\rebuild-e2e.bat` -- **Web UI:** http://localhost:8080/ -- **MySQL:** `localhost:3306`, database `loganalyzer`, user `loganalyzer` / `loganalyzer`, root password `loganalyzer_root` -- **Admin login:** `admin` / `pass` (seeded once per fresh MySQL volume) -- **Sample logs** are mounted read-only at `/samplelogs` from [tests/fixtures/samplelogs](https://github.com/rsyslog/loganalyzer/tree/master/tests/fixtures/samplelogs). The default seeded source reads `sampledata_syslog.log`. +Operational details mirror the handbook: **`http://localhost:8080/`**, **fresh admin** defaults to **`LOGANALYZER_ADMIN_*`** from Compose or `.env` (compose defaults **`admin`** / **`loganalyzer`** if unset). + +Developer stack exposes **MySQL** on **`localhost:3306`**. + +The default consumer compose sets **`LOGANALYZER_SEED_SAMPLE_SOURCES=0`**, giving a **clean first install**: schema + views + admin only (add sources manually). Fixtures still exist under **`/samplelogs`** if you bump that env var to **`1`**. Contributor/E2E stacks omit it or pin **`1`**, restoring the seeded demo disk sources Playwright relies on. + +Reset the developer stack volumes: `docker compose -f docker/docker-compose.dev.yml down -v`. + +### Publish image (maintainers) + +Pushing tag **`v*`** triggers [`.github/workflows/docker-publish.yml`](.github/workflows/docker-publish.yml): image **`ghcr.io//`** tagged with **`v…`** and **`latest`**. -Reset the database and config: `docker compose -f docker/docker-compose.yml down -v` then `up --build` again. +To pull from GHCR, point **`web.image`** at **`ghcr.io//:`** (matching the publish workflow outputs) alongside appropriate env/`LOGANALYZER_CONFIG_PATH` settings, instead of **`build`**. ### Environment variables (web container) -| Variable | Default | Purpose | -|----------|---------|---------| -| `LOGANALYZER_DB_HOST` | `db` | MySQL host | -| `LOGANALYZER_DB_PORT` | `3306` | MySQL port | -| `LOGANALYZER_DB_NAME` | `loganalyzer` | Database name | -| `LOGANALYZER_DB_USER` | `loganalyzer` | App DB user | -| `LOGANALYZER_DB_PASSWORD` | `loganalyzer` | App DB password | -| `MYSQL_ROOT_PASSWORD` | `loganalyzer_root` | Root (used by seed script only) | -| `LOGANALYZER_TABLE_PREFIX` | `logcon_` | Table prefix | -| `LOGANALYZER_ADMIN_USER` | `admin` | First admin username | -| `LOGANALYZER_ADMIN_PASSWORD` | `pass` | First admin password | -| `LOGANALYZER_LOGIN_REQUIRED` | `0` | Set `1` to force login on all pages | -| `LOGANALYZER_SAMPLE_LOG` | `/samplelogs/sampledata_syslog.log` | First seeded disk source (syslog) | -| `LOGANALYZER_SAMPLE_EVENTREPORTER` | `/samplelogs/EventReporter.log` | Second seeded disk source (Windows EventReporter / EvntSLog) | -| `LOGANALYZER_SKIP_SEED` | unset | Set `1` to skip MySQL seed | -| `LOGANALYZER_OVERWRITE_CONFIG` | `1` | Overwrite `src/config.php` on each start in dev | +| Variable | Typical default stack | Typical dev stack | Purpose | +|------------------|----------------|-------------------|---------| +| `LOGANALYZER_DB_HOST` | `db` | `db` | MySQL host | +| `LOGANALYZER_DB_PORT` | `3306` | `3306` | MySQL port | +| `(MySQL service)` `MYSQL_DATABASE`, `MYSQL_USER`, `MYSQL_PASSWORD` | `loganalyzer`/… compose defaults (`${…:-…}` pulls from repo-root **`.env`**) | same | Passed to **`db`** bootstrap + surfaced as **`LOGANALYZER_DB_*`** inside **`web`** | +| `MYSQL_ROOT_PASSWORD` | `loganalyzer_root` | `loganalyzer_root` | Seed script credential; keep synced with Compose healthchecks (`CMD-SHELL` uses **`$$MYSQL_ROOT_PASSWORD`**) | +| `LOGANALYZER_TABLE_PREFIX` | `logcon_` | `logcon_` | Table prefix | +| `LOGANALYZER_ADMIN_USER` | compose default `admin`; override `.env` | compose default `.env`/Compose | First admin username | +| `LOGANALYZER_ADMIN_PASSWORD` | compose default `loganalyzer`; override `.env` | same | First admin password (**customise before sharing host**) | +| `LOGANALYZER_LOGIN_REQUIRED` | `0` | `0` | Set `1` to force login on all pages | +| `LOGANALYZER_SAMPLE_LOG` / `LOGANALYZER_SAMPLE_EVENTREPORTER` | paths only when demo seeding | same | Used when **`LOGANALYZER_SEED_SAMPLE_SOURCES` enables demo disk sources | +| `LOGANALYZER_SEED_SAMPLE_SOURCES` | `0` (compose default, clean install) | unset / `1` | `0`/`false`/`no`/`off` skips demo sources; anything else seeds fixtures | +| `LOGANALYZER_SKIP_SEED` | unset | unset | Set `1` to skip MySQL seed | +| `LOGANALYZER_OVERWRITE_CONFIG` | `0` | `1` | Regenerate `config.php` on start when `1`; default stack persists config on the `/persist` volume | +| `LOGANALYZER_DISK_SOURCE_PATHS` | `.env`/unset | `.env`/unset | Comma-separated container paths → syslog disk sources seeded on first DB init (`basename` → display name) | +| `LOGANALYZER_DISK_SOURCES` | `.env`/unset | `.env`/unset | Records separated by `;;`, fields separated by `|` (see `docker/env.example`): name, path, kind `syslog` or `event`, optional description (description may contain `|` — only the first three delimiters split fields); deduped with `PATHS` by path | +| `LOGANALYZER_DISK_ALLOWED_EXTRA` | `.env`/unset | `.env`/unset | Extra `DiskAllowed` directory prefixes (comma-separated); auto-includes dirs of env disk paths plus `/var/log/` and `/samplelogs/` | +| `LOGANALYZER_CONFIG_PATH` | `/persist/config.php` | unset | When set (and not equal to `LOGANALYZER_DOCROOT/config.php`), config is stored there and symlinked into the docroot | ## PHPUnit diff --git a/README.md b/README.md index 472f58a..ccfff94 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,28 @@ ## Documentation -- **Handbook** (Docker & CI, **LogAnalyzer user guide**, third-party components): [rsyslog.github.io/loganalyzer](https://rsyslog.github.io/loganalyzer/) -- **[`AGENTS.md`](https://github.com/rsyslog/loganalyzer/blob/master/AGENTS.md)** — Docker development, environment variables, PHPUnit, Playwright E2E; start here for a working local stack +- **Handbook** (Docker install & development, **LogAnalyzer user guide**, third-party components): [rsyslog.github.io/loganalyzer](https://rsyslog.github.io/loganalyzer/) +- **[`docker/README.md`](docker/README.md)** — install walkthrough (`docker compose` from repo root, `.env`), file map (`Dockerfile`, compose files, scripts), handbook links +- **[`AGENTS.md`](https://github.com/rsyslog/loganalyzer/blob/master/AGENTS.md)** — contributor notes: developer Docker stack (`docker-compose.dev.yml`), environment variables, PHPUnit, Playwright E2E - **User guide chapters** (imported into the handbook) are maintained as HTML under [`doc/`](https://github.com/rsyslog/loganalyzer/tree/master/doc/) -## Quick start (Docker) +## Quick start (Docker — default stack) -From the repository root: +Step-by-step install: **[`docker/README.md`](docker/README.md)** · handbook: **[Docker install & development](https://rsyslog.github.io/loganalyzer/docker/)**. + +[`docker/docker-compose.yml`](https://github.com/rsyslog/loganalyzer/blob/master/docker/docker-compose.yml) ships a **fresh install** by default: the database bootstrap creates schema, default views, and the **admin user** — **no** auto-added sample log sources. Fixture files remain available under **`/samplelogs`** in the image if you set **`LOGANALYZER_SEED_SAMPLE_SOURCES=1`**. MySQL data and **`config.php`** persist in **named Compose volumes**. + +From the repository root, optionally copy [`docker/env.example`](https://github.com/rsyslog/loganalyzer/blob/master/docker/env.example) → **`.env`** to customise **`LOGANALYZER_ADMIN_*`**, **`MYSQL_*`**, **`LOGANALYZER_DISK_*`**, and demo seeding — or use the handbook section **[Docker helper scripts](doc-site/docs/docker.md#docker-helper-scripts)** (`init-env-interactive`, `rebuild-consumer`, etc.). ```bash docker compose -f docker/docker-compose.yml up --build ``` -Open **http://localhost:8080/**. Default dev credentials and MySQL settings are documented in [`AGENTS.md`](https://github.com/rsyslog/loganalyzer/blob/master/AGENTS.md). +Open **http://localhost:8080/**. + +- **Credentials / DB defaults:** see [`doc-site/docs/docker.md`](https://github.com/rsyslog/loganalyzer/blob/master/doc-site/docs/docker.md) (handbook section). Dev-only stack with a bind-mounted repo `src/` uses [`docker/docker-compose.dev.yml`](https://github.com/rsyslog/loganalyzer/blob/master/docker/docker-compose.dev.yml). + +**Clean reset everything** (drops MySQL and generated config volumes): `sh docker/rebuild-consumer.sh` or `docker\rebuild-consumer.bat`. ## Tests and CI @@ -26,7 +35,7 @@ Open **http://localhost:8080/**. Default dev credentials and MySQL settings are |------|--------| | PHPUnit | `composer install` then `composer test` | | Playwright E2E | [`e2e/README.md`](https://github.com/rsyslog/loganalyzer/blob/master/e2e/README.md), `docker/docker-compose.e2e.yml` | -| Workflows | [`.github/workflows/`](https://github.com/rsyslog/loganalyzer/tree/master/.github/workflows/) (PHP CI, Playwright E2E, GitHub Pages) | +| Workflows | [`.github/workflows/`](https://github.com/rsyslog/loganalyzer/tree/master/.github/workflows/) (PHP CI, Docker publish on tag `v*`, Playwright E2E, GitHub Pages) | On GitHub, the Playwright workflow can open an issue when E2E fails (same repository only); see [`AGENTS.md`](https://github.com/rsyslog/loganalyzer/blob/master/AGENTS.md) and [`e2e.yml`](https://github.com/rsyslog/loganalyzer/blob/master/.github/workflows/e2e.yml). diff --git a/doc-site/docs/docker.md b/doc-site/docs/docker.md index c88c8c0..bcfc997 100644 --- a/doc-site/docs/docker.md +++ b/doc-site/docs/docker.md @@ -1,63 +1,156 @@ -# LogAnalyzer agent / contributor notes +# Docker: local install & development -## Docker development +This handbook page tracks the same layout as **`AGENTS.md`** in the repository: two Compose files (default versus developer), environment variables, and CI notes. -From the repository root: +**Install checklist:** for a concise numbered procedure (prerequisites, `.env`, optional host **`LOGANALYZER_DISK_*`** mounts, reinstall), use [`docker/README.md`](https://github.com/rsyslog/loganalyzer/blob/master/docker/README.md) together with this page. + +--- + +## Quick install (recommended) + +The default stack ([`docker/docker-compose.yml`](https://github.com/rsyslog/loganalyzer/blob/master/docker/docker-compose.yml)) bakes the PHP application into the image. **First-time database seeding creates schema, default views, and the admin user only** — no bundled sample log **sources** unless you opt in (see below). Sample files still ship under **`/samplelogs`** for optional demos. ```bash docker compose -f docker/docker-compose.yml up --build ``` -**Clean rebuild** (drops MySQL data and re-seeds; `src/config.php` is regenerated on web container start): +Open **`http://localhost:8080/`**. -- Linux/macOS/Git Bash: `sh docker/rebuild-dev.sh` -- Windows (cmd): `docker\rebuild-dev.bat` +### Configure admin & database (`.env`, not `docker build`) -**E2E clean run** (fresh E2E DB + Playwright): +**Do not** prompt for secrets during `docker build`: images should stay generic and non-interactive. Instead: -- `sh docker/rebuild-e2e.sh` or `docker\rebuild-e2e.bat` +1. Create **`./.env`**: copy [**`docker/env.example`**](https://github.com/rsyslog/loganalyzer/blob/master/docker/env.example), **or** run **`bash docker/init-env-interactive.sh`** from the repo root (Windows: **`powershell -File docker\init-env-interactive.ps1 -RepoRoot .`**). The wizard shows each **`KEY=value`** line from **`env.example`**; **press Enter** to keep its default (**`*PASSWORD*`** keys are masked in Bash; masked in PowerShell **7.1+**, visible in **`5.x`** / **7.0**). +2. Keep **`LOGANALYZER_SEED_SAMPLE_SOURCES=0`** for a clean install (**default** in **`env.example`**), or set **`1`** to auto-create the two demo disk sources under **`/samplelogs`**. +3. **Real syslog / log files from the host (optional):** paths in **`LOGANALYZER_DISK_*`** env vars refer to paths **inside the web container**. Uncomment the commented **`volumes:`** line under **`web`** in [**`docker/docker-compose.yml`**](https://github.com/rsyslog/loganalyzer/blob/master/docker/docker-compose.yml) (e.g. **`/var/log:/mnt/hostlog:ro`** on Linux), then set **`LOGANALYZER_DISK_SOURCE_PATHS`** or **`LOGANALYZER_DISK_SOURCES`** as documented in **`docker/env.example`**. First MySQL seed creates those disk sources; **`docker/write-config.php`** extends **`DiskAllowed`** so PHP can browse those directories. Changing disk paths later usually needs **`LOGANALYZER_OVERWRITE_CONFIG=1`** once **or** removing the **`loganalyzer_config`** named volume plus a fresh seed if you also need new DB rows. -- **Web UI:** http://localhost:8080/ -- **MySQL:** `localhost:3306`, database `loganalyzer`, user `loganalyzer` / `loganalyzer`, root password `loganalyzer_root` -- **Admin login:** `admin` / `pass` (seeded once per fresh MySQL volume) -- **Sample logs** are mounted read-only at `/samplelogs` from [tests/fixtures/samplelogs](https://github.com/rsyslog/loganalyzer/tree/master/tests/fixtures/samplelogs). The default seeded source reads `sampledata_syslog.log`. +Docker Compose expands **`${VAR:-default}`** from that `.env` when you run `docker compose` from the repo root. **`.env` is gitignored**. -Reset the database and config: `docker compose -f docker/docker-compose.yml down -v` then `up --build` again. +**Persisted state** -### Environment variables (web container) +- Database files: compose volume **`loganalyzer_mysql_data`**. +- Generated config: compose volume **`loganalyzer_config`** (`/persist/config.php`, symlinked into the docroot). -| Variable | Default | Purpose | -|----------|---------|---------| -| `LOGANALYZER_DB_HOST` | `db` | MySQL host | -| `LOGANALYZER_DB_PORT` | `3306` | MySQL port | -| `LOGANALYZER_DB_NAME` | `loganalyzer` | Database name | -| `LOGANALYZER_DB_USER` | `loganalyzer` | App DB user | -| `LOGANALYZER_DB_PASSWORD` | `loganalyzer` | App DB password | -| `MYSQL_ROOT_PASSWORD` | `loganalyzer_root` | Root (used by seed script only) | -| `LOGANALYZER_TABLE_PREFIX` | `logcon_` | Table prefix | -| `LOGANALYZER_ADMIN_USER` | `admin` | First admin username | -| `LOGANALYZER_ADMIN_PASSWORD` | `pass` | First admin password | -| `LOGANALYZER_LOGIN_REQUIRED` | `0` | Set `1` to force login on all pages | -| `LOGANALYZER_SAMPLE_LOG` | `/samplelogs/sampledata_syslog.log` | First seeded disk source (syslog) | -| `LOGANALYZER_SAMPLE_EVENTREPORTER` | `/samplelogs/EventReporter.log` | Second seeded disk source (Windows EventReporter / EvntSLog) | -| `LOGANALYZER_SKIP_SEED` | unset | Set `1` to skip MySQL seed | -| `LOGANALYZER_OVERWRITE_CONFIG` | `1` | Overwrite `src/config.php` on each start in dev | +Use **`docker compose -f docker/docker-compose.yml down`** to stop without dropping volumes. **`down -v`** deletes named volumes (full reset). **`docker/rebuild-consumer.[sh|bat]`** runs the wizard when **`./.env`** is missing (same behaviour as **`docker/init-env-interactive.[sh|ps1]`**): prompts follow **`docker/env.example`** (**Enter** = keep default; **`*PASSWORD*`** prompts are masked in Bash; PowerShell **7.1+** masks (**`5.x`** / **7.0** echo)). Then **`down -v`** and **`up --build`**. -## PHPUnit +!!! note "Clean restore from scratch" + + From the repository root: + + - Linux/macOS/Git Bash: `sh docker/rebuild-consumer.sh` + - Windows cmd: `docker\rebuild-consumer.bat` + +### Docker helper scripts + +Small shell/cmd helpers beside the Compose files — run them from the **repository root** (paths below are under **`docker/`**). + +| Script | When to use | +|--------|--------------| +| [`docker/init-env-interactive.sh`](https://github.com/rsyslog/loganalyzer/blob/master/docker/init-env-interactive.sh) | Create **`.env`** once: prompts for each **`KEY=value`** from **`docker/env.example`** (Bash; **Enter** = default; **`*PASSWORD*`** masked). Skips if **`.env`** already exists. | +| [`docker/init-env-interactive.ps1`](https://github.com/rsyslog/loganalyzer/blob/master/docker/init-env-interactive.ps1) | Same as above on Windows: **`powershell -File docker\init-env-interactive.ps1 -RepoRoot .`**. | +| [`docker/rebuild-consumer.sh`](https://github.com/rsyslog/loganalyzer/blob/master/docker/rebuild-consumer.sh) / [`.bat`](https://github.com/rsyslog/loganalyzer/blob/master/docker/rebuild-consumer.bat) | **Default stack:** if **`.env`** is missing, runs the interactive wizard (or copies **`env.example`** when Bash is unavailable), then **`docker compose … down -v`** and **`up --build`**. | +| [`docker/rebuild-dev.sh`](https://github.com/rsyslog/loganalyzer/blob/master/docker/rebuild-dev.sh) / [`.bat`](https://github.com/rsyslog/loganalyzer/blob/master/docker/rebuild-dev.bat) | **Developer stack** clean rebuild (wipe dev volumes, then **`docker-compose.dev.yml up --build`**). | +| [`docker/rebuild-e2e.sh`](https://github.com/rsyslog/loganalyzer/blob/master/docker/rebuild-e2e.sh) / [`.bat`](https://github.com/rsyslog/loganalyzer/blob/master/docker/rebuild-e2e.bat) | **Playwright E2E** stack: fresh volumes + Compose E2E + tests (see [`e2e/README.md`](https://github.com/rsyslog/loganalyzer/blob/master/e2e/README.md)). | + +Bootstrap PHP used by the image (**`docker/write-config.php`**, **`docker/seed-database.php`**, **`docker/env-disk-sources.php`**) runs inside the container — not invoked directly unless you are debugging the Docker image build. + +!!! note "Advanced: expose MySQL to the host" + + The **default stack** keeps MySQL internal (`db` hostname inside the Compose network). To reach MySQL from the host temporarily, add **`ports`** on the `db` service (similar to **`docker-compose.dev.yml`**) and align credentials with your `.env`. + +### Pre-built image (`ghcr.io`) + +Tag pushes matching **`v*`** publish **`ghcr.io//`** (`:latest` and `:v…`) via [`.github/workflows/docker-publish.yml`](https://github.com/rsyslog/loganalyzer/blob/master/.github/workflows/docker-publish.yml). Use **`web.image`** (and drop **`build`**) in your own override file, or `docker run`, with the same environment variables as the Compose stack. + +--- + +### What you see in the UI (screenshots) + +PNG walkthrough assets under **`doc-site/docs/assets/user-guide/`** were captured with **demo sources seeded** (Playwright stack sets **`LOGANALYZER_SEED_SAMPLE_SOURCES=1`**). A **clean consumer install** starts without those sources; add your own under **Administration** or temporarily set **`LOGANALYZER_SEED_SAMPLE_SOURCES=1`** (then decide whether to delete demo sources in the UI). + +[![Main view after Docker install](assets/user-guide/index.png)](user-guide/quick-start.md) + +[![Sign in](assets/user-guide/login.png)](user-guide/quick-start.md) + +Browse the **[Quick start](user-guide/quick-start.md)** tutorial for fuller UI flow (admin centre, statistics, reports). + +!!! warning "Hosting note" + + The bundled container runs PHP’s **`php -S`** built‑in HTTP server (`docker/entrypoint.sh`). That is straightforward for desktops and demos; front it with nginx/Apache (+ PHP-FPM) if you expose LogAnalyzer broadly on a network. + +--- + +## Developer workflow (live `src` mount) + +Contributor workflow uses **`docker/docker-compose.dev.yml`**: **`src/`** mounts read‑write under `/var/www/html`, fixtures mount at `/samplelogs`, and **`LOGANALYZER_OVERWRITE_CONFIG=1`** regenerates **`src/config.php`** whenever the stack starts so iterative PHP edits stay aligned. ```bash -composer install -composer test +docker compose -f docker/docker-compose.dev.yml up --build +``` + +**Clean wipe** developer volumes + caches: + +```text +docker compose -f docker/docker-compose.dev.yml down -v ``` -MySQL-dependent integration tests are skipped unless `LOGANALYZER_INTEGRATION=1` and DB env vars match Docker defaults or your own host. +(or `sh docker/rebuild-dev.sh` / `docker\rebuild-dev.bat`). + +Expose **MySQL** on **`localhost:3306`** (database `loganalyzer`, user/pass `loganalyzer` / `loganalyzer`, unless you customised env variables). + +!!! note "When to rebuild the image itself" -## E2E (Playwright) + Any change touching **`Dockerfile`** (PHP extensions / entry scripts) needs `docker compose ... up --build`. Pure PHP tweaks only require reloading in the browser when using the developer stack—the bind mount reflects host edits instantly. + +--- + +## Playwright end-to-end CI + +Manual run (Playwright exits with the Compose stack): + +```bash +docker compose -f docker/docker-compose.e2e.yml up --build --abort-on-container-exit --exit-code-from playwright +``` -See [`e2e/README.md`](https://github.com/rsyslog/loganalyzer/blob/master/e2e/README.md) and `docker/docker-compose.e2e.yml`. +Or use **`docker/rebuild-e2e.[sh|bat]`**. + +Documentation: [`e2e/README.md`](https://github.com/rsyslog/loganalyzer/blob/master/e2e/README.md). ### CI on GitHub Actions -The [Playwright E2E workflow](https://github.com/rsyslog/loganalyzer/blob/master/.github/workflows/e2e.yml) runs on pull requests, pushes to `main` or `master`, and manual dispatch. It builds and runs the same Docker Compose stack as locally (`docker/docker-compose.e2e.yml`), then executes Playwright. +The **[Playwright E2E workflow](https://github.com/rsyslog/loganalyzer/blob/master/.github/workflows/e2e.yml)** runs on pull requests, pushes to **`main`** or **`master`**, plus manual workflow dispatch (`docker-compose.e2e.yml`). + +Failures can optionally open **`CI: playwright e2e failed (…)`** issues on this repository (`issues:write` gated). Fork PRs suppress issue creation (`gh issue create` guarded). + +--- + +## Environment variables (`web`) + +| Variable | Typical default compose | Typical dev compose | Meaning | +|-----------|-----------------|--------------|---------| +| `LOGANALYZER_DB_HOST` | `db` | `db` | MySQL hostname (Compose service name) | +| `LOGANALYZER_DB_PORT` | `3306` | `3306` | MySQL port | +| `(MySQL service)` `MYSQL_DATABASE`, `MYSQL_USER`, `MYSQL_PASSWORD` | via `.env` / compose defaults (`loganalyzer`/…) | same | Passed to **`db`** bootstrap + surfaced as **`LOGANALYZER_DB_*`** in **`web`** | +| `MYSQL_ROOT_PASSWORD` | `loganalyzer_root` | `loganalyzer_root` | Must match Compose healthchecks + seed script credential | +| `LOGANALYZER_TABLE_PREFIX` | `logcon_` | `logcon_` | Table prefix | +| `LOGANALYZER_ADMIN_USER` | `admin` (+ `.env` overrides) | `admin` (+ `.env` overrides) | First administrator | +| `LOGANALYZER_ADMIN_PASSWORD` | `loganalyzer` (+ `.env` overrides) | `loganalyzer` (+ `.env` overrides) | Override before exposing the host beyond trusted networks | +| `LOGANALYZER_LOGIN_REQUIRED` | `0` | `0` | `1` ⇒ force login globally | +| `LOGANALYZER_SAMPLE_LOG` / `LOGANALYZER_SAMPLE_EVENTREPORTER` | used when demo seeding | same | Disk paths consulted only while **`LOGANALYZER_SEED_SAMPLE_SOURCES` ≠ off list** | +| `LOGANALYZER_SEED_SAMPLE_SOURCES` | `0` (clean install via compose defaults) | unset / `1` | `0`/`false`/`no`/`off` skips demo sources; other values seed `/samplelogs` fixtures | +| `LOGANALYZER_SKIP_SEED` | unset | unset | `1` skips DB seed bootstrap | +| `LOGANALYZER_OVERWRITE_CONFIG` | `0` | `1` | Regenerate **`config.php`** every start versus retain persisted file | +| `LOGANALYZER_DISK_SOURCE_PATHS` | unset (`.env`) | unset (`.env`) | Comma-separated container paths → syslog disk rows on **first seed** (`basename` → name) | +| `LOGANALYZER_DISK_SOURCES` | unset (`.env`) | unset (`.env`) | Advanced: records separated by `;;`, fields separated by `|` — name, path, **`syslog`** or **`event`**, optional description (description may include further `|`s) | +| `LOGANALYZER_DISK_ALLOWED_EXTRA` | unset (`.env`) | unset (`.env`) | Extra **`DiskAllowed`** directory prefixes (comma-separated); dirs of env paths are included automatically plus **`/var/log/`**, **`/samplelogs/`** | +| `LOGANALYZER_CONFIG_PATH` | `/persist/config.php` *(typical)* | unset | Persist config **outside container layers** | + +--- + +## PHPUnit + +```bash +composer install && composer test +``` -If the job **fails**, the workflow can open a **GitHub issue** titled like `CI: playwright e2e failed (…)` with a link to the run and a **tail of the Docker/compose log**. That only happens when the workflow has `issues: write` and the run is for **this repository** (for pull requests from forks, issue creation is skipped). See the “Open issue on failure” step in [`.github/workflows/e2e.yml`](https://github.com/rsyslog/loganalyzer/blob/master/.github/workflows/e2e.yml) (`gh issue create`). +Integration suites stay disabled unless **`LOGANALYZER_INTEGRATION=1`** aligns DB env settings with reachable MySQL endpoints. diff --git a/doc-site/docs/index.md b/doc-site/docs/index.md index aeeb128..eb7b38a 100644 --- a/doc-site/docs/index.md +++ b/doc-site/docs/index.md @@ -2,7 +2,8 @@ LogAnalyzer is a PHP web front end for browsing syslog and related log data. This site collects **operational docs** (Docker, CI, fixtures) and a **LogAnalyzer user guide**: handbook-native topics plus chapters **imported** from upstream [`doc/*.html`](https://github.com/rsyslog/loganalyzer/tree/master/doc) manuals. -- Start with [Docker & local development](docker.md) (`AGENTS.md` from the repo root). +- In-repo **step-by-step install** (Compose from repo root, `.env`, optional host logs via **`LOGANALYZER_DISK_*`**, reset): [`docker/README.md`](https://github.com/rsyslog/loganalyzer/blob/master/docker/README.md). +- Full reference: **[Docker (install & development)](docker.md)** (`AGENTS.md` mirrors essentials for contributors). - **[Quick start](user-guide/quick-start.md)** — run with Docker and skim the UI with screenshots. - **[Interface map](user-guide/interface-map.md)** — main routes and where to read more. - [Third-party and bundled libraries](third-party.md) lists versions and update notes. diff --git a/doc-site/docs/project-readme.md b/doc-site/docs/project-readme.md index 472f58a..e8820e3 100644 --- a/doc-site/docs/project-readme.md +++ b/doc-site/docs/project-readme.md @@ -6,11 +6,16 @@ ## Documentation -- **Handbook** (Docker & CI, **LogAnalyzer user guide**, third-party components): [rsyslog.github.io/loganalyzer](https://rsyslog.github.io/loganalyzer/) -- **[`AGENTS.md`](https://github.com/rsyslog/loganalyzer/blob/master/AGENTS.md)** — Docker development, environment variables, PHPUnit, Playwright E2E; start here for a working local stack +- **Handbook** (Docker install & development, **LogAnalyzer user guide**, third-party components): [rsyslog.github.io/loganalyzer](https://rsyslog.github.io/loganalyzer/) +- **[`docker/README.md`](https://github.com/rsyslog/loganalyzer/blob/master/docker/README.md)** — install walkthrough (`docker compose` from repo root, `.env`), file map, handbook links +- **[`AGENTS.md`](https://github.com/rsyslog/loganalyzer/blob/master/AGENTS.md)** — contributor notes: developer Docker stack (`docker-compose.dev.yml`), environment variables, PHPUnit, Playwright E2E - **User guide chapters** (imported into the handbook) are maintained as HTML under [`doc/`](https://github.com/rsyslog/loganalyzer/tree/master/doc/) -## Quick start (Docker) +## Quick start (Docker — default stack) + +Step-by-step install: **[`docker/README.md`](https://github.com/rsyslog/loganalyzer/blob/master/docker/README.md)** · handbook: **[Docker install & development](docker.md)**. + +[`docker/docker-compose.yml`](https://github.com/rsyslog/loganalyzer/blob/master/docker/docker-compose.yml) defaults to a **clean install** (schema + admin; no demo disk sources). Optional fixture files live under **`/samplelogs`** if you enable **`LOGANALYZER_SEED_SAMPLE_SOURCES`**. MySQL and **`config.php`** persist in **named volumes**. Copy [`docker/env.example`](https://github.com/rsyslog/loganalyzer/blob/master/docker/env.example) to **`.env`** in the repo root to set passwords, optional **`LOGANALYZER_DISK_*`** paths, and seeding — or follow **[Docker helper scripts](docker.md#docker-helper-scripts)**. From the repository root: @@ -18,7 +23,11 @@ From the repository root: docker compose -f docker/docker-compose.yml up --build ``` -Open **http://localhost:8080/**. Default dev credentials and MySQL settings are documented in [`AGENTS.md`](https://github.com/rsyslog/loganalyzer/blob/master/AGENTS.md). +Open **http://localhost:8080/**. + +- **Credentials / DB defaults:** see [Docker: local install & development](docker.md). Dev-only stack with a bind-mounted repo `src/` uses [`docker/docker-compose.dev.yml`](https://github.com/rsyslog/loganalyzer/blob/master/docker/docker-compose.dev.yml). + +**Clean reset everything** (drops MySQL and generated config volumes): `sh docker/rebuild-consumer.sh` or `docker\rebuild-consumer.bat`. ## Tests and CI @@ -26,7 +35,7 @@ Open **http://localhost:8080/**. Default dev credentials and MySQL settings are |------|--------| | PHPUnit | `composer install` then `composer test` | | Playwright E2E | [`e2e/README.md`](https://github.com/rsyslog/loganalyzer/blob/master/e2e/README.md), `docker/docker-compose.e2e.yml` | -| Workflows | [`.github/workflows/`](https://github.com/rsyslog/loganalyzer/tree/master/.github/workflows/) (PHP CI, Playwright E2E, GitHub Pages) | +| Workflows | [`.github/workflows/`](https://github.com/rsyslog/loganalyzer/tree/master/.github/workflows/) (PHP CI, Docker publish on tag `v*`, Playwright E2E, GitHub Pages) | On GitHub, the Playwright workflow can open an issue when E2E fails (same repository only); see [`AGENTS.md`](https://github.com/rsyslog/loganalyzer/blob/master/AGENTS.md) and [`e2e.yml`](https://github.com/rsyslog/loganalyzer/blob/master/.github/workflows/e2e.yml). diff --git a/doc-site/docs/user-guide/quick-start.md b/doc-site/docs/user-guide/quick-start.md index 23d5f40..ab06d29 100644 --- a/doc-site/docs/user-guide/quick-start.md +++ b/doc-site/docs/user-guide/quick-start.md @@ -1,8 +1,10 @@ # Quick start -This page matches the seeded **Docker** stack described in [Docker & CI](../docker.md): sample log sources and default admin **`admin`** / **`pass`** (development only). +The **default Docker stack** embeds the app image and persists MySQL plus **`config.php`**. Fresh installs (**`LOGANALYZER_SEED_SAMPLE_SOURCES=0`**) create **schema + admin only** — add sources in Administration unless you enable demo seeding (**`LOGANALYZER_SEED_SAMPLE_SOURCES=1`**). -## Run LogAnalyzer locally +Credentials come from Compose or repo-root **`.env`**; default placeholders **`admin`** / **`loganalyzer`**. **[`docker/README.md` install](https://github.com/rsyslog/loganalyzer/blob/master/docker/README.md)** (numbered steps, optional **`LOGANALYZER_DISK_*`**); **[Docker handbook](../docker.md)** (helper scripts table, dev **`docker-compose.dev.yml`**, full **`web`** env table). The screenshots below use **demo sources** (Playwright/E2E). + +## Run LogAnalyzer locally (default Compose) From the repository root: diff --git a/doc-site/mkdocs.yml b/doc-site/mkdocs.yml index b550f77..df2da48 100644 --- a/doc-site/mkdocs.yml +++ b/doc-site/mkdocs.yml @@ -1,5 +1,5 @@ site_name: LogAnalyzer handbook -site_description: Adiscon LogAnalyzer — install, Docker dev, and bundled components +site_description: Adiscon LogAnalyzer — Docker install & dev, bundled components site_url: https://rsyslog.github.io/loganalyzer/ repo_url: https://github.com/rsyslog/loganalyzer repo_name: loganalyzer @@ -28,7 +28,7 @@ markdown_extensions: nav: - Home: index.md - - Docker & CI (from AGENTS.md): docker.md + - Docker (install & CI): docker.md - LogAnalyzer user guide: - 'Overview': user-guide/overview.md - 'Quick start': user-guide/quick-start.md diff --git a/docker/Dockerfile b/docker/Dockerfile index 846437c..f4fb020 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,9 @@ -# LogAnalyzer local dev / CI (PHP + extensions for web UI, charts, MySQL) -FROM php:8.2-cli +# Build from repository root (--context dir that contains docker/, src/, tests/): +# docker build -f docker/Dockerfile . +# +# Final image embeds php app + baked samplelogs; CI/dev/e2e may still bind-mount ../src. + +FROM php:8.2-cli AS loganalyzer-runtime RUN apt-get update \ && apt-get install -y --no-install-recommends \ @@ -8,12 +12,18 @@ RUN apt-get update \ && docker-php-ext-install -j$(nproc) mysqli pdo_mysql gd zip \ && rm -rf /var/lib/apt/lists/* -COPY entrypoint.sh /entrypoint.sh -COPY seed-database.php /usr/local/share/loganalyzer/seed-database.php -COPY write-config.php /usr/local/share/loganalyzer/write-config.php -# Windows checkouts may use CRLF; bash needs LF +COPY docker/entrypoint.sh /entrypoint.sh +COPY docker/seed-database.php /usr/local/share/loganalyzer/seed-database.php +COPY docker/write-config.php /usr/local/share/loganalyzer/write-config.php +COPY docker/env-disk-sources.php /usr/local/share/loganalyzer/env-disk-sources.php RUN sed -i 's/\r$//' /entrypoint.sh && chmod +x /entrypoint.sh WORKDIR /var/www/html EXPOSE 8080 + +FROM loganalyzer-runtime AS loganalyzer-web + +COPY src/ /var/www/html/ +COPY tests/fixtures/samplelogs /samplelogs + ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..96897d6 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,76 @@ +# Docker assets + +Files for building and running LogAnalyzer with **Docker Compose**. Build context is the **repository root** (parent of this directory); run **`docker compose`** from the repo root so paths and **`.env`** resolve correctly. + +**Full guide** (stacks, env vars, host log mounts, CI): [Handbook — Docker](../doc-site/docs/docker.md) · contributor notes: [`AGENTS.md`](../AGENTS.md). + +## Install + +### Prerequisites + +- **Docker** (Engine) and the **Compose plugin** (`docker compose`, v2 syntax). +- Clone this repository and open a shell in the **repository root** (`loganalyzer/`, the directory that contains `docker/`, `src/`, `README.md`). + +### Default install (consumer stack) + +Recommended for a local “appliance” install: app is **baked into the image**; **MySQL** and **`config.php`** live in **Compose named volumes** (`loganalyzer_mysql_data`, `loganalyzer_config`). + +1. **Environment (optional but recommended)** + Copy [`env.example`](env.example) to **`.env`** in the **repository root** (same level as `README.md`, **not** inside `docker/`). Edit admin and database passwords, and any optional flags (see comments in `env.example`). + Or run the interactive wizard once: + - POSIX: `bash docker/init-env-interactive.sh` + - Windows: `powershell -File docker\init-env-interactive.ps1 -RepoRoot .` + +2. **Start the stack** + + ```bash + docker compose -f docker/docker-compose.yml up --build + ``` + +3. **Open the app** + **http://localhost:8080/** + First-login admin user and password come from **`LOGANALYZER_ADMIN_USER`** / **`LOGANALYZER_ADMIN_PASSWORD`** in your `.env` (Compose defaults **`admin`** / **`loganalyzer`** if unset). + +4. **What you get on first boot** + MySQL is initialized; the **`web`** service generates **`config.php`**, waits for the DB, then **seeds schema, default views, and the admin user**. By default there are **no** demo disk sources (`LOGANALYZER_SEED_SAMPLE_SOURCES=0`); enable **`1`** in `.env` if you want the bundled **`/samplelogs`** sources, or configure **`LOGANALYZER_DISK_*`** for real files (below). + +### Optional: host log files under the default stack + +Paths in **`LOGANALYZER_DISK_SOURCE_PATHS`** / **`LOGANALYZER_DISK_SOURCES`** must exist **inside the web container**. Typical pattern: + +1. Uncomment the **`volumes`** line under **`web`** in [`docker-compose.yml`](docker-compose.yml) (example: `- /var/log:/mnt/hostlog:ro`). +2. Set the matching paths in `.env`, e.g. `LOGANALYZER_DISK_SOURCE_PATHS=/mnt/hostlog/syslog`. +3. **First seed only** creates those DB sources; changing paths later usually means resetting the **`loganalyzer_config`** volume and/or **`LOGANALYZER_OVERWRITE_CONFIG`** — see the [handbook](https://rsyslog.github.io/loganalyzer/docker/). + +### Stop, reset, reinstall + +- **Stop without deleting data:** `docker compose -f docker/docker-compose.yml down` +- **Wipe DB + persisted config** and bring the stack back: + `sh docker/rebuild-consumer.sh` or `docker\rebuild-consumer.bat` (runs the `.env` wizard if `.env` is missing, then **`down -v`** and **`up --build`**). + +### Developer install (live `src/`) + +For PHP hacking with the repo **`src/`** mounted into the container and **`config.php` regenerated each start**: + +```bash +docker compose -f docker/docker-compose.dev.yml up --build +``` + +MySQL on **`localhost:3306`**, wipe helper: **`docker/rebuild-dev.sh`** / **`docker\rebuild-dev.bat`**. Details: [Handbook — Developer workflow](../doc-site/docs/docker.md) (section *Developer workflow (live `src` mount)*). + +## What lives here + +| Item | Role | +|------|------| +| [`Dockerfile`](Dockerfile) | Multi-stage image: PHP app + fixtures; copies bootstrap scripts into `/usr/local/share/loganalyzer/`. | +| [`docker-compose.yml`](docker-compose.yml) | Default “consumer” stack: baked app, named volumes for MySQL + `config.php`. | +| [`docker-compose.dev.yml`](docker-compose.dev.yml) | Dev stack: bind-mount `src/`, overwrite `config.php` each start. | +| [`docker-compose.e2e.yml`](docker-compose.e2e.yml) | Playwright CI/local E2E (see [`e2e/README.md`](../e2e/README.md)). | +| [`env.example`](env.example) | Template for repo-root **`.env`**: admin, MySQL, optional demo seeding, optional **`LOGANALYZER_DISK_*`** disk sources. | +| [`entrypoint.sh`](entrypoint.sh) | Container start: `write-config`, DB wait, `seed-database`, `php -S`. | +| [`write-config.php`](write-config.php) | Generates `config.php` from `include/config.sample.php` + env (incl. `DiskAllowed` from disk-source env). | +| [`seed-database.php`](seed-database.php) | First-run MySQL schema/views/admin and optional sources. | +| [`env-disk-sources.php`](env-disk-sources.php) | Shared helpers for **`LOGANALYZER_DISK_SOURCE_PATHS`** / **`LOGANALYZER_DISK_SOURCES`** / **`LOGANALYZER_DISK_ALLOWED_EXTRA`**. | +| `init-env-interactive.*`, `rebuild-*.sh`, `rebuild-*.bat` | Host-side helpers ([handbook » Docker helper scripts](https://rsyslog.github.io/loganalyzer/docker/#docker-helper-scripts)). | + +Published handbook: **https://rsyslog.github.io/loganalyzer/docker/** (same content as [`doc-site/docs/docker.md`](../doc-site/docs/docker.md)). diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml new file mode 100644 index 0000000..8069bcf --- /dev/null +++ b/docker/docker-compose.dev.yml @@ -0,0 +1,70 @@ +# Developer stack: live bind-mount of repo src/, fixtures as /samplelogs, overwrite config each start. +# From repo root: docker compose -f docker/docker-compose.dev.yml up --build +# Clean rebuild: sh docker/rebuild-dev.sh | docker\rebuild-dev.bat + +services: + db: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-loganalyzer_root} + MYSQL_DATABASE: ${MYSQL_DATABASE:-loganalyzer} + MYSQL_USER: ${MYSQL_USER:-loganalyzer} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-loganalyzer} + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + ports: + - "3306:3306" + volumes: + - loganalyzer_dev_mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" || exit 1"] + interval: 5s + timeout: 5s + retries: 15 + + web: + build: + context: .. + dockerfile: docker/Dockerfile + ports: + - "8080:8080" + volumes: + - ../src:/var/www/html:rw + - ../tests/fixtures/samplelogs:/samplelogs:ro + environment: + LOGANALYZER_DOCROOT: /var/www/html + LOGANALYZER_DB_HOST: db + LOGANALYZER_DB_PORT: "3306" + LOGANALYZER_DB_NAME: ${MYSQL_DATABASE:-loganalyzer} + LOGANALYZER_DB_USER: ${MYSQL_USER:-loganalyzer} + LOGANALYZER_DB_PASSWORD: ${MYSQL_PASSWORD:-loganalyzer} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-loganalyzer_root} + LOGANALYZER_TABLE_PREFIX: logcon_ + LOGANALYZER_ADMIN_USER: ${LOGANALYZER_ADMIN_USER:-admin} + LOGANALYZER_ADMIN_PASSWORD: ${LOGANALYZER_ADMIN_PASSWORD:-loganalyzer} + LOGANALYZER_LOGIN_REQUIRED: "0" + LOGANALYZER_SAMPLE_LOG: /samplelogs/sampledata_syslog.log + LOGANALYZER_SAMPLE_EVENTREPORTER: /samplelogs/EventReporter.log + LOGANALYZER_DISK_SOURCE_PATHS: "${LOGANALYZER_DISK_SOURCE_PATHS:-}" + LOGANALYZER_DISK_SOURCES: "${LOGANALYZER_DISK_SOURCES:-}" + LOGANALYZER_DISK_ALLOWED_EXTRA: "${LOGANALYZER_DISK_ALLOWED_EXTRA:-}" + LOGANALYZER_OVERWRITE_CONFIG: "1" + depends_on: + db: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "php", + "-r", + "exit(@file_get_contents('http://127.0.0.1:8080/index.php')!==false?0:1);", + ] + interval: 5s + timeout: 8s + retries: 30 + start_period: 45s + +volumes: + loganalyzer_dev_mysql_data: diff --git a/docker/docker-compose.e2e.yml b/docker/docker-compose.e2e.yml index 5b99226..e561102 100644 --- a/docker/docker-compose.e2e.yml +++ b/docker/docker-compose.e2e.yml @@ -5,25 +5,25 @@ services: db: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD: loganalyzer_root - MYSQL_DATABASE: loganalyzer - MYSQL_USER: loganalyzer - MYSQL_PASSWORD: loganalyzer + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-loganalyzer_root} + MYSQL_DATABASE: ${MYSQL_DATABASE:-loganalyzer} + MYSQL_USER: ${MYSQL_USER:-loganalyzer} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-loganalyzer} command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci volumes: - loganalyzer_e2e_mysql:/var/lib/mysql healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-ploganalyzer_root"] + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" || exit 1"] interval: 5s timeout: 5s retries: 15 web: build: - context: . - dockerfile: Dockerfile + context: .. + dockerfile: docker/Dockerfile volumes: - ../src:/var/www/html:rw - ../tests/fixtures/samplelogs:/samplelogs:ro @@ -31,16 +31,17 @@ services: LOGANALYZER_DOCROOT: /var/www/html LOGANALYZER_DB_HOST: db LOGANALYZER_DB_PORT: "3306" - LOGANALYZER_DB_NAME: loganalyzer - LOGANALYZER_DB_USER: loganalyzer - LOGANALYZER_DB_PASSWORD: loganalyzer - MYSQL_ROOT_PASSWORD: loganalyzer_root + LOGANALYZER_DB_NAME: ${MYSQL_DATABASE:-loganalyzer} + LOGANALYZER_DB_USER: ${MYSQL_USER:-loganalyzer} + LOGANALYZER_DB_PASSWORD: ${MYSQL_PASSWORD:-loganalyzer} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-loganalyzer_root} LOGANALYZER_TABLE_PREFIX: logcon_ - LOGANALYZER_ADMIN_USER: admin - LOGANALYZER_ADMIN_PASSWORD: pass + LOGANALYZER_ADMIN_USER: ${LOGANALYZER_ADMIN_USER:-admin} + LOGANALYZER_ADMIN_PASSWORD: ${LOGANALYZER_ADMIN_PASSWORD:-loganalyzer} LOGANALYZER_LOGIN_REQUIRED: "0" LOGANALYZER_SAMPLE_LOG: /samplelogs/sampledata_syslog.log LOGANALYZER_SAMPLE_EVENTREPORTER: /samplelogs/EventReporter.log + LOGANALYZER_SEED_SAMPLE_SOURCES: "1" LOGANALYZER_OVERWRITE_CONFIG: "1" depends_on: db: @@ -65,8 +66,8 @@ services: condition: service_healthy environment: PLAYWRIGHT_BASE_URL: http://web:8080 - LOGANALYZER_ADMIN_USER: admin - LOGANALYZER_ADMIN_PASSWORD: pass + LOGANALYZER_ADMIN_USER: ${LOGANALYZER_ADMIN_USER:-admin} + LOGANALYZER_ADMIN_PASSWORD: ${LOGANALYZER_ADMIN_PASSWORD:-loganalyzer} HANDBOOK_USER_GUIDE_ASSETS: /handbook-user-guide-assets volumes: - ../e2e:/e2e diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 2639d90..55cc3dd 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,52 +1,57 @@ +# Default (consumer) stack: app baked into the image; config + DB persist in volumes. # From repo root: docker compose -f docker/docker-compose.yml up --build -# App: http://localhost:8080/ Login: admin / pass (see AGENTS.md) -# MySQL: localhost:3306 user loganalyzer / loganalyzer +# Optional: copy `docker/env.example` → `.env` in the repo root to set admin and DB passwords. +# Clean reset: sh docker/rebuild-consumer.sh | docker\rebuild-consumer.bat services: db: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD: loganalyzer_root - MYSQL_DATABASE: loganalyzer - MYSQL_USER: loganalyzer - MYSQL_PASSWORD: loganalyzer + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-loganalyzer_root} + MYSQL_DATABASE: ${MYSQL_DATABASE:-loganalyzer} + MYSQL_USER: ${MYSQL_USER:-loganalyzer} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-loganalyzer} command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - ports: - - "3306:3306" volumes: - loganalyzer_mysql_data:/var/lib/mysql healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-ploganalyzer_root"] + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" || exit 1"] interval: 5s timeout: 5s retries: 15 web: build: - context: . - dockerfile: Dockerfile + context: .. + dockerfile: docker/Dockerfile ports: - "8080:8080" volumes: - - ../src:/var/www/html:rw - - ../tests/fixtures/samplelogs:/samplelogs:ro + - loganalyzer_config:/persist + # Real host logs (Linux examples): uncomment and point LOGANALYZER_DISK_* at paths under /mnt/hostlog inside the container. + # - /var/log:/mnt/hostlog:ro environment: LOGANALYZER_DOCROOT: /var/www/html + LOGANALYZER_CONFIG_PATH: /persist/config.php LOGANALYZER_DB_HOST: db LOGANALYZER_DB_PORT: "3306" - LOGANALYZER_DB_NAME: loganalyzer - LOGANALYZER_DB_USER: loganalyzer - LOGANALYZER_DB_PASSWORD: loganalyzer - MYSQL_ROOT_PASSWORD: loganalyzer_root + LOGANALYZER_DB_NAME: ${MYSQL_DATABASE:-loganalyzer} + LOGANALYZER_DB_USER: ${MYSQL_USER:-loganalyzer} + LOGANALYZER_DB_PASSWORD: ${MYSQL_PASSWORD:-loganalyzer} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-loganalyzer_root} LOGANALYZER_TABLE_PREFIX: logcon_ - LOGANALYZER_ADMIN_USER: admin - LOGANALYZER_ADMIN_PASSWORD: pass + LOGANALYZER_ADMIN_USER: ${LOGANALYZER_ADMIN_USER:-admin} + LOGANALYZER_ADMIN_PASSWORD: ${LOGANALYZER_ADMIN_PASSWORD:-loganalyzer} LOGANALYZER_LOGIN_REQUIRED: "0" LOGANALYZER_SAMPLE_LOG: /samplelogs/sampledata_syslog.log LOGANALYZER_SAMPLE_EVENTREPORTER: /samplelogs/EventReporter.log - LOGANALYZER_OVERWRITE_CONFIG: "1" + LOGANALYZER_SEED_SAMPLE_SOURCES: "${LOGANALYZER_SEED_SAMPLE_SOURCES:-0}" + LOGANALYZER_DISK_SOURCE_PATHS: "${LOGANALYZER_DISK_SOURCE_PATHS:-}" + LOGANALYZER_DISK_SOURCES: "${LOGANALYZER_DISK_SOURCES:-}" + LOGANALYZER_DISK_ALLOWED_EXTRA: "${LOGANALYZER_DISK_ALLOWED_EXTRA:-}" + LOGANALYZER_OVERWRITE_CONFIG: "0" depends_on: db: condition: service_healthy @@ -65,3 +70,4 @@ services: volumes: loganalyzer_mysql_data: + loganalyzer_config: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 24ae862..5b5aa64 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -10,7 +10,7 @@ DB_PASS="${LOGANALYZER_DB_PASSWORD:-loganalyzer}" DB_ROOT_PASS="${MYSQL_ROOT_PASSWORD:-loganalyzer_root}" TABLE_PREFIX="${LOGANALYZER_TABLE_PREFIX:-logcon_}" ADMIN_USER="${LOGANALYZER_ADMIN_USER:-admin}" -ADMIN_PASS="${LOGANALYZER_ADMIN_PASSWORD:-pass}" +ADMIN_PASS="${LOGANALYZER_ADMIN_PASSWORD:-loganalyzer}" SAMPLE_LOG="${LOGANALYZER_SAMPLE_LOG:-/samplelogs/sampledata_syslog.log}" SAMPLE_EVENTREPORTER="${LOGANALYZER_SAMPLE_EVENTREPORTER:-/samplelogs/EventReporter.log}" @@ -28,9 +28,19 @@ export MYSQL_ROOT_PASSWORD="$DB_ROOT_PASS" export LOGANALYZER_DOCROOT="${LOGANALYZER_DOCROOT:-/var/www/html}" -if [ ! -f "${LOGANALYZER_CONFIG_PATH:-$LOGANALYZER_DOCROOT/config.php}" ] || [ "${LOGANALYZER_OVERWRITE_CONFIG:-0}" = "1" ]; then +DOC_CONFIG="${LOGANALYZER_DOCROOT}/config.php" +RESOLVED_CONFIG="${LOGANALYZER_CONFIG_PATH:-$DOC_CONFIG}" + +if [ ! -f "$RESOLVED_CONFIG" ] || [ "${LOGANALYZER_OVERWRITE_CONFIG:-0}" = "1" ]; then php /usr/local/share/loganalyzer/write-config.php - echo "Wrote config.php for LogAnalyzer (UserDB + disk allow /samplelogs)." + echo "Wrote config for LogAnalyzer (UserDB + disk allow /samplelogs)." +fi + +# When config lives outside the docroot (persisted volume), re-link each start — new containers lose the symlink. +if [ -n "${LOGANALYZER_CONFIG_PATH:-}" ] && [ "$LOGANALYZER_CONFIG_PATH" != "$DOC_CONFIG" ]; then + mkdir -p "$(dirname "$LOGANALYZER_CONFIG_PATH")" + rm -f "$DOC_CONFIG" + ln -sfn "$LOGANALYZER_CONFIG_PATH" "$DOC_CONFIG" fi echo "Waiting for MySQL at ${DB_HOST}:${DB_PORT}..." diff --git a/docker/env-disk-sources.php b/docker/env-disk-sources.php new file mode 100644 index 0000000..645a59d --- /dev/null +++ b/docker/env-disk-sources.php @@ -0,0 +1,107 @@ + + */ +function loganalyzer_disk_source_specs_from_env(): array +{ + $out = []; + $seen = []; + + $pathsEnv = getenv('LOGANALYZER_DISK_SOURCE_PATHS'); + if ($pathsEnv !== false && trim($pathsEnv) !== '') { + foreach (explode(',', $pathsEnv) as $raw) { + $p = trim($raw); + if ($p === '') { + continue; + } + $pNorm = str_replace('\\', '/', $p); + if (isset($seen[$pNorm])) { + continue; + } + $seen[$pNorm] = true; + $name = basename($pNorm); + if ($name === '' || $name === '/') { + $name = $pNorm; + } + $out[] = ['name' => $name, 'path' => $pNorm, 'kind' => 'syslog', 'description' => '']; + } + } + + $specEnv = getenv('LOGANALYZER_DISK_SOURCES'); + if ($specEnv !== false && trim($specEnv) !== '') { + foreach (explode(';;', $specEnv) as $segment) { + $segment = trim($segment); + if ($segment === '') { + continue; + } + // Limit splits so descriptions may contain '|' (e.g. Auth | PAM notes). + $parts = array_map('trim', explode('|', $segment, 4)); + if (count($parts) < 3) { + fwrite(STDERR, "Skipping LOGANALYZER_DISK_SOURCES record (need name|path|syslog|optional description): {$segment}\n"); + + continue; + } + $name = $parts[0]; + $path = $parts[1]; + $kind = strtolower($parts[2]); + $desc = $parts[3] ?? ''; + if ($name === '' || $path === '') { + continue; + } + if (!in_array($kind, ['syslog', 'event'], true)) { + fwrite(STDERR, "Skipping disk source '$name': LOGANALYZER_DISK_SOURCES kind must be 'syslog' or 'event' (got '{$parts[2]}').\n"); + + continue; + } + $pathNorm = str_replace('\\', '/', $path); + if (isset($seen[$pathNorm])) { + continue; + } + $seen[$pathNorm] = true; + $out[] = ['name' => $name, 'path' => $pathNorm, 'kind' => $kind, 'description' => $desc]; + } + } + + return $out; +} + +/** + * Paths LogAnalyzer PHP may scan for disk sources (trailing slashes). + * + * @return list + */ +function loganalyzer_disk_allowed_directories(): array +{ + $uniq = ['/var/log/' => true, '/samplelogs/' => true]; + + foreach (loganalyzer_disk_source_specs_from_env() as $s) { + $dir = dirname($s['path']); + $dir = str_replace('\\', '/', $dir); + if ($dir === '' || $dir === '.' || $dir === '/') { + continue; + } + $uniq[rtrim($dir, '/') . '/'] = true; + } + + $extra = getenv('LOGANALYZER_DISK_ALLOWED_EXTRA'); + if ($extra !== false && trim($extra) !== '') { + foreach (explode(',', $extra) as $raw) { + $dir = trim($raw); + if ($dir === '') { + continue; + } + $dir = str_replace('\\', '/', $dir); + $uniq[rtrim($dir, '/') . '/'] = true; + } + } + + return array_keys($uniq); +} diff --git a/docker/env.example b/docker/env.example new file mode 100644 index 0000000..7e76021 --- /dev/null +++ b/docker/env.example @@ -0,0 +1,33 @@ +# Copy to `.env` in the **repository root** (same folder as `README.md`) before +# `docker compose -f docker/docker-compose.yml up --build`. +# Never commit `.env`; it overrides the defaults documented in the handbook. + +# --- First admin account (applied on first seed only / password sync rules in seed-database.php) --- +LOGANALYZER_ADMIN_USER=admin +LOGANALYZER_ADMIN_PASSWORD=loganalyzer + +# --- MySQL (root used by seed; app user/pass used by LogAnalyzer UserDB connection) --- +MYSQL_ROOT_PASSWORD=loganalyzer_root +MYSQL_DATABASE=loganalyzer +MYSQL_USER=loganalyzer +MYSQL_PASSWORD=loganalyzer + +# --- Install mode --- +# Consumer default in compose is 0 (no demo disk sources). Set to 1/true to seed bundled sample logs. +LOGANALYZER_SEED_SAMPLE_SOURCES=0 + +# --- Automatic disk sources (first DB seed only) --- +# Paths are INSIDE THE WEB CONTAINER — bind-mount host directories with docker-compose +# (see commented volume in docker/docker-compose.yml under `web`). +# +# Quick list: comma-separated absolute files, syslog line type; display name = basename(path). +# Example (after mounting `- /var/log:/mnt/hostlog:ro` on the web service): +# LOGANALYZER_DISK_SOURCE_PATHS=/mnt/hostlog/syslog,/mnt/hostlog/messages +# +# Advanced: records separated by ;; . Each record: name|path|syslog|description OR name|path|event|description +# (four fields max; the description may contain '|' — only the first three '|' separate name / path / kind / rest.) +# syslog = RFC3164-ish text; event = EvntSLog / EventReporter file (winsyslog + eventlog parser). +# LOGANALYZER_DISK_SOURCES=Auth log|/mnt/hostlog/auth.log|syslog|PAM/ssh journal;;EVT|/mnt/winevt/report.log|event|Windows exporter +# +# Extra directories PHP may traverse (beyond auto-derived dirs from the paths above), comma-separated: +# LOGANALYZER_DISK_ALLOWED_EXTRA=/mnt/hostlog/,/srv/logs/ diff --git a/docker/init-env-interactive.ps1 b/docker/init-env-interactive.ps1 new file mode 100644 index 0000000..c05f14e --- /dev/null +++ b/docker/init-env-interactive.ps1 @@ -0,0 +1,62 @@ +# Requires: Windows PowerShell 5.1+ (uses -LiteralPath). +# Creates repo-root .env from docker/env.example with interactive prompts. +param( + [Parameter(Mandatory = $true)] + [string] $RepoRoot +) + +$RepoRoot = (Resolve-Path -LiteralPath $RepoRoot).Path +$Example = Join-Path $RepoRoot 'docker\env.example' +$Out = Join-Path $RepoRoot '.env' + +if (Test-Path -LiteralPath $Out) { + Write-Host ".env already exists: $Out" + exit 0 +} +if (-not (Test-Path -LiteralPath $Example)) { + Write-Error "Missing or unreadable: $Example" + exit 1 +} + +Write-Host '' +Write-Host '[.env missing] Press Enter at each prompt to keep the default from docker\env.example.' +Write-Host '' + +$list = New-Object System.Collections.Generic.List[string] + +Get-Content -LiteralPath $Example | ForEach-Object { + $line = $_ -replace "`r$", '' + if ($line -match '^\s*#' -or [string]::IsNullOrWhiteSpace($line)) { + $list.Add($line) | Out-Null + return + } + if ($line -match '^([A-Za-z_][A-Za-z0-9_]*)=(.*)$') { + $key = $matches[1] + $def = $matches[2] + if ($key -match 'PASSWORD') { + $prompt = "${key} (Enter keeps default '$def'): " + # -MaskInput requires PowerShell 7.1+ (not 7.0). + if ($PSVersionTable.PSVersion.Major -gt 7 -or ($PSVersionTable.PSVersion.Major -eq 7 -and $PSVersionTable.PSVersion.Minor -ge 1)) { + $val = Read-Host $prompt -MaskInput + } + else { + $val = Read-Host $prompt + } + } else { + $val = Read-Host "${key} [$def]: " + } + if ([string]::IsNullOrWhiteSpace($val)) { $val = $def } + $list.Add("${key}=${val}") | Out-Null + } else { + $list.Add($line) | Out-Null + } +} + +$content = ($list -join [Environment]::NewLine) + [Environment]::NewLine + +# UTF-8 without BOM (same as typical .env tools expect) +$utf8NoBom = New-Object System.Text.UTF8Encoding $false +[System.IO.File]::WriteAllText($Out, $content, $utf8NoBom) + +Write-Host '' +Write-Host "[.env written] $Out" diff --git a/docker/init-env-interactive.sh b/docker/init-env-interactive.sh new file mode 100644 index 0000000..e7f261a --- /dev/null +++ b/docker/init-env-interactive.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Create repo-root .env interactively using docker/env.example for defaults / comments. +set -euo pipefail + +REPO_ROOT="${1:-$(cd "$(dirname "$0")/.." && pwd)}" +EXAMPLE="$REPO_ROOT/docker/env.example" +OUT="$REPO_ROOT/.env" + +if [[ -f "$OUT" ]]; then + echo ".env already exists: $OUT" >&2 + exit 0 +fi +if [[ ! -r "$EXAMPLE" ]]; then + echo "Missing or unreadable: $EXAMPLE" >&2 + exit 1 +fi + +echo >&2 '' +echo >&2 '[.env missing] Press Enter at each prompt to keep the default from docker/env.example.' +echo >&2 '' + +if command -v mktemp >/dev/null 2>&1 && tmp="$(mktemp "${TMPDIR:-/tmp}/loganalyzer-env-init.XXXXXX" 2>/dev/null)"; then + : +else + tmp="${OUT}.tmp.$$.${RANDOM:-0}" +fi +trap 'rm -f "$tmp"' EXIT +: >"$tmp" + +# Read template from fd 3 so stdin stays the TTY (otherwise read would consume env.example lines). +while IFS= read -r -u 3 __line || [[ -n $__line ]]; do + line="${__line%$'\r'}" + if [[ $line =~ ^[[:space:]]*# ]] || [[ -z ${line//[[:space:]]/} ]]; then + printf '%s\n' "$line" >>"$tmp" + continue + fi + if [[ $line =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then + key="${BASH_REMATCH[1]}" + def="${BASH_REMATCH[2]}" + val="" + if [[ $key == *PASSWORD* ]]; then + read -rsp "${key} (Enter keeps default '${def}'): " val || true + echo >&2 + else + read -rp "${key} [${def}]: " val || true + fi + [[ -z $val ]] && val="$def" + printf '%s=%s\n' "$key" "$val" >>"$tmp" + else + printf '%s\n' "$line" >>"$tmp" + fi + +done 3<"$EXAMPLE" + +mv "$tmp" "$OUT" +trap - EXIT +echo >&2 '' +echo >&2 "[.env written] $OUT" diff --git a/docker/rebuild-consumer.bat b/docker/rebuild-consumer.bat new file mode 100644 index 0000000..e5a3c82 --- /dev/null +++ b/docker/rebuild-consumer.bat @@ -0,0 +1,17 @@ +@echo off +setlocal EnableExtensions +set "ROOT=%~dp0.." +cd /d "%ROOT%" + +if not exist "%ROOT%\.env" ( + echo. + echo [.env missing] Starting interactive setup ^(defaults from docker\env.example^)... + powershell -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\docker\init-env-interactive.ps1" -RepoRoot "%ROOT%" + if errorlevel 1 ( + echo ERROR: Interactive .env setup failed. + exit /b 1 + ) +) + +docker compose -f docker/docker-compose.yml down -v +docker compose -f docker/docker-compose.yml up --build diff --git a/docker/rebuild-consumer.sh b/docker/rebuild-consumer.sh new file mode 100644 index 0000000..b21f1a2 --- /dev/null +++ b/docker/rebuild-consumer.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env sh +# Consumer stack: prompts for .env when missing (see docker/init-env-interactive.sh), then clean rebuild + up. +set -e +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +if [ ! -f "$ROOT/.env" ]; then + echo >&2 '' + echo >&2 '[.env missing] Starting interactive setup (defaults from docker/env.example)...' + if command -v bash >/dev/null 2>&1; then + bash "$ROOT/docker/init-env-interactive.sh" "$ROOT" + else + echo >&2 'bash not found on PATH; copying docker/env.example → .env non-interactively. Install Git Bash or bash, or create .env manually.' >&2 + cp "$ROOT/docker/env.example" "$ROOT/.env" + fi +fi +docker compose -f docker/docker-compose.yml down -v +docker compose -f docker/docker-compose.yml up --build diff --git a/docker/rebuild-dev.bat b/docker/rebuild-dev.bat index bb34795..0d1a48b 100644 --- a/docker/rebuild-dev.bat +++ b/docker/rebuild-dev.bat @@ -1,7 +1,7 @@ @echo off -REM Wipe MySQL volume, rebuild images, and start the dev stack (foreground). Run from anywhere. -setlocal -cd /d "%~dp0\.." -docker compose -f docker/docker-compose.yml down -v -docker compose -f docker/docker-compose.yml up --build +setlocal EnableExtensions +set "ROOT=%~dp0.." +cd /d "%ROOT%" +docker compose -f docker/docker-compose.dev.yml down -v +docker compose -f docker/docker-compose.dev.yml up --build endlocal diff --git a/docker/rebuild-dev.sh b/docker/rebuild-dev.sh index 6fb3201..54e9f38 100644 --- a/docker/rebuild-dev.sh +++ b/docker/rebuild-dev.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh -# Wipe MySQL volume, rebuild images, and start the dev stack (foreground). Run from anywhere. +# Wipe MySQL volume, rebuild images, and start the dev stack (bind-mounted src). set -e ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" -docker compose -f docker/docker-compose.yml down -v -docker compose -f docker/docker-compose.yml up --build +docker compose -f docker/docker-compose.dev.yml down -v +docker compose -f docker/docker-compose.dev.yml up --build diff --git a/docker/seed-database.php b/docker/seed-database.php index f7f22d3..946d1c5 100644 --- a/docker/seed-database.php +++ b/docker/seed-database.php @@ -43,6 +43,15 @@ function run_statements(mysqli $m, array $stmts): void } } +/** Single statement after migrations; fail fast with stderr + exit(1). */ +function seed_exec(mysqli $m, string $sql): void +{ + if (!$m->query($sql)) { + fwrite(STDERR, "Seed query failed: {$m->error}\n---\n{$sql}\n"); + exit(1); + } +} + /** * Align seeded admin password with LOGANALYZER_ADMIN_PASSWORD (same md5 scheme as INSERT). * Persistent MySQL volumes skip full seed once database_installedversion is set; stale hashes cause login failures. @@ -90,10 +99,17 @@ function sync_admin_password_if_exists(mysqli $m, string $tableUsersQuoted, stri $docroot = getenv('LOGANALYZER_DOCROOT') ?: '/var/www/html'; $adminUser = getenv('LOGANALYZER_ADMIN_USER') ?: 'admin'; -$adminPass = getenv('LOGANALYZER_ADMIN_PASSWORD') ?: 'pass'; +$adminPass = getenv('LOGANALYZER_ADMIN_PASSWORD') ?: 'loganalyzer'; $sampleLog = getenv('LOGANALYZER_SAMPLE_LOG') ?: '/samplelogs/sampledata_syslog.log'; $sampleEventReporter = getenv('LOGANALYZER_SAMPLE_EVENTREPORTER') ?: '/samplelogs/EventReporter.log'; +$seedSampleSourcesEnv = getenv('LOGANALYZER_SEED_SAMPLE_SOURCES'); +// Unset or empty: seed demo disk sources (legacy default for dev/E2E). "0" / "false" / "no": clean install (schema + admin only). +$seedSampleSources = !($seedSampleSourcesEnv !== false && $seedSampleSourcesEnv !== '' && + in_array(strtolower(trim($seedSampleSourcesEnv)), ['0', 'false', 'no', 'off'], true)); + +require_once __DIR__ . '/env-disk-sources.php'; +$diskEnvSpecs = loganalyzer_disk_source_specs_from_env(); $tableConfig = '`' . $pref . 'config`'; $tableViews = '`' . $pref . 'views`'; $tableSources = '`' . $pref . 'sources`'; @@ -107,14 +123,25 @@ function sync_admin_password_if_exists(mysqli $m, string $tableUsersQuoted, stri $m->set_charset('utf8mb4'); $dbNameEsc = $m->real_escape_string($dbName); -$m->query("CREATE DATABASE IF NOT EXISTS `$dbNameEsc` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); -$m->select_db($dbName); +seed_exec($m, "CREATE DATABASE IF NOT EXISTS `$dbNameEsc` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); +if (!$m->select_db($dbName)) { + fwrite(STDERR, 'select_db failed: ' . $m->error . "\n"); + exit(1); +} $cfgTableBare = $pref . 'config'; $tst = $m->query("SHOW TABLES LIKE '" . $m->real_escape_string($cfgTableBare) . "'"); -if ($tst && $tst->num_rows > 0) { +if ($tst === false) { + fwrite(STDERR, 'SHOW TABLES failed: ' . $m->error . "\n"); + exit(1); +} +if ($tst->num_rows > 0) { $chk = $m->query('SELECT propvalue FROM ' . $tableConfig . " WHERE propname = 'database_installedversion' AND is_global = 1 LIMIT 1"); - if ($chk && ($row = $chk->fetch_assoc()) && strlen((string) ($row['propvalue'] ?? '')) > 0) { + if ($chk === false) { + fwrite(STDERR, 'SELECT database_installedversion failed: ' . $m->error . "\n"); + exit(1); + } + if (($row = $chk->fetch_assoc()) && strlen((string) ($row['propvalue'] ?? '')) > 0) { fwrite(STDOUT, "Database already seeded (database_installedversion=" . ($row['propvalue'] ?? '') . ").\n"); $syncResult = sync_admin_password_if_exists($m, $tableUsers, $adminUser, $adminPass); if ($syncResult > 0) { @@ -157,40 +184,93 @@ function sync_admin_password_if_exists(mysqli $m, string $tableUsersQuoted, stri } $version = '14'; -$m->query("INSERT INTO " . $tableConfig . " (propname, propvalue, is_global) VALUES ('database_installedversion', '" . $m->real_escape_string($version) . "', 1)"); +seed_exec($m, "INSERT INTO " . $tableConfig . " (propname, propvalue, is_global) VALUES ('database_installedversion', '" . $m->real_escape_string($version) . "', 1)"); $cols = 'timereported,syslogfacility,syslogseverity,FROMHOST,syslogtag,procid,IUT,msg'; -$m->query('INSERT INTO ' . $tableViews . " (DisplayName, Columns, userid, groupid) VALUES (" . +seed_exec($m, 'INSERT INTO ' . $tableViews . " (DisplayName, Columns, userid, groupid) VALUES (" . "'Syslog Fields', '" . $m->real_escape_string($cols) . "', NULL, NULL)"); $viewIdSyslog = (string) $m->insert_id; $colsEvt = 'timereported,FROMHOST,syslogseverity,NTEventLogType,sourceproc,id,user,msg'; -$m->query('INSERT INTO ' . $tableViews . " (DisplayName, Columns, userid, groupid) VALUES (" . +seed_exec($m, 'INSERT INTO ' . $tableViews . " (DisplayName, Columns, userid, groupid) VALUES (" . "'Event Log Fields', '" . $m->real_escape_string($colsEvt) . "', NULL, NULL)"); $viewIdEvent = (string) $m->insert_id; -$diskEsc = $m->real_escape_string($sampleLog); -$nameEsc = $m->real_escape_string('Docker sample (syslog)'); +$sourceId = ''; +$sourceIdEvent = ''; -$m->query('INSERT INTO ' . $tableSources . " (Name, Description, SourceType, MsgParserList, MsgNormalize, MsgSkipUnparseable, ViewID, LogLineType, DiskFile, userid, groupid) VALUES (" . - "'$nameEsc', '', 1, '', 0, 0, '" . $m->real_escape_string($viewIdSyslog) . "', 'syslog', '$diskEsc', NULL, NULL)"); +$firstSeededDiskSourceId = null; +foreach ($diskEnvSpecs as $spec) { + $nameEsc = $m->real_escape_string($spec['name']); + $pathEsc = $m->real_escape_string($spec['path']); + $descEsc = $m->real_escape_string($spec['description']); + if ($spec['kind'] === 'syslog') { + seed_exec( + $m, + 'INSERT INTO ' . $tableSources . + " (Name, Description, SourceType, MsgParserList, MsgNormalize, MsgSkipUnparseable, ViewID, LogLineType, DiskFile, userid, groupid) VALUES (" . + "'$nameEsc', '$descEsc', 1, '', 0, 0, '" . $m->real_escape_string($viewIdSyslog) . "', 'syslog', '$pathEsc', NULL, NULL)", + ); + } else { + $parserEsc = $m->real_escape_string('eventlog'); + seed_exec( + $m, + 'INSERT INTO ' . $tableSources . + " (Name, Description, SourceType, MsgParserList, MsgNormalize, MsgSkipUnparseable, ViewID, LogLineType, DiskFile, userid, groupid) VALUES (" . + "'$nameEsc', '$descEsc', 1, '$parserEsc', 0, 0, '" . $m->real_escape_string($viewIdEvent) . "', 'winsyslog', '$pathEsc', NULL, NULL)", + ); + } + $newId = (string) $m->insert_id; + if ($firstSeededDiskSourceId === null) { + $firstSeededDiskSourceId = $newId; + } + fwrite(STDOUT, "Seeded ENV disk source: ID $newId / {$spec['name']} → {$spec['path']}\n"); +} + +if ($seedSampleSources) { + $diskEsc = $m->real_escape_string($sampleLog); + $nameEsc = $m->real_escape_string('Docker sample (syslog)'); + + seed_exec($m, 'INSERT INTO ' . $tableSources . " (Name, Description, SourceType, MsgParserList, MsgNormalize, MsgSkipUnparseable, ViewID, LogLineType, DiskFile, userid, groupid) VALUES (" . + "'$nameEsc', '', 1, '', 0, 0, '" . $m->real_escape_string($viewIdSyslog) . "', 'syslog', '$diskEsc', NULL, NULL)"); -$sourceId = (string) $m->insert_id; + $sourceId = (string) $m->insert_id; -$diskEv = $m->real_escape_string($sampleEventReporter); -$nameEv = $m->real_escape_string('Docker sample (Windows EventReporter / EvntSLog)'); -$parserEv = $m->real_escape_string('eventlog'); + $diskEv = $m->real_escape_string($sampleEventReporter); + $nameEv = $m->real_escape_string('Docker sample (Windows EventReporter / EvntSLog)'); + $parserEv = $m->real_escape_string('eventlog'); -$m->query('INSERT INTO ' . $tableSources . " (Name, Description, SourceType, MsgParserList, MsgNormalize, MsgSkipUnparseable, ViewID, LogLineType, DiskFile, userid, groupid) VALUES (" . - "'$nameEv', 'Adiscon EventReporter-format file; LogLineType winsyslog + eventlog parser.', 1, '$parserEv', 0, 0, '" . $m->real_escape_string($viewIdEvent) . "', 'winsyslog', '$diskEv', NULL, NULL)"); + seed_exec($m, 'INSERT INTO ' . $tableSources . " (Name, Description, SourceType, MsgParserList, MsgNormalize, MsgSkipUnparseable, ViewID, LogLineType, DiskFile, userid, groupid) VALUES (" . + "'$nameEv', 'Adiscon EventReporter-format file; LogLineType winsyslog + eventlog parser.', 1, '$parserEv', 0, 0, '" . $m->real_escape_string($viewIdEvent) . "', 'winsyslog', '$diskEv', NULL, NULL)"); -$sourceIdEvent = (string) $m->insert_id; + $sourceIdEvent = (string) $m->insert_id; +} $hash = md5($adminPass); -$m->query('INSERT INTO ' . $tableUsers . " (username, password, is_admin, is_readonly) VALUES (" . +seed_exec($m, 'INSERT INTO ' . $tableUsers . " (username, password, is_admin, is_readonly) VALUES (" . "'" . $m->real_escape_string($adminUser) . "', '" . $m->real_escape_string($hash) . "', 1, 0)"); -$m->query("INSERT INTO " . $tableConfig . " (propname, propvalue, is_global) VALUES ('DefaultSourceID', '" . $m->real_escape_string($sourceId) . "', 1)"); -$m->query("INSERT INTO " . $tableConfig . " (propname, propvalue, is_global) VALUES ('DefaultViewsID', 'SYSLOG', 1)"); +$defaultSeedSourceId = null; +if ($firstSeededDiskSourceId !== null) { + $defaultSeedSourceId = $firstSeededDiskSourceId; +} elseif ($seedSampleSources && $sourceId !== '') { + $defaultSeedSourceId = $sourceId; +} -fwrite(STDOUT, "Seeded LogAnalyzer: admin user '$adminUser' / syslog source ID $sourceId / EventReporter source ID $sourceIdEvent\n"); +if ($defaultSeedSourceId !== null) { + seed_exec( + $m, + 'INSERT INTO ' . $tableConfig . " (propname, propvalue, is_global) VALUES ('DefaultSourceID', '" . $m->real_escape_string($defaultSeedSourceId) . "', 1)", + ); +} +seed_exec($m, "INSERT INTO " . $tableConfig . " (propname, propvalue, is_global) VALUES ('DefaultViewsID', 'SYSLOG', 1)"); + +if ($diskEnvSpecs !== [] && $seedSampleSources) { + fwrite(STDOUT, "Done: admin '$adminUser' / ENV disk sources: " . count($diskEnvSpecs) . " / demo syslog $sourceId / demo EventReporter $sourceIdEvent\n"); +} elseif ($diskEnvSpecs !== []) { + fwrite(STDOUT, "Done: admin '$adminUser' / ENV disk sources: " . count($diskEnvSpecs) . "\n"); +} elseif ($seedSampleSources) { + fwrite(STDOUT, "Done: admin '$adminUser' / demo syslog ID $sourceId / EventReporter ID $sourceIdEvent\n"); +} else { + fwrite(STDOUT, "Done: admin '$adminUser' — no disk sources (set LOGANALYZER_DISK_* in .env or add in Administration).\n"); +} diff --git a/docker/write-config.php b/docker/write-config.php index 20107de..d5736c9 100644 --- a/docker/write-config.php +++ b/docker/write-config.php @@ -2,13 +2,16 @@ declare(strict_types=1); /** - * Generate /var/www/html/config.php from config.sample.php for Docker / CI. - * Run inside the web container; requires mounted src at LOGANALYZER_DOCROOT. + * Generate config.php from config.sample.php for Docker / CI. + * Output path: LOGANALYZER_CONFIG_PATH or LOGANALYZER_DOCROOT/config.php. */ $docroot = getenv('LOGANALYZER_DOCROOT') ?: '/var/www/html'; $samplePath = $docroot . '/include/config.sample.php'; -$outPath = $docroot . '/config.php'; +$configPath = getenv('LOGANALYZER_CONFIG_PATH'); +$configPath = ($configPath !== false && $configPath !== '') + ? $configPath + : $docroot . '/config.php'; if (!is_readable($samplePath)) { fwrite(STDERR, "Cannot read: $samplePath\n"); @@ -24,6 +27,8 @@ $loginReq = getenv('LOGANALYZER_LOGIN_REQUIRED') !== '0' ? 'true' : 'false'; +require_once __DIR__ . '/env-disk-sources.php'; + $body = file_get_contents($samplePath); if ($body === false) { fwrite(STDERR, "Failed reading sample\n"); @@ -55,9 +60,17 @@ } $c = 0; -$body = str_replace('$CFG[\'DiskAllowed\'][] = "/var/log/"; ', '$CFG[\'DiskAllowed\'][] = "/var/log/"; ' . "\n" . '$CFG[\'DiskAllowed\'][] = "/samplelogs/"; ', $body, $c); +$needle = '$CFG[\'DiskAllowed\'][] = "/var/log/"; '; +$suffix = ''; +foreach (loganalyzer_disk_allowed_directories() as $dir) { + if (rtrim((string) $dir, '/') === '/var/log') { + continue; + } + $suffix .= "\n" . '$CFG[\'DiskAllowed\'][] = ' . var_export($dir, true) . '; '; +} +$body = str_replace($needle, $needle . $suffix, $body, $c); if ($c === 0) { - fwrite(STDERR, "Could not extend DiskAllowed\n"); + fwrite(STDERR, "Could not extend DiskAllowed (/var/log/ marker missing)\n"); exit(1); } @@ -70,9 +83,17 @@ exit(1); } -if (file_put_contents($outPath, $body) === false) { - fwrite(STDERR, "Cannot write $outPath\n"); +$parent = dirname($configPath); +if ($parent !== '' && $parent !== '.' && !is_dir($parent)) { + if (!@mkdir($parent, 0755, true) && !is_dir($parent)) { + fwrite(STDERR, "Cannot create directory: $parent\n"); + exit(1); + } +} + +if (file_put_contents($configPath, $body) === false) { + fwrite(STDERR, "Cannot write $configPath\n"); exit(1); } -fwrite(STDOUT, "Wrote $outPath\n"); +fwrite(STDOUT, "Wrote $configPath\n"); diff --git a/e2e/README.md b/e2e/README.md index b22dcf3..78b0d48 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -10,10 +10,10 @@ From repo root (requires Docker): docker compose -f docker/docker-compose.e2e.yml up --build --abort-on-container-exit --exit-code-from playwright ``` -## Interactive (against dev stack) +## Interactive (against dev stack — bind-mounted `src`) ```bash -docker compose -f docker/docker-compose.yml up -d +docker compose -f docker/docker-compose.dev.yml up -d cd e2e npm install ``` @@ -45,7 +45,7 @@ docker compose -f docker/docker-compose.e2e.yml up --build --abort-on-container- [`tests/handbook-screenshots.spec.ts`](tests/handbook-screenshots.spec.ts) writes **`doc-site/docs/assets/user-guide/*.png`** (the Playwright service mounts that directory at `HANDBOOK_USER_GUIDE_ASSETS`; see `docker/docker-compose.e2e.yml`). Commit updated PNGs together with doc changes. -**Local dev stack** (`docker-compose.yml`) with Playwright on the host: +**Local dev stack** (`docker-compose.dev.yml`) with Playwright on the host: ```bash cd e2e diff --git a/e2e/tests/handbook-screenshots.spec.ts b/e2e/tests/handbook-screenshots.spec.ts index 87ac0e5..0c68d41 100644 --- a/e2e/tests/handbook-screenshots.spec.ts +++ b/e2e/tests/handbook-screenshots.spec.ts @@ -11,7 +11,7 @@ const handbookAssets = : path.join(repoRoot, 'doc-site', 'docs', 'assets', 'user-guide'); const adminUser = process.env.LOGANALYZER_ADMIN_USER ?? 'admin'; -const adminPass = process.env.LOGANALYZER_ADMIN_PASSWORD ?? 'pass'; +const adminPass = process.env.LOGANALYZER_ADMIN_PASSWORD ?? 'loganalyzer'; /** After login submit, wait for redirect to main view (PHP may be slow in Docker CI; keep below test timeout). */ const LOGIN_REDIRECT_TIMEOUT_MS = 60_000; diff --git a/e2e/tests/main-smoke.spec.ts b/e2e/tests/main-smoke.spec.ts index 78938c2..696c7bb 100644 --- a/e2e/tests/main-smoke.spec.ts +++ b/e2e/tests/main-smoke.spec.ts @@ -5,7 +5,7 @@ import * as path from 'path'; const shots = path.join(__dirname, '..', 'test-results', 'screenshots'); const adminUser = process.env.LOGANALYZER_ADMIN_USER ?? 'admin'; -const adminPass = process.env.LOGANALYZER_ADMIN_PASSWORD ?? 'pass'; +const adminPass = process.env.LOGANALYZER_ADMIN_PASSWORD ?? 'loganalyzer'; /** After login submit, wait for redirect to main view (PHP may be slow in Docker CI; keep below test timeout). */ const LOGIN_REDIRECT_TIMEOUT_MS = 60_000; diff --git a/src/chartgenerator.php b/src/chartgenerator.php index 20bd30d..24b745e 100644 --- a/src/chartgenerator.php +++ b/src/chartgenerator.php @@ -149,7 +149,11 @@ // Get data and print on the image! if ( !$content['error_occured'] ) { - if ( isset($content['Sources'][$currentSourceID]) ) + if ( + isset($currentSourceID) + && isset($content['Sources'][$currentSourceID]['ObjRef']) + && is_object($content['Sources'][$currentSourceID]['ObjRef']) + ) { // Obtain and get the Config Object $stream_config = $content['Sources'][$currentSourceID]['ObjRef']; diff --git a/src/details.php b/src/details.php index d94c3dd..cfa3098 100644 --- a/src/details.php +++ b/src/details.php @@ -132,7 +132,11 @@ $content['EXPAND_HIGHLIGHT'] = "false"; // --- BEGIN Custom Code -if ( isset($content['Sources'][$currentSourceID]) ) // && $content['uid_current'] != UID_UNKNOWN ) // && $content['Sources'][$currentSourceID]['SourceType'] == SOURCE_DISK ) +if ( + isset($currentSourceID) + && isset($content['Sources'][$currentSourceID]['ObjRef']) + && is_object($content['Sources'][$currentSourceID]['ObjRef']) +) // && $content['uid_current'] != UID_UNKNOWN ) // && $content['Sources'][$currentSourceID]['SourceType'] == SOURCE_DISK ) { // Obtain and get the Config Object $stream_config = $content['Sources'][$currentSourceID]['ObjRef']; diff --git a/src/export.php b/src/export.php index 1b017bc..4c83466 100644 --- a/src/export.php +++ b/src/export.php @@ -133,7 +133,11 @@ // --- BEGIN Custom Code if ( !$content['error_occured'] ) { - if ( isset($content['Sources'][$currentSourceID]) ) + if ( + isset($currentSourceID) + && isset($content['Sources'][$currentSourceID]['ObjRef']) + && is_object($content['Sources'][$currentSourceID]['ObjRef']) + ) { // Obtain and get the Config Object $stream_config = $content['Sources'][$currentSourceID]['ObjRef']; diff --git a/src/include/functions_config.php b/src/include/functions_config.php index e01c508..7a7de9d 100644 --- a/src/include/functions_config.php +++ b/src/include/functions_config.php @@ -480,6 +480,31 @@ function InitReportModules($szRootPath = "") } +/* +* First configured source id with a usable ObjRef, or null. +*/ +function GetFirstOperationalSourceID(array $sources) +{ + foreach ($sources as $sid => $src) { + if (isset($src['ObjRef']) && is_object($src['ObjRef'])) { + return $sid; + } + } + + return null; +} + +/* +* Prepend SYSLOG_UID to the active view columns when debug mode requests it. +*/ +function AppendSyslogUidIfDebugEnabled(&$content, $viewId, $debugEnabled) +{ + if (!$debugEnabled || !isset($content['Views'][$viewId])) { + return; + } + array_unshift($content['Views'][$viewId]['Columns'], SYSLOG_UID); +} + /* * Init Source configs */ @@ -487,6 +512,8 @@ function InitSourceConfigs() { global $CFG, $content, $currentSourceID; + $miscShowDebugUid = (GetConfigSetting('MiscShowDebugMsg', 0, CFGLEVEL_USER) == 1); + // Init Source Configs! if ( isset($CFG['Sources']) ) { @@ -497,55 +524,82 @@ function InitSourceConfigs() } } + $currentSourceID = null; + // Read SourceID from GET Querystring - if ( isset($_GET['sourceid']) && isset($content['Sources'][$_GET['sourceid']]) ) - { + if (isset($_GET['sourceid'], $content['Sources'][$_GET['sourceid']]) + && isset($content['Sources'][$_GET['sourceid']]['ObjRef']) + && is_object($content['Sources'][$_GET['sourceid']]['ObjRef']) + ) { $currentSourceID = $_GET['sourceid']; $_SESSION['currentSourceID'] = $currentSourceID; - } - else - { - // Set Source from session if available! - if ( isset($_SESSION['currentSourceID']) && isset($content['Sources'][$_SESSION['currentSourceID']]) ) + } else { + if (isset($_SESSION['currentSourceID'])) { + if ( + !isset($content['Sources'][$_SESSION['currentSourceID']]['ObjRef']) + || !is_object($content['Sources'][$_SESSION['currentSourceID']]['ObjRef']) + ) { + unset($_SESSION['currentSourceID']); + } + } + if ( + isset($_SESSION['currentSourceID']) + && isset($content['Sources'][$_SESSION['currentSourceID']]['ObjRef']) + && is_object($content['Sources'][$_SESSION['currentSourceID']]['ObjRef']) + ) { $currentSourceID = $_SESSION['currentSourceID']; - else - { - $tmpVar = GetConfigSetting("DefaultSourceID", "", CFGLEVEL_USER); - if ( isset($content['Sources'][ $tmpVar ]) ) - { - // Set Source to preconfigured sourceID! + } else { + $currentSourceID = null; + $tmpVar = GetConfigSetting('DefaultSourceID', '', CFGLEVEL_USER); + if ( + $tmpVar !== '' + && isset($content['Sources'][$tmpVar]['ObjRef']) + && is_object($content['Sources'][$tmpVar]['ObjRef']) + ) { $_SESSION['currentSourceID'] = $tmpVar; $currentSourceID = $tmpVar; + } elseif (isset($content['Sources']) && is_array($content['Sources'])) { + $firstSid = GetFirstOperationalSourceID($content['Sources']); + if ($firstSid !== null) { + $currentSourceID = $firstSid; + $_SESSION['currentSourceID'] = $currentSourceID; + } } - else - // No Source stored in session, then to so now! - $_SESSION['currentSourceID'] = $currentSourceID; } } - - // Set for the selection box in the header - $content['Sources'][$currentSourceID]['selected'] = "selected"; - - // Set Description properties! - if ( isset($content['Sources'][$currentSourceID]['Description']) && strlen($content['Sources'][$currentSourceID]['Description']) > 0 ) - { - $content['SourceDescriptionEnabled'] = true; - $content['SourceDescription'] = $content['Sources'][$currentSourceID]['Description']; - } - - // --- Additional handling needed for the current view! global $currentViewID; - $currentViewID = $content['Sources'][$currentSourceID]['ViewID']; + if ( + isset($currentSourceID) + && isset($content['Sources'][$currentSourceID]['ObjRef']) + && is_object($content['Sources'][$currentSourceID]['ObjRef']) + ) { + $content['Sources'][$currentSourceID]['selected'] = 'selected'; + + if ( + isset($content['Sources'][$currentSourceID]['Description']) + && strlen((string) $content['Sources'][$currentSourceID]['Description']) > 0 + ) { + $content['SourceDescriptionEnabled'] = true; + $content['SourceDescription'] = $content['Sources'][$currentSourceID]['Description']; + } - // Set selected state for correct View, for selection box ^^ - $content['Views'][ $currentViewID ]['selected'] = "selected"; + $currentViewID = $content['Sources'][$currentSourceID]['ViewID']; + if (isset($content['Views'][$currentViewID])) { + $content['Views'][$currentViewID]['selected'] = 'selected'; + } + AppendSyslogUidIfDebugEnabled($content, $currentViewID, $miscShowDebugUid); - // If DEBUG Mode is enabled, we prepend the UID field into the col list! - - if ( GetConfigSetting("MiscShowDebugMsg", 0, CFGLEVEL_USER) == 1 && isset($content['Views'][$currentViewID]) ) - array_unshift( $content['Views'][$currentViewID]['Columns'], SYSLOG_UID); - // --- + return; + } + + $currentSourceID = null; + $fallbackViewId = GetConfigSetting('DefaultViewsID', 'SYSLOG', CFGLEVEL_USER); + $currentViewID = isset($content['Views'][$fallbackViewId]) ? $fallbackViewId : 'SYSLOG'; + if (isset($content['Views'][$currentViewID])) { + $content['Views'][$currentViewID]['selected'] = 'selected'; + } + AppendSyslogUidIfDebugEnabled($content, $currentViewID, $miscShowDebugUid); } /* diff --git a/src/index.php b/src/index.php index d182491..5989745 100644 --- a/src/index.php +++ b/src/index.php @@ -225,7 +225,11 @@ // Do not BLOCK other Site Calls WriteClosePHPSession(); -if ( isset($content['Sources'][$currentSourceID]) ) +if ( + isset($currentSourceID) + && isset($content['Sources'][$currentSourceID]['ObjRef']) + && is_object($content['Sources'][$currentSourceID]['ObjRef']) +) { // Obtain and get the Config Object $stream_config = $content['Sources'][$currentSourceID]['ObjRef'];