From 2307a1dd7f174918fa90c259d45b5fcf04737c11 Mon Sep 17 00:00:00 2001 From: Shyim Date: Mon, 10 Aug 2026 11:34:22 +0000 Subject: [PATCH 1/3] feat(log-viewer): add SIMD Monolog parser --- .gitignore | 9 + README.md | 2 +- composer.json | 6 +- native/lib/README.md | 24 + .../lib/libmonolog_parser-darwin-arm64.dylib | Bin 0 -> 53896 bytes .../lib/libmonolog_parser-darwin-x86_64.dylib | Bin 0 -> 24595 bytes native/lib/libmonolog_parser-linux-arm64.so | Bin 0 -> 52864 bytes .../lib/libmonolog_parser-linux-musl-arm64.so | Bin 0 -> 51384 bytes .../libmonolog_parser-linux-musl-x86_64.so | Bin 0 -> 46816 bytes native/lib/libmonolog_parser-linux-x86_64.so | Bin 0 -> 48176 bytes native/lib/monolog_parser.h | 184 ++++++ native/monolog_parser/Makefile | 129 ++++ native/monolog_parser/README.md | 49 ++ native/monolog_parser/bench/bench.c | 104 ++++ native/monolog_parser/build-zig.sh | 91 +++ .../monolog_parser/include/monolog_parser.h | 184 ++++++ native/monolog_parser/src/monolog_parser.c | 257 ++++++++ native/monolog_parser/src/monolog_reader.c | 584 ++++++++++++++++++ native/monolog_parser/src/simd_scan.c | 176 ++++++ native/monolog_parser/tests/test_parser.c | 320 ++++++++++ src/Components/Log/FfiMonologLogReader.php | 196 ++++++ src/Components/Log/FfiMonologParser.php | 121 ++++ .../Log/MonologLineParserFactory.php | 40 ++ .../Log/MonologLineParserInterface.php | 23 + .../Log/MonologLogReaderFactory.php | 40 ++ .../Log/MonologLogReaderInterface.php | 35 ++ src/Components/Log/NativeLibraryLocator.php | 239 +++++++ src/Components/Log/PregMonologLogReader.php | 49 ++ src/Components/Log/PregMonologParser.php | 40 ++ src/Controller/LogController.php | 42 +- src/Resources/config/services.php | 10 + tests/Unit/Log/PregMonologParserTest.php | 69 +++ 32 files changed, 2988 insertions(+), 35 deletions(-) create mode 100644 native/lib/README.md create mode 100755 native/lib/libmonolog_parser-darwin-arm64.dylib create mode 100755 native/lib/libmonolog_parser-darwin-x86_64.dylib create mode 100755 native/lib/libmonolog_parser-linux-arm64.so create mode 100755 native/lib/libmonolog_parser-linux-musl-arm64.so create mode 100755 native/lib/libmonolog_parser-linux-musl-x86_64.so create mode 100755 native/lib/libmonolog_parser-linux-x86_64.so create mode 100644 native/lib/monolog_parser.h create mode 100644 native/monolog_parser/Makefile create mode 100644 native/monolog_parser/README.md create mode 100644 native/monolog_parser/bench/bench.c create mode 100644 native/monolog_parser/build-zig.sh create mode 100644 native/monolog_parser/include/monolog_parser.h create mode 100644 native/monolog_parser/src/monolog_parser.c create mode 100644 native/monolog_parser/src/monolog_reader.c create mode 100644 native/monolog_parser/src/simd_scan.c create mode 100644 native/monolog_parser/tests/test_parser.c create mode 100644 src/Components/Log/FfiMonologLogReader.php create mode 100644 src/Components/Log/FfiMonologParser.php create mode 100644 src/Components/Log/MonologLineParserFactory.php create mode 100644 src/Components/Log/MonologLineParserInterface.php create mode 100644 src/Components/Log/MonologLogReaderFactory.php create mode 100644 src/Components/Log/MonologLogReaderInterface.php create mode 100644 src/Components/Log/NativeLibraryLocator.php create mode 100644 src/Components/Log/PregMonologLogReader.php create mode 100644 src/Components/Log/PregMonologParser.php create mode 100644 tests/Unit/Log/PregMonologParserTest.php diff --git a/.gitignore b/.gitignore index f65cb550..109e4dba 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,12 @@ /src/Resources/public/ /src/Resources/app/administration/node_modules/ /src/Resources/app/administration/.tmp/ +# monolog_parser local build only — keep packaged native/lib/*-linux-*.so / *-darwin-*.dylib +/native/monolog_parser/src/*.o +/native/monolog_parser/tests/test_parser +/native/monolog_parser/bench/bench +/native/monolog_parser/libmonolog_parser.so +/native/monolog_parser/libmonolog_parser.dylib +# generic alias from `make install` (optional local convenience) +/native/lib/libmonolog_parser.so +/native/lib/libmonolog_parser.dylib diff --git a/README.md b/README.md index e9c4450c..d8f7a440 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Dedicated list/detail UI for Shopware webhooks (including inline search). | **Queue** | Multi-transport support (Doctrine, Redis, AMQP, fallback); browse without consuming (or fetch+requeue); retry failed messages; purge transport; reset queue. | | **Scheduled Tasks** | List, run, deactivate; edit interval & next execution; register tasks. | | **State Machines** | Diagram viewer for any registered state machine. | -| **Log Viewer** | Read `var/log/*.log` in the browser. | +| **Log Viewer** | Read `var/log/*.log` in the browser. Optional SIMD C parser via PHP FFI (`native/monolog_parser`). | | **Feature Flags** | Inspect and toggle flags from core/plugins. | | **Elasticsearch** | Status, indices, reindex, alias switch, unused/orphaned cleanup, console. Optional `show_all_indices`. | | **Fastly** | Purge all / by URL and basic stats when Fastly is configured. | diff --git a/composer.json b/composer.json index 2df86e88..04aeea66 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,9 @@ "require-dev": { "shopware/elasticsearch": "~6.6.0 || ~6.7.0" }, + "suggest": { + "ext-ffi": "Optional: load native/lib/libmonolog_parser.so for SIMD-accelerated Monolog log parsing" + }, "config": { "allow-plugins": { "symfony/runtime": true @@ -54,6 +57,7 @@ "scripts": { "format": "docker run --rm -v $(pwd):/ext shopware/shopware-cli:latest extension format /ext", "check": "docker run --rm -v $(pwd):/ext shopware/shopware-cli:latest extension validate --full /ext", - "phpunit": "../../../vendor/bin/phpunit -c phpunit.xml" + "phpunit": "../../../vendor/bin/phpunit -c phpunit.xml", + "build-native": "make -C native/monolog_parser install" } } diff --git a/native/lib/README.md b/native/lib/README.md new file mode 100644 index 00000000..8f94504d --- /dev/null +++ b/native/lib/README.md @@ -0,0 +1,24 @@ +# Prebuilt `libmonolog_parser` (Zig cross-compile) + +| File | Platform | libc | +| --- | --- | --- | +| `libmonolog_parser-linux-x86_64.so` | Linux amd64 | glibc | +| `libmonolog_parser-linux-arm64.so` | Linux arm64 | glibc | +| `libmonolog_parser-linux-musl-x86_64.so` | Linux amd64 | musl | +| `libmonolog_parser-linux-musl-arm64.so` | Linux arm64 | musl | +| `libmonolog_parser-darwin-x86_64.dylib` | macOS Intel | — | +| `libmonolog_parser-darwin-arm64.dylib` | macOS Apple Silicon | — | + +## Rebuild all (one command) + +```bash +cd ../monolog_parser +make zig-release # needs zig ≥ 0.13 +# or: make zig-release ZIG=/tmp/zig-linux-x86_64-0.13.0/zig +``` + +Produced here with Zig 0.13 from a single Linux host — no Alpine/macOS runners required. + +## Runtime + +PHP `NativeLibraryLocator` auto-selects; users need `ext-ffi` only (optional). diff --git a/native/lib/libmonolog_parser-darwin-arm64.dylib b/native/lib/libmonolog_parser-darwin-arm64.dylib new file mode 100755 index 0000000000000000000000000000000000000000..f337409b7e6f4b49a81b3b94b0bf8b256f258f01 GIT binary patch literal 53896 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 F{r|iP)ms1n literal 0 HcmV?d00001 diff --git a/native/lib/libmonolog_parser-darwin-x86_64.dylib b/native/lib/libmonolog_parser-darwin-x86_64.dylib new file mode 100755 index 0000000000000000000000000000000000000000..b75f0ccecb5ea5a0e61bf21c3ca22695bd893deb GIT binary patch literal 24595 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 literal 0 HcmV?d00001 diff --git a/native/lib/libmonolog_parser-linux-arm64.so b/native/lib/libmonolog_parser-linux-arm64.so new file mode 100755 index 0000000000000000000000000000000000000000..d016ce33742cebd7d6e36441afd4d5b60dd269bd GIT binary patch literal 52864 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 NPfY#h5a&Ym{~z-H@ZA6a literal 0 HcmV?d00001 diff --git a/native/lib/libmonolog_parser-linux-musl-arm64.so b/native/lib/libmonolog_parser-linux-musl-arm64.so new file mode 100755 index 0000000000000000000000000000000000000000..797a0eba90024dd4222f90a30c28343900d09aa9 GIT binary patch literal 51384 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 XM8M(g9|dhB`MC#7!Im)N!sY)D=a-__ literal 0 HcmV?d00001 diff --git a/native/lib/libmonolog_parser-linux-musl-x86_64.so b/native/lib/libmonolog_parser-linux-musl-x86_64.so new file mode 100755 index 0000000000000000000000000000000000000000..428dc7e428457af3d67282b9e3cbb205d9ac825d GIT binary patch literal 46816 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 literal 0 HcmV?d00001 diff --git a/native/lib/libmonolog_parser-linux-x86_64.so b/native/lib/libmonolog_parser-linux-x86_64.so new file mode 100755 index 0000000000000000000000000000000000000000..05c98b3a597077d97c1f7afcd8d446731db4bc2e GIT binary patch literal 48176 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 literal 0 HcmV?d00001 diff --git a/native/lib/monolog_parser.h b/native/lib/monolog_parser.h new file mode 100644 index 00000000..18024ae6 --- /dev/null +++ b/native/lib/monolog_parser.h @@ -0,0 +1,184 @@ +/** + * monolog_parser — SIMD-accelerated Monolog line parser for PHP FFI. + * + * Matches FroshTools LogController LINE_MATCH: + * /\[(?.*)] (?.*)\.(?DEBUG|INFO|…):(?.*)/m + * + * Two usage modes for PHP: + * 1) Parse a line/buffer you already hold (monolog_parse_line / _buffer) + * 2) Open the log file in C and yield/page parsed entries + * monolog_reader_open_backwards → monolog_reader_next (iterator) + * monolog_file_read_backwards (one-shot page) + */ +#ifndef MONOLOG_PARSER_H +#define MONOLOG_PARSER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(_WIN32) || defined(__CYGWIN__) +# ifdef MONOLOG_PARSER_BUILD +# define MONOLOG_API __declspec(dllexport) +# else +# define MONOLOG_API __declspec(dllimport) +# endif +#else +# define MONOLOG_API __attribute__((visibility("default"))) +#endif + +/** Parsed field offsets relative to the start of the input string. */ +typedef struct monolog_fields { + int32_t date_off; + int32_t date_len; + int32_t channel_off; + int32_t channel_len; + int32_t level_off; + int32_t level_len; + int32_t message_off; + int32_t message_len; + /** 1 if the Monolog pattern matched, 0 otherwise. */ + int32_t matched; +} monolog_fields_t; + +/** + * One line within a multi-line buffer (offsets relative to buffer start). + * line_len excludes a trailing '\\n' / '\\r\\n'. + */ +typedef struct monolog_line_span { + int32_t off; + int32_t len; +} monolog_line_span_t; + +/** Library version string (static storage). */ +MONOLOG_API const char *monolog_parser_version(void); + +/** + * Which SIMD path is active: "sse2", "neon", or "scalar". + */ +MONOLOG_API const char *monolog_parser_simd(void); + +/** + * Parse a single Monolog line (need not be NUL-terminated). + * Returns 1 on match, 0 on no match. out must be non-NULL. + */ +MONOLOG_API int monolog_parse_line(const char *line, size_t len, monolog_fields_t *out); + +/** + * Find newline byte offsets in [buf, buf+len) using SIMD scan. + * Writes up to max_offsets offsets into offsets[]; returns count found + * (may be > max_offsets if truncated — check return vs max). + * If offsets is NULL, only counts. + */ +MONOLOG_API size_t monolog_find_newlines(const char *buf, size_t len, + size_t *offsets, size_t max_offsets); + +/** + * Split buffer into line spans (no trailing CR/LF). Handles \\n and \\r\\n. + * Returns number of lines written (capped by max_lines). + */ +MONOLOG_API size_t monolog_split_lines(const char *buf, size_t len, + monolog_line_span_t *lines, size_t max_lines); + +/** + * Parse every line in a buffer. entries[i] fields are relative to buf. + * Unmatched lines still get an entry with matched=0; for those the whole + * line is reported as message (message_off/len set, others 0). + * Returns number of lines processed (capped by max_entries). + */ +MONOLOG_API size_t monolog_parse_buffer(const char *buf, size_t len, + monolog_fields_t *entries, size_t max_entries); + +/** + * SIMD memchr — find first occurrence of byte c in [buf, buf+len). + * Returns pointer to the byte, or NULL. + */ +MONOLOG_API const char *monolog_memchr(const char *buf, int c, size_t len); + +/** + * Find last occurrence of byte c in [buf, buf+len). + * Returns pointer to the byte, or NULL. + */ +MONOLOG_API const char *monolog_memrchr(const char *buf, int c, size_t len); + +/* ===================================================================== + * File reader — open in C, yield parsed lines (newest first) + * ===================================================================== */ + +typedef struct monolog_reader monolog_reader_t; + +/** + * Open path for reverse iteration (newest log line first). + * Returns NULL on open failure. + */ +MONOLOG_API monolog_reader_t *monolog_reader_open_backwards(const char *path); + +/** Close reader and free all buffers. Safe on NULL. */ +MONOLOG_API void monolog_reader_close(monolog_reader_t *r); + +/** Total line count (SIMD newline scan on open). */ +MONOLOG_API uint64_t monolog_reader_total_lines(const monolog_reader_t *r); + +/** Last error message, or NULL. */ +MONOLOG_API const char *monolog_reader_error(const monolog_reader_t *r); + +/** + * Yield next line (newest-first), parse it. + * + * Returns: + * 1 — entry written; *line_out points at NUL-terminated line + * (valid until next next()/close). fields offsets relative to *line_out. + * 0 — EOF + * -1 — error (see monolog_reader_error) + * + * Unmatched lines: fields->matched == 0, message = full line. + */ +MONOLOG_API int monolog_reader_next(monolog_reader_t *r, + monolog_fields_t *out, + const char **line_out); + +/** + * Discard the next n lines (newest-first). Returns 1 ok, 0 hit EOF early, -1 error. + */ +MONOLOG_API int monolog_reader_skip(monolog_reader_t *r, uint64_t n); + +/* ===================================================================== + * One-shot page — preferred for LogController offset/limit + * ===================================================================== */ + +/** Owned strings; free via monolog_page_free only. */ +typedef struct monolog_page_entry { + int32_t matched; + const char *date; + const char *channel; + const char *level; + const char *message; +} monolog_page_entry_t; + +typedef struct monolog_page monolog_page_t; + +/** + * Read [offset, offset+limit) lines from end of file (offset 0 = newest), + * parse each. On success *out is allocated; free with monolog_page_free. + * Returns 0 on success, -1 on error. + */ +MONOLOG_API int monolog_file_read_backwards(const char *path, + uint64_t offset, + uint64_t limit, + monolog_page_t **out); + +MONOLOG_API void monolog_page_free(monolog_page_t *page); + +MONOLOG_API uint64_t monolog_page_total_lines(const monolog_page_t *page); +MONOLOG_API uint32_t monolog_page_count(const monolog_page_t *page); +MONOLOG_API const monolog_page_entry_t *monolog_page_entry(const monolog_page_t *page, + uint32_t index); + +#ifdef __cplusplus +} +#endif + +#endif /* MONOLOG_PARSER_H */ diff --git a/native/monolog_parser/Makefile b/native/monolog_parser/Makefile new file mode 100644 index 00000000..351ba64d --- /dev/null +++ b/native/monolog_parser/Makefile @@ -0,0 +1,129 @@ +# monolog_parser — SIMD Monolog library +# +# Native (host toolchain): +# make / make test / make package +# +# Cross-compile ALL release targets with Zig (preferred for CI + local): +# make zig-release # needs `zig` on PATH or ZIG=/path/to/zig +# make zig-release ZIG=... +# +# Artifacts → ../lib/ +# libmonolog_parser-linux-x86_64.so +# libmonolog_parser-linux-arm64.so +# libmonolog_parser-linux-musl-x86_64.so +# libmonolog_parser-linux-musl-arm64.so +# libmonolog_parser-darwin-x86_64.dylib +# libmonolog_parser-darwin-arm64.dylib + +CC ?= cc +CFLAGS ?= -O3 -fPIC -Wall -Wextra -Wpedantic -std=c11 +CFLAGS += -Iinclude +ZIG ?= zig + +UNAME_S := $(shell uname -s) +UNAME_M := $(shell uname -m) + +ARCH_RAW := $(UNAME_M) +ifeq ($(ARCH_RAW),amd64) + ARCH := x86_64 +else ifeq ($(ARCH_RAW),x86_64) + ARCH := x86_64 +else ifeq ($(ARCH_RAW),aarch64) + ARCH := arm64 +else ifeq ($(ARCH_RAW),arm64) + ARCH := arm64 +else + ARCH := $(ARCH_RAW) +endif + +ifndef LIBC + ifneq ($(wildcard /lib/ld-musl-*.so.1),) + LIBC := musl + else + LIBC := gnu + endif +endif + +ifeq ($(UNAME_S),Darwin) + OS := darwin + LIBEXT := dylib + LIBC := n/a + LDFLAGS += -dynamiclib -install_name @rpath/libmonolog_parser.$(LIBEXT) + CFLAGS += -mmacosx-version-min=11.0 + TEST_RPATH := -Wl,-rpath,@loader_path/.. + LIB_PACKAGED := libmonolog_parser-darwin-$(ARCH).$(LIBEXT) +else + OS := linux + LIBEXT := so + LDFLAGS += -shared + TEST_RPATH := -Wl,-rpath,'$$ORIGIN/..' + ifeq ($(LIBC),musl) + LIB_PACKAGED := libmonolog_parser-linux-musl-$(ARCH).$(LIBEXT) + else + LIB_PACKAGED := libmonolog_parser-linux-$(ARCH).$(LIBEXT) + endif +endif + +ifeq ($(ARCH),arm64) + ifneq ($(UNAME_S),Darwin) + CFLAGS += -march=armv8-a+simd + endif +endif + +SRC := src/simd_scan.c src/monolog_parser.c src/monolog_reader.c +OBJ := $(SRC:.c=.o) + +LIB_LOCAL := libmonolog_parser.$(LIBEXT) +INSTALL_DIR ?= ../lib + +.PHONY: all clean test bench install package shared info zig-release zig-one + +all: shared + +info: + @echo "os=$(OS) arch=$(ARCH) libc=$(LIBC) ext=$(LIBEXT)" + @echo "local=$(LIB_LOCAL) packaged=$(LIB_PACKAGED)" + @echo "zig=$(ZIG)" + +shared: $(LIB_LOCAL) + +$(LIB_LOCAL): $(OBJ) + $(CC) $(LDFLAGS) -o $@ $^ + @echo "built $@" + +src/%.o: src/%.c include/monolog_parser.h + $(CC) $(CFLAGS) -DMONOLOG_PARSER_BUILD -c -o $@ $< + +tests/test_parser: tests/test_parser.c $(LIB_LOCAL) + $(CC) $(CFLAGS) -o $@ tests/test_parser.c -L. -lmonolog_parser $(TEST_RPATH) + +bench/bench: bench/bench.c $(LIB_LOCAL) + $(CC) $(CFLAGS) -o $@ bench/bench.c -L. -lmonolog_parser $(TEST_RPATH) + +test: tests/test_parser + LD_LIBRARY_PATH="$(CURDIR):$$LD_LIBRARY_PATH" ./tests/test_parser + +bench: bench/bench + LD_LIBRARY_PATH="$(CURDIR):$$LD_LIBRARY_PATH" ./bench/bench 100000 + +package: $(LIB_LOCAL) + mkdir -p $(INSTALL_DIR) + cp -f $(LIB_LOCAL) $(INSTALL_DIR)/$(LIB_PACKAGED) + cp -f include/monolog_parser.h $(INSTALL_DIR)/ + @echo "packaged $(INSTALL_DIR)/$(LIB_PACKAGED) [libc=$(LIBC)]" + +install: package + cp -f $(LIB_LOCAL) $(INSTALL_DIR)/$(LIB_LOCAL) + +# ---- Zig cross-compile (all platforms from one machine) ------------------ + +zig-release: + ZIG="$(ZIG)" OUT="$(abspath $(INSTALL_DIR))" sh ./build-zig.sh + +# make zig-one TARGET=linux-musl-arm64 +zig-one: + @test -n "$(TARGET)" || { echo "usage: make zig-one TARGET=linux-musl-arm64"; exit 1; } + ZIG="$(ZIG)" OUT="$(abspath $(INSTALL_DIR))" sh ./build-zig.sh $(TARGET) + +clean: + rm -f $(OBJ) $(LIB_LOCAL) tests/test_parser bench/bench diff --git a/native/monolog_parser/README.md b/native/monolog_parser/README.md new file mode 100644 index 00000000..b5115c55 --- /dev/null +++ b/native/monolog_parser/README.md @@ -0,0 +1,49 @@ +# monolog_parser + +SIMD Monolog file reader for FroshTools — **prebuilt** shared libs for PHP FFI. +**End users never compile.** + +## Cross-compile everything with Zig (recommended) + +One machine, six artifacts: + +```bash +# zig on PATH, or: make zig-release ZIG=/path/to/zig +make zig-release + +# single target: +make zig-one TARGET=linux-musl-arm64 +``` + +`build-zig.sh` maps: + +| Artifact | Zig target | +| --- | --- | +| `libmonolog_parser-linux-x86_64.so` | `x86_64-linux-gnu` | +| `libmonolog_parser-linux-arm64.so` | `aarch64-linux-gnu` | +| `libmonolog_parser-linux-musl-x86_64.so` | `x86_64-linux-musl` | +| `libmonolog_parser-linux-musl-arm64.so` | `aarch64-linux-musl` | +| `libmonolog_parser-darwin-x86_64.dylib` | `x86_64-macos-none` | +| `libmonolog_parser-darwin-arm64.dylib` | `aarch64-macos-none` | + +CI (`.github/workflows/native-monolog-parser.yml`) installs Zig 0.13 and runs `make zig-release` on a single `ubuntu-22.04` runner. + +## Runtime selection + +`NativeLibraryLocator` picks OS + arch + musl/glibc automatically. +Override: `MONOLOG_PARSER_LIB`, `MONOLOG_PARSER_LIBC=musl|gnu`. + +## Host-only build (optional) + +```bash +make test package # current OS/arch only +``` + +## API + +```c +monolog_file_read_backwards(path, offset, limit, &page); +// iterator: monolog_reader_open_backwards → next → close +``` + +See `include/monolog_parser.h`. diff --git a/native/monolog_parser/bench/bench.c b/native/monolog_parser/bench/bench.c new file mode 100644 index 00000000..1ad8982c --- /dev/null +++ b/native/monolog_parser/bench/bench.c @@ -0,0 +1,104 @@ +/** + * Micro-benchmark: SIMD monolog_parse_buffer throughput. + */ +#define _POSIX_C_SOURCE 199309L + +#include "monolog_parser.h" + +#include +#include +#include +#include + +static double now_sec(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +static char *build_corpus(size_t lines, size_t *out_len) +{ + static const char *tpl = + "[2024-03-15T10:30:00.%06zu+00:00] request.ERROR: Uncaught PHP Exception " + "RuntimeException: \"Something broke at line %zu\" at /var/www/html/src/Foo.php " + "line 42 {\"exception\":[]} []\n"; + size_t cap = lines * 256; + char *buf = malloc(cap); + if (!buf) { + return NULL; + } + size_t used = 0; + for (size_t i = 0; i < lines; i++) { + char line[320]; + int n = snprintf(line, sizeof(line), tpl, i % 1000000, i); + if (n < 0) { + free(buf); + return NULL; + } + if (used + (size_t)n > cap) { + cap *= 2; + char *nb = realloc(buf, cap); + if (!nb) { + free(buf); + return NULL; + } + buf = nb; + } + memcpy(buf + used, line, (size_t)n); + used += (size_t)n; + } + *out_len = used; + return buf; +} + +int main(int argc, char **argv) +{ + size_t lines = 100000; + if (argc > 1) { + lines = (size_t)strtoull(argv[1], NULL, 10); + } + + size_t len = 0; + char *buf = build_corpus(lines, &len); + if (!buf) { + fprintf(stderr, "alloc failed\n"); + return 1; + } + + monolog_fields_t *entries = calloc(lines, sizeof(*entries)); + if (!entries) { + free(buf); + return 1; + } + + printf("corpus: %zu lines, %zu bytes, simd=%s, version=%s\n", + lines, len, monolog_parser_simd(), monolog_parser_version()); + + /* warmup */ + (void)monolog_parse_buffer(buf, len, entries, lines); + + const int rounds = 20; + double t0 = now_sec(); + size_t parsed = 0; + for (int r = 0; r < rounds; r++) { + parsed = monolog_parse_buffer(buf, len, entries, lines); + } + double t1 = now_sec(); + double elapsed = t1 - t0; + double lines_per_sec = ((double)lines * (double)rounds) / elapsed; + double mb_per_sec = ((double)len * (double)rounds / (1024.0 * 1024.0)) / elapsed; + + size_t matched = 0; + for (size_t i = 0; i < parsed; i++) { + matched += (size_t)entries[i].matched; + } + + printf("parsed=%zu matched=%zu rounds=%d time=%.4fs\n", parsed, matched, rounds, elapsed); + printf("throughput: %.2f M lines/s | %.2f MiB/s\n", + lines_per_sec / 1e6, mb_per_sec); + + free(entries); + free(buf); + return 0; +} diff --git a/native/monolog_parser/build-zig.sh b/native/monolog_parser/build-zig.sh new file mode 100644 index 00000000..414c6210 --- /dev/null +++ b/native/monolog_parser/build-zig.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env sh +# Cross-compile libmonolog_parser for all release platforms with Zig. +# +# Usage: +# ./build-zig.sh # all targets → ../lib/ +# ./build-zig.sh linux-musl-arm64 +# ZIG=/path/to/zig ./build-zig.sh +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname "$0")" && pwd) +OUT="${OUT:-$ROOT/../lib}" +SRC="$ROOT/src/simd_scan.c $ROOT/src/monolog_parser.c $ROOT/src/monolog_reader.c" +INC="-I$ROOT/include" +# Keep flags portable across zig targets (no host -march) +CFLAGS_COMMON="-O3 -fPIC -std=c11 -Wall -Wextra -DMONOLOG_PARSER_BUILD" + +ZIG="${ZIG:-zig}" +if ! command -v "$ZIG" >/dev/null 2>&1; then + for c in \ + "$ROOT/../../.tools/zig/zig" \ + /tmp/zig-linux-x86_64-*/zig \ + /opt/zig/zig + do + for hit in $c; do + if [ -x "$hit" ]; then + ZIG=$hit + break 2 + fi + done + done +fi + +if ! command -v "$ZIG" >/dev/null 2>&1 && [ ! -x "$ZIG" ]; then + echo "zig not found — install from https://ziglang.org or set ZIG=" >&2 + exit 1 +fi + +echo "using: $ZIG ($("$ZIG" version 2>/dev/null || true))" +mkdir -p "$OUT" + +# name|zig-target|ext +# aarch64 NEON is baseline — no extra -march needed (zig rejects gcc-style -march) +TARGETS=" +linux-x86_64|x86_64-linux-gnu|so +linux-arm64|aarch64-linux-gnu|so +linux-musl-x86_64|x86_64-linux-musl|so +linux-musl-arm64|aarch64-linux-musl|so +darwin-x86_64|x86_64-macos-none|dylib +darwin-arm64|aarch64-macos-none|dylib +" + +build_one() { + name=$1 + target=$2 + ext=$3 + out="$OUT/libmonolog_parser-${name}.${ext}" + + echo "→ $name ($target) → $out" + + # shellcheck disable=SC2086 + "$ZIG" cc \ + -target "$target" \ + -shared \ + $CFLAGS_COMMON \ + $INC \ + -o "$out" \ + $SRC + + ls -la "$out" +} + +FILTER="${1:-}" + +echo "$TARGETS" | while IFS= read -r line; do + [ -z "$line" ] && continue + name=$(echo "$line" | cut -d'|' -f1) + target=$(echo "$line" | cut -d'|' -f2) + ext=$(echo "$line" | cut -d'|' -f3) + + if [ -n "$FILTER" ] && [ "$FILTER" != "$name" ]; then + continue + fi + + build_one "$name" "$target" "$ext" +done + +cp -f "$ROOT/include/monolog_parser.h" "$OUT/" + +echo "" +echo "Done. Artifacts in $OUT:" +ls -la "$OUT"/libmonolog_parser-* 2>/dev/null || true diff --git a/native/monolog_parser/include/monolog_parser.h b/native/monolog_parser/include/monolog_parser.h new file mode 100644 index 00000000..18024ae6 --- /dev/null +++ b/native/monolog_parser/include/monolog_parser.h @@ -0,0 +1,184 @@ +/** + * monolog_parser — SIMD-accelerated Monolog line parser for PHP FFI. + * + * Matches FroshTools LogController LINE_MATCH: + * /\[(?.*)] (?.*)\.(?DEBUG|INFO|…):(?.*)/m + * + * Two usage modes for PHP: + * 1) Parse a line/buffer you already hold (monolog_parse_line / _buffer) + * 2) Open the log file in C and yield/page parsed entries + * monolog_reader_open_backwards → monolog_reader_next (iterator) + * monolog_file_read_backwards (one-shot page) + */ +#ifndef MONOLOG_PARSER_H +#define MONOLOG_PARSER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(_WIN32) || defined(__CYGWIN__) +# ifdef MONOLOG_PARSER_BUILD +# define MONOLOG_API __declspec(dllexport) +# else +# define MONOLOG_API __declspec(dllimport) +# endif +#else +# define MONOLOG_API __attribute__((visibility("default"))) +#endif + +/** Parsed field offsets relative to the start of the input string. */ +typedef struct monolog_fields { + int32_t date_off; + int32_t date_len; + int32_t channel_off; + int32_t channel_len; + int32_t level_off; + int32_t level_len; + int32_t message_off; + int32_t message_len; + /** 1 if the Monolog pattern matched, 0 otherwise. */ + int32_t matched; +} monolog_fields_t; + +/** + * One line within a multi-line buffer (offsets relative to buffer start). + * line_len excludes a trailing '\\n' / '\\r\\n'. + */ +typedef struct monolog_line_span { + int32_t off; + int32_t len; +} monolog_line_span_t; + +/** Library version string (static storage). */ +MONOLOG_API const char *monolog_parser_version(void); + +/** + * Which SIMD path is active: "sse2", "neon", or "scalar". + */ +MONOLOG_API const char *monolog_parser_simd(void); + +/** + * Parse a single Monolog line (need not be NUL-terminated). + * Returns 1 on match, 0 on no match. out must be non-NULL. + */ +MONOLOG_API int monolog_parse_line(const char *line, size_t len, monolog_fields_t *out); + +/** + * Find newline byte offsets in [buf, buf+len) using SIMD scan. + * Writes up to max_offsets offsets into offsets[]; returns count found + * (may be > max_offsets if truncated — check return vs max). + * If offsets is NULL, only counts. + */ +MONOLOG_API size_t monolog_find_newlines(const char *buf, size_t len, + size_t *offsets, size_t max_offsets); + +/** + * Split buffer into line spans (no trailing CR/LF). Handles \\n and \\r\\n. + * Returns number of lines written (capped by max_lines). + */ +MONOLOG_API size_t monolog_split_lines(const char *buf, size_t len, + monolog_line_span_t *lines, size_t max_lines); + +/** + * Parse every line in a buffer. entries[i] fields are relative to buf. + * Unmatched lines still get an entry with matched=0; for those the whole + * line is reported as message (message_off/len set, others 0). + * Returns number of lines processed (capped by max_entries). + */ +MONOLOG_API size_t monolog_parse_buffer(const char *buf, size_t len, + monolog_fields_t *entries, size_t max_entries); + +/** + * SIMD memchr — find first occurrence of byte c in [buf, buf+len). + * Returns pointer to the byte, or NULL. + */ +MONOLOG_API const char *monolog_memchr(const char *buf, int c, size_t len); + +/** + * Find last occurrence of byte c in [buf, buf+len). + * Returns pointer to the byte, or NULL. + */ +MONOLOG_API const char *monolog_memrchr(const char *buf, int c, size_t len); + +/* ===================================================================== + * File reader — open in C, yield parsed lines (newest first) + * ===================================================================== */ + +typedef struct monolog_reader monolog_reader_t; + +/** + * Open path for reverse iteration (newest log line first). + * Returns NULL on open failure. + */ +MONOLOG_API monolog_reader_t *monolog_reader_open_backwards(const char *path); + +/** Close reader and free all buffers. Safe on NULL. */ +MONOLOG_API void monolog_reader_close(monolog_reader_t *r); + +/** Total line count (SIMD newline scan on open). */ +MONOLOG_API uint64_t monolog_reader_total_lines(const monolog_reader_t *r); + +/** Last error message, or NULL. */ +MONOLOG_API const char *monolog_reader_error(const monolog_reader_t *r); + +/** + * Yield next line (newest-first), parse it. + * + * Returns: + * 1 — entry written; *line_out points at NUL-terminated line + * (valid until next next()/close). fields offsets relative to *line_out. + * 0 — EOF + * -1 — error (see monolog_reader_error) + * + * Unmatched lines: fields->matched == 0, message = full line. + */ +MONOLOG_API int monolog_reader_next(monolog_reader_t *r, + monolog_fields_t *out, + const char **line_out); + +/** + * Discard the next n lines (newest-first). Returns 1 ok, 0 hit EOF early, -1 error. + */ +MONOLOG_API int monolog_reader_skip(monolog_reader_t *r, uint64_t n); + +/* ===================================================================== + * One-shot page — preferred for LogController offset/limit + * ===================================================================== */ + +/** Owned strings; free via monolog_page_free only. */ +typedef struct monolog_page_entry { + int32_t matched; + const char *date; + const char *channel; + const char *level; + const char *message; +} monolog_page_entry_t; + +typedef struct monolog_page monolog_page_t; + +/** + * Read [offset, offset+limit) lines from end of file (offset 0 = newest), + * parse each. On success *out is allocated; free with monolog_page_free. + * Returns 0 on success, -1 on error. + */ +MONOLOG_API int monolog_file_read_backwards(const char *path, + uint64_t offset, + uint64_t limit, + monolog_page_t **out); + +MONOLOG_API void monolog_page_free(monolog_page_t *page); + +MONOLOG_API uint64_t monolog_page_total_lines(const monolog_page_t *page); +MONOLOG_API uint32_t monolog_page_count(const monolog_page_t *page); +MONOLOG_API const monolog_page_entry_t *monolog_page_entry(const monolog_page_t *page, + uint32_t index); + +#ifdef __cplusplus +} +#endif + +#endif /* MONOLOG_PARSER_H */ diff --git a/native/monolog_parser/src/monolog_parser.c b/native/monolog_parser/src/monolog_parser.c new file mode 100644 index 00000000..25f125a2 --- /dev/null +++ b/native/monolog_parser/src/monolog_parser.c @@ -0,0 +1,257 @@ +/** + * Monolog line parser — mirrors FroshTools PCRE LINE_MATCH semantics. + * + * Pattern: + * \[ (?.*) ][ ] (?.*) \. (?LEVEL) : (?.*) + * + * channel is greedy → rightmost ".LEVEL:" wins (same as PCRE). + */ +#include "monolog_parser.h" + +#include + +#define MONOLOG_PARSER_VERSION_STR "1.0.0" + +typedef struct { + const char *name; + int len; +} level_def_t; + +/* Longest first (levels are prefix-free). */ +static const level_def_t LEVELS[] = { + {"EMERGENCY", 9}, + {"CRITICAL", 8}, + {"WARNING", 7}, + {"NOTICE", 6}, + {"DEBUG", 5}, + {"ERROR", 5}, + {"ALERT", 5}, + {"INFO", 4}, +}; +static const size_t N_LEVELS = sizeof(LEVELS) / sizeof(LEVELS[0]); + +const char *monolog_parser_version(void) +{ + return MONOLOG_PARSER_VERSION_STR; +} + +static int level_at(const char *s, size_t avail, int *out_len) +{ + for (size_t i = 0; i < N_LEVELS; i++) { + int n = LEVELS[i].len; + if ((size_t)n + 1 > avail) { + continue; + } + if (memcmp(s, LEVELS[i].name, (size_t)n) == 0 && s[n] == ':') { + *out_len = n; + return 1; + } + } + return 0; +} + +/** + * Find rightmost ".LEVEL:" in [start, start+len). + */ +static int find_rightmost_level(const char *start, size_t len, + const char **level_name, int *level_len, + const char **dot_pos) +{ + const char *best_dot = NULL; + const char *best_level = NULL; + int best_llen = 0; + + const char *p = start; + const char *end = start + len; + + while (p < end) { + const char *dot = monolog_memchr(p, '.', (size_t)(end - p)); + if (!dot) { + break; + } + size_t avail = (size_t)(end - (dot + 1)); + int llen = 0; + if (avail > 0 && level_at(dot + 1, avail, &llen)) { + best_dot = dot; + best_level = dot + 1; + best_llen = llen; + } + p = dot + 1; + } + + if (!best_dot) { + return 0; + } + *dot_pos = best_dot; + *level_name = best_level; + *level_len = best_llen; + return 1; +} + +int monolog_parse_line(const char *line, size_t len, monolog_fields_t *out) +{ + if (!out) { + return 0; + } + memset(out, 0, sizeof(*out)); + + if (!line || len == 0) { + return 0; + } + + /* Strip trailing CR (from CRLF). */ + if (line[len - 1] == '\r') { + len--; + } + if (len == 0) { + return 0; + } + + /* 1) '[' date start */ + const char *lb = monolog_memchr(line, '[', len); + if (!lb) { + return 0; + } + + /* 2) first "] " after '[' */ + const char *rb = NULL; + { + const char *scan = lb + 1; + while (scan < line + len) { + const char *cand = monolog_memchr(scan, ']', (size_t)(line + len - scan)); + if (!cand) { + break; + } + if ((size_t)(line + len - cand) >= 2 && cand[1] == ' ') { + rb = cand; + break; + } + scan = cand + 1; + } + } + if (!rb) { + return 0; + } + + const char *date_start = lb + 1; + size_t date_len = (size_t)(rb - date_start); + const char *body = rb + 2; /* skip "] " */ + size_t body_len = (size_t)(line + len - body); + + /* 3) rightmost ".LEVEL:" in body (PCRE greedy channel) */ + const char *level_name = NULL; + int level_len = 0; + const char *dot = NULL; + if (!find_rightmost_level(body, body_len, &level_name, &level_len, &dot)) { + return 0; + } + + const char *channel_start = body; + size_t channel_len = (size_t)(dot - body); + const char *message_start = level_name + level_len + 1; /* skip "LEVEL:" */ + size_t message_len = (size_t)(line + len - message_start); + + out->date_off = (int32_t)(date_start - line); + out->date_len = (int32_t)date_len; + out->channel_off = (int32_t)(channel_start - line); + out->channel_len = (int32_t)channel_len; + out->level_off = (int32_t)(level_name - line); + out->level_len = (int32_t)level_len; + out->message_off = (int32_t)(message_start - line); + out->message_len = (int32_t)message_len; + out->matched = 1; + return 1; +} + +size_t monolog_split_lines(const char *buf, size_t len, + monolog_line_span_t *lines, size_t max_lines) +{ + if (!buf || !lines || max_lines == 0) { + return 0; + } + + size_t n = 0; + size_t start = 0; + + while (n < max_lines) { + const char *hit = NULL; + if (start < len) { + hit = monolog_memchr(buf + start, '\n', len - start); + } + size_t end = hit ? (size_t)(hit - buf) : len; + size_t line_len = end - start; + if (line_len > 0 && buf[start + line_len - 1] == '\r') { + line_len--; + } + /* skip pure empty trailing line caused by final newline */ + if (!hit && line_len == 0 && n > 0) { + break; + } + lines[n].off = (int32_t)start; + lines[n].len = (int32_t)line_len; + n++; + if (!hit) { + break; + } + start = end + 1; + if (start >= len) { + break; + } + } + return n; +} + +size_t monolog_parse_buffer(const char *buf, size_t len, + monolog_fields_t *entries, size_t max_entries) +{ + if (!buf || !entries || max_entries == 0) { + return 0; + } + + size_t produced = 0; + size_t start = 0; + + while (produced < max_entries) { + const char *hit = NULL; + if (start < len) { + hit = monolog_memchr(buf + start, '\n', len - start); + } + size_t end = hit ? (size_t)(hit - buf) : len; + size_t line_len = end - start; + if (line_len > 0 && buf[start + line_len - 1] == '\r') { + line_len--; + } + + if (!hit && line_len == 0 && produced > 0) { + break; + } + if (start >= len && line_len == 0) { + break; + } + + monolog_fields_t local; + int ok = monolog_parse_line(buf + start, line_len, &local); + if (ok) { + local.date_off += (int32_t)start; + local.channel_off += (int32_t)start; + local.level_off += (int32_t)start; + local.message_off += (int32_t)start; + entries[produced] = local; + } else { + memset(&entries[produced], 0, sizeof(entries[produced])); + entries[produced].matched = 0; + entries[produced].message_off = (int32_t)start; + entries[produced].message_len = (int32_t)line_len; + } + produced++; + + if (!hit) { + break; + } + start = end + 1; + if (start >= len) { + break; + } + } + return produced; +} diff --git a/native/monolog_parser/src/monolog_reader.c b/native/monolog_parser/src/monolog_reader.c new file mode 100644 index 00000000..fff1d5c5 --- /dev/null +++ b/native/monolog_parser/src/monolog_reader.c @@ -0,0 +1,584 @@ +/** + * Reverse log file reader — open file in C, yield/page parsed lines for PHP FFI. + * + * Chunked reverse scan mirrors Frosh\Tools\Components\LineReader::readLinesBackwards, + * then monolog_parse_line on each yielded line. + */ +#include "monolog_parser.h" + +#include +#include +#include + +#define CHUNK_SIZE 65536 + +struct monolog_reader { + FILE *fp; + size_t file_size; + size_t pos; /* byte offset of next older unread region start */ + + char *chunk; + size_t chunk_cap; + + /* buffer[0] from LineReader — incomplete oldest fragment, or sole remaining line */ + char *head; + size_t head_len; + size_t head_cap; + int has_head; + + /* complete lines ready to yield, oldest→newest; pop from end */ + char **ready; + size_t ready_n; + size_t ready_cap; + + char *current; + size_t current_cap; + + uint64_t total_lines; + int first_chunk; + int eof; + int err; + char errbuf[128]; +}; + +struct monolog_page { + uint64_t total_lines; + uint32_t count; + monolog_page_entry_t *entries; +}; + +static int set_err(monolog_reader_t *r, const char *msg) +{ + r->err = 1; + snprintf(r->errbuf, sizeof(r->errbuf), "%s", msg); + return -1; +} + +static int ready_push(monolog_reader_t *r, char *line) +{ + if (r->ready_n + 1 > r->ready_cap) { + size_t ncap = r->ready_cap ? r->ready_cap * 2 : 16; + char **nr = (char **)realloc(r->ready, ncap * sizeof(char *)); + if (!nr) { + free(line); + return set_err(r, "oom ready"); + } + r->ready = nr; + r->ready_cap = ncap; + } + r->ready[r->ready_n++] = line; + return 0; +} + +static int set_head(monolog_reader_t *r, const char *s, size_t len) +{ + if (r->head_cap < len + 1) { + free(r->head); + r->head = (char *)malloc(len + 1); + if (!r->head) { + return set_err(r, "oom head"); + } + r->head_cap = len + 1; + } + memcpy(r->head, s, len); + r->head[len] = '\0'; + r->head_len = len; + r->has_head = 1; + return 0; +} + +static char *dup_slice(const char *s, size_t len) +{ + char *p = (char *)malloc(len + 1); + if (!p) { + return NULL; + } + memcpy(p, s, len); + p[len] = '\0'; + return p; +} + +static uint64_t count_lines_fp(FILE *fp, size_t file_size) +{ + if (file_size == 0) { + return 0; + } + + char buf[CHUNK_SIZE]; + uint64_t newlines = 0; + size_t got_total = 0; + int last_was_nl = 0; + + if (fseek(fp, 0, SEEK_SET) != 0) { + return 0; + } + + while (got_total < file_size) { + size_t want = file_size - got_total; + if (want > sizeof(buf)) { + want = sizeof(buf); + } + size_t n = fread(buf, 1, want, fp); + if (n == 0) { + break; + } + newlines += (uint64_t)monolog_find_newlines(buf, n, NULL, 0); + last_was_nl = (buf[n - 1] == '\n'); + got_total += n; + } + + if (!last_was_nl) { + newlines += 1; + } + return newlines; +} + +/** + * Explode `data` by '\\n' into ready[] + head, matching LineReader: + * - first chunk: strip one trailing '\\n' + * - if has_head: data is older_chunk + head + * - all parts go into a temporary list; last part becomes head if pos>0 + * wait — LineReader keeps buffer as full explode; yields when count>1 from end, + * only buffer[0] is held when needing more. So after explode L0..Lk: + * ready gets L1..Lk (to be popped from end = Lk first), head = L0 if more data + * or if pos==0 everything including L0 is ready. + */ +static int explode_into_reader(monolog_reader_t *r, char *data, size_t len, int at_file_start) +{ + /* strip one trailing newline on first chunk only (LineReader) */ + if (r->first_chunk) { + r->first_chunk = 0; + if (len > 0 && data[len - 1] == '\n') { + len--; + if (len > 0 && data[len - 1] == '\r') { + len--; + } + } + } + + size_t start = 0; + size_t part_count = 0; + /* first pass count */ + for (size_t i = 0; i < len; i++) { + if (data[i] == '\n') { + part_count++; + } + } + part_count += 1; /* last segment */ + + char **parts = (char **)calloc(part_count, sizeof(char *)); + if (!parts) { + return set_err(r, "oom parts"); + } + + size_t pi = 0; + for (size_t i = 0; i <= len; i++) { + if (i == len || data[i] == '\n') { + size_t plen = i - start; + if (plen > 0 && data[start + plen - 1] == '\r') { + plen--; + } + parts[pi] = dup_slice(data + start, plen); + if (!parts[pi]) { + for (size_t j = 0; j < pi; j++) { + free(parts[j]); + } + free(parts); + return set_err(r, "oom part"); + } + pi++; + start = i + 1; + } + } + + /* + * parts[0] .. parts[pi-1] are oldest → newest within this combined chunk. + * If not at file start, parts[0] may be incomplete → becomes head. + * Remaining parts are complete lines → push to ready (still oldest→newest). + */ + size_t from = 0; + if (!at_file_start) { + /* keep parts[0] as head for next older merge */ + if (set_head(r, parts[0], strlen(parts[0])) != 0) { + for (size_t j = 0; j < pi; j++) { + free(parts[j]); + } + free(parts); + return -1; + } + free(parts[0]); + from = 1; + } else { + r->has_head = 0; + } + + for (size_t j = from; j < pi; j++) { + if (ready_push(r, parts[j]) != 0) { + for (size_t k = j + 1; k < pi; k++) { + free(parts[k]); + } + free(parts); + return -1; + } + /* ownership transferred */ + } + free(parts); + return 0; +} + +static int load_older_chunk(monolog_reader_t *r) +{ + if (r->pos == 0) { + /* flush head as final line */ + if (r->has_head) { + char *line = dup_slice(r->head, r->head_len); + if (!line) { + return set_err(r, "oom head flush"); + } + r->has_head = 0; + if (ready_push(r, line) != 0) { + return -1; + } + return 0; + } + r->eof = 1; + return 0; + } + + size_t nread = r->pos > CHUNK_SIZE ? CHUNK_SIZE : r->pos; + size_t start = r->pos - nread; + + if (r->chunk_cap < nread + (r->has_head ? r->head_len : 0) + 1) { + free(r->chunk); + r->chunk_cap = nread + (r->has_head ? r->head_len : 0) + 1; + r->chunk = (char *)malloc(r->chunk_cap); + if (!r->chunk) { + return set_err(r, "oom chunk"); + } + } + + if (fseek(r->fp, (long)start, SEEK_SET) != 0) { + return set_err(r, "fseek failed"); + } + size_t got = fread(r->chunk, 1, nread, r->fp); + if (got != nread) { + return set_err(r, "fread failed"); + } + + /* combine older_chunk + head (LineReader: chunk . buffer[0]) */ + size_t combined_len = got; + if (r->has_head) { + memcpy(r->chunk + got, r->head, r->head_len); + combined_len = got + r->head_len; + r->has_head = 0; + } + r->chunk[combined_len] = '\0'; + + r->pos = start; + int at_start = (r->pos == 0); + + return explode_into_reader(r, r->chunk, combined_len, at_start); +} + +static int ensure_ready(monolog_reader_t *r) +{ + while (r->ready_n == 0 && !r->eof && !r->err) { + /* + * LineReader yields when count(buffer) > 1, else loads more. + * We only keep complete lines in ready[]; head is incomplete. + * So ready_n==0 ⇒ need older chunk (or flush head at pos==0). + */ + if (load_older_chunk(r) != 0) { + return -1; + } + } + return r->err ? -1 : 0; +} + +/* ---- public ----------------------------------------------------------- */ + +monolog_reader_t *monolog_reader_open_backwards(const char *path) +{ + if (!path) { + return NULL; + } + + monolog_reader_t *r = (monolog_reader_t *)calloc(1, sizeof(*r)); + if (!r) { + return NULL; + } + + r->fp = fopen(path, "rb"); + if (!r->fp) { + free(r); + return NULL; + } + + if (fseek(r->fp, 0, SEEK_END) != 0) { + fclose(r->fp); + free(r); + return NULL; + } + long sz = ftell(r->fp); + if (sz < 0) { + fclose(r->fp); + free(r); + return NULL; + } + + r->file_size = (size_t)sz; + r->pos = r->file_size; + r->first_chunk = 1; + r->total_lines = count_lines_fp(r->fp, r->file_size); + + if (r->file_size == 0) { + r->eof = 1; + } + + return r; +} + +void monolog_reader_close(monolog_reader_t *r) +{ + if (!r) { + return; + } + if (r->fp) { + fclose(r->fp); + } + free(r->chunk); + free(r->head); + free(r->current); + for (size_t i = 0; i < r->ready_n; i++) { + free(r->ready[i]); + } + free(r->ready); + free(r); +} + +uint64_t monolog_reader_total_lines(const monolog_reader_t *r) +{ + return r ? r->total_lines : 0; +} + +const char *monolog_reader_error(const monolog_reader_t *r) +{ + if (!r || !r->err) { + return NULL; + } + return r->errbuf; +} + +int monolog_reader_next(monolog_reader_t *r, monolog_fields_t *out, const char **line_out) +{ + if (!r || !out || !line_out) { + return -1; + } + if (r->err) { + return -1; + } + + /* Prefer ready lines; if only head and more file, LineReader would load more + before yielding head — ensure_ready handles that. If ready empty and head + is sole line with pos==0, load_older flushes head into ready. */ + if (ensure_ready(r) != 0) { + return -1; + } + + /* LineReader: if count > 1 pop; if count == 1 and pos==0 yield last. + We never put incomplete head into ready, so pop any ready line. */ + if (r->ready_n == 0) { + return 0; + } + + char *line = r->ready[--r->ready_n]; + size_t len = strlen(line); + + if (r->current_cap < len + 1) { + free(r->current); + r->current = (char *)malloc(len + 1); + if (!r->current) { + free(line); + set_err(r, "oom current"); + return -1; + } + r->current_cap = len + 1; + } + memcpy(r->current, line, len + 1); + free(line); + + memset(out, 0, sizeof(*out)); + if (!monolog_parse_line(r->current, len, out)) { + out->matched = 0; + out->message_off = 0; + out->message_len = (int32_t)len; + } + + *line_out = r->current; + return 1; +} + +int monolog_reader_skip(monolog_reader_t *r, uint64_t n) +{ + if (!r) { + return -1; + } + monolog_fields_t tmp; + const char *line = NULL; + for (uint64_t i = 0; i < n; i++) { + int rc = monolog_reader_next(r, &tmp, &line); + if (rc <= 0) { + return rc; + } + } + return 1; +} + +static char *xstrndup(const char *s, size_t n) +{ + char *p = (char *)malloc(n + 1); + if (!p) { + return NULL; + } + memcpy(p, s, n); + p[n] = '\0'; + return p; +} + +int monolog_file_read_backwards(const char *path, uint64_t offset, uint64_t limit, + monolog_page_t **out) +{ + if (!path || !out) { + return -1; + } + *out = NULL; + + monolog_reader_t *r = monolog_reader_open_backwards(path); + if (!r) { + return -1; + } + + monolog_page_t *page = (monolog_page_t *)calloc(1, sizeof(*page)); + if (!page) { + monolog_reader_close(r); + return -1; + } + page->total_lines = monolog_reader_total_lines(r); + + if (limit == 0) { + monolog_reader_close(r); + *out = page; + return 0; + } + + if (offset > 0) { + int src = monolog_reader_skip(r, offset); + if (src < 0) { + monolog_page_free(page); + monolog_reader_close(r); + return -1; + } + } + + monolog_page_entry_t *entries = + (monolog_page_entry_t *)calloc((size_t)limit, sizeof(*entries)); + if (!entries) { + monolog_page_free(page); + monolog_reader_close(r); + return -1; + } + + uint32_t n = 0; + for (uint64_t i = 0; i < limit; i++) { + monolog_fields_t f; + const char *line = NULL; + int rc = monolog_reader_next(r, &f, &line); + if (rc < 0) { + for (uint32_t j = 0; j < n; j++) { + free((void *)entries[j].date); + free((void *)entries[j].channel); + free((void *)entries[j].level); + free((void *)entries[j].message); + } + free(entries); + monolog_page_free(page); + monolog_reader_close(r); + return -1; + } + if (rc == 0) { + break; + } + + monolog_page_entry_t *e = &entries[n]; + e->matched = f.matched; + if (f.matched) { + e->date = xstrndup(line + f.date_off, (size_t)f.date_len); + e->channel = xstrndup(line + f.channel_off, (size_t)f.channel_len); + e->level = xstrndup(line + f.level_off, (size_t)f.level_len); + e->message = xstrndup(line + f.message_off, (size_t)f.message_len); + } else { + e->date = xstrndup("unknown", 7); + e->channel = xstrndup("unknown", 7); + e->level = xstrndup("unknown", 7); + e->message = xstrndup(line, strlen(line)); + } + if (!e->date || !e->channel || !e->level || !e->message) { + free((void *)e->date); + free((void *)e->channel); + free((void *)e->level); + free((void *)e->message); + for (uint32_t j = 0; j < n; j++) { + free((void *)entries[j].date); + free((void *)entries[j].channel); + free((void *)entries[j].level); + free((void *)entries[j].message); + } + free(entries); + monolog_page_free(page); + monolog_reader_close(r); + return -1; + } + n++; + } + + page->entries = entries; + page->count = n; + monolog_reader_close(r); + *out = page; + return 0; +} + +void monolog_page_free(monolog_page_t *page) +{ + if (!page) { + return; + } + if (page->entries) { + for (uint32_t i = 0; i < page->count; i++) { + free((void *)page->entries[i].date); + free((void *)page->entries[i].channel); + free((void *)page->entries[i].level); + free((void *)page->entries[i].message); + } + free(page->entries); + } + free(page); +} + +uint64_t monolog_page_total_lines(const monolog_page_t *page) +{ + return page ? page->total_lines : 0; +} + +uint32_t monolog_page_count(const monolog_page_t *page) +{ + return page ? page->count : 0; +} + +const monolog_page_entry_t *monolog_page_entry(const monolog_page_t *page, uint32_t index) +{ + if (!page || index >= page->count) { + return NULL; + } + return &page->entries[index]; +} diff --git a/native/monolog_parser/src/simd_scan.c b/native/monolog_parser/src/simd_scan.c new file mode 100644 index 00000000..90a08aac --- /dev/null +++ b/native/monolog_parser/src/simd_scan.c @@ -0,0 +1,176 @@ +/** + * SIMD byte scanners (SSE2 / NEON / scalar fallback). + */ +#include "monolog_parser.h" + +#include + +#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) +# define MONOLOG_X86 1 +# include +#elif defined(__aarch64__) || defined(__ARM_NEON) +# define MONOLOG_NEON 1 +# include +#endif + +const char *monolog_parser_simd(void) +{ +#if defined(MONOLOG_X86) + return "sse2"; +#elif defined(MONOLOG_NEON) + return "neon"; +#else + return "scalar"; +#endif +} + +/* ---- scalar helpers ---------------------------------------------------- */ + +static const char *memchr_scalar(const char *buf, int c, size_t len) +{ + const unsigned char *p = (const unsigned char *)buf; + const unsigned char ch = (unsigned char)c; + for (size_t i = 0; i < len; i++) { + if (p[i] == ch) { + return (const char *)(p + i); + } + } + return NULL; +} + +static const char *memrchr_scalar(const char *buf, int c, size_t len) +{ + const unsigned char *p = (const unsigned char *)buf; + const unsigned char ch = (unsigned char)c; + for (size_t i = len; i > 0; i--) { + if (p[i - 1] == ch) { + return (const char *)(p + i - 1); + } + } + return NULL; +} + +#if defined(MONOLOG_X86) + +/* ---- SSE2 -------------------------------------------------------------- */ + +static const char *memchr_sse2(const char *buf, int c, size_t len) +{ + const unsigned char *p = (const unsigned char *)buf; + const unsigned char ch = (unsigned char)c; + const __m128i needle = _mm_set1_epi8((char)ch); + + while (len >= 16) { + __m128i chunk = _mm_loadu_si128((const __m128i *)p); + __m128i eq = _mm_cmpeq_epi8(chunk, needle); + int mask = _mm_movemask_epi8(eq); + if (mask) { + return (const char *)(p + __builtin_ctz((unsigned)mask)); + } + p += 16; + len -= 16; + } + return memchr_scalar((const char *)p, c, len); +} + +static const char *memrchr_sse2(const char *buf, int c, size_t len) +{ + const unsigned char *base = (const unsigned char *)buf; + const unsigned char ch = (unsigned char)c; + const __m128i needle = _mm_set1_epi8((char)ch); + size_t i = len; + + while (i >= 16) { + i -= 16; + __m128i chunk = _mm_loadu_si128((const __m128i *)(base + i)); + __m128i eq = _mm_cmpeq_epi8(chunk, needle); + int mask = _mm_movemask_epi8(eq); + if (mask) { + return (const char *)(base + i + (31 - __builtin_clz((unsigned)mask))); + } + } + return memrchr_scalar((const char *)base, c, i); +} + +#endif /* MONOLOG_X86 */ + +#if defined(MONOLOG_NEON) + +static const char *memchr_neon(const char *buf, int c, size_t len) +{ + const unsigned char *p = (const unsigned char *)buf; + const uint8x16_t needle = vdupq_n_u8((uint8_t)c); + + while (len >= 16) { + uint8x16_t chunk = vld1q_u8(p); + uint8x16_t eq = vceqq_u8(chunk, needle); + uint8x8_t narrow = vshrn_n_u16(vreinterpretq_u16_u8(eq), 4); + uint64_t mask = vget_lane_u64(vreinterpret_u64_u8(narrow), 0); + if (mask) { + for (int i = 0; i < 16; i++) { + if (p[i] == (unsigned char)c) { + return (const char *)(p + i); + } + } + } + p += 16; + len -= 16; + } + return memchr_scalar((const char *)p, c, len); +} + +static const char *memrchr_neon(const char *buf, int c, size_t len) +{ + return memrchr_scalar(buf, c, len); +} + +#endif /* MONOLOG_NEON */ + +const char *monolog_memchr(const char *buf, int c, size_t len) +{ + if (!buf || len == 0) { + return NULL; + } +#if defined(MONOLOG_X86) + return memchr_sse2(buf, c, len); +#elif defined(MONOLOG_NEON) + return memchr_neon(buf, c, len); +#else + return memchr_scalar(buf, c, len); +#endif +} + +const char *monolog_memrchr(const char *buf, int c, size_t len) +{ + if (!buf || len == 0) { + return NULL; + } +#if defined(MONOLOG_X86) + return memrchr_sse2(buf, c, len); +#elif defined(MONOLOG_NEON) + return memrchr_neon(buf, c, len); +#else + return memrchr_scalar(buf, c, len); +#endif +} + +size_t monolog_find_newlines(const char *buf, size_t len, + size_t *offsets, size_t max_offsets) +{ + size_t count = 0; + const char *p = buf; + const char *end = buf + len; + + while (p < end) { + const char *hit = monolog_memchr(p, '\n', (size_t)(end - p)); + if (!hit) { + break; + } + if (offsets && count < max_offsets) { + offsets[count] = (size_t)(hit - buf); + } + count++; + p = hit + 1; + } + return count; +} diff --git a/native/monolog_parser/tests/test_parser.c b/native/monolog_parser/tests/test_parser.c new file mode 100644 index 00000000..8d3af729 --- /dev/null +++ b/native/monolog_parser/tests/test_parser.c @@ -0,0 +1,320 @@ +/** + * Unit tests for monolog_parser — line parse + reverse file reader. + */ +#define _POSIX_C_SOURCE 200809L + +#include "monolog_parser.h" + +#include +#include +#include +#include +#include + +static int failures = 0; + +#define EXPECT(cond, msg) do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \ + failures++; \ + } \ +} while (0) + +static int streq_span(const char *base, int32_t off, int32_t len, const char *expect) +{ + size_t elen = strlen(expect); + if ((size_t)len != elen) { + return 0; + } + return memcmp(base + off, expect, elen) == 0; +} + +static void test_basic(void) +{ + const char *line = + "[2024-03-15T10:30:00.123456+00:00] request.ERROR: Uncaught PHP Exception [] []"; + monolog_fields_t f; + int ok = monolog_parse_line(line, strlen(line), &f); + EXPECT(ok == 1, "basic match"); + EXPECT(f.matched == 1, "matched flag"); + EXPECT(streq_span(line, f.date_off, f.date_len, "2024-03-15T10:30:00.123456+00:00"), "date"); + EXPECT(streq_span(line, f.channel_off, f.channel_len, "request"), "channel"); + EXPECT(streq_span(line, f.level_off, f.level_len, "ERROR"), "level"); + EXPECT(streq_span(line, f.message_off, f.message_len, " Uncaught PHP Exception [] []"), "message"); +} + +static void test_dotted_channel(void) +{ + const char *line = + "[2024-03-15T10:29:59.000000+00:00] doctrine.dbal.INFO: Connecting [] []"; + monolog_fields_t f; + int ok = monolog_parse_line(line, strlen(line), &f); + EXPECT(ok == 1, "dotted channel match"); + EXPECT(streq_span(line, f.channel_off, f.channel_len, "doctrine.dbal"), "channel doctrine.dbal"); + EXPECT(streq_span(line, f.level_off, f.level_len, "INFO"), "level INFO"); +} + +static void test_rightmost_level(void) +{ + const char *line = + "[2024-01-01T00:00:00.000000+00:00] app.INFO: Failed with .ERROR: boom"; + monolog_fields_t f; + int ok = monolog_parse_line(line, strlen(line), &f); + EXPECT(ok == 1, "rightmost match"); + EXPECT(streq_span(line, f.channel_off, f.channel_len, "app.INFO: Failed with "), "greedy channel"); + EXPECT(streq_span(line, f.level_off, f.level_len, "ERROR"), "rightmost level ERROR"); + EXPECT(streq_span(line, f.message_off, f.message_len, " boom"), "message"); +} + +static void test_all_levels(void) +{ + const char *levels[] = { + "DEBUG", "INFO", "NOTICE", "WARNING", "ERROR", "CRITICAL", "ALERT", "EMERGENCY" + }; + char buf[256]; + for (size_t i = 0; i < sizeof(levels) / sizeof(levels[0]); i++) { + snprintf(buf, sizeof(buf), + "[2024-01-01T00:00:00+00:00] ch.%s: msg", levels[i]); + monolog_fields_t f; + int ok = monolog_parse_line(buf, strlen(buf), &f); + EXPECT(ok == 1, levels[i]); + EXPECT(streq_span(buf, f.level_off, f.level_len, levels[i]), levels[i]); + } +} + +static void test_unmatched(void) +{ + const char *line = "not a monolog line"; + monolog_fields_t f; + int ok = monolog_parse_line(line, strlen(line), &f); + EXPECT(ok == 0, "unmatched"); + EXPECT(f.matched == 0, "matched=0"); +} + +static void test_buffer(void) +{ + const char *buf = + "[2024-01-01T00:00:00+00:00] app.INFO: one\n" + "garbage line\n" + "[2024-01-01T00:00:01+00:00] app.ERROR: two\n"; + monolog_fields_t entries[8]; + size_t n = monolog_parse_buffer(buf, strlen(buf), entries, 8); + EXPECT(n == 3, "3 lines"); + EXPECT(entries[0].matched == 1, "line0 match"); + EXPECT(entries[1].matched == 0, "line1 no match"); + EXPECT(entries[2].matched == 1, "line2 match"); + EXPECT(streq_span(buf, entries[0].level_off, entries[0].level_len, "INFO"), "line0 INFO"); + EXPECT(streq_span(buf, entries[2].level_off, entries[2].level_len, "ERROR"), "line2 ERROR"); + EXPECT(streq_span(buf, entries[1].message_off, entries[1].message_len, "garbage line"), "garbage as message"); +} + +static void test_newlines_simd(void) +{ + size_t cap = 1 << 16; + char *buf = malloc(cap); + EXPECT(buf != NULL, "alloc"); + if (!buf) { + return; + } + memset(buf, 'x', cap); + size_t expect = 0; + for (size_t i = 100; i < cap; i += 97) { + buf[i] = '\n'; + expect++; + } + size_t offsets[1024]; + size_t found = monolog_find_newlines(buf, cap, offsets, 1024); + EXPECT(found == expect, "newline count"); + free(buf); +} + +static void test_memrchr(void) +{ + const char *s = "a.b.c.ERROR:x"; + const char *p = monolog_memrchr(s, '.', strlen(s)); + EXPECT(p != NULL && p == s + 5, "memrchr last dot"); +} + +static char *write_temp_log(const char *contents, size_t len) +{ + char tmpl[] = "/tmp/monolog_test_XXXXXX"; + int fd = mkstemp(tmpl); + if (fd < 0) { + perror("mkstemp"); + return NULL; + } + if (len > 0) { + ssize_t w = write(fd, contents, len); + if (w < 0 || (size_t)w != len) { + perror("write"); + close(fd); + unlink(tmpl); + return NULL; + } + } + close(fd); + return strdup(tmpl); +} + +static void test_reader_yield_order(void) +{ + const char *body = + "[2024-01-01T00:00:00+00:00] app.INFO: first\n" + "[2024-01-01T00:00:01+00:00] app.WARNING: second\n" + "[2024-01-01T00:00:02+00:00] app.ERROR: third\n"; + char *path = write_temp_log(body, strlen(body)); + EXPECT(path != NULL, "temp path"); + if (!path) { + return; + } + + monolog_reader_t *r = monolog_reader_open_backwards(path); + EXPECT(r != NULL, "open"); + if (!r) { + unlink(path); + free(path); + return; + } + + EXPECT(monolog_reader_total_lines(r) == 3, "total 3"); + + monolog_fields_t f; + const char *line = NULL; + int rc; + + rc = monolog_reader_next(r, &f, &line); + EXPECT(rc == 1 && f.matched == 1, "yield 1"); + EXPECT(streq_span(line, f.level_off, f.level_len, "ERROR"), "newest ERROR"); + EXPECT(streq_span(line, f.message_off, f.message_len, " third"), "newest msg"); + + rc = monolog_reader_next(r, &f, &line); + EXPECT(rc == 1 && streq_span(line, f.level_off, f.level_len, "WARNING"), "second WARNING"); + + rc = monolog_reader_next(r, &f, &line); + EXPECT(rc == 1 && streq_span(line, f.level_off, f.level_len, "INFO"), "oldest INFO"); + + rc = monolog_reader_next(r, &f, &line); + EXPECT(rc == 0, "eof"); + + monolog_reader_close(r); + unlink(path); + free(path); +} + +static void test_reader_page_offset_limit(void) +{ + const char *body = + "[2024-01-01T00:00:00+00:00] app.INFO: L0\n" + "[2024-01-01T00:00:01+00:00] app.INFO: L1\n" + "[2024-01-01T00:00:02+00:00] app.INFO: L2\n" + "[2024-01-01T00:00:03+00:00] app.INFO: L3\n" + "[2024-01-01T00:00:04+00:00] app.INFO: L4\n"; + char *path = write_temp_log(body, strlen(body)); + EXPECT(path != NULL, "temp path"); + if (!path) { + return; + } + + monolog_page_t *page = NULL; + int rc = monolog_file_read_backwards(path, 1, 2, &page); + EXPECT(rc == 0 && page != NULL, "page ok"); + if (!page) { + unlink(path); + free(path); + return; + } + + EXPECT(monolog_page_total_lines(page) == 5, "total 5"); + EXPECT(monolog_page_count(page) == 2, "count 2"); + + const monolog_page_entry_t *e0 = monolog_page_entry(page, 0); + const monolog_page_entry_t *e1 = monolog_page_entry(page, 1); + EXPECT(e0 && strcmp(e0->message, " L3") == 0, "page[0]=L3"); + EXPECT(e1 && strcmp(e1->message, " L2") == 0, "page[1]=L2"); + + monolog_page_free(page); + unlink(path); + free(path); +} + +static void test_reader_large_multichunk(void) +{ + size_t lines = 2000; + size_t cap = lines * 120; + char *body = malloc(cap); + EXPECT(body != NULL, "alloc body"); + if (!body) { + return; + } + size_t used = 0; + for (size_t i = 0; i < lines; i++) { + int n = snprintf(body + used, cap - used, + "[2024-01-01T00:00:00+%04zu] ch.INFO: line-%zu-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n", + i, i); + if (n < 0 || (size_t)n >= cap - used) { + break; + } + used += (size_t)n; + } + + char *path = write_temp_log(body, used); + free(body); + EXPECT(path != NULL, "temp large"); + if (!path) { + return; + } + + monolog_page_t *page = NULL; + int rc = monolog_file_read_backwards(path, 0, 5, &page); + EXPECT(rc == 0 && page != NULL, "large page"); + if (page) { + EXPECT(monolog_page_total_lines(page) == lines, "large total"); + EXPECT(monolog_page_count(page) == 5, "large count 5"); + char expect[64]; + snprintf(expect, sizeof(expect), " line-%zu-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", lines - 1); + const monolog_page_entry_t *e0 = monolog_page_entry(page, 0); + EXPECT(e0 && strcmp(e0->message, expect) == 0, "newest message"); + monolog_page_free(page); + } + + monolog_reader_t *r = monolog_reader_open_backwards(path); + EXPECT(r != NULL, "large open"); + if (r) { + uint64_t n = 0; + monolog_fields_t f; + const char *line = NULL; + while (monolog_reader_next(r, &f, &line) == 1) { + n++; + } + EXPECT(n == lines, "yielded all lines"); + monolog_reader_close(r); + } + + unlink(path); + free(path); +} + +int main(void) +{ + printf("monolog_parser %s simd=%s\n", + monolog_parser_version(), monolog_parser_simd()); + + test_basic(); + test_dotted_channel(); + test_rightmost_level(); + test_all_levels(); + test_unmatched(); + test_buffer(); + test_newlines_simd(); + test_memrchr(); + test_reader_yield_order(); + test_reader_page_offset_limit(); + test_reader_large_multichunk(); + + if (failures) { + fprintf(stderr, "\n%d failure(s)\n", failures); + return 1; + } + printf("All tests passed.\n"); + return 0; +} diff --git a/src/Components/Log/FfiMonologLogReader.php b/src/Components/Log/FfiMonologLogReader.php new file mode 100644 index 00000000..e605f1f4 --- /dev/null +++ b/src/Components/Log/FfiMonologLogReader.php @@ -0,0 +1,196 @@ +ffi = $ffi; + } + + /** + * @throws \Throwable + */ + public static function create(?string $libraryPath = null): self + { + if (!\extension_loaded('ffi') || !class_exists(\FFI::class, false)) { + throw new \RuntimeException('ext-ffi is not available'); + } + + $path = $libraryPath ?? NativeLibraryLocator::locate(); + if ($path === null || !is_readable($path)) { + throw new \RuntimeException( + 'libmonolog_parser not found for ' . NativeLibraryLocator::describe() + ); + } + + /** @var \FFI $ffi */ + $ffi = \FFI::cdef(self::CDEF, $path); + + return new self($ffi); + } + + public static function tryCreate(?string $libraryPath = null): ?self + { + try { + return self::create($libraryPath); + } catch (\Throwable) { + return null; + } + } + + public function readBackwards(string $filePath, int $offset, int $limit): array + { + /** @var \FFI\CData $pagePtr */ + $pagePtr = $this->ffi->new('monolog_page_t*'); + $rc = $this->ffi->monolog_file_read_backwards( + $filePath, + (int) max(0, $offset), + (int) max(0, $limit), + \FFI::addr($pagePtr), + ); + + if ($rc !== 0 || $pagePtr === null) { + throw new \RuntimeException(\sprintf('monolog_file_read_backwards failed for %s', $filePath)); + } + + try { + $total = (int) $this->ffi->monolog_page_total_lines($pagePtr); + $count = (int) $this->ffi->monolog_page_count($pagePtr); + $entries = []; + + for ($i = 0; $i < $count; $i++) { + $entry = $this->ffi->monolog_page_entry($pagePtr, $i); + if ($entry === null) { + continue; + } + $entries[] = [ + 'date' => (string) $entry->date, + 'channel' => (string) $entry->channel, + 'level' => (string) $entry->level, + 'message' => (string) $entry->message, + ]; + } + + return [ + 'entries' => $entries, + 'total' => $total, + ]; + } finally { + $this->ffi->monolog_page_free($pagePtr); + } + } + + public function yieldBackwards(string $filePath): \Generator + { + $reader = $this->ffi->monolog_reader_open_backwards($filePath); + if ($reader === null) { + throw new \RuntimeException(\sprintf('Cannot open log file: %s', $filePath)); + } + + try { + /** @var \FFI\CData $fields */ + $fields = $this->ffi->new('monolog_fields_t'); + /** @var \FFI\CData $linePtr */ + $linePtr = $this->ffi->new('const char*'); + + while (true) { + $rc = $this->ffi->monolog_reader_next($reader, \FFI::addr($fields), \FFI::addr($linePtr)); + if ($rc === 0) { + break; + } + if ($rc < 0) { + $err = $this->ffi->monolog_reader_error($reader); + throw new \RuntimeException('monolog_reader_next: ' . ($err ?? 'unknown error')); + } + + $line = \FFI::string($linePtr); + if ($fields->matched === 1) { + yield [ + 'date' => substr($line, $fields->date_off, $fields->date_len), + 'channel' => substr($line, $fields->channel_off, $fields->channel_len), + 'level' => substr($line, $fields->level_off, $fields->level_len), + 'message' => substr($line, $fields->message_off, $fields->message_len), + ]; + } else { + yield [ + 'date' => 'unknown', + 'channel' => 'unknown', + 'level' => 'unknown', + 'message' => $line, + ]; + } + } + } finally { + $this->ffi->monolog_reader_close($reader); + } + } + + public function backend(): string + { + return 'ffi'; + } + + public function simd(): string + { + return (string) $this->ffi->monolog_parser_simd(); + } + + public function version(): string + { + return (string) $this->ffi->monolog_parser_version(); + } +} diff --git a/src/Components/Log/FfiMonologParser.php b/src/Components/Log/FfiMonologParser.php new file mode 100644 index 00000000..2b818dda --- /dev/null +++ b/src/Components/Log/FfiMonologParser.php @@ -0,0 +1,121 @@ +ffi = $ffi; + } + + /** + * @throws \Throwable when FFI/.so cannot be used + */ + public static function create(?string $libraryPath = null): self + { + if (!\extension_loaded('ffi')) { + throw new \RuntimeException('ext-ffi is not loaded'); + } + + if (!class_exists(\FFI::class, false)) { + throw new \RuntimeException('FFI class unavailable'); + } + + $path = $libraryPath ?? NativeLibraryLocator::locate(); + if ($path === null || !is_readable($path)) { + throw new \RuntimeException( + 'libmonolog_parser not found for ' . NativeLibraryLocator::describe() + . ' — ship prebuilds under native/lib/ or set MONOLOG_PARSER_LIB' + ); + } + + /** @var \FFI $ffi */ + $ffi = \FFI::cdef(self::CDEF, $path); + + return new self($ffi); + } + + public static function tryCreate(?string $libraryPath = null): ?self + { + try { + return self::create($libraryPath); + } catch (\Throwable) { + return null; + } + } + + public function parse(string $line): array + { + $len = \strlen($line); + /** @var \FFI\CData $fields */ + $fields = $this->ffi->new('monolog_fields_t'); + $ok = $this->ffi->monolog_parse_line($line, $len, \FFI::addr($fields)); + + if ($ok !== 1 || $fields->matched !== 1) { + return [ + 'message' => $line, + 'channel' => 'unknown', + 'date' => 'unknown', + 'level' => 'unknown', + ]; + } + + return [ + 'date' => substr($line, $fields->date_off, $fields->date_len), + 'channel' => substr($line, $fields->channel_off, $fields->channel_len), + 'level' => substr($line, $fields->level_off, $fields->level_len), + 'message' => substr($line, $fields->message_off, $fields->message_len), + ]; + } + + public function backend(): string + { + return 'ffi'; + } + + public function simd(): string + { + return (string) $this->ffi->monolog_parser_simd(); + } + + public function version(): string + { + return (string) $this->ffi->monolog_parser_version(); + } + + /** @deprecated use NativeLibraryLocator::locate() */ + public static function detectLibraryPath(): ?string + { + return NativeLibraryLocator::locate(); + } +} diff --git a/src/Components/Log/MonologLineParserFactory.php b/src/Components/Log/MonologLineParserFactory.php new file mode 100644 index 00000000..b3fbaa93 --- /dev/null +++ b/src/Components/Log/MonologLineParserFactory.php @@ -0,0 +1,40 @@ +, total: int} + */ +interface MonologLogReaderInterface +{ + /** + * Read a page of log lines from the end of the file. + * + * offset 0 = newest line. Lines are already parsed. + * + * @return MonologPage + */ + public function readBackwards(string $filePath, int $offset, int $limit): array; + + /** + * Stream parsed entries newest-first (PHP generator / "yield"). + * + * @return \Generator + */ + public function yieldBackwards(string $filePath): \Generator; + + /** + * Implementation id: "ffi" or "preg". + */ + public function backend(): string; +} diff --git a/src/Components/Log/NativeLibraryLocator.php b/src/Components/Log/NativeLibraryLocator.php new file mode 100644 index 00000000..b0f0e3b1 --- /dev/null +++ b/src/Components/Log/NativeLibraryLocator.php @@ -0,0 +1,239 @@ + 'darwin', + 'Linux' => 'linux', + default => strtolower($osFamily), + }; + + $arch = match (true) { + \in_array($rawArch, ['x86_64', 'amd64', 'x64'], true) => 'x86_64', + \in_array($rawArch, ['aarch64', 'arm64'], true) => 'arm64', + default => $rawArch, + }; + + $ext = match ($os) { + 'darwin' => 'dylib', + 'windows' => 'dll', + default => 'so', + }; + + // libc only meaningful on Linux (gnu vs musl). Darwin/Windows → "n/a". + $libc = $os === 'linux' ? (self::isMusl() ? 'musl' : 'gnu') : 'n/a'; + + $triple = $os === 'linux' && $libc === 'musl' + ? \sprintf('linux-musl-%s', $arch) + : \sprintf('%s-%s', $os, $arch); + + return [ + 'os' => $os, + 'arch' => $arch, + 'libc' => $libc, + 'ext' => $ext, + 'triple' => $triple, + ]; + } + + /** + * Detect musl (Alpine, void, etc.). + * + * Order: + * 1. MONOLOG_PARSER_LIBC=musl|gnu|glibc + * 2. Presence of /lib/ld-musl-*.so.1 + * 3. ldd on PHP_BINARY mentions musl + * 4. phpinfo() / PHP_OS extras (last resort) + */ + public static function isMusl(): bool + { + static $cached = null; + if ($cached !== null) { + return $cached; + } + + $env = getenv('MONOLOG_PARSER_LIBC'); + if (\is_string($env) && $env !== '') { + $v = strtolower($env); + if (\in_array($v, ['musl'], true)) { + return $cached = true; + } + if (\in_array($v, ['gnu', 'glibc', 'gcompat'], true)) { + return $cached = false; + } + } + + if (\PHP_OS_FAMILY !== 'Linux') { + return $cached = false; + } + + // Canonical musl dynamic linker paths (Alpine multi-arch) + $loaders = [ + '/lib/ld-musl-x86_64.so.1', + '/lib/ld-musl-aarch64.so.1', + '/lib/ld-musl-armhf.so.1', + '/lib/ld-musl-i386.so.1', + '/lib/ld-musl-riscv64.so.1', + ]; + foreach ($loaders as $loader) { + if (is_readable($loader)) { + return $cached = true; + } + } + + // Glob fallback (some layouts) + foreach (glob('/lib/ld-musl-*.so.*') ?: [] as $loader) { + if (is_readable($loader)) { + return $cached = true; + } + } + + // Inspect what PHP itself is linked against + if (\defined('PHP_BINARY') && \is_string(\PHP_BINARY) && \PHP_BINARY !== '' && is_readable(\PHP_BINARY)) { + $ldd = self::safeLdd(\PHP_BINARY); + if ($ldd !== null) { + if (str_contains($ldd, 'musl')) { + return $cached = true; + } + // Explicit glibc markers + if (str_contains($ldd, 'libc.so.6') || str_contains($ldd, 'ld-linux')) { + return $cached = false; + } + } + } + + return $cached = false; + } + + /** + * Preferred basenames, most specific first. + * + * @return list + */ + public static function candidateBasenames(): array + { + $p = self::platform(); + $names = []; + + if ($p['os'] === 'linux' && $p['libc'] === 'musl') { + // musl hosts must not load a glibc .so (hard ELF failure) + $names[] = \sprintf('libmonolog_parser-linux-musl-%s.so', $p['arch']); + if ($p['arch'] === 'arm64') { + $names[] = 'libmonolog_parser-linux-musl-aarch64.so'; + } + } elseif ($p['os'] === 'linux') { + $names[] = \sprintf('libmonolog_parser-linux-%s.so', $p['arch']); + if ($p['arch'] === 'arm64') { + $names[] = 'libmonolog_parser-linux-aarch64.so'; + } + // Do not fall back to musl on glibc — wrong ABI + } else { + $names[] = \sprintf('libmonolog_parser-%s-%s.%s', $p['os'], $p['arch'], $p['ext']); + } + + // Legacy unprefixed names (local `make install`) + $names[] = 'libmonolog_parser.' . $p['ext']; + + return $names; + } + + /** + * Human-readable platform string for diagnostics. + */ + public static function describe(): string + { + $p = self::platform(); + $candidates = self::candidateBasenames(); + + return \sprintf( + '%s libc=%s (php_uname=%s, looking for %s)', + $p['triple'], + $p['libc'], + php_uname('m'), + $candidates[0] ?? '?' + ); + } + + private static function safeLdd(string $binary): ?string + { + // Avoid shell if disabled; skip in restricted SAPIs + if (!\function_exists('shell_exec') || !\is_callable('shell_exec')) { + return null; + } + + // open_basedir / disabled functions may still kill this + try { + $cmd = 'ldd ' . escapeshellarg($binary) . ' 2>&1'; + $out = @shell_exec($cmd); + } catch (\Throwable) { + return null; + } + + return \is_string($out) ? $out : null; + } +} diff --git a/src/Components/Log/PregMonologLogReader.php b/src/Components/Log/PregMonologLogReader.php new file mode 100644 index 00000000..073edf7f --- /dev/null +++ b/src/Components/Log/PregMonologLogReader.php @@ -0,0 +1,49 @@ +seek(\PHP_INT_MAX); + $total = $file->key(); + + $entries = []; + $reader = new \LimitIterator(LineReader::readLinesBackwards($filePath), $offset, $limit); + /** @var string $line */ + foreach ($reader as $line) { + $entries[] = $this->parser->parse($line); + } + + return [ + 'entries' => $entries, + 'total' => $total, + ]; + } + + public function yieldBackwards(string $filePath): \Generator + { + foreach (LineReader::readLinesBackwards($filePath) as $line) { + yield $this->parser->parse($line); + } + } + + public function backend(): string + { + return 'preg'; + } +} diff --git a/src/Components/Log/PregMonologParser.php b/src/Components/Log/PregMonologParser.php new file mode 100644 index 00000000..0d9f4c2f --- /dev/null +++ b/src/Components/Log/PregMonologParser.php @@ -0,0 +1,40 @@ +.*)] (?.*)\.(?(DEBUG|INFO|NOTICE|WARNING|ERROR|CRITICAL|ALERT|EMERGENCY)):(?.*)/m'; + + public function parse(string $line): array + { + if (preg_match(self::LINE_MATCH, $line, $matches) !== 1) { + return [ + 'message' => $line, + 'channel' => 'unknown', + 'date' => 'unknown', + 'level' => 'unknown', + ]; + } + + return [ + 'message' => $matches['message'], + 'channel' => $matches['channel'], + 'date' => $matches['date'], + 'level' => $matches['level'], + ]; + } + + public function backend(): string + { + return 'preg'; + } +} diff --git a/src/Controller/LogController.php b/src/Controller/LogController.php index ba18402d..d55b3c90 100644 --- a/src/Controller/LogController.php +++ b/src/Controller/LogController.php @@ -4,7 +4,7 @@ namespace Frosh\Tools\Controller; -use Frosh\Tools\Components\LineReader; +use Frosh\Tools\Components\Log\MonologLogReaderInterface; use Shopware\Core\Framework\Routing\RoutingException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\DependencyInjection\Attribute\Autowire; @@ -17,14 +17,12 @@ #[Route(path: '/api/_action/frosh-tools', defaults: ['_routeScope' => ['api'], '_acl' => ['frosh_tools:read']])] class LogController extends AbstractController { - // https://regex101.com/r/bp4YYL/1 - private const LINE_MATCH = '/\[(?.*)] (?.*)\.(?(DEBUG|INFO|NOTICE|WARNING|ERROR|CRITICAL|ALERT|EMERGENCY)):(?.*)/m'; - private readonly string $logDir; public function __construct( #[Autowire(param: 'kernel.logs_dir')] string $logDir, + private readonly MonologLogReaderInterface $monologLogReader, ) { $this->logDir = rtrim($logDir, '/') . '/'; } @@ -42,36 +40,14 @@ public function getLog(Request $request): Response $offset = $request->query->getInt('offset'); $limit = $request->query->getInt('limit', 20); - $lineGenerator = LineReader::readLinesBackwards($filePath); - $file = new \SplFileObject($filePath, 'r'); - $file->seek(\PHP_INT_MAX); - - $reader = new \LimitIterator($lineGenerator, $offset, $limit); - - $result = []; - - /** @var string $item */ - foreach ($reader as $item) { - if (preg_match(self::LINE_MATCH, $item, $matches) !== 1) { - $result[] = [ - 'message' => $item, - 'channel' => 'unknown', - 'date' => 'unknown', - 'level' => 'unknown', - ]; - - continue; - } - - $result[] = [ - 'message' => $matches['message'], - 'channel' => $matches['channel'], - 'date' => $matches['date'], - 'level' => $matches['level'], - ]; - } + // Native path: open file in C, reverse-scan + SIMD parse, return page. + // Fallback path: PHP LineReader + preg_match (same response shape). + $page = $this->monologLogReader->readBackwards($filePath, $offset, $limit); - return new JsonResponse($result, Response::HTTP_OK, ['file-size' => $file->key()]); + return new JsonResponse($page['entries'], Response::HTTP_OK, [ + 'file-size' => $page['total'], + 'x-monolog-parser' => $this->monologLogReader->backend(), + ]); } private function getFilePathByBag(Request $request): string diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php index 7e71980a..c754015a 100644 --- a/src/Resources/config/services.php +++ b/src/Resources/config/services.php @@ -2,6 +2,10 @@ declare(strict_types=1); +use Frosh\Tools\Components\Log\MonologLineParserFactory; +use Frosh\Tools\Components\Log\MonologLineParserInterface; +use Frosh\Tools\Components\Log\MonologLogReaderFactory; +use Frosh\Tools\Components\Log\MonologLogReaderInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $container): void { @@ -12,4 +16,10 @@ $services->load('Frosh\Tools\\', '../../') ->exclude('../../{DependencyInjection,Resources,FroshTools.php}'); + + $services->set(MonologLineParserInterface::class) + ->factory([MonologLineParserFactory::class, 'create']); + + $services->set(MonologLogReaderInterface::class) + ->factory([MonologLogReaderFactory::class, 'create']); }; diff --git a/tests/Unit/Log/PregMonologParserTest.php b/tests/Unit/Log/PregMonologParserTest.php new file mode 100644 index 00000000..ff04356c --- /dev/null +++ b/tests/Unit/Log/PregMonologParserTest.php @@ -0,0 +1,69 @@ +parser = new PregMonologParser(); + } + + public function testParsesStandardLine(): void + { + $line = '[2024-03-15T10:30:00.123456+00:00] request.ERROR: Uncaught PHP Exception [] []'; + $entry = $this->parser->parse($line); + + static::assertSame('2024-03-15T10:30:00.123456+00:00', $entry['date']); + static::assertSame('request', $entry['channel']); + static::assertSame('ERROR', $entry['level']); + static::assertSame(' Uncaught PHP Exception [] []', $entry['message']); + } + + public function testDottedChannel(): void + { + $line = '[2024-03-15T10:29:59.000000+00:00] doctrine.dbal.INFO: Connecting [] []'; + $entry = $this->parser->parse($line); + + static::assertSame('doctrine.dbal', $entry['channel']); + static::assertSame('INFO', $entry['level']); + } + + public function testUnmatchedFallsBackToUnknown(): void + { + $entry = $this->parser->parse('not a log line'); + + static::assertSame('unknown', $entry['channel']); + static::assertSame('unknown', $entry['level']); + static::assertSame('unknown', $entry['date']); + static::assertSame('not a log line', $entry['message']); + } + + /** + * @return iterable + */ + public static function levelsProvider(): iterable + { + foreach (['DEBUG', 'INFO', 'NOTICE', 'WARNING', 'ERROR', 'CRITICAL', 'ALERT', 'EMERGENCY'] as $level) { + yield $level => [$level]; + } + } + + #[DataProvider('levelsProvider')] + public function testAllLevels(string $level): void + { + $line = \sprintf('[2024-01-01T00:00:00+00:00] ch.%s: msg', $level); + $entry = $this->parser->parse($line); + static::assertSame($level, $entry['level']); + } +} From bbb186b2e3b7b163dc0a72471e73c37d6cc6ca5e Mon Sep 17 00:00:00 2001 From: Shyim Date: Mon, 10 Aug 2026 11:43:32 +0000 Subject: [PATCH 2/3] style(log-viewer): format native parser classes --- src/Components/Log/FfiMonologLogReader.php | 2 +- src/Components/Log/FfiMonologParser.php | 4 +++- src/Components/Log/MonologLineParserFactory.php | 4 +++- src/Components/Log/MonologLogReaderFactory.php | 4 +++- src/Components/Log/NativeLibraryLocator.php | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Components/Log/FfiMonologLogReader.php b/src/Components/Log/FfiMonologLogReader.php index e605f1f4..4aeab286 100644 --- a/src/Components/Log/FfiMonologLogReader.php +++ b/src/Components/Log/FfiMonologLogReader.php @@ -112,7 +112,7 @@ public function readBackwards(string $filePath, int $offset, int $limit): array $count = (int) $this->ffi->monolog_page_count($pagePtr); $entries = []; - for ($i = 0; $i < $count; $i++) { + for ($i = 0; $i < $count; ++$i) { $entry = $this->ffi->monolog_page_entry($pagePtr, $i); if ($entry === null) { continue; diff --git a/src/Components/Log/FfiMonologParser.php b/src/Components/Log/FfiMonologParser.php index 2b818dda..1622a299 100644 --- a/src/Components/Log/FfiMonologParser.php +++ b/src/Components/Log/FfiMonologParser.php @@ -113,7 +113,9 @@ public function version(): string return (string) $this->ffi->monolog_parser_version(); } - /** @deprecated use NativeLibraryLocator::locate() */ + /** + * @deprecated use NativeLibraryLocator::locate() + */ public static function detectLibraryPath(): ?string { return NativeLibraryLocator::locate(); diff --git a/src/Components/Log/MonologLineParserFactory.php b/src/Components/Log/MonologLineParserFactory.php index b3fbaa93..67ea6572 100644 --- a/src/Components/Log/MonologLineParserFactory.php +++ b/src/Components/Log/MonologLineParserFactory.php @@ -32,7 +32,9 @@ public static function create(bool $forcePreg = false): MonologLineParserInterfa return self::$cached; } - /** @internal tests */ + /** + * @internal tests + */ public static function reset(): void { self::$cached = null; diff --git a/src/Components/Log/MonologLogReaderFactory.php b/src/Components/Log/MonologLogReaderFactory.php index a2d61107..425cbce4 100644 --- a/src/Components/Log/MonologLogReaderFactory.php +++ b/src/Components/Log/MonologLogReaderFactory.php @@ -32,7 +32,9 @@ public static function create(bool $forcePreg = false): MonologLogReaderInterfac return self::$cached; } - /** @internal tests */ + /** + * @internal tests + */ public static function reset(): void { self::$cached = null; diff --git a/src/Components/Log/NativeLibraryLocator.php b/src/Components/Log/NativeLibraryLocator.php index b0f0e3b1..89f04d2d 100644 --- a/src/Components/Log/NativeLibraryLocator.php +++ b/src/Components/Log/NativeLibraryLocator.php @@ -191,7 +191,7 @@ public static function candidateBasenames(): array if ($p['arch'] === 'arm64') { $names[] = 'libmonolog_parser-linux-aarch64.so'; } - // Do not fall back to musl on glibc — wrong ABI + // Do not fall back to musl on glibc — wrong ABI } else { $names[] = \sprintf('libmonolog_parser-%s-%s.%s', $p['os'], $p['arch'], $p['ext']); } From acaccb3e9b68cff367598947491e2e749000b2e0 Mon Sep 17 00:00:00 2001 From: Shyim Date: Mon, 10 Aug 2026 12:10:02 +0000 Subject: [PATCH 3/3] fix: resolve extension validation errors --- phpstan.neon.dist | 18 ++++++++++++++++++ src/Command/ExtensionChecksumCheckCommand.php | 8 ++++---- src/Command/ExtensionChecksumCreateCommand.php | 4 ++-- src/Command/MonitorCommand.php | 4 ++-- src/Components/Log/NativeLibraryLocator.php | 4 ++-- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index b8a6666e..7b689ac4 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,5 +6,23 @@ parameters: ignoreErrors: - message: "#no value type specified in iterable type array#" + # FFI symbols and CData fields are defined by the native CDEF at runtime. + - + message: "#Call to an undefined method FFI::#" + paths: + - src/Components/Log/FfiMonologLogReader.php + - src/Components/Log/FfiMonologParser.php + - + message: "#Access to an undefined property FFI\\\\CData::#" + paths: + - src/Components/Log/FfiMonologLogReader.php + - src/Components/Log/FfiMonologParser.php + - + message: "#Strict comparison using === between FFI\\\\CData and null will always evaluate to false.#" + path: src/Components/Log/FfiMonologLogReader.php + # EntityRepository::search() returns EntitySearchResult on supported 6.6/6.7 releases. + # Its v6.8 hierarchy transition is handled by Shopware core; this plugin only reads entities. + - + message: '#Call to method (getEntities|first)\(\) of deprecated class Shopware\\Core\\Framework\\DataAbstractionLayer\\Search\\EntitySearchResult#' excludePaths: - vendor (?) diff --git a/src/Command/ExtensionChecksumCheckCommand.php b/src/Command/ExtensionChecksumCheckCommand.php index 607ee172..29a2c24d 100644 --- a/src/Command/ExtensionChecksumCheckCommand.php +++ b/src/Command/ExtensionChecksumCheckCommand.php @@ -5,7 +5,6 @@ namespace Frosh\Tools\Command; use Frosh\Tools\Components\ExtensionChecksum\ExtensionFileHashService; -use Shopware\Core\Framework\Adapter\Console\ShopwareStyle; use Shopware\Core\Framework\Context; use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository; use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria; @@ -17,6 +16,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( name: 'frosh:extension:checksum:check', @@ -44,7 +44,7 @@ protected function configure(): void */ protected function execute(InputInterface $input, OutputInterface $output): int { - $io = new ShopwareStyle($input, $output); + $io = new SymfonyStyle($input, $output); $extensions = $this->getExtension((string) $input->getArgument('extension'), $io); if ($extensions->count() < 1) { @@ -93,7 +93,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $success ? self::SUCCESS : self::FAILURE; } - private function getExtension(string $name, ShopwareStyle $io): PluginCollection + private function getExtension(string $name, SymfonyStyle $io): PluginCollection { // @phpstan-ignore-next-line $context = method_exists(Context::class, 'createCLIContext') ? Context::createCLIContext() : Context::createDefaultContext(); @@ -124,7 +124,7 @@ private function getExtension(string $name, ShopwareStyle $io): PluginCollection /** * @param string[] $files */ - private function outputFileChanges(ShopwareStyle $io, string $text, array $files): void + private function outputFileChanges(SymfonyStyle $io, string $text, array $files): void { if ($files) { $io->warning($text); diff --git a/src/Command/ExtensionChecksumCreateCommand.php b/src/Command/ExtensionChecksumCreateCommand.php index 8d2d9e26..5f9d5b3a 100644 --- a/src/Command/ExtensionChecksumCreateCommand.php +++ b/src/Command/ExtensionChecksumCreateCommand.php @@ -5,7 +5,6 @@ namespace Frosh\Tools\Command; use Frosh\Tools\Components\ExtensionChecksum\ExtensionFileHashService; -use Shopware\Core\Framework\Adapter\Console\ShopwareStyle; use Shopware\Core\Framework\Context; use Shopware\Core\Framework\DataAbstractionLayer\Entity; use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository; @@ -18,6 +17,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( name: 'frosh:extension:checksum:create', @@ -45,7 +45,7 @@ protected function configure(): void */ protected function execute(InputInterface $input, OutputInterface $output): int { - $io = new ShopwareStyle($input, $output); + $io = new SymfonyStyle($input, $output); // @phpstan-ignore-next-line $context = method_exists(Context::class, 'createCLIContext') ? Context::createCLIContext() : Context::createDefaultContext(); diff --git a/src/Command/MonitorCommand.php b/src/Command/MonitorCommand.php index b0fd959d..71006e10 100644 --- a/src/Command/MonitorCommand.php +++ b/src/Command/MonitorCommand.php @@ -10,7 +10,6 @@ use Frosh\Tools\Components\Health\SettingsResult; use Shopware\Core\Content\Mail\Service\AbstractMailSender; use Shopware\Core\Content\Mail\Service\MailSender; -use Shopware\Core\Framework\Adapter\Console\ShopwareStyle; use Shopware\Core\System\SystemConfig\SystemConfigService; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -18,6 +17,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Mime\Email; @@ -46,7 +46,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $io = new ShopwareStyle($input, $output); + $io = new SymfonyStyle($input, $output); if ($input->getArgument('sales-channel')) { $io->warning('The sales channel argument is deprecated and has no effect. It will be removed in a future release.'); diff --git a/src/Components/Log/NativeLibraryLocator.php b/src/Components/Log/NativeLibraryLocator.php index 89f04d2d..9656d4ba 100644 --- a/src/Components/Log/NativeLibraryLocator.php +++ b/src/Components/Log/NativeLibraryLocator.php @@ -154,7 +154,7 @@ public static function isMusl(): bool } // Inspect what PHP itself is linked against - if (\defined('PHP_BINARY') && \is_string(\PHP_BINARY) && \PHP_BINARY !== '' && is_readable(\PHP_BINARY)) { + if (is_readable(\PHP_BINARY)) { $ldd = self::safeLdd(\PHP_BINARY); if ($ldd !== null) { if (str_contains($ldd, 'musl')) { @@ -222,7 +222,7 @@ public static function describe(): string private static function safeLdd(string $binary): ?string { // Avoid shell if disabled; skip in restricted SAPIs - if (!\function_exists('shell_exec') || !\is_callable('shell_exec')) { + if (!\function_exists('shell_exec')) { return null; }