Compare commits
62
Commits
v0.1.0
...
fc8de1a5b0
@@ -0,0 +1,45 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What this is
|
||||
|
||||
ottyl turns a piped command's stdout (or a tailed file) into a local observability dashboard: searchable log timeline, ad-hoc metrics extracted from log lines via regex, and Grafana-style dashboards — all served from a single self-contained binary for the lifetime of that process. It can also poll a Prometheus/OpenMetrics endpoint alongside log ingestion. See `README.md` for the user-facing feature list and CLI usage.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
make build # builds webapp (npm install + vite build) then the release Rust binary; webapp/dist is embedded into the binary
|
||||
make dev # frontend dev server with hot reload, proxying API calls to a separately running `cargo run` backend (see webapp/vite.config.ts)
|
||||
make test # cargo test + frontend build (frontend has no test suite, just `tsc -b && vite build`)
|
||||
make lint # cargo clippy --all-targets + oxlint (webapp)
|
||||
make run # build then run ./target/release/ottyl
|
||||
```
|
||||
|
||||
- Single backend test: `cargo test <test_name>`
|
||||
- Backend tests live inline in each `src/*.rs` file (`#[cfg(test)] mod tests`), not in a separate `tests/` dir.
|
||||
- The webapp has no unit tests — `npm run build` (tsc + vite) is the only frontend check besides lint.
|
||||
- `webapp/dist` must exist before `cargo build` will produce a working binary (it's embedded via `fs_embed!`), so always build the frontend first — `make build` handles ordering; don't run `cargo build` alone after frontend changes without rebuilding `webapp/dist`.
|
||||
|
||||
## Architecture
|
||||
|
||||
Backend: Rust (axum + tokio), single binary, in `src/`. Frontend: React 19 + TypeScript + Tailwind + Recharts, in `webapp/`, built with Vite and embedded into the binary at build time (`src/api.rs`'s `WEBAPP_DIST` via `fs_embed!("webapp/dist")`).
|
||||
|
||||
**Data flow:** `main.rs` wires everything together at startup — loads `Settings`, spawns the engine actor task, replays the active profile's param/metric declarations into it (before ingest starts, since there's no way to reprocess earlier lines later), then starts ingest (stdin/file tailing and/or Prometheus polling) and the axum HTTP server.
|
||||
|
||||
- **`engine.rs`** (largest file, ~3400 lines) — the actor owning all live state: ingested log lines, declared params/metrics, computed metric buckets, alert evaluation. Runs as a single tokio task; all access is through `EngineHandle` sending `EngineMessage` over an mpsc channel and awaiting a response via oneshot — there is no shared mutable engine state accessed directly from request handlers. This is the file to read first to understand how declarations, filtering, and bucketing actually work.
|
||||
- **`api.rs`** — axum routes and HTTP-facing `ApiState` (holds `EngineHandle` + `SettingsHandle`). Translates HTTP requests into `EngineMessage`s and DTOs back into JSON. Also serves the embedded webapp.
|
||||
- **`settings.rs` / `profile.rs`** — persisted configuration. A **profile** is per-app: parsing rules (param/metric declarations) and dashboards, saved so they're there again next time that app runs through ottyl (see README's "Profiles"). Declarations are persisted here so the engine can replay them on restart; `api.rs` handlers that mutate engine state (e.g. `declare_metric`) call a corresponding persist method here so the two stay in sync — this dual-write is a recurring pattern (see `ApiState::persist_param_declaration` / `persist_metric_declaration` in `api.rs`), and adding a new declaration-mutating endpoint means updating both sides plus the load-time replay path.
|
||||
- **`filter_parser.rs`** (grammar in `filter_parser.pest`, via `pest`) + **`filter_datetime.rs`** — parse the search/filter bar syntax (regex matches, metric comparisons like `latency_ms[value]<200`, relative/absolute datetimes).
|
||||
- **`promql.rs`** — parses and evaluates the PromQL-style queries backing dashboard panels.
|
||||
- **`logfmt.rs` / `ingest.rs`** — line ingestion: tailing stdin/a file, structured logfmt-style extraction, and polling a Prometheus/OpenMetrics `--metrics-url`.
|
||||
- **`alerts.rs`** — alert rule evaluation and ntfy delivery.
|
||||
- **`dto.rs`** — shared request/response types between `api.rs` and the frontend's `webapp/src/lib/api.ts`; **`data_value.rs`** / **`metric_primitive.rs`** — the value/type model for params and metrics; **`index_key.rs`** / **`rate.rs`** — bucket indexing and rate computation helpers.
|
||||
|
||||
Frontend structure: `webapp/src/App.tsx` is the shell; `webapp/src/components/` has one component per feature area (`FilterBar`, `LogTimeline`, `Metrics`, `Params`, `Dashboards`, `Alerts`, `Docs`, `NavMenu`, ...) that map closely to the backend concepts above. `webapp/src/lib/api.ts` is the typed client for the Rust API; `webapp/src/lib/polling.ts` handles refresh polling; `webapp/src/lib/themes.ts` covers the style-preset/CSS system.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **`tickets/`** holds scoped, written-up work items (problem statement, pointers to exact call sites with `file.rs:line` references, and an explicit scope section) — check here for planned/in-flight work before starting something that might overlap. Tickets are removed from this directory once implemented (see recent commit history — e.g. "Remove ticket 047 (implemented: ...)").
|
||||
- Declaration-related features (params, metrics) consistently need three coordinated changes: an `EngineMessage` variant + handler in `engine.rs`, a persistence method in `settings.rs`/`profile.rs`, and an `api.rs` route wiring them together (plus a webapp UI affordance). Grep for an existing similar declaration (e.g. how metric declaration is wired) as the template before adding a new one.
|
||||
- Doc comments in this codebase tend to explain *why* a non-obvious choice was made (e.g. ordering constraints, a workaround, a past ticket's rationale) rather than restating what the code does — match that style rather than adding descriptive comments.
|
||||
Generated
+308
@@ -70,6 +70,12 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
@@ -200,6 +206,7 @@ dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
@@ -265,6 +272,40 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
|
||||
dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "6.0.0"
|
||||
@@ -297,6 +338,18 @@ dependencies = [
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
@@ -355,6 +408,21 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.34"
|
||||
@@ -362,6 +430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -370,6 +439,40 @@ version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
@@ -382,8 +485,13 @@ version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
@@ -415,6 +523,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -639,6 +753,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
@@ -679,6 +799,18 @@ dependencies = [
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.1"
|
||||
@@ -774,6 +906,12 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.3"
|
||||
@@ -785,6 +923,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -816,6 +964,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
name = "ottyl"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"chrono",
|
||||
"clap",
|
||||
@@ -824,10 +973,13 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"humantime",
|
||||
"include_dir",
|
||||
"nom",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"petname",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rmcp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
@@ -858,6 +1010,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -906,6 +1064,27 @@ dependencies = [
|
||||
"pest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "petname"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfb4653a82ec48e6d324c788597aadf46449bfc7b8e76d2a03b513a944325c88"
|
||||
dependencies = [
|
||||
"petname-macros",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "petname-macros"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c406fb544296f598ccf69b82bcf1b12440c8f23ebf75946105f86304f16eb2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
@@ -1065,6 +1244,26 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
|
||||
dependencies = [
|
||||
"ref-cast-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast-impl"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
@@ -1146,6 +1345,41 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmcp"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42b6914fac0be956fe704a38239c3f44a9f841d1b06a5713d2f638065593f5b5"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"futures",
|
||||
"indexmap",
|
||||
"pastey",
|
||||
"pin-project-lite",
|
||||
"rmcp-macros",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmcp-macros"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdf1c49bd4d52014b94db0877410db273c2008f01628b0252a2e9460ad9b7fda"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_json",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.3"
|
||||
@@ -1221,6 +1455,32 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -1257,6 +1517,17 @@ dependencies = [
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
@@ -1493,6 +1764,20 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"libc",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
@@ -1547,9 +1832,21 @@ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
@@ -1607,6 +1904,17 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
|
||||
+5
-1
@@ -10,6 +10,7 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
regex = "1"
|
||||
nom = "7"
|
||||
pest = "2"
|
||||
pest_derive = "2"
|
||||
chrono = "0.4"
|
||||
@@ -17,7 +18,10 @@ humantime = "2"
|
||||
directories = "6.0.0"
|
||||
fs-embed = "0.2.4"
|
||||
include_dir = "0.7.4"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
||||
petname = { version = "3.2.0", default-features = false, features = ["default-rng", "default-words"] }
|
||||
rmcp = { version = "3.2.0", default-features = false, features = ["server", "macros", "transport-io"] }
|
||||
anyhow = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.27.0"
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
name: ottyl
|
||||
description: Query and configure a running ottyl observability instance (logs, params, metrics, dashboards) via its MCP server.
|
||||
---
|
||||
|
||||
# ottyl
|
||||
|
||||
ottyl turns a piped command's stdout into a searchable log timeline plus ad-hoc metrics and
|
||||
dashboards, served from a single process. This doc covers how an agent connects to and drives a
|
||||
*running* instance.
|
||||
|
||||
## Find a session
|
||||
|
||||
Each `ottyl` process registers itself under a name (random, e.g. `comic-gazelle`, unless started
|
||||
with `--name <name>`). List known sessions:
|
||||
|
||||
```
|
||||
ottyl sessions
|
||||
# comic-gazelle port=51423 pid=8421
|
||||
```
|
||||
|
||||
## Connect an MCP client
|
||||
|
||||
Start the MCP server (a separate short-lived process) pointed at that session, and register it
|
||||
with your MCP client (e.g. Claude Code's `.mcp.json`):
|
||||
|
||||
```
|
||||
ottyl mcp --session comic-gazelle
|
||||
# or, if you already know the port:
|
||||
ottyl mcp --port 51423
|
||||
```
|
||||
|
||||
This exposes six tools, each a thin proxy onto the target instance's HTTP API.
|
||||
|
||||
## Tools
|
||||
|
||||
### `query_logs`
|
||||
Count or read log lines matching a FilterBar query.
|
||||
|
||||
```json
|
||||
{ "filter": "regex(\"error\")", "count_only": true }
|
||||
```
|
||||
|
||||
### `declare_param`
|
||||
Extract a named value from matching log lines.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "status_code",
|
||||
"message_match_regex": "status=(\\d+)",
|
||||
"capture_group": "1",
|
||||
"kind": "number"
|
||||
}
|
||||
```
|
||||
|
||||
### `declare_metric`
|
||||
Bucket event counts/values by label, derived from matching log lines.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "errors_total",
|
||||
"message_match_regex": "level=error",
|
||||
"label_params": ["status_code"]
|
||||
}
|
||||
```
|
||||
|
||||
### `create_dashboard`
|
||||
Create an empty dashboard in a profile (defaults to the active profile).
|
||||
|
||||
```json
|
||||
{ "name": "Errors" }
|
||||
```
|
||||
|
||||
### `add_panel`
|
||||
Add a panel to a dashboard, or update one in place by `panel_id`.
|
||||
|
||||
```json
|
||||
{
|
||||
"dashboard_id": "<id from create_dashboard>",
|
||||
"title": "Errors over time",
|
||||
"promql": "rate(errors_total[5m])",
|
||||
"chart_type": "line"
|
||||
}
|
||||
```
|
||||
|
||||
### `get_dashboard`
|
||||
Read a dashboard's current panels.
|
||||
|
||||
```json
|
||||
{ "dashboard_id": "<id>" }
|
||||
```
|
||||
+122
-48
@@ -1,9 +1,16 @@
|
||||
//! Alert rules: definitions, trigger evaluation, and ntfy delivery.
|
||||
//!
|
||||
//! Alert queries are restricted to regex/sequence/timestamp/json[field] — a metric[param] comparison would require a full bucket rescan per line, breaking the proportional-cost guarantee. json[field] is fine here since, like regex/sequence/timestamp, it only inspects the line's own text/fields, no index lookup.
|
||||
//! Alert queries are restricted to regex/ottyl_seq/ottyl_ts/ottyl_fmt/json[field] — a metric[param] comparison would require a full bucket rescan per line, breaking the proportional-cost guarantee. json[field] is fine here since, like regex/ottyl_seq/ottyl_ts/ottyl_fmt, it only inspects the line's own text/fields, no index lookup.
|
||||
//!
|
||||
//! `Threshold`/`Ratio` windows are maintained via `crate::rate::RateWindow` — the same windowed-rate
|
||||
//! implementation PromQL's `rate()` uses (`crate::engine::evaluate_promql`) — rather than a
|
||||
//! bespoke `VecDeque`+prune loop, so the two agree on what "windowed rate" means (ticket 040). A
|
||||
//! window is also backfilled once, on first evaluation, from already-ingested logs within its own
|
||||
//! trailing window (see `backfill_window`): a rule created mid-session (or a process that replays
|
||||
//! existing log content, e.g. `--file` tailing) no longer starts with a silently empty window.
|
||||
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io;
|
||||
@@ -13,9 +20,12 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::engine::{apply_compare_op, eval_json_comparison};
|
||||
use crate::data_value::DataValue;
|
||||
use crate::engine::{self, apply_compare_op, eval_json_comparison};
|
||||
use crate::filter_datetime;
|
||||
use crate::filter_parser::{self, CompareOp, FilterExpr, FilterValue};
|
||||
use crate::index_key::IndexKey;
|
||||
use crate::rate::RateWindow;
|
||||
use crate::settings::Settings;
|
||||
use crate::timestamp::Timestamp;
|
||||
|
||||
@@ -135,24 +145,43 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-rule in-memory evaluation state; lives only for the process's lifetime — not persisted, not backfilled against already-ingested logs.
|
||||
/// Per-rule in-memory evaluation state; lives only for the process's lifetime — not persisted
|
||||
/// across a restart with no replayed log history. Each `RateWindow` is backfilled once (see
|
||||
/// `backfill_window`) from whatever matching history is already in the index at first evaluation,
|
||||
/// so — unlike before ticket 040 — a rule doesn't start blind to matches ingested before it was
|
||||
/// declared, or before this evaluation loop last started, as long as that history is still in the
|
||||
/// in-memory log index (e.g. `--file` tailing replays existing file content from the start on
|
||||
/// every process start).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AlertRuntimeState {
|
||||
threshold_matches: VecDeque<i64>,
|
||||
ratio_a_matches: VecDeque<i64>,
|
||||
ratio_b_matches: VecDeque<i64>,
|
||||
threshold_window: RateWindow,
|
||||
threshold_backfilled: bool,
|
||||
ratio_a_window: RateWindow,
|
||||
ratio_a_backfilled: bool,
|
||||
ratio_b_window: RateWindow,
|
||||
ratio_b_backfilled: bool,
|
||||
seen_values: HashSet<String>,
|
||||
}
|
||||
|
||||
/// `timestamps` is push_back-ordered, so once the front entry is within the window, every entry behind it is too.
|
||||
fn prune_window(timestamps: &mut VecDeque<i64>, now_ms: i64, window_ms: i64) {
|
||||
while let Some(&front) = timestamps.front() {
|
||||
if now_ms - front > window_ms {
|
||||
timestamps.pop_front();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
/// Builds a `RateWindow` seeded from already-ingested logs matching `query`, bounded to
|
||||
/// `window_ms` before `now` (not the full log history) so the cost stays proportional to the
|
||||
/// window rather than everything ever ingested. `before_sequence` excludes the line currently
|
||||
/// being evaluated, which the caller pushes separately if it matches.
|
||||
fn backfill_window(
|
||||
index: &BTreeMap<IndexKey, DataValue>,
|
||||
query: &str,
|
||||
before_sequence: u64,
|
||||
now: Timestamp,
|
||||
window_ms: i64,
|
||||
) -> RateWindow {
|
||||
let since_ms = now.as_millis() - window_ms;
|
||||
let matching_timestamps =
|
||||
engine::logs_since(index, before_sequence, since_ms, now.as_millis()).filter_map(
|
||||
|(sequence, timestamp, text)| {
|
||||
(eval_alert_query(query, sequence, timestamp, text) == Ok(true)).then_some(timestamp)
|
||||
},
|
||||
);
|
||||
RateWindow::backfill(matching_timestamps, now, window_ms)
|
||||
}
|
||||
|
||||
/// Evaluates a FilterBar query against a single line's own fields only (metric[param] comparisons aren't supported — see module doc; json[field] is supported since it's also line-local).
|
||||
@@ -167,16 +196,16 @@ fn eval_alert_expr(expr: &FilterExpr, sequence: u64, timestamp: Timestamp, text:
|
||||
let re = Regex::new(pattern).map_err(|err| format!("invalid regex in alert query: {err}"))?;
|
||||
Ok(re.is_match(text))
|
||||
}
|
||||
FilterExpr::Comparison { metric, param: None, op, value } if metric == "sequence" => {
|
||||
FilterExpr::Comparison { metric, param: None, op, value } if metric == "ottyl_seq" => {
|
||||
let target = match value {
|
||||
FilterValue::Number(n) => *n,
|
||||
FilterValue::Raw(raw) => raw
|
||||
.parse::<f64>()
|
||||
.map_err(|_| format!("'sequence' comparisons require a numeric value, got {raw:?}"))?,
|
||||
.map_err(|_| format!("'ottyl_seq' comparisons require a numeric value, got {raw:?}"))?,
|
||||
};
|
||||
Ok(apply_compare_op((sequence as f64).total_cmp(&target), *op))
|
||||
}
|
||||
FilterExpr::Comparison { metric, param: None, op, value } if metric == "timestamp" => {
|
||||
FilterExpr::Comparison { metric, param: None, op, value } if metric == "ottyl_ts" => {
|
||||
let target_ms = match value {
|
||||
FilterValue::Number(n) => *n as i64,
|
||||
FilterValue::Raw(raw) => filter_datetime::parse_datetime(raw, Timestamp::now())
|
||||
@@ -185,8 +214,15 @@ fn eval_alert_expr(expr: &FilterExpr, sequence: u64, timestamp: Timestamp, text:
|
||||
};
|
||||
Ok(apply_compare_op(timestamp.as_millis().cmp(&target_ms), *op))
|
||||
}
|
||||
FilterExpr::Comparison { metric, param: None, op, value } if metric == "ottyl_fmt" => {
|
||||
let target = match value {
|
||||
FilterValue::Number(n) => n.to_string(),
|
||||
FilterValue::Raw(raw) => raw.clone(),
|
||||
};
|
||||
Ok(apply_compare_op(crate::logfmt::detect_format(text).cmp(target.as_str()), *op))
|
||||
}
|
||||
FilterExpr::Comparison { metric, .. } => Err(format!(
|
||||
"alert queries only support regex()/sequence/timestamp (not metric comparisons like \
|
||||
"alert queries only support regex()/ottyl_seq/ottyl_ts/ottyl_fmt (not metric comparisons like \
|
||||
'{metric}[param]'): metric-based alert conditions aren't supported yet"
|
||||
)),
|
||||
FilterExpr::JsonComparison { field, op, value } => {
|
||||
@@ -205,6 +241,7 @@ fn eval_alert_expr(expr: &FilterExpr, sequence: u64, timestamp: Timestamp, text:
|
||||
pub fn evaluate_alerts(
|
||||
rules: &[AlertRule],
|
||||
states: &mut HashMap<String, AlertRuntimeState>,
|
||||
index: &BTreeMap<IndexKey, DataValue>,
|
||||
sequence: u64,
|
||||
timestamp: Timestamp,
|
||||
text: &str,
|
||||
@@ -215,7 +252,9 @@ pub fn evaluate_alerts(
|
||||
continue;
|
||||
}
|
||||
let state = states.entry(rule.id.clone()).or_default();
|
||||
if let Some(message) = evaluate_condition(&rule.condition, state, sequence, timestamp, text, &rule.name) {
|
||||
if let Some(message) =
|
||||
evaluate_condition(&rule.condition, state, index, sequence, timestamp, text, &rule.name)
|
||||
{
|
||||
fired.push((rule.clone(), message));
|
||||
}
|
||||
}
|
||||
@@ -225,6 +264,7 @@ pub fn evaluate_alerts(
|
||||
fn evaluate_condition(
|
||||
condition: &AlertCondition,
|
||||
state: &mut AlertRuntimeState,
|
||||
index: &BTreeMap<IndexKey, DataValue>,
|
||||
sequence: u64,
|
||||
timestamp: Timestamp,
|
||||
text: &str,
|
||||
@@ -240,12 +280,14 @@ fn evaluate_condition(
|
||||
}
|
||||
},
|
||||
AlertCondition::Threshold { query, op, threshold, window_ms } => {
|
||||
if !state.threshold_backfilled {
|
||||
state.threshold_window = backfill_window(index, query, sequence, timestamp, *window_ms);
|
||||
state.threshold_backfilled = true;
|
||||
}
|
||||
match eval_alert_query(query, sequence, timestamp, text) {
|
||||
Ok(true) => {
|
||||
let now_ms = timestamp.as_millis();
|
||||
state.threshold_matches.push_back(now_ms);
|
||||
prune_window(&mut state.threshold_matches, now_ms, *window_ms);
|
||||
let count = state.threshold_matches.len() as f64;
|
||||
state.threshold_window.push(timestamp, *window_ms);
|
||||
let count = state.threshold_window.count() as f64;
|
||||
apply_compare_op(count.total_cmp(threshold), *op).then(|| {
|
||||
format!("{rule_name}: match count {count} {op:?} threshold {threshold} (window {window_ms}ms)")
|
||||
})
|
||||
@@ -285,17 +327,22 @@ fn evaluate_condition(
|
||||
if !matched_a && !matched_b {
|
||||
return None;
|
||||
}
|
||||
let now_ms = timestamp.as_millis();
|
||||
if !state.ratio_a_backfilled {
|
||||
state.ratio_a_window = backfill_window(index, query_a, sequence, timestamp, *window_ms);
|
||||
state.ratio_a_backfilled = true;
|
||||
}
|
||||
if !state.ratio_b_backfilled {
|
||||
state.ratio_b_window = backfill_window(index, query_b, sequence, timestamp, *window_ms);
|
||||
state.ratio_b_backfilled = true;
|
||||
}
|
||||
if matched_a {
|
||||
state.ratio_a_matches.push_back(now_ms);
|
||||
prune_window(&mut state.ratio_a_matches, now_ms, *window_ms);
|
||||
state.ratio_a_window.push(timestamp, *window_ms);
|
||||
}
|
||||
if matched_b {
|
||||
state.ratio_b_matches.push_back(now_ms);
|
||||
prune_window(&mut state.ratio_b_matches, now_ms, *window_ms);
|
||||
state.ratio_b_window.push(timestamp, *window_ms);
|
||||
}
|
||||
let count_a = state.ratio_a_matches.len() as f64;
|
||||
let count_b = state.ratio_b_matches.len() as f64;
|
||||
let count_a = state.ratio_a_window.count() as f64;
|
||||
let count_b = state.ratio_b_window.count() as f64;
|
||||
if count_b == 0.0 {
|
||||
// Zero denominator: skip rather than treat the ratio as infinite.
|
||||
return None;
|
||||
@@ -417,12 +464,13 @@ mod tests {
|
||||
fn immediate_fires_on_matching_line_only() {
|
||||
let rules = vec![immediate_rule(r#"regex("error")"#)];
|
||||
let mut states = HashMap::new();
|
||||
let index: BTreeMap<IndexKey, DataValue> = BTreeMap::new();
|
||||
|
||||
let (seq, ts, text) = line("all good", 0, 0);
|
||||
assert!(evaluate_alerts(&rules, &mut states, seq, ts, text).is_empty());
|
||||
assert!(evaluate_alerts(&rules, &mut states, &index, seq, ts, text).is_empty());
|
||||
|
||||
let (seq, ts, text) = line("an error happened", 1, 1);
|
||||
let fired = evaluate_alerts(&rules, &mut states, seq, ts, text);
|
||||
let fired = evaluate_alerts(&rules, &mut states, &index, seq, ts, text);
|
||||
assert_eq!(fired.len(), 1);
|
||||
assert!(fired[0].1.contains("an error happened"));
|
||||
}
|
||||
@@ -432,8 +480,9 @@ mod tests {
|
||||
let mut rule = immediate_rule(r#"regex("error")"#);
|
||||
rule.enabled = false;
|
||||
let mut states = HashMap::new();
|
||||
let index: BTreeMap<IndexKey, DataValue> = BTreeMap::new();
|
||||
let (seq, ts, text) = line("error!", 0, 0);
|
||||
assert!(evaluate_alerts(&[rule], &mut states, seq, ts, text).is_empty());
|
||||
assert!(evaluate_alerts(&[rule], &mut states, &index, seq, ts, text).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -450,14 +499,15 @@ mod tests {
|
||||
},
|
||||
};
|
||||
let mut states = HashMap::new();
|
||||
let index: BTreeMap<IndexKey, DataValue> = BTreeMap::new();
|
||||
for (seq, ms) in [(0u64, 0i64), (1, 1000)] {
|
||||
let fired = evaluate_alerts(&[rule.clone()], &mut states, seq, Timestamp::from_millis(ms), "error");
|
||||
let fired = evaluate_alerts(&[rule.clone()], &mut states, &index, seq, Timestamp::from_millis(ms), "error");
|
||||
assert!(fired.is_empty(), "should not fire before threshold is crossed");
|
||||
}
|
||||
let fired = evaluate_alerts(&[rule.clone()], &mut states, 2, Timestamp::from_millis(2000), "error");
|
||||
let fired = evaluate_alerts(&[rule.clone()], &mut states, &index, 2, Timestamp::from_millis(2000), "error");
|
||||
assert_eq!(fired.len(), 1);
|
||||
// Stays quiet for a non-matching line even though the window is full.
|
||||
let fired = evaluate_alerts(&[rule], &mut states, 3, Timestamp::from_millis(2100), "fine");
|
||||
let fired = evaluate_alerts(&[rule], &mut states, &index, 3, Timestamp::from_millis(2100), "fine");
|
||||
assert!(fired.is_empty());
|
||||
}
|
||||
|
||||
@@ -475,9 +525,10 @@ mod tests {
|
||||
},
|
||||
};
|
||||
let mut states = HashMap::new();
|
||||
assert!(evaluate_alerts(&[rule.clone()], &mut states, 0, Timestamp::from_millis(0), "error").is_empty());
|
||||
let index: BTreeMap<IndexKey, DataValue> = BTreeMap::new();
|
||||
assert!(evaluate_alerts(&[rule.clone()], &mut states, &index, 0, Timestamp::from_millis(0), "error").is_empty());
|
||||
// Second match arrives well after the first has aged out of the window.
|
||||
assert!(evaluate_alerts(&[rule], &mut states, 1, Timestamp::from_millis(5_000), "error").is_empty());
|
||||
assert!(evaluate_alerts(&[rule], &mut states, &index, 1, Timestamp::from_millis(5_000), "error").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -492,11 +543,12 @@ mod tests {
|
||||
},
|
||||
};
|
||||
let mut states = HashMap::new();
|
||||
let fired = evaluate_alerts(&[rule.clone()], &mut states, 0, Timestamp::from_millis(0), "agent=curl");
|
||||
let index: BTreeMap<IndexKey, DataValue> = BTreeMap::new();
|
||||
let fired = evaluate_alerts(&[rule.clone()], &mut states, &index, 0, Timestamp::from_millis(0), "agent=curl");
|
||||
assert_eq!(fired.len(), 1);
|
||||
let fired = evaluate_alerts(&[rule.clone()], &mut states, 1, Timestamp::from_millis(1), "agent=curl");
|
||||
let fired = evaluate_alerts(&[rule.clone()], &mut states, &index, 1, Timestamp::from_millis(1), "agent=curl");
|
||||
assert!(fired.is_empty());
|
||||
let fired = evaluate_alerts(&[rule], &mut states, 2, Timestamp::from_millis(2), "agent=wget");
|
||||
let fired = evaluate_alerts(&[rule], &mut states, &index, 2, Timestamp::from_millis(2), "agent=wget");
|
||||
assert_eq!(fired.len(), 1);
|
||||
}
|
||||
|
||||
@@ -515,16 +567,37 @@ mod tests {
|
||||
},
|
||||
};
|
||||
let mut states = HashMap::new();
|
||||
let index: BTreeMap<IndexKey, DataValue> = BTreeMap::new();
|
||||
// Only an "error" so far; no "request" yet, so denominator is 0 -> skip.
|
||||
assert!(evaluate_alerts(&[rule.clone()], &mut states, 0, Timestamp::from_millis(0), "error").is_empty());
|
||||
assert!(evaluate_alerts(&[rule.clone()], &mut states, &index, 0, Timestamp::from_millis(0), "error").is_empty());
|
||||
// One request: ratio 1/1 = 1.0 >= 0.5 -> fires.
|
||||
let fired = evaluate_alerts(&[rule.clone()], &mut states, 1, Timestamp::from_millis(1), "request");
|
||||
let fired = evaluate_alerts(&[rule.clone()], &mut states, &index, 1, Timestamp::from_millis(1), "request");
|
||||
assert_eq!(fired.len(), 1);
|
||||
// A second request: ratio 1/2 = 0.5 still >= 0.5 -> fires again.
|
||||
let fired = evaluate_alerts(&[rule.clone()], &mut states, 2, Timestamp::from_millis(2), "request");
|
||||
let fired = evaluate_alerts(&[rule.clone()], &mut states, &index, 2, Timestamp::from_millis(2), "request");
|
||||
assert_eq!(fired.len(), 1);
|
||||
// A third request with no more errors: ratio drops to 1/3 -> no longer fires.
|
||||
let fired = evaluate_alerts(&[rule], &mut states, 3, Timestamp::from_millis(3), "request");
|
||||
let fired = evaluate_alerts(&[rule], &mut states, &index, 3, Timestamp::from_millis(3), "request");
|
||||
assert!(fired.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ottyl_fmt_alert_query_matches_detected_format() {
|
||||
let rule = immediate_rule("ottyl_fmt==json");
|
||||
let mut states = HashMap::new();
|
||||
let index: BTreeMap<IndexKey, DataValue> = BTreeMap::new();
|
||||
|
||||
let fired = evaluate_alerts(
|
||||
&[rule.clone()],
|
||||
&mut states,
|
||||
&index,
|
||||
0,
|
||||
Timestamp::from_millis(0),
|
||||
r#"{"level":"error"}"#,
|
||||
);
|
||||
assert_eq!(fired.len(), 1);
|
||||
|
||||
let fired = evaluate_alerts(&[rule], &mut states, &index, 1, Timestamp::from_millis(1), "plain text");
|
||||
assert!(fired.is_empty());
|
||||
}
|
||||
|
||||
@@ -532,8 +605,9 @@ mod tests {
|
||||
fn metric_comparison_in_alert_query_is_rejected_not_panicking() {
|
||||
let rule = immediate_rule("some_metric[param]==5");
|
||||
let mut states = HashMap::new();
|
||||
let index: BTreeMap<IndexKey, DataValue> = BTreeMap::new();
|
||||
// Should log a warning and simply not fire, never panic.
|
||||
let fired = evaluate_alerts(&[rule], &mut states, 0, Timestamp::from_millis(0), "anything");
|
||||
let fired = evaluate_alerts(&[rule], &mut states, &index, 0, Timestamp::from_millis(0), "anything");
|
||||
assert!(fired.is_empty());
|
||||
}
|
||||
|
||||
|
||||
+570
-119
File diff suppressed because it is too large
Load Diff
+76
-28
@@ -3,29 +3,50 @@ use crate::timestamp::Timestamp;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RegexToMetricPrimitiveMatcher {
|
||||
pub param_name: String,
|
||||
/// A named, standalone parameter: how to pull a value out of a matching log line, and what kind it
|
||||
/// parses as. Ticket 037 unified this with the metric-declaration-embedded matcher that used to
|
||||
/// exist alongside it (`RegexToMetricPrimitiveMatcher`) — a metric now references params by name
|
||||
/// (`MetricDeclaration::label_params`/`value_param`) instead of redeclaring its own extractor per
|
||||
/// param, so a param declared once (e.g. via the Params panel) can be reused as a label by one
|
||||
/// metric and as the value by another without re-entering its regex/capture-group/kind.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ParamDeclaration {
|
||||
pub name: String,
|
||||
pub message_match_regex: String,
|
||||
pub capture_group: String,
|
||||
/// Alternative extraction source to `capture_group`: a dot-separated JSON field path (e.g.
|
||||
/// `foo.bar`), read from lines that parse as a JSON object instead of via the regex capture
|
||||
/// group. When present and non-empty, this takes precedence over `capture_group`.
|
||||
/// `#[serde(default)]` so profile files/requests saved before this field existed still load
|
||||
/// as plain regex-capture-group matchers.
|
||||
/// as plain regex-capture-group params.
|
||||
#[serde(default)]
|
||||
pub json_field_path: Option<String>,
|
||||
pub kind: MetricPrimitiveKind,
|
||||
/// Whether this param is a bucket label or its captured value (at most one Value-role param per metric); `#[serde(default)]` so profile files saved before this field existed still load as `Label`.
|
||||
/// A third extraction source, alongside `capture_group`/`json_field_path`: when set, this
|
||||
/// param's value is never parsed from text at all — it's computed as elapsed time since the
|
||||
/// previous event that produced the same label set (ticket 038). Takes precedence over
|
||||
/// `json_field_path`/`capture_group` when set, and `message_match_regex`/`capture_group`/
|
||||
/// `json_field_path` are simply unused in that case (`declare_param` doesn't validate them for
|
||||
/// this shape). Only meaningful paired with `kind == Duration`, since elapsed time is the only
|
||||
/// primitive this computation can produce — `declare_param` enforces that pairing.
|
||||
/// `#[serde(default)]` so params declared before this field existed keep parsing as plain
|
||||
/// regex/JSON-field extraction.
|
||||
#[serde(default)]
|
||||
pub role: ParamRole,
|
||||
pub elapsed_since_last: bool,
|
||||
/// Set when this param was discovered by `crate::engine::auto_declare_params` (a nom-based
|
||||
/// `key=value` scan run on every ingested line) rather than declared through `POST /api/params`.
|
||||
/// `declare_param` refuses to overwrite an auto-derived param by name, and it's never persisted
|
||||
/// to a profile — see `crate::engine::declare_param` and `crate::api::reload_engine_declarations`.
|
||||
/// `#[serde(default)]` so profiles/requests saved before this field existed still load as
|
||||
/// ordinary (non-auto) params.
|
||||
#[serde(default)]
|
||||
pub auto_derived: bool,
|
||||
}
|
||||
|
||||
/// Reads a value out of a JSON object by dot-separated field path (e.g. `"foo.bar"`), for
|
||||
/// [`RegexToMetricPrimitiveMatcher::json_field_path`] and the sibling `json_field_path` on a
|
||||
/// standalone param declaration. Returns `None` when `text` doesn't parse as a JSON object, the
|
||||
/// path doesn't resolve to a value, or the resolved value isn't a scalar (string/number/bool) —
|
||||
/// there's no sensible flat text form of a nested object/array to feed into
|
||||
/// [`ParamDeclaration::json_field_path`]. Returns `None` when `text` doesn't parse as a JSON
|
||||
/// object, the path doesn't resolve to a value, or the resolved value isn't a scalar
|
||||
/// (string/number/bool) — there's no sensible flat text form of a nested object/array to feed into
|
||||
/// `parse_metric_primitive`.
|
||||
pub fn extract_json_field(text: &str, field_path: &str) -> Option<String> {
|
||||
let root: serde_json::Value = serde_json::from_str(text).ok()?;
|
||||
@@ -46,13 +67,38 @@ pub fn extract_json_field(text: &str, field_path: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`RegexToMetricPrimitiveMatcher::role`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ParamRole {
|
||||
#[default]
|
||||
Label,
|
||||
Value,
|
||||
/// Flattens a JSON object into dot-separated field paths mapped to each leaf scalar's raw text form
|
||||
/// (string/number/bool), for [`crate::engine::auto_declare_params`] when a line is JSON
|
||||
/// (`crate::logfmt::detect_format`) rather than logfmt — the JSON equivalent of
|
||||
/// `crate::logfmt::extract_pairs`'s `key=value` scan. Nested objects recurse, dot-joining the path
|
||||
/// (`"foo": {"bar": 1}` yields `"foo.bar"`); arrays and null are skipped since there's no single leaf
|
||||
/// scalar to declare a param against, matching `extract_json_field`'s own restriction to scalars.
|
||||
/// Returns an empty vec for anything that isn't a JSON object, same as `extract_pairs` returns empty
|
||||
/// for a line with no `key=value` pairs.
|
||||
pub fn flatten_json_object(text: &str) -> Vec<(String, String)> {
|
||||
let Ok(serde_json::Value::Object(root)) = serde_json::from_str::<serde_json::Value>(text) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut results = Vec::new();
|
||||
flatten_json_object_into(&root, "", &mut results);
|
||||
results
|
||||
}
|
||||
|
||||
fn flatten_json_object_into(
|
||||
object: &serde_json::Map<String, serde_json::Value>,
|
||||
prefix: &str,
|
||||
results: &mut Vec<(String, String)>,
|
||||
) {
|
||||
for (key, value) in object {
|
||||
let path = if prefix.is_empty() { key.clone() } else { format!("{prefix}.{key}") };
|
||||
match value {
|
||||
serde_json::Value::String(s) => results.push((path, s.clone())),
|
||||
serde_json::Value::Number(n) => results.push((path, n.to_string())),
|
||||
serde_json::Value::Bool(b) => results.push((path, b.to_string())),
|
||||
serde_json::Value::Object(nested) => flatten_json_object_into(nested, &path, results),
|
||||
serde_json::Value::Null | serde_json::Value::Array(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `rename_all = "lowercase"` matches other `kind`-style enums, so JSON bodies use "string"/"number"/"timestamp".
|
||||
@@ -63,7 +109,11 @@ pub enum MetricPrimitiveKind {
|
||||
String,
|
||||
Number,
|
||||
Timestamp,
|
||||
/// On a Value-role param this is derived as elapsed time since the previous event (not parsed from text); otherwise it's a literal i64 millisecond count.
|
||||
/// A literal i64 millisecond count. Note this is unrelated to
|
||||
/// `ParamDeclaration::elapsed_since_last`, which is a separate, independent extraction source —
|
||||
/// a param using that source always parses/produces `Duration`, but not every `Duration`-kind
|
||||
/// param is computed that way (one can also just parse a literal duration out of captured text,
|
||||
/// like any other kind).
|
||||
Duration,
|
||||
/// A standalone param declared with this kind doesn't require (or use) a capture group: its value is
|
||||
/// simply whether `message_match_regex` matches the line, case-insensitively or per the regex's own
|
||||
@@ -79,17 +129,15 @@ pub enum DataValue {
|
||||
/// Paired with `IndexKey::IngestedLog`.
|
||||
LogLiteral { text: String },
|
||||
/// Paired with `IndexKey::ParamDeclaration`.
|
||||
ParamDeclaration {
|
||||
message_match_regex: String,
|
||||
capture_group: String,
|
||||
/// See `RegexToMetricPrimitiveMatcher::json_field_path`.
|
||||
json_field_path: Option<String>,
|
||||
kind: MetricPrimitiveKind,
|
||||
},
|
||||
/// Paired with `IndexKey::MetricDeclaration`.
|
||||
ParamDeclaration(ParamDeclaration),
|
||||
/// Paired with `IndexKey::MetricDeclaration`. `label_params`/`value_param` reference standalone
|
||||
/// `ParamDeclaration`s by name (resolved against the engine's param store in
|
||||
/// `crate::engine::apply_metric_to_log`) rather than embedding their own extractor.
|
||||
MetricDeclaration {
|
||||
message_match_regex: String,
|
||||
params: Vec<RegexToMetricPrimitiveMatcher>,
|
||||
label_params: Vec<String>,
|
||||
/// At most one value param per metric; its primitive becomes the event's bucket value instead of a label.
|
||||
value_param: Option<String>,
|
||||
/// See `crate::engine::DeclareMetricRequest::matched_labels`.
|
||||
matched_labels: Option<BTreeMap<String, String>>,
|
||||
/// See `crate::engine::DeclareMetricRequest::unmatched_labels`.
|
||||
|
||||
+29
-5
@@ -1,5 +1,5 @@
|
||||
use crate::alerts::AlertCondition;
|
||||
use crate::data_value::{MetricPrimitiveKind, RegexToMetricPrimitiveMatcher};
|
||||
use crate::data_value::MetricPrimitiveKind;
|
||||
use crate::metric_primitive::MetricPrimitive;
|
||||
use crate::profile::{ChartType, Dashboard, Panel, Profile};
|
||||
use crate::settings::{ColorScheme, CssEntry, Settings};
|
||||
@@ -20,16 +20,32 @@ pub struct ParamDeclarationDto {
|
||||
pub name: String,
|
||||
pub message_match_regex: String,
|
||||
pub capture_group: String,
|
||||
/// See `crate::data_value::RegexToMetricPrimitiveMatcher::json_field_path`.
|
||||
/// See `crate::data_value::ParamDeclaration::json_field_path`.
|
||||
pub json_field_path: Option<String>,
|
||||
pub kind: MetricPrimitiveKind,
|
||||
/// See `crate::data_value::ParamDeclaration::elapsed_since_last`.
|
||||
pub elapsed_since_last: bool,
|
||||
/// See `crate::data_value::ParamDeclaration::auto_derived`. The frontend uses this to disable
|
||||
/// editing — `POST /api/params` also rejects a request naming an auto-derived param.
|
||||
pub auto_derived: bool,
|
||||
/// Set only for the built-in `ottyl_seq`/`ottyl_ts`/`ottyl_fmt` names
|
||||
/// (`crate::engine::OTTYL_RESERVED_PARAM_NAMES`), synthesized into the `GET /api/params`
|
||||
/// response by `crate::api::list_params` rather than backed by any index entry — distinct from
|
||||
/// `auto_derived` (which *is* a real, if machine-discovered, `ParamDeclaration`). Always
|
||||
/// `false` for every other entry. There is no removal path for these at all (not just a
|
||||
/// disabled one), so the frontend should treat `reserved` as non-deletable/non-editable.
|
||||
#[serde(default)]
|
||||
pub reserved: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct MetricDeclarationDto {
|
||||
pub name: String,
|
||||
pub message_match_regex: String,
|
||||
pub params: Vec<RegexToMetricPrimitiveMatcher>,
|
||||
/// Names of standalone `ParamDeclaration`s used as this metric's bucket labels.
|
||||
pub label_params: Vec<String>,
|
||||
/// Name of the standalone `ParamDeclaration` (if any) whose primitive becomes this metric's per-event bucket value instead of a label.
|
||||
pub value_param: Option<String>,
|
||||
/// See `crate::engine::DeclareMetricRequest::matched_labels`.
|
||||
pub matched_labels: Option<BTreeMap<String, String>>,
|
||||
/// See `crate::engine::DeclareMetricRequest::unmatched_labels`.
|
||||
@@ -70,8 +86,16 @@ pub struct LogsParams {
|
||||
pub start_ms: Option<i64>,
|
||||
pub end_ms: Option<i64>,
|
||||
pub limit: Option<usize>,
|
||||
/// Raw FilterBar query string, e.g. `regex("error") and sequence<5`.
|
||||
/// Raw FilterBar query string, e.g. `regex("error") and ottyl_seq<5`.
|
||||
pub filter: Option<String>,
|
||||
/// Ticket 045: an already-declared param name to order the returned entries by, in place of the
|
||||
/// default ingest order (which tracks `ottyl_ts`). `None` (the default) leaves ordering
|
||||
/// unchanged.
|
||||
pub sort_by: Option<String>,
|
||||
/// Ticket 045: only meaningful alongside `sort_by`. `true` (the default) orders lines missing
|
||||
/// that param among themselves by `ottyl_seq` and appends them after the sorted lines; `false`
|
||||
/// drops those lines from the response entirely.
|
||||
pub sort_missing_fallback: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -190,7 +214,7 @@ impl From<&Settings> for SettingsDto {
|
||||
fn from(settings: &Settings) -> Self {
|
||||
SettingsDto {
|
||||
color_scheme: settings.color_scheme,
|
||||
active_profile: settings.profiles.active_profile.clone(),
|
||||
active_profile: settings.active_profile.clone(),
|
||||
max_logs: settings.max_logs,
|
||||
custom_theme_id: settings.custom_theme_id.clone(),
|
||||
ntfy_topic: settings.ntfy_topic.clone(),
|
||||
|
||||
+2305
-461
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,9 @@
|
||||
// - regex("...") raw-text regex search over log lines
|
||||
// - metric_name[param] <op> value metric comparisons
|
||||
// - json[field] <op> value JSON field comparisons (line parsed as JSON)
|
||||
// - sequence <op> value reserved metric name (no [param])
|
||||
// - timestamp <op> value reserved metric name (no [param])
|
||||
// - ottyl_seq <op> value reserved metric name (no [param])
|
||||
// - ottyl_ts <op> value reserved metric name (no [param])
|
||||
// - ottyl_fmt <op> value reserved metric name (no [param])
|
||||
// - `and` / `or` boolean combinators, with `and` binding tighter than `or`
|
||||
// - parenthesized sub-expressions
|
||||
|
||||
|
||||
+28
-14
@@ -284,12 +284,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_reserved_metric_sequence() {
|
||||
let expr = parse("sequence<5").expect("should parse");
|
||||
fn parses_reserved_metric_ottyl_seq() {
|
||||
let expr = parse("ottyl_seq<5").expect("should parse");
|
||||
assert_eq!(
|
||||
expr,
|
||||
FilterExpr::Comparison {
|
||||
metric: "sequence".to_string(),
|
||||
metric: "ottyl_seq".to_string(),
|
||||
param: None,
|
||||
op: CompareOp::Lt,
|
||||
value: FilterValue::Number(5.0),
|
||||
@@ -298,12 +298,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_reserved_metric_timestamp_as_raw_value() {
|
||||
let expr = parse("timestamp>12:00").expect("should parse");
|
||||
fn parses_reserved_metric_ottyl_ts_as_raw_value() {
|
||||
let expr = parse("ottyl_ts>12:00").expect("should parse");
|
||||
assert_eq!(
|
||||
expr,
|
||||
FilterExpr::Comparison {
|
||||
metric: "timestamp".to_string(),
|
||||
metric: "ottyl_ts".to_string(),
|
||||
param: None,
|
||||
op: CompareOp::Gt,
|
||||
value: FilterValue::Raw("12:00".to_string()),
|
||||
@@ -311,6 +311,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_reserved_metric_ottyl_fmt() {
|
||||
let expr = parse("ottyl_fmt==json").expect("should parse");
|
||||
assert_eq!(
|
||||
expr,
|
||||
FilterExpr::Comparison {
|
||||
metric: "ottyl_fmt".to_string(),
|
||||
param: None,
|
||||
op: CompareOp::Eq,
|
||||
value: FilterValue::Raw("json".to_string()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_json_field_comparison() {
|
||||
let expr = parse("json[level]==error").expect("should parse");
|
||||
@@ -339,7 +353,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn json_field_composes_with_and_or() {
|
||||
let expr = parse(r#"json[level]==error and sequence<5"#).expect("should parse");
|
||||
let expr = parse(r#"json[level]==error and ottyl_seq<5"#).expect("should parse");
|
||||
let expected = FilterExpr::And(
|
||||
Box::new(FilterExpr::JsonComparison {
|
||||
field: "level".to_string(),
|
||||
@@ -347,7 +361,7 @@ mod tests {
|
||||
value: FilterValue::Raw("error".to_string()),
|
||||
}),
|
||||
Box::new(FilterExpr::Comparison {
|
||||
metric: "sequence".to_string(),
|
||||
metric: "ottyl_seq".to_string(),
|
||||
param: None,
|
||||
op: CompareOp::Lt,
|
||||
value: FilterValue::Number(5.0),
|
||||
@@ -358,21 +372,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_and_or_with_and_binding_tighter() {
|
||||
let expr = parse(r#"regex("error") and sequence<5 or timestamp>12:00"#)
|
||||
let expr = parse(r#"regex("error") and ottyl_seq<5 or ottyl_ts>12:00"#)
|
||||
.expect("should parse");
|
||||
|
||||
let expected = FilterExpr::Or(
|
||||
Box::new(FilterExpr::And(
|
||||
Box::new(FilterExpr::Regex("error".to_string())),
|
||||
Box::new(FilterExpr::Comparison {
|
||||
metric: "sequence".to_string(),
|
||||
metric: "ottyl_seq".to_string(),
|
||||
param: None,
|
||||
op: CompareOp::Lt,
|
||||
value: FilterValue::Number(5.0),
|
||||
}),
|
||||
)),
|
||||
Box::new(FilterExpr::Comparison {
|
||||
metric: "timestamp".to_string(),
|
||||
metric: "ottyl_ts".to_string(),
|
||||
param: None,
|
||||
op: CompareOp::Gt,
|
||||
value: FilterValue::Raw("12:00".to_string()),
|
||||
@@ -384,20 +398,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parenthesized_expression_overrides_precedence() {
|
||||
let expr = parse(r#"regex("error") and (sequence<5 or timestamp>12:00)"#)
|
||||
let expr = parse(r#"regex("error") and (ottyl_seq<5 or ottyl_ts>12:00)"#)
|
||||
.expect("should parse");
|
||||
|
||||
let expected = FilterExpr::And(
|
||||
Box::new(FilterExpr::Regex("error".to_string())),
|
||||
Box::new(FilterExpr::Or(
|
||||
Box::new(FilterExpr::Comparison {
|
||||
metric: "sequence".to_string(),
|
||||
metric: "ottyl_seq".to_string(),
|
||||
param: None,
|
||||
op: CompareOp::Lt,
|
||||
value: FilterValue::Number(5.0),
|
||||
}),
|
||||
Box::new(FilterExpr::Comparison {
|
||||
metric: "timestamp".to_string(),
|
||||
metric: "ottyl_ts".to_string(),
|
||||
param: None,
|
||||
op: CompareOp::Gt,
|
||||
value: FilterValue::Raw("12:00".to_string()),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{"time":"2026-09-06T10:35:34.499921+01:00","level":"WARN","msg":"eventC","labelC":1}
|
||||
{"time":"2026-09-06T10:35:35.000837+01:00","level":"ERROR","msg":"eventB","labelC":2,"labelA":3}
|
||||
{"time":"2026-09-06T10:35:36.504236+01:00","level":"INFO","msg":"eventB","labelB":"three"}
|
||||
@@ -0,0 +1,697 @@
|
||||
Timestamp Thread Type Activity PID TTL
|
||||
2026-09-06 01:48:54.465144+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:54.466387+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.196298]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:54.466396+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.196300]: arm_cpu_init(): cpu 11 online
|
||||
2026-09-06 01:48:54.466397+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.196305]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:54.466397+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.196308]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:54.466386+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.496702+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:54.500366+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.518117+0100 0x4dfed2 Default 0x0 606 0 WindowServer: (HSTouchHIDService) [com.apple.Multitouch:Plugin] [TP] Dispatching digitizer event with 3 children, _eventMask=0x0 _childEventMask=0x2 Cancel=0 Touching=1 inRange=1
|
||||
2026-09-06 01:48:54.532524+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.554814+0100 0x144b Default 0x0 606 7 WindowServer: (SkyLight) [com.apple.SkyLight:KeyboardEvent] kCGSEventKeyUp was part of a sequence with destination <keyboardFocus; pid: 1343; token: viewbridge-key-window>
|
||||
2026-09-06 01:48:54.555315+0100 0x2e72 Default 0x0 1343 0 kitty: (TextInputUIMacHelper) [com.apple.TextInputUI:CursorUI] -[TUINSCursorUIController scheduleUpdateCursorLocation]
|
||||
2026-09-06 01:48:54.562153+0100 0x3d1 Default 0x0 0 0 kernel: (AppleAVD) AppleAVD: INFO: logPerfAndUserClientStats(): core 0, Vmin/nom/max: 99% 0% 0%, clientCount 1 1 1 1 1
|
||||
2026-09-06 01:48:54.564722+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.598759+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.632768+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.666589+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.671971+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) [com.apple.WiFiManager:] Driver Event: <airport[642]> _bsd_80211_event_callback: APPLE80211_M_WEIGHT_AVG_LQM_UPDATE/145 (en0)
|
||||
2026-09-06 01:48:54.680948+0100 0x4e20a1 Default 0x0 538 8 powerd: [com.apple.powerd:displayState] DesktopMode check on Battery 0
|
||||
2026-09-06 01:48:54.700119+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.732678+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.766150+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.801202+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.807884+0100 0x4e1fd2 Activity 0x9cc91e 975 0 CoreServicesUIAgent: Handle XPC Request
|
||||
2026-09-06 01:48:54.807881+0100 0x4e214c Activity 0x9cc91d 975 0 CoreServicesUIAgent: Handle XPC Request
|
||||
2026-09-06 01:48:54.807903+0100 0x4e23c3 Activity 0x9cc91f 975 0 CoreServicesUIAgent: Handle XPC Request
|
||||
2026-09-06 01:48:54.808141+0100 0x4e23c4 Error 0x9cc91e 975 2 CoreServicesUIAgent: [com.apple.launchservices:uiagent] CSUIGetDisplayName, failing with -10810 for path '<private>
|
||||
2026-09-06 01:48:54.808194+0100 0x4e23c4 Error 0x9cc91f 975 2 CoreServicesUIAgent: [com.apple.launchservices:uiagent] CSUIGetDisplayName, failing with -10810 for path '<private>
|
||||
2026-09-06 01:48:54.808238+0100 0x4e23c4 Error 0x9cc91d 975 2 CoreServicesUIAgent: [com.apple.launchservices:uiagent] CSUIGetDisplayName, failing with -10810 for path '<private>
|
||||
2026-09-06 01:48:54.823648+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9668] : In AW: Tx:0 Rx:0 Prev Tx:0 Rx:0 time since 255552387 indatamode 1 infraPriority 1 cpc 0
|
||||
2026-09-06 01:48:54.823667+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9673] : Ignore changing the state as infraPriority 1 P2P-CPC 0
|
||||
2026-09-06 01:48:54.832643+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.845410+0100 0x4dff68 Default 0x0 589 0 bluetoothd: [com.apple.bluetooth:Server.LE.Scan] Matched UUID 0xFCB2 for device "A1161273-82A9-54D0-9283-720AC7DD464C"
|
||||
2026-09-06 01:48:54.845529+0100 0x4dff68 Default 0x0 589 0 bluetoothd: [com.apple.bluetooth:Server.LE.Scan] Matched UUID 0xFCB2 for device "A1161273-82A9-54D0-9283-720AC7DD464C"
|
||||
2026-09-06 01:48:54.845773+0100 0x4df240 Default 0x0 572 0 locationd: [com.apple.locationd.Core:Harvester] {"msg":"onAvengerAdvertisementDetected: got avenger advertisement", "subHarvester":"Avenger"}
|
||||
2026-09-06 01:48:54.845815+0100 0x4defc1 Default 0x0 572 0 locationd: (SPFinder) [com.apple.icloud.SPFinder:advertisementCache] saveBeaconPayload: 1
|
||||
2026-09-06 01:48:54.845818+0100 0x4df240 Default 0x0 572 0 locationd: [com.apple.locationd.Core:Harvester] {"msg":"convertToSPAdvertisement", "address":<private>, "data":<private>, "date":<private>, "rssi":<private>, "status":<private>, "reserved":<private>, "subHarvester":"Avenger"}
|
||||
2026-09-06 01:48:54.845837+0100 0x4defc1 Activity 0x9d0e2a 572 0 locationd: (SPFinder) SPAdvertisementCache saveBeaconPayloads:completion:
|
||||
2026-09-06 01:48:54.845847+0100 0x4df240 Default 0x0 572 0 locationd: [com.apple.locationd.Core:Harvester] {"msg":"onAvengerAdvertisementDetected: advertisement is near-owner from other people and do not process it", "subHarvester":"Avenger"}
|
||||
2026-09-06 01:48:54.846284+0100 0x4dfe20 Activity 0x9d0dbf 746 0 searchpartyd: (FindMyBase) SaveObservedAdvertisment
|
||||
2026-09-06 01:48:54.846660+0100 0x4dfe20 Default 0x9d0dbf 746 0 searchpartyd: (FindMyBase) [com.apple.icloud.searchpartyd:observationStoring] TIME: ObservationStoring.Reconcile: <private>
|
||||
2026-09-06 01:48:54.865403+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.889179+0100 0x4de289 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:54.899419+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.932587+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.940343+0100 0x4e20a1 Default 0x0 538 8 powerd: [com.apple.powerd:displayState] DesktopMode check on Battery 0
|
||||
2026-09-06 01:48:54.965716+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:54.969316+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:54.999946+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.002756+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLState[10141] : Active Sockets false ValidSvc 4 NumAirplay 0 AFHandlePending 0 TimeOfLastAFHandleRequest 141710 ms
|
||||
2026-09-06 01:48:55.002786+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLState[10239]: BonJourTrig 1 ValidSvc 4 RTApp 0 TSReq 0 HasActAirDrop 0 SocketsActive 0
|
||||
2026-09-06 01:48:55.002799+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: setScheduleState[11041]: reason:UserTriggered sc:Infra Priority and force:NO, AWDL-restore:No
|
||||
2026-09-06 01:48:55.002824+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: AWDLStateDump: AWDL up time: 15732 Secs, 82:45:A3:D5:31:34 peers:0, cached:0(0), rtg(0), <SDB: :(00:00:00:00:00:00) 44, infraRT:Yes, country:GB>
|
||||
2026-09-06 01:48:55.002862+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: AWDLStateDump: Services[Valid 4 Airplay 0 WiFid 0 AirDrop 0 nonAirplay:4 rdlink:0 clink:0]
|
||||
2026-09-06 01:48:55.002872+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: AWDLStateDump: AirPlay:0 AirPlayToHT:0 AirPlayOverInfra:0 SideCar:0 SideCarSender:0 SideCarPeer:0x0 TimeSync:0 HighThroughput:0
|
||||
2026-09-06 01:48:55.002892+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: AWDLStateDump: AWDL enabled with no data connection from 15732 seconds
|
||||
2026-09-06 01:48:55.002901+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: AWDLStateDump: PSFDwell Support/Active: 1/0 {0ms 0ms}, miconfigured:0 sessin count:7, MIoNDwell:35, dwelEvent:0
|
||||
2026-09-06 01:48:55.002922+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: AWDL ON:[infra(44) 90%] [sdb:0] (6/44/44) [44 0 44 0 0 0 0 0 6 0 44 0 0 0 0 0] Infra Priority
|
||||
2026-09-06 01:48:55.002931+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.stats.awdl0: -------------------------------------------------------------------------------------------------------------
|
||||
2026-09-06 01:48:55.002941+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.stats.awdl0: AFSPerSlot : [0 0 0 0] [0 0 0 0] [0 0 0 0] [0 0 0 0]
|
||||
2026-09-06 01:48:55.002945+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.stats.awdl0: AFsPerSignal (batchSize -1) : 0 <= 1, 0 <= 10, 0 <= 30, 0 <= 50, 0 <= 100, 0 > 100
|
||||
2026-09-06 01:48:55.002949+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.stats.awdl0: TimePerSignal : 0 < 250uSec, 0 250-500uSec, 0 500-1000uSec, 0 1000-2000uSec, 0 > 2000uSec
|
||||
2026-09-06 01:48:55.002951+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.stats.awdl0: TimePerAF : 0 < 100uSec, 0 100-250uSec, 0 250-500uSec, 0 500-1000uSec, 0 > 1000uSec
|
||||
2026-09-06 01:48:55.002954+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.stats.awdl0: -------------------------------------------------------------------------------------------------------------
|
||||
2026-09-06 01:48:55.002965+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.wifi.awdl0: LQM-WiFi:AWDL State #16 Infra Priority(29) DutyCycle 10 StateDuration 45lu.791s[255506.775 - 0.000] StateComplete 0
|
||||
2026-09-06 01:48:55.002969+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.wifi.awdl0: LQM-WiFi:AWDL Active Time 500ms(9%) 5.002s[255547.563s - 255552.566s]
|
||||
2026-09-06 01:48:55.003060+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) IO80211PeerMonitor::checkForDPS() Enter
|
||||
2026-09-06 01:48:55.003076+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: MonitorTimer en0/5000: packet counts: I239 Q0 O295
|
||||
2026-09-06 01:48:55.003089+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: postMessageOnInfraInterface:3463 posting msgId:169 on skywalk infraInterface
|
||||
2026-09-06 01:48:55.003106+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) LQM-WiFi: BE : Count: 292 avgLatencyMs:3 maxLatencyMs:7
|
||||
2026-09-06 01:48:55.003109+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) LQM-WiFi: BK : Count: 2 avgLatencyMs:1 maxLatencyMs:1
|
||||
2026-09-06 01:48:55.003110+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) LQM-WiFi: VI : Count: 0 avgLatencyMs:0 maxLatencyMs:0
|
||||
2026-09-06 01:48:55.003112+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) LQM-WiFi: VO : Count: 1 avgLatencyMs:1 maxLatencyMs:1
|
||||
2026-09-06 01:48:55.020289+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:55.032976+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.056189+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:55.066877+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.099602+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.134018+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.165500+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.914235]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:55.165510+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.914238]: arm_cpu_init(): cpu 11 online
|
||||
2026-09-06 01:48:55.165510+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.914246]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:55.165500+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.200023+0100 0x4e20a1 Default 0x0 538 8 powerd: [com.apple.powerd:displayState] DesktopMode check on Battery 0
|
||||
2026-09-06 01:48:55.200400+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.925162]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:55.200409+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.925165]: arm_cpu_init(): cpu 11 online
|
||||
2026-09-06 01:48:55.200409+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.925172]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:55.200409+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.934699]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:55.200410+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.934702]: arm_cpu_init(): cpu 11 online
|
||||
2026-09-06 01:48:55.200410+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.934708]: arm_cpu_init(): cpu 12 online
|
||||
2026-09-06 01:48:55.200410+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.934712]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:55.200411+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.934718]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:55.200415+0100 0x4a3b07 Default 0x0 0 0 kernel: [745895.934722]: arm_cpu_init(): cpu 15 online
|
||||
2026-09-06 01:48:55.200398+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.232960+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.236607+0100 0x4e2059 Default 0x0 12260 0 firefox: (Network) [com.apple.network:path] nw_path_libinfo_path_check [C2C8616C-6980-4FDC-9314-BD5FEF18542E Hostname#d4e6e062:0 tcp, legacy-socket, attribution: developer]
|
||||
libinfo check path: <private>
|
||||
2026-09-06 01:48:55.236975+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114820] DNSServiceQueryRecord START -- qname: <mask.hash: 'rKTEtG7JmnThhoKPbZiVrA=='>, qtype: A, flags: 0x1D000, interface index: 0, client pid: 12260 (firefox), name hash: 70429ff7
|
||||
2026-09-06 01:48:55.237323+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114820->Q2661] Question assigned DNS service 84
|
||||
2026-09-06 01:48:55.237423+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114821] DNSServiceQueryRecord START -- qname: <mask.hash: 'rKTEtG7JmnThhoKPbZiVrA=='>, qtype: AAAA, flags: 0x1D000, interface index: 0, client pid: 12260 (firefox), name hash: 70429ff7
|
||||
2026-09-06 01:48:55.237588+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114821->Q58675] Question assigned DNS service 84
|
||||
2026-09-06 01:48:55.237601+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [Q58675] DetermineUnicastQuerySuppression: Query suppressed for <mask.hash: 'rKTEtG7JmnThhoKPbZiVrA=='> AAAA (AAAA records are unusable)
|
||||
2026-09-06 01:48:55.237640+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [Q58675] GenerateNegativeResponse: Generating negative response for question
|
||||
2026-09-06 01:48:55.237651+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114821->Q58675] DNSServiceQueryRecord result -- event: add, expired: no, ifindex: 0, name hash: 70429ff7, type: AAAA, rdata: <none>
|
||||
2026-09-06 01:48:55.237903+0100 0x4e172c Default 0x0 639 0 mDNSResponder: [com.apple.mdns:resolver] [Q2661] Sent 55-byte query #1 to <IPv4:BBEFvEFo> over UDP via en0/14 -- id: 0x089F (2207), flags: 0x0100 (Q/Query, RD, NoError), counts: 1/0/0/0, BBgyliWZ IN A?
|
||||
2026-09-06 01:48:55.254247+0100 0x4e172c Default 0x0 639 0 mDNSResponder: [com.apple.mdns:resolver] [Q2661] Received acceptable 155-byte response from <IPv4:BBEFvEFo> over UDP via en0/14 -- id: 0x089F (2207), flags: 0x8180 (R/Query, RD, RA, NoError), counts: 1/5/0/0, BBgyliWZ IN A?, 45 IN CNAME BBRnzZbz, 34 IN A BBXtrByy, 34 IN A BBEFKvtH, 34 IN A BBOpbcRV, 34 IN A BBcTGmrX
|
||||
2026-09-06 01:48:55.254376+0100 0x4e2058 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [Q2661] Handling concluded querier: BBgyliWZ A IN
|
||||
2026-09-06 01:48:55.254443+0100 0x4e2058 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114820->Q2661] DNSServiceQueryRecord result -- event: add, expired: no, ifindex: 0, name hash: 70429ff7, type: CNAME, rdata: <mask.hash: 'cDKUsCTEZJB+B/gEdA8N4g=='>
|
||||
2026-09-06 01:48:55.254463+0100 0x4e2058 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [Q2661] AnswerQuestionByFollowingCNAME: following CNAME referral -- CNAME: <mask.hash: 'hCeMQekHxjAP1gJ3ZlTg5A=='>, referral count: 0
|
||||
2026-09-06 01:48:55.254711+0100 0x4e2058 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114820->Q8111] Question assigned DNS service 84
|
||||
2026-09-06 01:48:55.254778+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114820->Q8111] DNSServiceQueryRecord result -- event: add, expired: no, ifindex: 0, name hash: 4d30d13e, type: A, rdata: <mask.hash: '6b9WEaPm7Gqw4Flcl9+ddQ=='>
|
||||
2026-09-06 01:48:55.254807+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114820->Q8111] DNSServiceQueryRecord result -- event: add, expired: no, ifindex: 0, name hash: 4d30d13e, type: A, rdata: <mask.hash: 'TfCnnBV+Z8bdyTgzgKVY6A=='>
|
||||
2026-09-06 01:48:55.254820+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114820->Q8111] DNSServiceQueryRecord result -- event: add, expired: no, ifindex: 0, name hash: 4d30d13e, type: A, rdata: <mask.hash: 'I/iOn17GO6bh2gByjdYhqw=='>
|
||||
2026-09-06 01:48:55.254831+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114820->Q8111] DNSServiceQueryRecord result -- event: add, expired: no, ifindex: 0, name hash: 4d30d13e, type: A, rdata: <mask.hash: 'J24bRFaq4i4juI0qgCq6/g=='>
|
||||
2026-09-06 01:48:55.254894+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114820] DNSServiceQueryRecord STOP -- name hash: 4d30d13e, duration: 1s
|
||||
2026-09-06 01:48:55.254919+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114821] DNSServiceQueryRecord STOP -- name hash: 70429ff7, duration: 1s
|
||||
2026-09-06 01:48:55.255418+0100 0x11696 Default 0x0 0 0 kernel: [745895.993200]: arm_cpu_init(): cpu 11 online
|
||||
2026-09-06 01:48:55.255424+0100 0x11696 Default 0x0 0 0 kernel: [745895.993205]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:55.255425+0100 0x11696 Default 0x0 0 0 kernel: [745895.993207]: arm_cpu_init(): cpu 15 online
|
||||
2026-09-06 01:48:55.255417+0100 0x11696 Default 0x0 0 0 kernel: SK[6]: flow_entry_alloc fe "0 proc kernel_task(0)firefox nx_port 1 flow_uuid 74BDAEDA-44EF-49A6-9EF0-A23219CFCCE0 flags 0x14120<CONNECTED,QOS_MARKING,EXT_PORT,EXT_FLOWID> ipver=4,src=<IPv4-redacted>.53525,dst=<IPv4-redacted>.443,proto=0x06 mask=0x0000003f,hash=0x7baac1cc tp_proto=0x06"
|
||||
2026-09-06 01:48:55.255454+0100 0x11696 Default 0x0 0 0 kernel: tcp connect outgoing: [<IPv4-redacted>:53525<-><IPv4-redacted>:443] interface: en0 (skipped: 593)
|
||||
so_gencnt: 3510228 t_state: SYN_SENT process: firefox:12260 SYN in/out: 0/1 bytes in/out: 0/0 pkts in/out: 0/0 rtt: 0.0 ms rttvar: 250.0 ms base_rtt: 5 ms error: 0 so_error: 0 svc/tc: 0 flow: 0x96139917
|
||||
2026-09-06 01:48:55.261279+0100 0x1c98 Default 0x0 0 0 kernel: tcp connected: [<IPv4-redacted>:53525<-><IPv4-redacted>:443] interface: en0 (skipped: 593)
|
||||
so_gencnt: 3510228 t_state: ESTABLISHED process: firefox:12260 SYN in/out: 1/1 bytes in/out: 0/0 pkts in/out: 0/0 rtt: 0.0 ms rttvar: 250.0 ms base_rtt: 5 ms error: 0 so_error: 0 svc/tc: 0 flow: 0x96139917
|
||||
2026-09-06 01:48:55.267182+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.300375+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.043247]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:55.300382+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.043252]: arm_cpu_init(): cpu 12 online
|
||||
2026-09-06 01:48:55.300383+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.043256]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:55.300383+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.043260]: arm_cpu_init(): cpu 15 online
|
||||
2026-09-06 01:48:55.300384+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.053765]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:55.300385+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.053794]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:55.300386+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.053827]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:55.300386+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.053860]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:55.300388+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.053892]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:55.300388+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.053923]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:55.300389+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.053958]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:55.300395+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.053990]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:55.300396+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.054022]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:55.300365+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.311658+0100 0x4e2059 Default 0x0 12260 0 firefox: (Network) [com.apple.network:path] nw_path_libinfo_path_check [B2491361-91B6-46C7-842E-F521AA802C01 Hostname#12130b20:0 tcp, legacy-socket, attribution: developer]
|
||||
libinfo check path: <private>
|
||||
2026-09-06 01:48:55.311755+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114822] DNSServiceQueryRecord START -- qname: <mask.hash: 'jTFU35oMx3mxTHXGo40ISg=='>, qtype: A, flags: 0x1D000, interface index: 0, client pid: 12260 (firefox), name hash: cc4a6c20
|
||||
2026-09-06 01:48:55.312122+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114822->Q38154] Question assigned DNS service 84
|
||||
2026-09-06 01:48:55.312209+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114823] DNSServiceQueryRecord START -- qname: <mask.hash: 'jTFU35oMx3mxTHXGo40ISg=='>, qtype: AAAA, flags: 0x1D000, interface index: 0, client pid: 12260 (firefox), name hash: cc4a6c20
|
||||
2026-09-06 01:48:55.312387+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114823->Q15486] Question assigned DNS service 84
|
||||
2026-09-06 01:48:55.312402+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [Q15486] DetermineUnicastQuerySuppression: Query suppressed for <mask.hash: 'jTFU35oMx3mxTHXGo40ISg=='> AAAA (AAAA records are unusable)
|
||||
2026-09-06 01:48:55.312448+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [Q15486] GenerateNegativeResponse: Generating negative response for question
|
||||
2026-09-06 01:48:55.312465+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114823->Q15486] DNSServiceQueryRecord result -- event: add, expired: no, ifindex: 0, name hash: cc4a6c20, type: AAAA, rdata: <none>
|
||||
2026-09-06 01:48:55.312719+0100 0x4e2058 Default 0x0 639 0 mDNSResponder: [com.apple.mdns:resolver] [Q38154] Sent 53-byte query #1 to <IPv4:BBEFvEFo> over UDP via en0/14 -- id: 0xCBF5 (52213), flags: 0x0100 (Q/Query, RD, NoError), counts: 1/0/0/0, BBmCsKYR IN A?
|
||||
2026-09-06 01:48:55.329218+0100 0x4e2058 Default 0x0 639 0 mDNSResponder: [com.apple.mdns:resolver] [Q38154] Received acceptable 69-byte response from <IPv4:BBEFvEFo> over UDP via en0/14 -- id: 0xCBF5 (52213), flags: 0x8180 (R/Query, RD, RA, NoError), counts: 1/1/0/0, BBmCsKYR IN A?, 2008 IN A BBYEzGsX
|
||||
2026-09-06 01:48:55.329361+0100 0x4e172c Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [Q38154] Handling concluded querier: BBmCsKYR A IN
|
||||
2026-09-06 01:48:55.329425+0100 0x4e172c Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114822->Q38154] DNSServiceQueryRecord result -- event: add, expired: no, ifindex: 0, name hash: cc4a6c20, type: A, rdata: <mask.hash: 'JGEmga7PvGVKnDgblBSHtg=='>
|
||||
2026-09-06 01:48:55.329646+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114822] DNSServiceQueryRecord STOP -- name hash: cc4a6c20, duration: 0s
|
||||
2026-09-06 01:48:55.329671+0100 0x1639 Default 0x0 639 0 mDNSResponder: [com.apple.mDNSResponder:Default] [R114823] DNSServiceQueryRecord STOP -- name hash: cc4a6c20, duration: 0s
|
||||
2026-09-06 01:48:55.329920+0100 0x11696 Default 0x0 0 0 kernel: [745896.057369]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:55.329927+0100 0x11696 Default 0x0 0 0 kernel: [745896.057375]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:55.329927+0100 0x11696 Default 0x0 0 0 kernel: [745896.065099]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:55.329927+0100 0x11696 Default 0x0 0 0 kernel: [745896.065103]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:55.329927+0100 0x11696 Default 0x0 0 0 kernel: [745896.065106]: arm_cpu_init(): cpu 15 online
|
||||
2026-09-06 01:48:55.329928+0100 0x11696 Default 0x0 0 0 kernel: [745896.072609]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:55.329927+0100 0x11696 Default 0x0 0 0 kernel: [745896.072615]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:55.329920+0100 0x11696 Default 0x0 0 0 kernel: SK[7]: flow_entry_alloc fe "0 proc kernel_task(0)firefox nx_port 1 flow_uuid 543F6B4B-FE19-4D0E-B3DD-2A550A742A3D flags 0x14120<CONNECTED,QOS_MARKING,EXT_PORT,EXT_FLOWID> ipver=4,src=<IPv4-redacted>.53540,dst=<IPv4-redacted>.443,proto=0x06 mask=0x0000003f,hash=0x7c762c2b tp_proto=0x06"
|
||||
2026-09-06 01:48:55.329956+0100 0x11696 Default 0x0 0 0 kernel: tcp connect outgoing: [<IPv4-redacted>:53540<-><IPv4-redacted>:443] interface: en0 (skipped: 593)
|
||||
so_gencnt: 3510252 t_state: SYN_SENT process: firefox:12260 SYN in/out: 0/1 bytes in/out: 0/0 pkts in/out: 0/0 rtt: 0.0 ms rttvar: 250.0 ms base_rtt: 0 ms error: 0 so_error: 0 svc/tc: 0 flow: 0x90dee0b7
|
||||
2026-09-06 01:48:55.333766+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.336428+0100 0x1c98 Default 0x0 0 0 kernel: tcp connected: [<IPv4-redacted>:53540<-><IPv4-redacted>:443] interface: en0 (skipped: 593)
|
||||
so_gencnt: 3510252 t_state: ESTABLISHED process: firefox:12260 SYN in/out: 1/1 bytes in/out: 0/0 pkts in/out: 0/0 rtt: 0.0 ms rttvar: 250.0 ms base_rtt: 0 ms error: 0 so_error: 0 svc/tc: 0 flow: 0x90dee0b7
|
||||
2026-09-06 01:48:55.348649+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: isInfraRealtimePacketThresholdAllowed allowed:1 option:32 threshold:50 noRegistrations:1 cachedPeerCount:0 fastDiscoveryInactive:1 fastDiscoveryOnSince:255552912
|
||||
2026-09-06 01:48:55.348674+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: currentInfraTrafficType:9277 checking if realtime upgrade required with inputPackets:62 outputPackets:53 packetThreshold:50
|
||||
2026-09-06 01:48:55.348689+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: peerChannelSteer: PEER_CHANNEL_STEER isSteeringRequired 0 Attempt Count 0
|
||||
2026-09-06 01:48:55.348696+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: peerChannelSteer: PEER_CHANNEL_STEER isRestartSteeringRequired is false
|
||||
2026-09-06 01:48:55.348709+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9668] : In AW: Tx:0 Rx:0 Prev Tx:0 Rx:0 time since 255552912 indatamode 1 infraPriority 1 cpc 0
|
||||
2026-09-06 01:48:55.348719+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9673] : Ignore changing the state as infraPriority 1 P2P-CPC 0
|
||||
2026-09-06 01:48:55.365920+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.381034+0100 0x4de1c1 Default 0x0 589 0 bluetoothd: [com.apple.bluetooth:Server.MacCoex] Posting Bluetooth Status Notification <private>
|
||||
2026-09-06 01:48:55.399752+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.130195]: arm_cpu_init(): cpu 12 online
|
||||
2026-09-06 01:48:55.399761+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.130199]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:55.399761+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.130202]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:55.399766+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.141340]: arm_cpu_init(): cpu 11 online
|
||||
2026-09-06 01:48:55.399766+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.141344]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:55.399766+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.141347]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:55.399750+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.407537+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:55.415091+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:55.433664+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.466363+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.190261]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:55.466373+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.190271]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:55.466361+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.499477+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.533288+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.544460+0100 0x4de289 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:55.556747+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:55.566993+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.600082+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.632344+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.666250+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.679793+0100 0x1f39 Default 0x0 1018 3 sharingd: (CoreUtils) [com.apple.sharing:SFBLEScanner] [NearbyInfo] Clearing duplicate cache
|
||||
2026-09-06 01:48:55.679857+0100 0x1f39 Default 0x0 1018 0 sharingd: (WirelessProximity) [com.apple.bluetooth:WirelessProximity] Nearby clear duplicate filter cache type 1
|
||||
2026-09-06 01:48:55.679911+0100 0x1f39 Default 0x0 1018 3 sharingd: (CoreUtils) [com.apple.sharing:SFBLEScanner] [NearbyAction] Clearing duplicate cache
|
||||
2026-09-06 01:48:55.679932+0100 0x1f39 Default 0x0 1018 0 sharingd: (WirelessProximity) [com.apple.bluetooth:WirelessProximity] Nearby clear duplicate filter cache type 0
|
||||
2026-09-06 01:48:55.680056+0100 0x1f39 Default 0x0 1018 3 sharingd: (CoreUtils) [com.apple.sharing:SDNearbyAgentCore] Identification removed stale 27b8d190-4d1a-f393-5364-ee9e347ff0ff
|
||||
2026-09-06 01:48:55.680234+0100 0x4de1c1 Default 0x0 589 0 bluetoothd: (WPDaemon) [com.apple.bluetooth:WirelessProximity] clear dup filter cache for process sharingd (1018) with scan request of type 16, blob: {length = 0, bytes = 0x}, mask {length = 0, bytes = 0x}, active: 0, duplicates: 0, screen on: No Scan, screen off: No Scan, locked: 1, rssi: -90, peers: (
|
||||
) nearby scan mode: 10, advbuf: 0, priority critical: 0, range: 0, usecases:
|
||||
2026-09-06 01:48:55.680281+0100 0x4de1c1 Default 0x0 589 0 bluetoothd: (WPDaemon) [com.apple.bluetooth:WirelessProximity] About to clearDuplicateFilterCache for type: 16
|
||||
2026-09-06 01:48:55.680354+0100 0x4dbefc Default 0x0 589 0 bluetoothd: [com.apple.bluetooth:Server.XPC] Received XPC message "CBMsgIdClearDuplicateFilterCache" from session "com.apple.bluetoothd-central-589-1"
|
||||
2026-09-06 01:48:55.680398+0100 0x4de1c1 Default 0x0 589 0 bluetoothd: (WPDaemon) [com.apple.bluetooth:WirelessProximity] clear dup filter cache for process sharingd (1018) with scan request of type 15, blob: {length = 0, bytes = 0x}, mask {length = 0, bytes = 0x}, active: 0, duplicates: 0, screen on: No Scan, screen off: No Scan, locked: 1, rssi: -90, peers: (
|
||||
) nearby scan mode: 10, advbuf: 0, priority critical: 0, range: 0, usecases:
|
||||
2026-09-06 01:48:55.680422+0100 0x4de1c1 Default 0x0 589 0 bluetoothd: (WPDaemon) [com.apple.bluetooth:WirelessProximity] About to clearDuplicateFilterCache for type: 15
|
||||
2026-09-06 01:48:55.684985+0100 0x4dbefc Default 0x0 589 0 bluetoothd: [com.apple.bluetooth:Server.App] clearDuplicateFilterCache for session "com.apple.bluetoothd" type 16
|
||||
2026-09-06 01:48:55.685072+0100 0x4dbefc Default 0x0 589 0 bluetoothd: [com.apple.bluetooth:Server.XPC] Received XPC message "CBMsgIdClearDuplicateFilterCache" from session "com.apple.bluetoothd-central-589-1"
|
||||
2026-09-06 01:48:55.688823+0100 0x4dbefc Default 0x0 589 0 bluetoothd: [com.apple.bluetooth:Server.App] clearDuplicateFilterCache for session "com.apple.bluetoothd" type 15
|
||||
2026-09-06 01:48:55.699243+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.719580+0100 0x4e1c4e Default 0x0 1376 0 WallpaperAerialsExtension: (MediaToolbox) [com.apple.coremedia:] <<<< FigVideoQueue >>>> FigVideoQueueGMStats: (0x86f1eb400) 0 frames enqueued in the last 6 seconds; 0 were suspended. sbuf queue contains 0 frames (0.000 sec), max PTS: nan; LWM: 1.000 HWM: 2.000. Timebase time 238.895, rate 0.00. (pid 1376)
|
||||
2026-09-06 01:48:55.728451+0100 0x4e1c4e Default 0x0 1376 0 WallpaperAerialsExtension: (MediaToolbox) [com.apple.coremedia:] <<<< FigVideoQueue >>>> FigVideoQueueGMStats: (0x86f1eb800) 0 frames enqueued in the last 6 seconds; 0 were suspended. sbuf queue contains 0 frames (0.000 sec), max PTS: nan; LWM: 1.000 HWM: 2.000. Timebase time 238.895, rate 0.00. (pid 1376)
|
||||
2026-09-06 01:48:55.728480+0100 0x4e1c50 Default 0x0 1376 0 WallpaperAerialsExtension: (MediaToolbox) [com.apple.coremedia:] <<<< FigVideoQueue >>>> FigVideoQueueGMStats: (0x86f1ea800) 0 frames enqueued in the last 6 seconds; 0 were suspended. sbuf queue contains 0 frames (0.000 sec), max PTS: nan; LWM: 1.000 HWM: 2.000. Timebase time 238.895, rate 0.00. (pid 1376)
|
||||
2026-09-06 01:48:55.728505+0100 0x4e23d2 Default 0x0 1376 0 WallpaperAerialsExtension: (MediaToolbox) [com.apple.coremedia:] <<<< FigVideoQueue >>>> FigVideoQueueGMStats: (0x86f1ebc00) 0 frames enqueued in the last 6 seconds; 0 were suspended. sbuf queue contains 0 frames (0.000 sec), max PTS: nan; LWM: 1.000 HWM: 2.000. Timebase time 238.895, rate 0.00. (pid 1376)
|
||||
2026-09-06 01:48:55.731950+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.736436+0100 0x4defc2 Default 0x0 572 0 locationd: (SPFinder) [com.apple.icloud.SPFinder:advertisementCache] saveBeaconPayload: 1
|
||||
2026-09-06 01:48:55.736480+0100 0x4df240 Default 0x0 572 0 locationd: [com.apple.locationd.Core:Harvester] {"msg":"onAvengerAdvertisementDetected: got avenger advertisement", "subHarvester":"Avenger"}
|
||||
2026-09-06 01:48:55.736490+0100 0x4defc2 Activity 0x9d0e2b 572 0 locationd: (SPFinder) SPAdvertisementCache saveBeaconPayloads:completion:
|
||||
2026-09-06 01:48:55.736553+0100 0x4df240 Default 0x0 572 0 locationd: [com.apple.locationd.Core:Harvester] {"msg":"convertToSPAdvertisement", "address":<private>, "data":<private>, "date":<private>, "rssi":<private>, "status":<private>, "reserved":<private>, "subHarvester":"Avenger"}
|
||||
2026-09-06 01:48:55.736607+0100 0x4df240 Default 0x0 572 0 locationd: [com.apple.locationd.Core:Harvester] {"msg":"onAvengerAdvertisementDetected: advertisement is near-owner from other people and do not process it", "subHarvester":"Avenger"}
|
||||
2026-09-06 01:48:55.736837+0100 0x4dfe20 Activity 0x9d0ff0 746 0 searchpartyd: (FindMyBase) SaveObservedAdvertisment
|
||||
2026-09-06 01:48:55.737231+0100 0x4dfe20 Default 0x9d0ff0 746 0 searchpartyd: (FindMyBase) [com.apple.icloud.searchpartyd:observationStoring] TIME: ObservationStoring.Reconcile: <private>
|
||||
2026-09-06 01:48:55.765790+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.799736+0100 0x4e0051 Activity 0x9d0fd0 553 0 mds: (CoreDuetContext) CoreDuet: ClientContext objectForContextualKeyPath:
|
||||
2026-09-06 01:48:55.801580+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.824926+0100 0x4e20a1 Default 0x0 538 8 powerd: [com.apple.powerd:displayState] DesktopMode check on Battery 0
|
||||
2026-09-06 01:48:55.830347+0100 0x4e1ff6 Error 0x0 0 0 kernel: (Sandbox) Sandbox: spotlightknowledged(19183) deny(1) mach-lookup com.apple.diagnosticd
|
||||
2026-09-06 01:48:55.831847+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.841825+0100 0x4db1a8 Default 0x0 1157 0 mediaanalysisd: Running task MADServiceTextProcessingTask (57542) [QoS: 21]
|
||||
2026-09-06 01:48:55.841897+0100 0x4db1a8 Default 0x0 1157 0 mediaanalysisd: Watchdog already enabled
|
||||
2026-09-06 01:48:55.841929+0100 0x4db1a8 Default 0x0 1157 5 mediaanalysisd: [com.apple.mediaanalysis:persistent] [Scheduler] Running <private> (57542) [QoS: 21 Cost: 0.100]; remaining resource: 0.90
|
||||
2026-09-06 01:48:55.841976+0100 0x4db1a8 Default 0x0 1157 5 mediaanalysisd: [com.apple.mediaanalysis:persistent] [QueuingScheduler] Scheduled foreground task MADServiceTextProcessingTask (MADRequestID 377, VCPMADTaskID 57542) for client <private>
|
||||
2026-09-06 01:48:55.842274+0100 0x4d2d86 Default 0x0 1157 0 mediaanalysisd: (CoreSceneUnderstanding) Setting new context length 512
|
||||
2026-09-06 01:48:55.860531+0100 0x4d2d86 Default 0x0 1157 0 mediaanalysisd: (MIL) MMapped <private> to base address 100d80000 with size 2008
|
||||
2026-09-06 01:48:55.866558+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.872246+0100 0x4de289 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9668] : In AW: Tx:0 Rx:0 Prev Tx:0 Rx:0 time since 255553435 indatamode 1 infraPriority 1 cpc 0
|
||||
2026-09-06 01:48:55.872263+0100 0x4de289 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9673] : Ignore changing the state as infraPriority 1 P2P-CPC 0
|
||||
2026-09-06 01:48:55.875121+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:55.881387+0100 0x4d2d86 Default 0x0 1157 0 mediaanalysisd: (MIL) MMapped <private> to base address 10c840000 with size 677474
|
||||
2026-09-06 01:48:55.899719+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.934529+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.673815]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:55.934541+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.673820]: arm_cpu_init(): cpu 12 online
|
||||
2026-09-06 01:48:55.934542+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.673825]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:55.934527+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.937693+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:55.966206+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:55.966800+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:55.992284+0100 0x35de04 Default 0x0 0 0 kernel: [745896.723885]: arm_cpu_init(): cpu 12 online
|
||||
2026-09-06 01:48:55.992293+0100 0x35de04 Default 0x0 0 0 kernel: [745896.723889]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:55.992294+0100 0x35de04 Default 0x0 0 0 kernel: [745896.723892]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:55.992294+0100 0x35de04 Default 0x0 0 0 kernel: [745896.732976]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:55.992294+0100 0x35de04 Default 0x0 0 0 kernel: [745896.732979]: arm_cpu_init(): cpu 12 online
|
||||
2026-09-06 01:48:55.992294+0100 0x35de04 Default 0x0 0 0 kernel: [745896.732987]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:55.992282+0100 0x35de04 Default 0x0 0 0 kernel: process_is_plugin_host: running binary "sh" in keys-off mode due to identity: com.apple.sh
|
||||
2026-09-06 01:48:55.993293+0100 0x4e23d7 Default 0x0 0 0 kernel: sh[40634] triggered unnest of range 0x1ee000000->0x1f2000000 of DYLD shared region in VM map 0xe6c827f1353fb483. While not abnormal for debuggers, this increases system memory footprint almost permanently (until the shared region is re-slid).
|
||||
2026-09-06 01:48:55.994465+0100 0x4e23d7 Default 0x0 0 0 kernel: process_is_plugin_host: running binary "bash" in keys-off mode due to identity: com.apple.bash
|
||||
2026-09-06 01:48:55.995238+0100 0x4e23d8 Default 0x0 0 0 kernel: bash[40634] triggered unnest of range 0x1ee000000->0x1f2000000 of DYLD shared region in VM map 0x797c4c4fe647b2b9. While not abnormal for debuggers, this increases system memory footprint almost permanently (until the shared region is re-slid).
|
||||
2026-09-06 01:48:56.000882+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLState[10141] : Active Sockets false ValidSvc 4 NumAirplay 0 AFHandlePending 0 TimeOfLastAFHandleRequest 142708 ms
|
||||
2026-09-06 01:48:56.000916+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLState[10239]: BonJourTrig 1 ValidSvc 4 RTApp 0 TSReq 0 HasActAirDrop 0 SocketsActive 0
|
||||
2026-09-06 01:48:56.000928+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: setScheduleState[11041]: reason:UserTriggered sc:Infra Priority and force:NO, AWDL-restore:No
|
||||
2026-09-06 01:48:56.001188+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.001370+0100 0x4e23d9 Default 0x0 0 0 kernel: python3.13[40634] triggered unnest of range 0x1ee000000->0x1f2000000 of DYLD shared region in VM map 0x38801cc981e88d6f. While not abnormal for debuggers, this increases system memory footprint almost permanently (until the shared region is re-slid).
|
||||
2026-09-06 01:48:56.032459+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.772342]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:56.032468+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.772347]: arm_cpu_init(): cpu 12 online
|
||||
2026-09-06 01:48:56.032468+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.772353]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:56.032469+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.772356]: arm_cpu_init(): cpu 15 online
|
||||
2026-09-06 01:48:56.032458+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.049546+0100 0x35de04 Default 0x0 0 0 kernel: process_is_plugin_host: running binary "zsh" in keys-off mode due to identity: com.apple.zsh
|
||||
2026-09-06 01:48:56.050416+0100 0x4e23da Default 0x0 0 0 kernel: zsh[40635] triggered unnest of range 0x1ee000000->0x1f2000000 of DYLD shared region in VM map 0xe6c827f1353fb483. While not abnormal for debuggers, this increases system memory footprint almost permanently (until the shared region is re-slid).
|
||||
2026-09-06 01:48:56.052423+0100 0x4e23da Activity 0x9d1000 40635 0 zsh: (libsystem_info.dylib) Retrieve User by ID
|
||||
2026-09-06 01:48:56.056832+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:56.066425+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.806256]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:56.066433+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.806262]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:56.066423+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.068814+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:56.070421+0100 0x4e23de Default 0x0 0 0 kernel: 2.1.261[40637] triggered unnest of range 0x1ee000000->0x1f2000000 of DYLD shared region in VM map 0xddbc6b7757294f33. While not abnormal for debuggers, this increases system memory footprint almost permanently (until the shared region is re-slid).
|
||||
2026-09-06 01:48:56.100262+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.832882]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:56.100272+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.832886]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:56.100273+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.832889]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:56.100273+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.843348]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:56.100274+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.843354]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:56.100261+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.133625+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.166361+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.200737+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.233953+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.961670]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:56.233962+0100 0x4a3b07 Default 0x0 0 0 kernel: [745896.961676]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:56.233952+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.247221+0100 0x4e20a1 Default 0x0 538 8 powerd: [com.apple.powerd:displayState] DesktopMode check on Battery 0
|
||||
2026-09-06 01:48:56.267171+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.300298+0100 0x4a3b07 Default 0x0 0 0 kernel: [745897.047876]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:56.300305+0100 0x4a3b07 Default 0x0 0 0 kernel: [745897.047905]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:56.300307+0100 0x4a3b07 Default 0x0 0 0 kernel: [745897.047936]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:56.300307+0100 0x4a3b07 Default 0x0 0 0 kernel: [745897.047980]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:56.300309+0100 0x4a3b07 Default 0x0 0 0 kernel: [745897.048016]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:56.300314+0100 0x4a3b07 Default 0x0 0 0 kernel: [745897.048045]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:56.300315+0100 0x4a3b07 Default 0x0 0 0 kernel: [745897.048094]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:56.300317+0100 0x4a3b07 Default 0x0 0 0 kernel: [745897.048123]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:56.300317+0100 0x4a3b07 Default 0x0 0 0 kernel: [745897.048155]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:56.300318+0100 0x4a3b07 Default 0x0 0 0 kernel: [745897.048200]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:56.300319+0100 0x4a3b07 Default 0x0 0 0 kernel: [745897.048233]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:56.300320+0100 0x4a3b07 Default 0x0 0 0 kernel: [745897.048263]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:56.300320+0100 0x4a3b07 Default 0x0 0 0 kernel: [745897.048311]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/ad
|
||||
2026-09-06 01:48:56.300287+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.333609+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.366240+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.396619+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: isInfraRealtimePacketThresholdAllowed allowed:1 option:32 threshold:50 noRegistrations:1 cachedPeerCount:0 fastDiscoveryInactive:1 fastDiscoveryOnSince:255553960
|
||||
2026-09-06 01:48:56.396649+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: currentInfraTrafficType:9277 checking if realtime upgrade required with inputPackets:106 outputPackets:155 packetThreshold:50
|
||||
2026-09-06 01:48:56.396662+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: peerChannelSteer: PEER_CHANNEL_STEER isSteeringRequired 0 Attempt Count 0
|
||||
2026-09-06 01:48:56.396671+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: peerChannelSteer: PEER_CHANNEL_STEER isRestartSteeringRequired is false
|
||||
2026-09-06 01:48:56.396691+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9668] : In AW: Tx:0 Rx:0 Prev Tx:0 Rx:0 time since 255553960 indatamode 1 infraPriority 1 cpc 0
|
||||
2026-09-06 01:48:56.396701+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9673] : Ignore changing the state as infraPriority 1 P2P-CPC 0
|
||||
2026-09-06 01:48:56.400278+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.408316+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:56.432865+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.462152+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:56.466080+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.499132+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.506657+0100 0x4e20a1 Default 0x0 538 8 powerd: [com.apple.powerd:displayState] DesktopMode check on Battery 0
|
||||
2026-09-06 01:48:56.533255+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.556058+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:56.565352+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.591174+0100 0x4e226a Default 0x0 1165 0 parsecd: (ApplePushService) allConsoleUserIDs <private>
|
||||
2026-09-06 01:48:56.591400+0100 0x4e226a Default 0x0 1165 0 parsecd: (ApplePushService) [com.apple.apsd:connection] <private> creating an xpc connection {syncXPC: YES}
|
||||
2026-09-06 01:48:56.591427+0100 0x4e226a Default 0x0 1165 0 parsecd: (libxpc.dylib) [com.apple.xpc:connection] [0xba30a1b80] activating connection: mach=true listener=false peer=false name=com.apple.apsd
|
||||
2026-09-06 01:48:56.591483+0100 0x4e1fdb Error 0x0 0 0 kernel: (Sandbox) 2 duplicate reports for Sandbox: spotlightknowledged(19183) deny(1) mach-lookup com.apple.diagnosticd
|
||||
2026-09-06 01:48:56.591494+0100 0x4e1fdb Error 0x0 0 0 kernel: (Sandbox) Sandbox: parsecd(1165) deny(1) mach-lookup com.apple.apsd
|
||||
2026-09-06 01:48:56.591677+0100 0x4e226a Default 0x0 1165 0 parsecd: (libxpc.dylib) [com.apple.xpc:connection] [0xba30a1b80] failed to do a bootstrap look-up: xpc_error=[159: Unknown error: 159]
|
||||
2026-09-06 01:48:56.591714+0100 0x4e226a Default 0x0 1165 0 parsecd: (libxpc.dylib) [com.apple.xpc:connection] [0xba30a1b80] invalidated after a failed init
|
||||
2026-09-06 01:48:56.591861+0100 0x4e1e08 Error 0x0 1165 0 parsecd: (ApplePushService) [com.apple.apsd:xpc] Connection Invalid for service com.apple.apsd
|
||||
2026-09-06 01:48:56.591878+0100 0x4e1e08 Default 0x0 1165 0 parsecd: (ApplePushService) [com.apple.apsd:connection] invalidationHandler called for <private>
|
||||
2026-09-06 01:48:56.591962+0100 0x4e226a Default 0x0 1165 0 parsecd: (ApplePushService) [com.apple.apsd:connection] <private> Bad response from apsd: <private>
|
||||
2026-09-06 01:48:56.592001+0100 0x4e226a Default 0x0 1165 0 parsecd: (ApplePushService) [com.apple.apsd:connection] Daemon connection did not succeed. Is apsd running?
|
||||
2026-09-06 01:48:56.592065+0100 0x4e226a Default 0x0 1165 0 parsecd: (ApplePushService) [com.apple.apsd:connection] <private>: Will attempt reconnect in 300.000000 seconds
|
||||
2026-09-06 01:48:56.592102+0100 0x4e226a Default 0x0 1165 0 parsecd: (ApplePushService) [com.apple.apsd:connection] <private> disconnected from apsd...
|
||||
2026-09-06 01:48:56.592111+0100 0x4e226a Default 0x0 1165 0 parsecd: (ApplePushService) [com.apple.apsd:connection] <private> failing 0 pending outgoing messages.
|
||||
2026-09-06 01:48:56.592118+0100 0x4e226a Default 0x0 1165 0 parsecd: (ApplePushService) [com.apple.apsd:connection] <private> calling connectIfNecessary
|
||||
2026-09-06 01:48:56.592205+0100 0x4e226a Default 0x0 1165 0 parsecd: (ApplePushService) [com.apple.apsd:connection] <private>: Reconnect scheduled, skipping immediate connection to com.apple.apsd
|
||||
2026-09-06 01:48:56.593107+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:56.599921+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.632882+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.656803+0100 0x4d6c29 Default 0x0 643 0 symptomsd: (SymptomEvaluator) [com.apple.symptomsd:bbh] TCP metrics iteration:270 since 30.00 secs: allflows=44/C=0/R=0/W=0/flows=3/unacked=0/rxbytes=14768/txbytes=4622/rxooo=0/rxdup=0/retx=0
|
||||
2026-09-06 01:48:56.656853+0100 0x4d6c29 Default 0x0 643 0 symptomsd: (SymptomEvaluator) [com.apple.symptomsd:bbh] TCP progress metrics score: 0, problem ratio: 0.00 (baseline: 0.00)
|
||||
2026-09-06 01:48:56.667344+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.699360+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.732739+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.766028+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.766434+0100 0x4e20a1 Default 0x0 538 8 powerd: [com.apple.powerd:displayState] DesktopMode check on Battery 0
|
||||
2026-09-06 01:48:56.800349+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.832644+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.866678+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.897602+0100 0x4dfc8c Activity 0x9d0fd1 553 0 mds: (CoreDuetContext) CoreDuet: ClientContext objectForContextualKeyPath:
|
||||
2026-09-06 01:48:56.897908+0100 0x4dfc8c Activity 0x9d0fd2 553 0 mds: (CoreDuetContext) CoreDuet: ClientContext objectForContextualKeyPath:
|
||||
2026-09-06 01:48:56.898157+0100 0x4dfc8c Activity 0x9d0fd3 553 0 mds: (CoreDuetContext) CoreDuet: ClientContext objectForContextualKeyPath:
|
||||
2026-09-06 01:48:56.898614+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.920820+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9668] : In AW: Tx:0 Rx:0 Prev Tx:0 Rx:0 time since 255554484 indatamode 1 infraPriority 1 cpc 0
|
||||
2026-09-06 01:48:56.920848+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9673] : Ignore changing the state as infraPriority 1 P2P-CPC 0
|
||||
2026-09-06 01:48:56.923048+0100 0x4d2d86 Default 0x0 1157 5 mediaanalysisd: [com.apple.mediaanalysis:persistent] [Scheduler] Completed <private> (57542) [QoS: 21]
|
||||
2026-09-06 01:48:56.923306+0100 0x4e1ff6 Error 0x0 0 0 kernel: (Sandbox) Sandbox: spotlightknowledged(19183) deny(1) mach-lookup com.apple.diagnosticd
|
||||
2026-09-06 01:48:56.927493+0100 0x1f39 Default 0x0 1018 3 sharingd: (CoreUtils) [com.apple.sharing:SFBLEScanner] [Prox] Clearing duplicate cache
|
||||
2026-09-06 01:48:56.927537+0100 0x1f39 Default 0x0 1018 0 sharingd: (WirelessProximity) [com.apple.bluetooth:WirelessProximity] ProximityPairing clear duplicate cache
|
||||
2026-09-06 01:48:56.927786+0100 0x4dff68 Default 0x0 589 0 bluetoothd: (WPDaemon) [com.apple.bluetooth:WirelessProximity] clear dup filter cache for process sharingd (1018) with scan request of type 7, blob: {length = 0, bytes = 0x}, mask {length = 0, bytes = 0x}, active: 0, duplicates: 0, screen on: No Scan, screen off: No Scan, locked: 1, rssi: -90, peers: (
|
||||
) nearby scan mode: 10, advbuf: 0, priority critical: 0, range: 0, usecases:
|
||||
2026-09-06 01:48:56.927825+0100 0x4dff68 Default 0x0 589 0 bluetoothd: (WPDaemon) [com.apple.bluetooth:WirelessProximity] About to clearDuplicateFilterCache for type: 7
|
||||
2026-09-06 01:48:56.927883+0100 0x4de1c1 Default 0x0 589 0 bluetoothd: [com.apple.bluetooth:Server.XPC] Received XPC message "CBMsgIdClearDuplicateFilterCache" from session "com.apple.bluetoothd-central-589-1"
|
||||
2026-09-06 01:48:56.930165+0100 0x4de1c1 Default 0x0 589 0 bluetoothd: [com.apple.bluetooth:Server.App] clearDuplicateFilterCache for session "com.apple.bluetoothd" type 7
|
||||
2026-09-06 01:48:56.933261+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.966964+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:56.986301+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:57.000034+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.001909+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLState[10141] : Active Sockets false ValidSvc 4 NumAirplay 0 AFHandlePending 0 TimeOfLastAFHandleRequest 143709 ms
|
||||
2026-09-06 01:48:57.001941+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLState[10239]: BonJourTrig 1 ValidSvc 4 RTApp 0 TSReq 0 HasActAirDrop 0 SocketsActive 0
|
||||
2026-09-06 01:48:57.001953+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: setScheduleState[11041]: reason:UserTriggered sc:Infra Priority and force:NO, AWDL-restore:No
|
||||
2026-09-06 01:48:57.015059+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:57.026106+0100 0x4e20a1 Default 0x0 538 8 powerd: [com.apple.powerd:displayState] DesktopMode check on Battery 0
|
||||
2026-09-06 01:48:57.033034+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.056352+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:57.066726+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.099720+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.118079+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:57.134134+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.166618+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.196253+0100 0x4df240 Default 0x0 572 0 locationd: (SPFinder) [com.apple.icloud.SPFinder:advertisementCache] saveBeaconPayload: 1
|
||||
2026-09-06 01:48:57.196249+0100 0x4defc1 Default 0x0 572 0 locationd: [com.apple.locationd.Core:Harvester] {"msg":"onAvengerAdvertisementDetected: got avenger advertisement", "subHarvester":"Avenger"}
|
||||
2026-09-06 01:48:57.196286+0100 0x4df240 Activity 0x9d0e2c 572 0 locationd: (SPFinder) SPAdvertisementCache saveBeaconPayloads:completion:
|
||||
2026-09-06 01:48:57.196317+0100 0x4defc1 Default 0x0 572 0 locationd: [com.apple.locationd.Core:Harvester] {"msg":"convertToSPAdvertisement", "address":<private>, "data":<private>, "date":<private>, "rssi":<private>, "status":<private>, "reserved":<private>, "subHarvester":"Avenger"}
|
||||
2026-09-06 01:48:57.196359+0100 0x4defc1 Default 0x0 572 0 locationd: [com.apple.locationd.Core:Harvester] {"msg":"onAvengerAdvertisementDetected: advertisement is near-owner from other people and do not process it", "subHarvester":"Avenger"}
|
||||
2026-09-06 01:48:57.196618+0100 0x4dfe20 Activity 0x9d0ff1 746 0 searchpartyd: (FindMyBase) SaveObservedAdvertisment
|
||||
2026-09-06 01:48:57.197020+0100 0x4e069c Default 0x9d0ff1 746 0 searchpartyd: (FindMyBase) [com.apple.icloud.searchpartyd:observationStoring] TIME: ObservationStoring.Reconcile: <private>
|
||||
2026-09-06 01:48:57.201156+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.231679+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.266391+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.285883+0100 0x4e20a1 Default 0x0 538 8 powerd: [com.apple.powerd:displayState] DesktopMode check on Battery 0
|
||||
2026-09-06 01:48:57.300070+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.047797]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:57.300086+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.047821]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:57.300087+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.047850]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:57.300088+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.047904]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:57.300089+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.047934]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:57.300090+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.047960]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:57.300091+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.048015]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:57.300091+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.048040]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:57.300093+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.048071]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:57.300093+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.048118]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:57.300095+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.048150]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:57.300096+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.048184]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:57.300097+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.048237]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/ad
|
||||
2026-09-06 01:48:57.300059+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.333480+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.366348+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.400255+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.408573+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:57.412060+0100 0x4e2016 Activity 0x9d0fd4 553 0 mds: (CoreDuetContext) CoreDuet: ClientContext objectForContextualKeyPath:
|
||||
2026-09-06 01:48:57.433241+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.436836+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) 745898.192389 wlan0.E[819960] [wcl] checkTrafficCounters@2686:realTime before<0> -> after<0>, realTimeNumPackets =<2> inactive before<0> -> after<0>, infraNumPackets =<752> statsIntervalMultiplier=<5>
|
||||
2026-09-06 01:48:57.436954+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) 745898.192514 wlan0.E[819961] [wcl] computeAdvisoryScores@666:LQM-WiFi: WiFi Latency: TxLat-hist [3(100 %) / 0(0 %) / 0(0 %) / 0(0 %)] RxLat-hist [0(0 %) / 0(0 %) / 0(0 %) / 0(0 %)]
|
||||
2026-09-06 01:48:57.437120+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16356
|
||||
2026-09-06 01:48:57.437176+0100 0x4db330 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16356
|
||||
2026-09-06 01:48:57.437219+0100 0x4db330 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16360
|
||||
2026-09-06 01:48:57.437246+0100 0x4db330 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16364
|
||||
2026-09-06 01:48:57.437279+0100 0x4e2150 Default 0x0 929 0 wifip2pd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16356
|
||||
2026-09-06 01:48:57.437351+0100 0x4e2150 Default 0x0 929 0 wifip2pd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16360
|
||||
2026-09-06 01:48:57.437383+0100 0x4e2150 Default 0x0 929 0 wifip2pd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16364
|
||||
2026-09-06 01:48:57.437270+0100 0x4db330 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16368
|
||||
2026-09-06 01:48:57.437406+0100 0x4e2150 Default 0x0 929 0 wifip2pd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16368
|
||||
2026-09-06 01:48:57.437428+0100 0x4e2150 Default 0x0 929 0 wifip2pd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16372
|
||||
2026-09-06 01:48:57.437450+0100 0x4e2150 Default 0x0 929 0 wifip2pd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16376
|
||||
2026-09-06 01:48:57.437472+0100 0x4e2150 Default 0x0 929 0 wifip2pd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16380
|
||||
2026-09-06 01:48:57.438233+0100 0x4db330 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16372
|
||||
2026-09-06 01:48:57.438397+0100 0x4db330 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16376
|
||||
2026-09-06 01:48:57.438965+0100 0x4db330 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16380
|
||||
2026-09-06 01:48:57.439214+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16360
|
||||
2026-09-06 01:48:57.439350+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16364
|
||||
2026-09-06 01:48:57.439506+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16368
|
||||
2026-09-06 01:48:57.439540+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16372
|
||||
2026-09-06 01:48:57.439564+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16376
|
||||
2026-09-06 01:48:57.439587+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) Apple80211EventMonitoringHelper:5877 Skipping Padding Read offset is 16380
|
||||
2026-09-06 01:48:57.439664+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) [com.apple.WiFiManager:] Driver Event: <airport[642]> _bsd_80211_event_callback: APPLE80211_M_REALTIME_SESSION_NOTIFICATION/156 (en0)
|
||||
2026-09-06 01:48:57.439751+0100 0x4db330 Default 0x0 642 0 airportd: (IO80211) [com.apple.WiFiManager:] Info: <airport[642]> 11ax ASR session: new-state = 0x0
|
||||
2026-09-06 01:48:57.439802+0100 0x4db330 Default 0x0 642 0 airportd: (IO80211) [com.apple.WiFiManager:] Info: <airport[642]> airportdProcessRealtimeSessionNotificationEvent channel score: chq = 3, tx-lat = 5, rx-lat = 3, tx-loss = 5, rx-loss = 5, p95-lat = 3, RT = 0x0, link-reco = 0x0
|
||||
2026-09-06 01:48:57.442603+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) 745898.198158 wlan0.A[819962] [dk] updateLinkQualityMetrics@750:For current session updated fRxDurMulticast 5892240, fRxDurBroadcast 1909060
|
||||
2026-09-06 01:48:57.445101+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: isInfraRealtimePacketThresholdAllowed allowed:1 option:32 threshold:50 noRegistrations:1 cachedPeerCount:0 fastDiscoveryInactive:1 fastDiscoveryOnSince:255555008
|
||||
2026-09-06 01:48:57.445119+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: currentInfraTrafficType:9277 checking if realtime upgrade required with inputPackets:34 outputPackets:35 packetThreshold:50
|
||||
2026-09-06 01:48:57.445143+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: peerChannelSteer: PEER_CHANNEL_STEER isSteeringRequired 0 Attempt Count 0
|
||||
2026-09-06 01:48:57.445152+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: peerChannelSteer: PEER_CHANNEL_STEER isRestartSteeringRequired is false
|
||||
2026-09-06 01:48:57.445166+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9668] : In AW: Tx:0 Rx:0 Prev Tx:0 Rx:0 time since 255555008 indatamode 1 infraPriority 1 cpc 0
|
||||
2026-09-06 01:48:57.445176+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9673] : Ignore changing the state as infraPriority 1 P2P-CPC 0
|
||||
2026-09-06 01:48:57.449237+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: IO80211PeerManager::reportDataPathEvents APPLE80211_M_RSSI_CHANGED RSSI -68 noise -94 snr 27. Core0Rssi:-66 Core1Rssi:-71
|
||||
2026-09-06 01:48:57.449468+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) [com.apple.WiFiManager:] Driver Event: <airport[642]> _bsd_80211_event_callback: APPLE80211_M_RSSI_CHANGED/39 (en0)
|
||||
2026-09-06 01:48:57.449638+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) [com.apple.WiFiManager:] Info: <airport[642]> Per-MLO link stats
|
||||
2026-09-06 01:48:57.449750+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) airportdProcessTrafficEngineeringEvents: <en0> eventType[39] @[745898.205068], took 0.000171
|
||||
2026-09-06 01:48:57.449808+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) [com.apple.WiFiManager:] <airport[642]> airportdProcessTrafficEngineeringEvents: <en0> eventType[39] @[745898.205068], took 0.000171
|
||||
2026-09-06 01:48:57.450027+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM: rssi=-68dBm per_ant_rssi=(-66dBm, -71dBm) noise=-94dBm snr=27 cca=6.0% ccaSelfTotal=1 ccaOtherTotal=1 interferenceTotal=4 txRate=324.3Mbps txFrames=293 txFail=0 txRetrans=48 txFallbackRate=108.1Mbps rxRate=648.5Mbps rxFrames=322 rxRetryFrames=3 rxToss=0 beaconRecv=49 beaconSched=49 txFwFrames=0 txFwFail=0 txFwRetrans=0 network = <redacted> bssid = <redacted> channel = 44 BW = 80 band = 2
|
||||
2026-09-06 01:48:57.450274+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: WeightAvgLQM rssi=-69 snr=27 txRate=301662 rxRate=576470
|
||||
2026-09-06 01:48:57.450293+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: (HighBand) rxCrsGlitch=179 rxBphyCrsGlitch=0 rxStart=2993 rxBadPLCP=18 rxBphyBadPLCP=0 rxBadFCS=6 rxFifo0Ovfl=0 rxFifo1Ovfl=0 rx_nobuf=0 rxAnyErr=1643 rxResponseTimeout=16 rxNoDelim=91 rxFrmTooLong=0 rxFrmTooShort=0
|
||||
2026-09-06 01:48:57.450303+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: (HighBand) txRTSFrm=169 txRTSFail=0 rxCTSUcast=168 rxRTSUcast=201 txCTSFrm=201 txAMPDU=170 rxBACK=155 txPhyError=0 txAllFrm=763 txMPDU=0 txUcast=170 rxACKUcast=0 OfdmDesense=0 dB
|
||||
2026-09-06 01:48:57.450311+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: (HighBand) rxBeaconMbss=49 rxBeaconObss=0 rxDataUcastMbss=189 rxMgmtUcastMbss=0 rxCNTRLUcast=549 txACKFrm=0 txBACK=184 ctxFifoFull=0 ctxFifo2Full=0 rxDataMcast=4 rxMgmtMcast=49 rxDrop20s=210
|
||||
2026-09-06 01:48:57.450316+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: RX AMPDU (HighBand) rxAmpdu=205 txBACK(Ucode)=184 rxMpduInAmpdu=214 rxholes=0 rxdup=0 rxstuck=0 rxoow=0 rxoos=1 rxaddbareq=0 txaddbaresp=0 rxbar=0 txdelba=0 rxdelba=0 rxQueued=1 rxRetryNoBA=0
|
||||
2026-09-06 01:48:57.450322+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: WME RX MPDUs (rxPER 0 %) in tids 0:292, 1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0
|
||||
2026-09-06 01:48:57.450328+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: Stats12(5G): bt5g_status=0x3 bt5g_defer_cnt=0 bt5g_no_defer_cnt=0 bt5g_defer_max_switch_dur=0 bt5g_no_defer_max_switch_dur=0 bt5g_switch_succ_cnt=0 bt5g_switch_fail_cnt=0 bt5g_switch_reason_bm=0
|
||||
2026-09-06 01:48:57.450335+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: Stats12(2G): btDur=10ms(0%) status=0x10d reqTypeMap=0x500000 req=23 gnt=14 abort=2 rx1ovfl=0 latency=0 pmAttempt=0 sucPM=0 sucCTS2S=0 wlanTXPreempt=19 wlanRXPreempt=0 apTXafterPM=0 gntAudio=0 denyAudio=0 gntA2DP=0 denyA2DP=0
|
||||
2026-09-06 01:48:57.450342+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: (HighBand) rxToss=0 rxLastTossRsn=39 rxNoFrag=0 rxNoCmplId=0 rxNoHaddr=0 rxMulti=2 rxUndec=0
|
||||
2026-09-06 01:48:57.450354+0100 0x4db330 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] BEGIN REQ [GET INTF NAME] (pid=643 proc=symptomsd bundleID=com.apple.symptomsd codesignID=com.apple.symptomsd service=com.apple.private.corewifi-xpc qos=17 intf=(null) uuid=62638 info=(null))
|
||||
2026-09-06 01:48:57.450365+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: Stats12(2G): CrtPriCnt=69 PriCnt=88 gntSniff=0 denySniff=0 antGrant0~10ms=157 antGrant10~30ms=0 antGrant30~60ms=0 antGrant>60ms=0 APLeakiness=0 rr_cnt=0 rr_succ_cnt=0
|
||||
2026-09-06 01:48:57.450398+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: Stats12(2G): btDurAir=97ms(1%) desenseMode=0 wlrssi=0 btrssi=0 profile2gActive=0 profile5gActive=6 inactiveDur=4485 btPmAttemptNoackCnt=0 status2=0x0 [5000ms]
|
||||
2026-09-06 01:48:57.450406+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: 15.4 Coex Stats V3: Mode=0 Request=0 Grant=0 Duration=0us AckedPM=0 TXedCTS2A=0 GrantDelay=0 CritPhyCal=0 CritRateRecovery=0 CritBcnLoss=0 CritHpp=0 CritBT=0 CritAwdlNan=0 CritMaxDur=1050 CirtAggregate=0 CritBitMapMax=2
|
||||
2026-09-06 01:48:57.450429+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: 15.4 Coex Stats V3: 154_tx_req_cnt=0 154_rx_req_cnt=0 154_tx_deny_cnt=0 154_rx_deny_cnt=0 [5000ms]
|
||||
2026-09-06 01:48:57.450417+0100 0x4db330 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] [62638] Incoming QoS is less than 'default', promoting to 'default'
|
||||
2026-09-06 01:48:57.450437+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: TX(00:00:00:00:00:00) AC<SU MS NB NRS NA CM EX TF FFP MRET FLE> BE<292 0 0 0 0 0 0 0 0 0 0> (5002ms)
|
||||
2026-09-06 01:48:57.450474+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: TX(00:00:00:00:00:00) AC<SU MS NB NRS NA CM EX TF FFP MRET FLE> BK<2 0 0 0 0 0 0 0 0 0 0> (5002ms)
|
||||
2026-09-06 01:48:57.450483+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: TX(00:00:00:00:00:00) AC<SU MS NB NRS NA CM EX TF FFP MRET FLE> VI<0 0 0 0 0 0 0 0 0 0 0> (5002ms)
|
||||
2026-09-06 01:48:57.450504+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: TX(00:00:00:00:00:00) AC<SU MS NB NRS NA CM EX TF FFP MRET FLE> VO<1 0 0 0 0 0 0 0 0 0 0> (5002ms)
|
||||
2026-09-06 01:48:57.450596+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] LQM-WiFi: L3 Control VO TX(00:00:00:00:00:00) Success=1 NoACK=0 Expired=0 OtherErr=0
|
||||
2026-09-06 01:48:57.450869+0100 0x4db330 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] END REQ [GET INTF NAME] took 0.000471584s (pid=643 proc=symptomsd bundleID=com.apple.symptomsd codesignID=com.apple.symptomsd service=com.apple.private.corewifi-xpc qos=21 intf=(null) uuid=62638 err=0 reply=<redacted>
|
||||
2026-09-06 01:48:57.450959+0100 0x4df3b0 Default 0x0 643 0 symptomsd: (SymptomEvaluator) [com.apple.symptomsd:rnf] L2 Metrics on en0: rssi: -68 [-1,-1] -> -68, snr: 27 (cca [wake/total] self/other/intf): [1,1]/[0,1]/[4,4]/6 (txFrames/txReTx/txFail): 293/48/0 -> (was/is) 0/0, txRate: 324.0, rxRate: 648.0, rssiEMA: -67.6, snrEMA: 27.0, txRateEMA: 312.7, rxRateEMA: 534.6, isPrimary: 1, radio: 802.11ax
|
||||
2026-09-06 01:48:57.451145+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] BEGIN REQ [GET BSSID] (pid=643 proc=symptomsd bundleID=com.apple.symptomsd codesignID=com.apple.symptomsd service=com.apple.private.corewifi-xpc qos=17 intf=(null) uuid=67C47 info=(null))
|
||||
2026-09-06 01:48:57.451176+0100 0x4dfc53 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] [67C47] Incoming QoS is less than 'default', promoting to 'default'
|
||||
2026-09-06 01:48:57.451603+0100 0x4dc356 Default 0x0 642 0 airportd: (CoreWiFi) [com.apple.WiFiManager:] [corewifi] END REQ [GET BSSID] took 0.000425417s (pid=643 proc=symptomsd bundleID=com.apple.symptomsd codesignID=com.apple.symptomsd service=com.apple.private.corewifi-xpc qos=21 intf=(null) uuid=67C47 err=0 reply=<redacted>
|
||||
2026-09-06 01:48:57.467726+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.499311+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.510770+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: setSyncFramePeriod:25958 +++ Configuring the sync params(period, length) {16 16}, extlen = 16
|
||||
2026-09-06 01:48:57.510803+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: setSyncFramePeriod::Setting PSF period to 110
|
||||
2026-09-06 01:48:57.510824+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: programChipWithNewMaster: Programming new Mst: 82:45:A3:D5:31:34 at 255555.074ms HC 0 met 510 my met 510
|
||||
2026-09-06 01:48:57.510847+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: setSyncFramePeriod:25958 +++ Configuring the sync params(period, length) {16 16}, extlen = 16
|
||||
2026-09-06 01:48:57.510863+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: setSyncFramePeriod::Setting PSF period to 110
|
||||
2026-09-06 01:48:57.510873+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: runElection: ElectionParamsChanged - Prog (F:0, 0) New P:82:45:A3:D5:31:34 R:82:45:A3:D5:31:34 [RLFC:49492 HC:0 RMet:510 SLFC:49492 SMet:510]
|
||||
2026-09-06 01:48:57.510891+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:57.533659+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.558289+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:57.565927+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.586473+0100 0x4e20a1 Default 0x0 538 8 powerd: [com.apple.powerd:displayState] DesktopMode check on Battery 0
|
||||
2026-09-06 01:48:57.599662+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.633376+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.641692+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:57.665974+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.699771+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.733070+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.766563+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.800210+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.832349+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.561636]: arm_cpu_init(): cpu 12 online
|
||||
2026-09-06 01:48:57.832356+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.561642]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:57.832347+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.854407+0100 0x4e20a1 Default 0x0 538 8 powerd: [com.apple.powerd:displayState] DesktopMode check on Battery 0
|
||||
2026-09-06 01:48:57.866599+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.899028+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.932110+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.966597+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:57.969365+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9668] : In AW: Tx:0 Rx:0 Prev Tx:0 Rx:0 time since 255555533 indatamode 1 infraPriority 1 cpc 0
|
||||
2026-09-06 01:48:57.969385+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9673] : Ignore changing the state as infraPriority 1 P2P-CPC 0
|
||||
2026-09-06 01:48:58.000845+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLState[10141] : Active Sockets false ValidSvc 4 NumAirplay 0 AFHandlePending 0 TimeOfLastAFHandleRequest 144708 ms
|
||||
2026-09-06 01:48:58.000878+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLState[10239]: BonJourTrig 1 ValidSvc 4 RTApp 0 TSReq 0 HasActAirDrop 0 SocketsActive 0
|
||||
2026-09-06 01:48:58.000891+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: setScheduleState[11041]: reason:UserTriggered sc:Infra Priority and force:NO, AWDL-restore:No
|
||||
2026-09-06 01:48:58.001361+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.033116+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.034822+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:58.056677+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:58.066963+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.099429+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.132720+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.133661+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:58.139542+0100 0x4e2016 Activity 0x9d0fd5 553 0 mds: (CoreDuetContext) CoreDuet: ClientContext objectForContextualKeyPath:
|
||||
2026-09-06 01:48:58.166016+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:58.166457+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.200626+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.233134+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.970309]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:58.233143+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.970312]: arm_cpu_init(): cpu 11 online
|
||||
2026-09-06 01:48:58.233143+0100 0x4a3b07 Default 0x0 0 0 kernel: [745898.970319]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:58.233133+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.266457+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.299153+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.047915]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:58.299159+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.047958]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:58.299161+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.047998]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:58.299168+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.048043]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:58.299168+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.048079]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:58.299169+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.048108]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:58.299170+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.048147]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:58.299171+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.048187]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:58.299171+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.048211]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:58.299173+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.048265]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:58.299173+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.048290]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:58.299174+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.048321]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:58.299175+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.048363]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/ad
|
||||
2026-09-06 01:48:58.299142+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.333347+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.366017+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.400317+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.409677+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:58.432904+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.466670+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.494467+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: isInfraRealtimePacketThresholdAllowed allowed:1 option:32 threshold:50 noRegistrations:1 cachedPeerCount:0 fastDiscoveryInactive:1 fastDiscoveryOnSince:255556058
|
||||
2026-09-06 01:48:58.494496+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: currentInfraTrafficType:9277 checking if realtime upgrade required with inputPackets:33 outputPackets:32 packetThreshold:50
|
||||
2026-09-06 01:48:58.494510+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: peerChannelSteer: PEER_CHANNEL_STEER isSteeringRequired 0 Attempt Count 0
|
||||
2026-09-06 01:48:58.494517+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: peerChannelSteer: PEER_CHANNEL_STEER isRestartSteeringRequired is false
|
||||
2026-09-06 01:48:58.494532+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9668] : In AW: Tx:0 Rx:0 Prev Tx:0 Rx:0 time since 255556058 indatamode 1 infraPriority 1 cpc 0
|
||||
2026-09-06 01:48:58.494541+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9673] : Ignore changing the state as infraPriority 1 P2P-CPC 0
|
||||
2026-09-06 01:48:58.498467+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.533601+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.558028+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:58.561375+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:58.566902+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.599663+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.633427+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.665831+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.683749+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:58.690410+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:58.699997+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.732558+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.765115+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.799353+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.832325+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.865970+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.898628+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.933533+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.966260+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:58.989972+0100 0x4defc1 Default 0x0 572 0 locationd: [com.apple.locationd.Core:Harvester] {"msg":"onAvengerAdvertisementDetected: got avenger advertisement", "subHarvester":"Avenger"}
|
||||
2026-09-06 01:48:58.989993+0100 0x4defc2 Default 0x0 572 0 locationd: (SPFinder) [com.apple.icloud.SPFinder:advertisementCache] saveBeaconPayload: 1
|
||||
2026-09-06 01:48:58.990030+0100 0x4defc2 Activity 0x9d0e2d 572 0 locationd: (SPFinder) SPAdvertisementCache saveBeaconPayloads:completion:
|
||||
2026-09-06 01:48:58.990047+0100 0x4defc1 Default 0x0 572 0 locationd: [com.apple.locationd.Core:Harvester] {"msg":"convertToSPAdvertisement", "address":<private>, "data":<private>, "date":<private>, "rssi":<private>, "status":<private>, "reserved":<private>, "subHarvester":"Avenger"}
|
||||
2026-09-06 01:48:58.990092+0100 0x4defc1 Default 0x0 572 0 locationd: [com.apple.locationd.Core:Harvester] {"msg":"onAvengerAdvertisementDetected: advertisement is near-owner from other people and do not process it", "subHarvester":"Avenger"}
|
||||
2026-09-06 01:48:58.990439+0100 0x4e069c Activity 0x9d0ff2 746 0 searchpartyd: (FindMyBase) SaveObservedAdvertisment
|
||||
2026-09-06 01:48:58.990963+0100 0x4e069c Default 0x9d0ff2 746 0 searchpartyd: (FindMyBase) [com.apple.icloud.searchpartyd:observationStoring] TIME: ObservationStoring.Reconcile: <private>
|
||||
2026-09-06 01:48:59.000898+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.002606+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLState[10141] : Active Sockets false ValidSvc 4 NumAirplay 0 AFHandlePending 0 TimeOfLastAFHandleRequest 145710 ms
|
||||
2026-09-06 01:48:59.002639+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLState[10239]: BonJourTrig 1 ValidSvc 4 RTApp 0 TSReq 0 HasActAirDrop 0 SocketsActive 0
|
||||
2026-09-06 01:48:59.002652+0100 0x4df69c Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: setScheduleState[11041]: reason:UserTriggered sc:Infra Priority and force:NO, AWDL-restore:No
|
||||
2026-09-06 01:48:59.017950+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9668] : In AW: Tx:0 Rx:0 Prev Tx:0 Rx:0 time since 255556581 indatamode 1 infraPriority 1 cpc 0
|
||||
2026-09-06 01:48:59.017974+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9673] : Ignore changing the state as infraPriority 1 P2P-CPC 0
|
||||
2026-09-06 01:48:59.031972+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.056221+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:59.066719+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.081521+0100 0x4e20a1 Default 0x0 538 8 powerd: [com.apple.powerd:displayState] DesktopMode check on Battery 0
|
||||
2026-09-06 01:48:59.081758+0100 0x4dfe54 Default 0x0 538 8 powerd: [com.apple.powerd:BDC] Battery percentage last 80 now 79
|
||||
2026-09-06 01:48:59.082100+0100 0x144b Default 0x0 606 0 WindowServer: (BackBoardHIDEventFoundation) [com.apple.BackBoard:EventDelivery] destinations for Keyboard event: (<keyboardFocus; kitty.1343; token: viewbridge-key-window>)
|
||||
2026-09-06 01:48:59.082142+0100 0x144b Default 0x0 606 7 WindowServer: (SkyLight) [com.apple.SkyLight:KeyboardEvent] delivery manager destinations for kCGSEventFlagsChanged event: <keyboardFocus; pid: 1343; token: viewbridge-key-window>
|
||||
2026-09-06 01:48:59.082554+0100 0x2e72 Default 0x0 1343 0 kitty: (TextInputUIMacHelper) [com.apple.TextInputUI:CursorUI] -[TUINSCursorUIController scheduleUpdateCursorLocation]
|
||||
2026-09-06 01:48:59.083180+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:59.083489+0100 0x4e002b Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:59.087177+0100 0x4dfe54 Default 0x0 538 8 powerd: [com.apple.powerd:BDC] Sending data to powerlog for group BDC_SBC
|
||||
2026-09-06 01:48:59.099446+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.110515+0100 0x1bf1 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) IO80211ControllerMonitor::setAMPDUstat unhandled kAMPDUStat_ type 14
|
||||
2026-09-06 01:48:59.110533+0100 0x1bf1 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) IO80211ControllerMonitor::setAMPDUstat unhandled kAMPDUStat_ type 13
|
||||
2026-09-06 01:48:59.110544+0100 0x1bf1 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) IO80211ControllerMonitor::setAMPDUstat unhandled kAMPDUStat_ type 18
|
||||
2026-09-06 01:48:59.115191+0100 0x1bf1 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) 745899.870747 wlan0.A[819963] [dk] parsePHYEcounterV5@31099: Version = 5, ChanSpec=0xe22a, noise_req=13095, noise_crsbit=8021, cal_counter=4181, rxchain=15, chiptemp=37, femtemp=-50, btcx_mode=0, ltecx_mode=0, weakest_rssi=-74, chan_switc
|
||||
2026-09-06 01:48:59.115222+0100 0x1bf1 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) 745899.870782 wlan0.A[819964] [dk] parsePHYEcounterV5@31104: gbd_ofdm_desense=0, gbd_bphy_desense=0, featureflag=96, crsmin_pwr_apply_cnt=33882, preempt_status2=64278, ed_crs_status=0, measurehold=0, ocl_disable_reqs=0, gci_rst_tx= 15463,
|
||||
2026-09-06 01:48:59.115234+0100 0x1bf1 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) 745899.870797 wlan0.A[819965] [dk] parsePHYEcounterV5@31109: asym_intf_avg_noise[0/1]= [-98, -98], asym_intf_latest_noise[0/1]=[-98, -98], pwridx_init[0/1]=[35, 37], baseindxval[0/1]=[36, 37]
|
||||
2026-09-06 01:48:59.115247+0100 0x1bf1 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) 745899.870809 wlan0.A[819966] [dk] parsePHYEcounterV5@31115: estpwradj[0/1]=[60, 60], ed_threshold[0/1]=[-74, -75], crsmin_pwr_idx[0/1]=[7, 7], crsmin_pwr[0/1]=[-95, -94], noise_level_inst[0/1]=[-96, -95]
|
||||
2026-09-06 01:48:59.120723+0100 0x1bf1 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) 745899.876283 wlan0.A[819967] [dk] updateChanSeqCommonStats@3612:P2P chan 44: dur 744287836, entry 18673, crsglitch 9822232, start 132520737, badplcp 763283, badfcs 3170566, f0ov 420, f1ov 577, anyerr 30581679, resptout 1168070, frmtooln 0
|
||||
2026-09-06 01:48:59.120738+0100 0x1bf1 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) 745899.876300 wlan0.A[819968] [dk] updateChanSeqCommonStats@3612:P2P chan 6: dur 15735521, entry 1, crsglitch 696648, start 188156, badplcp 6484, badfcs 33786, f0ov 0, f1ov 0, anyerr 23863, resptout 0, frmtooln 0, frmtoosh 7630
|
||||
2026-09-06 01:48:59.120744+0100 0x1bf1 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) 745899.876308 wlan0.A[819969] NAV stats: ch 44 (135039794 689529 490534 167738 748 86) ch 6 (132084 1835 1281 106 1750 5)
|
||||
2026-09-06 01:48:59.120783+0100 0x1bf1 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) 745899.876346 wlan0.A[819970] [dk] printDeltaSliceUcodeRxErrs@1925:(HighBand) rxNotPrimary20s=1245 ucodeLateDrop=0 rxInvalidUcodeLateWrongMACPHY=0 rxLate30us=0 rxLegacyFrmInvalid=0 txSifsErr=0 ooseqMacSusp=0
|
||||
2026-09-06 01:48:59.132741+0100 0x1551 Default 0x0 642 0 airportd: (IO80211) [com.apple.WiFiManager:] Driver Event: <airport[642]> _bsd_80211_event_callback: APPLE80211_M_BT_COEX_MODE_STATS_EVENT/190 (en0)
|
||||
2026-09-06 01:48:59.133026+0100 0x4dc356 Default 0x0 642 0 airportd: (IO80211) [com.apple.WiFiManager:] Info: <airport[642]> handle airportdProcessBtCoexStatsEvent
|
||||
2026-09-06 01:48:59.133418+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.165682+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.170964+0100 0x35de04 Default 0x0 0 0 kernel: process_is_plugin_host: running binary "sh" in keys-off mode due to identity: com.apple.sh
|
||||
2026-09-06 01:48:59.172051+0100 0x4e2411 Default 0x0 0 0 kernel: sh[40639] triggered unnest of range 0x1ee000000->0x1f2000000 of DYLD shared region in VM map 0xe6c827f1353fb483. While not abnormal for debuggers, this increases system memory footprint almost permanently (until the shared region is re-slid).
|
||||
2026-09-06 01:48:59.173175+0100 0x4e2411 Default 0x0 0 0 kernel: [745899.928319]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:59.173180+0100 0x4e2411 Default 0x0 0 0 kernel: [745899.928329]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:59.173174+0100 0x4e2411 Default 0x0 0 0 kernel: process_is_plugin_host: running binary "bash" in keys-off mode due to identity: com.apple.bash
|
||||
2026-09-06 01:48:59.174017+0100 0x4e2412 Default 0x0 0 0 kernel: bash[40639] triggered unnest of range 0x1ee000000->0x1f2000000 of DYLD shared region in VM map 0xb2a61f073e8c5e91. While not abnormal for debuggers, this increases system memory footprint almost permanently (until the shared region is re-slid).
|
||||
2026-09-06 01:48:59.200132+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.949345]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:59.200143+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.949348]: arm_cpu_init(): cpu 11 online
|
||||
2026-09-06 01:48:59.200144+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.949354]: arm_cpu_init(): cpu 12 online
|
||||
2026-09-06 01:48:59.200145+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.949357]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:59.200145+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.949360]: arm_cpu_init(): cpu 15 online
|
||||
2026-09-06 01:48:59.200126+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.214584+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.en0: set Datapath Open needsTrigger 0
|
||||
2026-09-06 01:48:59.235340+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.984752]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:59.235348+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.984754]: arm_cpu_init(): cpu 11 online
|
||||
2026-09-06 01:48:59.235349+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.984760]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:59.235350+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.984763]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:59.235339+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.267868+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.997722]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:59.267880+0100 0x4a3b07 Default 0x0 0 0 kernel: [745899.997727]: arm_cpu_init(): cpu 15 online
|
||||
2026-09-06 01:48:59.267881+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.010921]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:59.267882+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.010924]: arm_cpu_init(): cpu 12 online
|
||||
2026-09-06 01:48:59.267883+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.010929]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:59.267892+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.020472]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:59.267892+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.020475]: arm_cpu_init(): cpu 11 online
|
||||
2026-09-06 01:48:59.267892+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.020480]: arm_cpu_init(): cpu 12 online
|
||||
2026-09-06 01:48:59.267894+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.020483]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:59.267894+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.020486]: arm_cpu_init(): cpu 15 online
|
||||
2026-09-06 01:48:59.267864+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.300117+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.030709]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:59.300125+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.030712]: arm_cpu_init(): cpu 11 online
|
||||
2026-09-06 01:48:59.300125+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.030718]: arm_cpu_init(): cpu 12 online
|
||||
2026-09-06 01:48:59.300125+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.030721]: arm_cpu_init(): cpu 14 online
|
||||
2026-09-06 01:48:59.300126+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.046426]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:59.300128+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.046472]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:59.300129+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.046504]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:59.300129+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.046540]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:59.300130+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.046572]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:59.300131+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.046618]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:59.300133+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.046638]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:59.300134+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.046668]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:59.300135+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.046700]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:59.300141+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.046749]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:59.300142+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.046778]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:59.300143+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.046809]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrdincr)/0x0 vc/sc/tz/crh/tid:0x2/0x1/0x0/0x0/0x2f size:0x3 piopkt:0x0 user:0x4 userext:0x0 hasdata/has2data:0x0/0x0 dstchildid/dstparentid:0x3f/0x2 srcchildid/srcparentid:0x8ea/0x1d addrl/addrh:0x0/0x0 timestamp:0x0 errorvec:0x10
|
||||
2026-09-06 01:48:59.300143+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.047460]: Ignored NI0 NI0 niGeneral 2 NO_ACCESS error (0x00000010) cmd/st:0x14(ncrd
|
||||
2026-09-06 01:48:59.300105+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.333193+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.366943+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.098299]: arm_cpu_init(): cpu 10 online
|
||||
2026-09-06 01:48:59.366951+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.098302]: arm_cpu_init(): cpu 12 online
|
||||
2026-09-06 01:48:59.366952+0100 0x4a3b07 Default 0x0 0 0 kernel: [745900.098309]: arm_cpu_init(): cpu 13 online
|
||||
2026-09-06 01:48:59.366941+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.400043+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.409533+0100 0x2e72 Default 0x0 1343 7 kitty: (SkyLight) [com.apple.SkyLight:default] SLSGetDisplaysWithOpenGLDisplayMask, 3614: Display 0x00000001: Mode 54, (0 x 0) [1728 x 1117], Depth 32 BPP, active
|
||||
2026-09-06 01:48:59.432119+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.467080+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.499502+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.533324+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.543029+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: isInfraRealtimePacketThresholdAllowed allowed:1 option:32 threshold:50 noRegistrations:1 cachedPeerCount:0 fastDiscoveryInactive:1 fastDiscoveryOnSince:255557106
|
||||
2026-09-06 01:48:59.543059+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: currentInfraTrafficType:9277 checking if realtime upgrade required with inputPackets:99 outputPackets:144 packetThreshold:50
|
||||
2026-09-06 01:48:59.543072+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: peerChannelSteer: PEER_CHANNEL_STEER isSteeringRequired 0 Attempt Count 0
|
||||
2026-09-06 01:48:59.543090+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: peerChannelSteer: PEER_CHANNEL_STEER isRestartSteeringRequired is false
|
||||
2026-09-06 01:48:59.543104+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9668] : In AW: Tx:0 Rx:0 Prev Tx:0 Rx:0 time since 255557106 indatamode 1 infraPriority 1 cpc 0
|
||||
2026-09-06 01:48:59.543150+0100 0x4de131 Default 0x0 0 0 kernel: (com.apple.DriverKit-AppleBCMWLAN.dext) wlan0:com.apple.p2p.awdl0: monitorAWDLDataMode[9673] : Ignore changing the state as infraPriority 1 P2P-CPC 0
|
||||
2026-09-06 01:48:59.565970+0100 0x4a3b07 Error 0x0 0 0 kernel: (IOSurface) SID: 0x0 task: <decode: mismatch for [%p] got [SCALAR private sz:0]> buffer: 0x0 buffer->fClientTask = 0x0 not found
|
||||
2026-09-06 01:48:59.577680+0100 0x4dfe54 Default 0x0 538 8 powerd: [com.apple.powerd:displayState] DesktopMode check on Battery 0
|
||||
2026-09-06 01:48:59.578339+0100 0x144b Default 0x0 606 0 WindowServer: (BackBoardHIDEventFoundation) [com.apple.BackBoard:EventDelivery] destinations for Keyboard event: (<keyboardFocus; kitty.1343; token: viewbridge-key-window>)
|
||||
2026-09-06 01:48:59.578385+0100 0x144b Default 0x0 606 7 WindowServer: (SkyLight) [com.apple.SkyLight:KeyboardEvent] delivery manager destinations for kCGSEventKeyDown event: <keyboardFocus; pid: 1343; token: viewbridge-key-window>
|
||||
2026-09-06 01:48:59.579704+0100 0x2e72 Default 0x0 1343 0 kitty: (SidecarCore) [com.apple.sidecar:core] updateDevices: deviceGeneration=0 (prev=0) displayGeneration=0 (prev=0)
|
||||
2026-09-06 01:48:59.579746+0100 0x2e72 Default 0x0 1343 0 kitty: (SidecarCore) [com.apple.sidecar:core] updateDevices(connectedDevices): returning 0 device(s) to caller: (
|
||||
)
|
||||
2026-09-06 01:48:59.579759+0100 0x2e72 Default 0x0 1343 0 kitty: (SidecarCore) [com.apple.sidecar:core] updateDevices: deviceGeneration=0 (prev=0) displayGeneration=0 (prev=0)
|
||||
2026-09-06 01:48:59.579773+0100 0x2e72 Default 0x0 1343 0 kitty: (SidecarCore) [com.apple.sidecar:core] updateDevices(devices): returning 0 device(s) to caller: (
|
||||
)
|
||||
2026-09-06 01:48:59.580218+0100 0x2e72 Default 0x0 1343 0 kitty: (SidecarCore) [com.apple.sidecar:core] updateDevices: deviceGeneration=0 (prev=0) displayGeneration=0 (prev=0)
|
||||
2026-09-06 01:48:59.580238+0100 0x2e72 Default 0x0 1343 0 kitty: (SidecarCore) [com.apple.sidecar:core] updateDevices(connectedDevices): returning 0 device(s) to caller: (
|
||||
)
|
||||
2026-09-06 01:48:59.580248+0100 0x2e72 Default 0x0 1343 0 kitty: (SidecarCore) [com.apple.sidecar:core] updateDevices: deviceGeneration=0 (prev=0) displayGeneration=0 (prev=0)
|
||||
2026-09-06 01:48:59.580273+0100 0x2e72 Default 0x0 1343 0 kitty: (SidecarCore) [com.apple.sidecar:core] updateDevices(devices): returning 0 device(s) to caller: (
|
||||
)
|
||||
2026-09-06 01:48:59.580663+0100 0x2e72 Default 0x0 1343 0 kitty: (TextInputUIMacHelper) [com.apple.TextInputUI:CursorUI] -[TUINSCursorUIController scheduleUpdateCursorLocation]
|
||||
@@ -0,0 +1,3 @@
|
||||
2026-09-06 10:35:55,217 INFO delete-py-log event=eventC labelC=1
|
||||
2026-09-06 10:35:55,627 INFO delete-py-log event=eventB labelB=one
|
||||
2026-09-06 10:35:56,442 INFO delete-py-log event=eventB labelA=1
|
||||
+651
@@ -0,0 +1,651 @@
|
||||
//! Fast, tolerant `key=value`/`key="value"` tokenizer (logfmt-ish), used by
|
||||
//! `crate::engine::auto_declare_params` to discover param names as lines are ingested. Scans
|
||||
//! anywhere in the line rather than requiring the whole line to be logfmt, so it works on lines
|
||||
//! with an arbitrary non-logfmt prefix/suffix (e.g. `ts=... msg="..." x1=-4 y1=34`).
|
||||
|
||||
use nom::branch::alt;
|
||||
use nom::bytes::complete::{escaped, tag, take_until, take_while1};
|
||||
use nom::character::complete::{char, none_of, one_of};
|
||||
use nom::error::{Error, ErrorKind};
|
||||
use nom::sequence::delimited;
|
||||
use nom::IResult;
|
||||
use regex::Regex;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
fn key(input: &str) -> IResult<&str, &str> {
|
||||
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-')(input)
|
||||
}
|
||||
|
||||
fn quoted_value(input: &str) -> IResult<&str, &str> {
|
||||
delimited(
|
||||
char('"'),
|
||||
alt((escaped(none_of("\\\""), '\\', one_of("\"\\nt")), nom::combinator::success(""))),
|
||||
char('"'),
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn bareword_value(input: &str) -> IResult<&str, &str> {
|
||||
take_while1(|c: char| !c.is_whitespace())(input)
|
||||
}
|
||||
|
||||
/// A bareword value starting with a bare `=` means the raw text actually had `key==...`/`key===...` —
|
||||
/// never a real logfmt pair. This is exactly what `pair` would otherwise produce when it lands
|
||||
/// mid-way through a base64/hash blob whose padding (`=`/`==`) happens to sit right after a run of
|
||||
/// alnum chars that `key()` greedily consumed — e.g. the `huig_dump.txt` fixture's
|
||||
/// `<mask.hash: 'rKTEtG7JmnThhoKPbZiVrA=='>,`: `key()` eats `rKTEtG7JmnThhoKPbZiVrA`, the first `=` is
|
||||
/// consumed as the pair's `=`, leaving a bareword value of `='>,` (ticket 050). Rejected here rather
|
||||
/// than post-filtered by a caller so `extract_pairs` never reports the false positive to anyone.
|
||||
fn looks_like_base64_padding_artifact(value: &str) -> bool {
|
||||
value.starts_with('=')
|
||||
}
|
||||
|
||||
/// One `key=value` pair found on a line: the key, the raw captured value (unescaped as-is, quotes
|
||||
/// stripped but any inner `\"`/`\\` left literal since callers only need it for numeric-kind
|
||||
/// inference), and whether the value was quoted.
|
||||
fn pair(input: &str) -> IResult<&str, (&str, &str, bool)> {
|
||||
let (input, k) = key(input)?;
|
||||
let (input, _) = char('=')(input)?;
|
||||
if let Ok((rest, v)) = quoted_value(input) {
|
||||
Ok((rest, (k, v, true)))
|
||||
} else {
|
||||
let (rest, v) = bareword_value(input)?;
|
||||
if looks_like_base64_padding_artifact(v) {
|
||||
return Err(nom::Err::Error(Error::new(input, ErrorKind::Verify)));
|
||||
}
|
||||
Ok((rest, (k, v, false)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds every `key=value`/`key="value"` pair anywhere in `line`. A position that doesn't start a
|
||||
/// pair is skipped one char at a time, so this never fails outright — a line with no pairs at all
|
||||
/// (plain prose, a JSON blob) just returns an empty vec.
|
||||
pub fn extract_pairs(line: &str) -> Vec<(&str, &str, bool)> {
|
||||
let mut results = Vec::new();
|
||||
let mut rest = line;
|
||||
while !rest.is_empty() {
|
||||
match pair(rest) {
|
||||
Ok((remaining, found)) => {
|
||||
results.push(found);
|
||||
rest = remaining;
|
||||
}
|
||||
Err(_) => {
|
||||
let mut chars = rest.chars();
|
||||
chars.next();
|
||||
rest = chars.as_str();
|
||||
}
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
/// Fixed-column shape of macOS `log stream` output (ticket 050/054): a line starts with
|
||||
/// `Timestamp Thread Type Activity PID TTL` (date, time+tz, a hex thread id, a bareword type, a hex
|
||||
/// activity id, a decimal pid, a decimal ttl) followed by a free-text `process: message` tail — see
|
||||
/// `huig_dump.txt`'s header row and body. Anchored at the very start of the line and requires the
|
||||
/// hex/decimal shape of each column, so it won't accidentally fire on ordinary logfmt/JSON/prose that
|
||||
/// merely resembles it in one column. Checked ahead of JSON/logfmt in `detect_format` since it's the
|
||||
/// most specific of the three (an all-columns-plus-hex-ids match is a much stronger signal than an
|
||||
/// incidental `key=value` substring), and is deliberately a *format label* rather than a dedicated
|
||||
/// `ottyl_fmt` value change to JSON/logfmt detection — a `log stream` line's message tail can still
|
||||
/// separately contain real `key=value` pairs (see the WiFi/radio stat lines in the fixture), which
|
||||
/// `crate::engine::auto_declare_params` keeps discovering via `extract_pairs` regardless of this
|
||||
/// line's `ottyl_fmt` label.
|
||||
static LOG_STREAM_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"^\d{4}-\d{2}-\d{2}\s+\S+\s+0x[0-9a-fA-F]+\s+\S+\s+0x[0-9a-fA-F]+\s+\d+\s+\d+\s+[^\s:]+:\s",
|
||||
)
|
||||
.expect("static log_stream regex must compile")
|
||||
});
|
||||
|
||||
pub fn is_log_stream_line(line: &str) -> bool {
|
||||
LOG_STREAM_RE.is_match(line)
|
||||
}
|
||||
|
||||
/// Regex fragment for one column separator under the generic whitespace-columnar rule ticket 059
|
||||
/// asks for: a run of 2+ whitespace chars, or a lone tab, separates columns; a single plain space does
|
||||
/// not (so a value that legitimately contains one -- e.g. `log stream`'s `date time+tz` timestamp --
|
||||
/// stays intact within its own column). `is_separator_run` is the same rule applied at split time;
|
||||
/// this is that rule re-expressed as a regex fragment for the trailing message field's
|
||||
/// `message_match_regex`, which re-derives its value the same way whenever a caller queries by it
|
||||
/// later (declarations don't cache extracted values -- see `crate::engine::extract_param_raw`).
|
||||
const COLUMN_SEPARATOR_PATTERN: &str = r"(?:\t|[ \t]{2,})";
|
||||
|
||||
/// A column's value: one or more non-whitespace tokens joined by single spaces, with no upper bound
|
||||
/// on length. Used for the trailing (unbounded-width) message field and the last named column, where
|
||||
/// there's no next column's start position to bound against.
|
||||
const COLUMN_VALUE_PATTERN: &str = r"\S+(?: \S+)*";
|
||||
|
||||
/// Same idea as `COLUMN_VALUE_PATTERN`, but bounded to a known column `width` (in chars) -- needed for
|
||||
/// every column except the last, since two adjacent `log stream` values can legitimately be separated
|
||||
/// by only a single space when the first one nearly fills its column's on-screen width (e.g. the
|
||||
/// `Timestamp` column: its own `date time+tz` value already contains one internal space, so a
|
||||
/// content-only pattern can't tell that boundary apart from the one-space gap before the next column
|
||||
/// starts -- seed this from `LogStreamColumn::start` positions instead). Greedily matches up to
|
||||
/// `width` chars while requiring the match to end on a non-whitespace char, which is what excludes the
|
||||
/// column's own trailing padding spaces from the capture.
|
||||
fn bounded_column_value_pattern(width: usize) -> String {
|
||||
if width <= 1 {
|
||||
r"\S".to_string()
|
||||
} else {
|
||||
format!(r"\S(?:.{{0,{}}}\S)?", width - 2)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_separator_run(run: &str) -> bool {
|
||||
run.len() >= 2 || run.contains('\t')
|
||||
}
|
||||
|
||||
static WHITESPACE_RUN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[ \t]+").expect("static regex must compile"));
|
||||
|
||||
/// Splits `line` at whitespace runs that qualify as a column separator (`is_separator_run`), returning
|
||||
/// each piece alongside its byte offset in `line`; a lone plain space never splits. Consumes at most
|
||||
/// `max_columns.map(|n| n - 1)` separators -- the final piece returned is the untouched remainder of
|
||||
/// the line, including any further whitespace runs inside it. That's what lets a trailing unlabelled
|
||||
/// `log stream` message tail (whose own internal spacing can be arbitrary) come back as one field
|
||||
/// instead of being chopped up by it.
|
||||
fn split_columns_with_offsets(line: &str, max_columns: Option<usize>) -> Vec<(usize, &str)> {
|
||||
let mut columns = Vec::new();
|
||||
let mut start = 0usize;
|
||||
let mut consumed = 0usize;
|
||||
for m in WHITESPACE_RUN.find_iter(line) {
|
||||
if max_columns.is_some_and(|max| consumed + 1 >= max) {
|
||||
break;
|
||||
}
|
||||
let run = &line[m.start()..m.end()];
|
||||
if is_separator_run(run) {
|
||||
columns.push((start, &line[start..m.start()]));
|
||||
start = m.end();
|
||||
consumed += 1;
|
||||
}
|
||||
}
|
||||
columns.push((start, &line[start..]));
|
||||
columns
|
||||
}
|
||||
|
||||
fn split_columns(line: &str, max_columns: Option<usize>) -> Vec<&str> {
|
||||
split_columns_with_offsets(line, max_columns).into_iter().map(|(_, s)| s).collect()
|
||||
}
|
||||
|
||||
/// A column-name identifier a generated `message_match_regex`'s named capture group can use as-is
|
||||
/// (the regex crate requires `[A-Za-z_][0-9A-Za-z_]*`).
|
||||
static COLUMN_NAME: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^[A-Za-z][A-Za-z0-9_]*$").expect("static regex must compile"));
|
||||
|
||||
/// One column named by a detected `log stream` header (ticket 059). `start` is that column's position
|
||||
/// in `char`s (not bytes -- see `char_slice`) within the header line -- meaningful only when
|
||||
/// `LogStreamHeader::tab_delimited` is `false` (see that field's doc comment).
|
||||
pub struct LogStreamColumn {
|
||||
pub name: String,
|
||||
start: usize,
|
||||
}
|
||||
|
||||
/// A detected `log stream` header: its named columns, plus whether they were tab- or space-separated.
|
||||
/// `log stream`'s real output right-pads each header name to a fixed on-screen width using runs of
|
||||
/// 2+ spaces, and every data line's values land in that same fixed alignment regardless of how much
|
||||
/// of that width they individually fill -- so for that shape, a data line's column boundaries are
|
||||
/// best found at the header's own fixed character positions, not by re-splitting the data line's own
|
||||
/// whitespace (see `split_log_stream_row`'s doc comment for why that alone isn't reliable there).
|
||||
/// A tab-separated header carries no such alignment guarantee (values aren't padded to a shared
|
||||
/// width), but a lone tab is itself an unambiguous separator, so that case instead re-splits each data
|
||||
/// line directly.
|
||||
pub struct LogStreamHeader {
|
||||
pub columns: Vec<LogStreamColumn>,
|
||||
tab_delimited: bool,
|
||||
}
|
||||
|
||||
/// Byte-indexed `str` slicing doesn't work directly with `LogStreamColumn::start` (a *char* offset),
|
||||
/// since `log stream` output is expected to stay ASCII in practice but callers shouldn't panic if a
|
||||
/// message contains multi-byte chars before the slice point. `end` of `None` slices to end of string.
|
||||
fn char_slice(s: &str, start: usize, end: Option<usize>) -> &str {
|
||||
if end.is_some_and(|end| end <= start) {
|
||||
return "";
|
||||
}
|
||||
let mut indices = s.char_indices();
|
||||
let byte_start = indices.nth(start).map(|(i, _)| i).unwrap_or(s.len());
|
||||
let byte_end = match end {
|
||||
Some(end) => s.char_indices().nth(end).map(|(i, _)| i).unwrap_or(s.len()),
|
||||
None => s.len(),
|
||||
};
|
||||
&s[byte_start..byte_end.max(byte_start)]
|
||||
}
|
||||
|
||||
/// Recognizes a `log stream`-style header row generically (ticket 059), instead of assuming the fixed
|
||||
/// `Timestamp Thread Type Activity PID TTL` names: a short run of bareword column names separated by
|
||||
/// qualifying whitespace runs (`split_columns_with_offsets`) that is, critically, *not itself* a
|
||||
/// `log stream` data line (`is_log_stream_line`) -- so a header is never mistaken for the data line
|
||||
/// immediately following it. `crate::engine::auto_declare_log_stream_params` caches the result across
|
||||
/// ingested lines, since `log stream` prints its header once, before any data line, and this only ever
|
||||
/// sees one line at a time.
|
||||
pub fn detect_log_stream_header(line: &str) -> Option<LogStreamHeader> {
|
||||
if is_log_stream_line(line) {
|
||||
return None;
|
||||
}
|
||||
let mut columns = Vec::new();
|
||||
for (byte_start, slice) in split_columns_with_offsets(line, None) {
|
||||
let trimmed = slice.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let leading_ws = slice.len() - slice.trim_start().len();
|
||||
let start = line[..byte_start + leading_ws].chars().count();
|
||||
columns.push(LogStreamColumn { name: trimmed.to_string(), start });
|
||||
}
|
||||
if columns.len() < 2 || !columns.iter().all(|c| COLUMN_NAME.is_match(&c.name)) {
|
||||
return None;
|
||||
}
|
||||
let tab_delimited = WHITESPACE_RUN.find_iter(line).any(|m| m.as_str().contains('\t'));
|
||||
Some(LogStreamHeader { columns, tab_delimited })
|
||||
}
|
||||
|
||||
/// Splits a `log stream` data line into `header`'s named columns plus, when present, one trailing
|
||||
/// unlabelled tail -- `crate::engine::auto_declare_log_stream_params` maps the first
|
||||
/// `header.columns.len()` pieces onto the header's names and the optional last piece onto the
|
||||
/// `message` param.
|
||||
///
|
||||
/// For a space-padded fixed-width header, every column but the last is sliced at the header's own
|
||||
/// fixed character positions (`LogStreamColumn::start`) rather than by re-splitting the data line's
|
||||
/// own whitespace, since two adjacent values can legitimately be joined by only a single space when
|
||||
/// the first nearly fills its column's width (e.g. `log stream`'s `Timestamp` column, whose value
|
||||
/// already contains one internal space). Only the last named column's end is whitespace-run-detected
|
||||
/// (`split_columns`), since that's what distinguishes its own value from the free-text message tail
|
||||
/// `log stream` appends after it.
|
||||
///
|
||||
/// For a tab-delimited header, there's no shared on-screen width to align against, but a lone tab is
|
||||
/// itself unambiguous, so this instead re-splits the data line directly on qualifying separators.
|
||||
pub fn split_log_stream_row<'a>(line: &'a str, header: &LogStreamHeader) -> Vec<&'a str> {
|
||||
if header.tab_delimited {
|
||||
return split_columns(line, Some(header.columns.len() + 1));
|
||||
}
|
||||
let mut result = Vec::with_capacity(header.columns.len() + 1);
|
||||
for (i, column) in header.columns.iter().enumerate() {
|
||||
match header.columns.get(i + 1) {
|
||||
Some(next) => result.push(char_slice(line, column.start, Some(next.start)).trim()),
|
||||
None => {
|
||||
let tail = char_slice(line, column.start, None);
|
||||
let pieces = split_columns(tail, Some(2));
|
||||
result.push(pieces[0].trim());
|
||||
if let Some(message) = pieces.get(1) {
|
||||
result.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Builds the `message_match_regex` for the `index`-th (0-based) of `header`'s named columns.
|
||||
///
|
||||
/// Fixed-width case: skips exactly `header.columns[index].start` chars, then captures this column's
|
||||
/// own value bounded to its known width (`bounded_column_value_pattern`) -- unbounded for the last
|
||||
/// column, which has no next column to bound against.
|
||||
///
|
||||
/// Tab-delimited case: skips `index` preceding columns' own value+separator pairs instead, since
|
||||
/// there's no shared fixed width to anchor a character-position skip against.
|
||||
///
|
||||
/// Anchored at the start of the line since a `log stream` line never carries this fixed-per-run
|
||||
/// column prefix anywhere but the very start.
|
||||
pub fn log_stream_column_pattern(header: &LogStreamHeader, index: usize, capture_group: &str) -> String {
|
||||
if header.tab_delimited {
|
||||
let mut pattern = String::from("^");
|
||||
for _ in 0..index {
|
||||
pattern.push_str(COLUMN_VALUE_PATTERN);
|
||||
pattern.push_str(COLUMN_SEPARATOR_PATTERN);
|
||||
}
|
||||
pattern.push_str(&format!("(?P<{capture_group}>{COLUMN_VALUE_PATTERN})"));
|
||||
return pattern;
|
||||
}
|
||||
let start = header.columns[index].start;
|
||||
let value_pattern = match header.columns.get(index + 1) {
|
||||
Some(next) => bounded_column_value_pattern(next.start.saturating_sub(start)),
|
||||
None => COLUMN_VALUE_PATTERN.to_string(),
|
||||
};
|
||||
format!("^.{{{start}}}(?P<{capture_group}>{value_pattern})")
|
||||
}
|
||||
|
||||
/// Builds the `message_match_regex` for the trailing unlabelled tail after `header`'s last named
|
||||
/// column: everything from the end of that column's own separator to end of line, verbatim -- its own
|
||||
/// internal whitespace, however irregular, is never treated as a further separator.
|
||||
pub fn log_stream_message_pattern(header: &LogStreamHeader, capture_group: &str) -> String {
|
||||
if header.tab_delimited {
|
||||
let mut pattern = String::from("^");
|
||||
for _ in 0..header.columns.len() {
|
||||
pattern.push_str(COLUMN_VALUE_PATTERN);
|
||||
pattern.push_str(COLUMN_SEPARATOR_PATTERN);
|
||||
}
|
||||
pattern.push_str(&format!("(?P<{capture_group}>.*)$"));
|
||||
return pattern;
|
||||
}
|
||||
let start = header.columns.last().map(|c| c.start).unwrap_or(0);
|
||||
format!("^.{{{start}}}{COLUMN_VALUE_PATTERN}{COLUMN_SEPARATOR_PATTERN}(?P<{capture_group}>.*)$")
|
||||
}
|
||||
|
||||
/// Go `log/slog` `NewJSONHandler` output (the `go-log` label): a single-line JSON object whose first
|
||||
/// three fields are always `time`, `level`, `msg` in that fixed order (slog's `JSONHandler` writes the
|
||||
/// built-in attrs before any caller-supplied ones), followed by zero or more arbitrary attrs and a
|
||||
/// closing `}`. Parsed with nom rather than a full JSON decode since only that fixed prefix shape
|
||||
/// needs verifying; the variable tail is deliberately not parsed field-by-field the way
|
||||
/// `crate::engine::auto_declare_params` will separately pick up JSON leaf fields from the line.
|
||||
/// Checked ahead of the generic JSON object check in `detect_format` since it's the more specific of
|
||||
/// the two (see that function's doc comment for the general ordering rationale).
|
||||
fn go_log_prefix(input: &str) -> IResult<&str, (&str, &str, &str)> {
|
||||
let (input, _) = tag("{\"time\":\"")(input)?;
|
||||
let (input, time) = take_until("\"")(input)?;
|
||||
let (input, _) = tag("\",\"level\":\"")(input)?;
|
||||
let (input, level) = take_until("\"")(input)?;
|
||||
let (input, _) = tag("\",\"msg\":\"")(input)?;
|
||||
let (input, msg) = take_until("\"")(input)?;
|
||||
let (input, _) = char('"')(input)?;
|
||||
Ok((input, (time, level, msg)))
|
||||
}
|
||||
|
||||
pub fn is_go_log_line(text: &str) -> bool {
|
||||
match go_log_prefix(text) {
|
||||
Ok((rest, _)) => rest.trim_end().ends_with('}'),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Python `logging` module output in the shape `"%(asctime)s %(levelname)s %(name)s %(message)s"`
|
||||
/// (the `py-log` label): a `YYYY-MM-DD HH:MM:SS,mmm` timestamp, an uppercase level word, a logger
|
||||
/// name, then a message tail that (for `delete-py-log`, and any caller using `logger.info("event=%s
|
||||
/// %s=%s", ...)`-style calls) is itself logfmt `key=value` pairs. Only the fixed prefix is parsed with
|
||||
/// nom; the tail is handed to `extract_pairs` both to confirm the shape (a py-log line with no pairs
|
||||
/// at all wouldn't be distinguishable from plain prose after this prefix) and because that's the same
|
||||
/// extraction `crate::engine::auto_declare_params` needs anyway. Checked ahead of the generic logfmt
|
||||
/// check in `detect_format` since it's the more specific of the two.
|
||||
fn py_log_prefix(input: &str) -> IResult<&str, (&str, &str, &str, &str)> {
|
||||
let (input, date) = take_while1(|c: char| c.is_ascii_digit() || c == '-')(input)?;
|
||||
let (input, _) = char(' ')(input)?;
|
||||
let (input, time) = take_while1(|c: char| c.is_ascii_digit() || c == ':' || c == ',')(input)?;
|
||||
let (input, _) = char(' ')(input)?;
|
||||
let (input, level) = take_while1(|c: char| c.is_ascii_uppercase())(input)?;
|
||||
let (input, _) = char(' ')(input)?;
|
||||
let (input, logger) =
|
||||
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')(input)?;
|
||||
let (input, _) = char(' ')(input)?;
|
||||
Ok((input, (date, time, level, logger)))
|
||||
}
|
||||
|
||||
pub fn is_py_log_line(text: &str) -> bool {
|
||||
match py_log_prefix(text) {
|
||||
Ok((rest, _)) => !extract_pairs(rest).is_empty(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of the `ottyl_fmt` reserved param (`crate::engine::compile_filter`/`eval_alert_expr`):
|
||||
/// which format a line was recognized as when parsed. Checked most-specific-first: `log stream`'s
|
||||
/// fixed-column shape (see `LOG_STREAM_RE`'s doc comment for why it's the most specific), then the two
|
||||
/// named-tool shapes `go-log` (a `time`/`level`/`msg`-prefixed JSON object) and `py-log` (a
|
||||
/// timestamp/level/logger prefix over logfmt pairs) — each is a strict subset of the generic JSON/logfmt
|
||||
/// case checked right after it, so they'd otherwise never be reached. JSON is checked next since a JSON
|
||||
/// object can incidentally contain `key=value`-shaped substrings that `extract_pairs` would otherwise
|
||||
/// pick up; only a top-level object counts (arrays/scalars aren't what `crate::engine::eval_json_comparison`
|
||||
/// treats as JSON either). Anything else falls back to logfmt-style pair extraction, and a line with
|
||||
/// none of the above is `"none"`.
|
||||
pub fn detect_format(text: &str) -> &'static str {
|
||||
if is_log_stream_line(text) {
|
||||
"log_stream"
|
||||
} else if is_go_log_line(text) {
|
||||
"go-log"
|
||||
} else if is_py_log_line(text) {
|
||||
"py-log"
|
||||
} else if serde_json::from_str::<serde_json::Value>(text).is_ok_and(|value| value.is_object()) {
|
||||
"json"
|
||||
} else if !extract_pairs(text).is_empty() {
|
||||
"logfmt"
|
||||
} else {
|
||||
"none"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn extracts_quoted_value() {
|
||||
let pairs = extract_pairs(r#"msg="entity_moved""#);
|
||||
assert_eq!(pairs, vec![("msg", "entity_moved", true)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_unquoted_bareword_value() {
|
||||
let pairs = extract_pairs("x1=-4");
|
||||
assert_eq!(pairs, vec![("x1", "-4", false)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_mixed_quoted_and_unquoted_pairs_in_one_line() {
|
||||
let pairs = extract_pairs(r#"ts=1788648105773 msg="entity_moved" x1=-4 y1=34"#);
|
||||
assert_eq!(
|
||||
pairs,
|
||||
vec![
|
||||
("ts", "1788648105773", false),
|
||||
("msg", "entity_moved", true),
|
||||
("x1", "-4", false),
|
||||
("y1", "34", false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_empty_for_lines_with_no_pairs() {
|
||||
assert!(extract_pairs("just some plain prose here").is_empty());
|
||||
assert!(extract_pairs(r#"{"foo": {"bar": 42}}"#).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_panic_on_empty_value_at_end_of_line() {
|
||||
assert_eq!(extract_pairs("key="), Vec::<(&str, &str, bool)>::new());
|
||||
assert_eq!(extract_pairs(r#"key="""#), vec![("key", "", true)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_escaped_quote_inside_quoted_value() {
|
||||
let pairs = extract_pairs(r#"msg="he said \"hi\"""#);
|
||||
assert_eq!(pairs, vec![("msg", r#"he said \"hi\""#, true)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_json_object_lines_as_json() {
|
||||
assert_eq!(detect_format(r#"{"level":"error","code":500}"#), "json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_key_value_lines_as_logfmt() {
|
||||
assert_eq!(detect_format(r#"ts=1788648105773 msg="entity_moved" x1=-4"#), "logfmt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_plain_prose_as_none() {
|
||||
assert_eq!(detect_format("just some plain prose here"), "none");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bare_json_array_or_scalar_is_not_treated_as_json() {
|
||||
// Not an object, so falls through to logfmt/none like `eval_json_comparison` would treat it.
|
||||
assert_eq!(detect_format("[1,2,3]"), "none");
|
||||
assert_eq!(detect_format("42"), "none");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_base64_padding_fragment_as_a_pair() {
|
||||
// The exact false-positive shape from ticket 050's fixture: `key()` greedily consumes the
|
||||
// tail of the base64 blob as if it were a key, landing right before the `==` padding.
|
||||
let pairs = extract_pairs(
|
||||
"qname: <mask.hash: 'rKTEtG7JmnThhoKPbZiVrA=='>, qtype: A, client pid: 12260 (firefox)",
|
||||
);
|
||||
assert!(
|
||||
pairs.iter().all(|(k, _, _)| *k != "rKTEtG7JmnThhoKPbZiVrA"),
|
||||
"base64 padding fragment must not be extracted as a pair: {pairs:?}"
|
||||
);
|
||||
}
|
||||
|
||||
const LOG_STREAM_FIXTURE: &str = include_str!("fixtures/log_stream_sample.txt");
|
||||
|
||||
#[test]
|
||||
fn detects_log_stream_shaped_lines() {
|
||||
let data_line = LOG_STREAM_FIXTURE.lines().nth(1).unwrap();
|
||||
assert_eq!(detect_format(data_line), "log_stream");
|
||||
assert!(is_log_stream_line(data_line));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_row_is_not_detected_as_log_stream() {
|
||||
let header = LOG_STREAM_FIXTURE.lines().next().unwrap();
|
||||
assert!(!is_log_stream_line(header));
|
||||
}
|
||||
|
||||
fn column_names(header: &LogStreamHeader) -> Vec<&str> {
|
||||
header.columns.iter().map(|c| c.name.as_str()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_log_stream_header_columns_generically() {
|
||||
let header_line = LOG_STREAM_FIXTURE.lines().next().unwrap();
|
||||
let header = detect_log_stream_header(header_line).expect("fixture header must be detected");
|
||||
assert_eq!(column_names(&header), vec!["Timestamp", "Thread", "Type", "Activity", "PID", "TTL"]);
|
||||
assert!(!header.tab_delimited);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_log_stream_data_line_is_never_detected_as_its_own_header() {
|
||||
let data_line = LOG_STREAM_FIXTURE.lines().nth(1).unwrap();
|
||||
assert!(detect_log_stream_header(data_line).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_prose_is_not_detected_as_a_log_stream_header() {
|
||||
assert!(detect_log_stream_header("just some plain prose here").is_none());
|
||||
assert!(detect_log_stream_header("ts=1788648105773 msg=\"entity_moved\"").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splits_log_stream_row_into_named_columns_plus_trailing_message() {
|
||||
let header_line = LOG_STREAM_FIXTURE.lines().next().unwrap();
|
||||
let header = detect_log_stream_header(header_line).unwrap();
|
||||
let data_line = "2026-09-06 01:48:54.518117+0100 0x4dfed2 Default 0x0 606 0 WindowServer: (HSTouchHIDService) something happened";
|
||||
let row = split_log_stream_row(data_line, &header);
|
||||
assert_eq!(row.len(), header.columns.len() + 1);
|
||||
assert_eq!(row[0], "2026-09-06 01:48:54.518117+0100");
|
||||
assert_eq!(row[1], "0x4dfed2");
|
||||
assert_eq!(row[2], "Default");
|
||||
assert_eq!(row[3], "0x0");
|
||||
assert_eq!(row[4], "606");
|
||||
assert_eq!(row[5], "0");
|
||||
// The message tail is never chopped up by its own internal single/double spacing.
|
||||
assert_eq!(row[6], "WindowServer: (HSTouchHIDService) something happened");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_single_tab_separates_columns_exactly_like_a_run_of_spaces() {
|
||||
let header = detect_log_stream_header("Timestamp\tThread\tType").unwrap();
|
||||
assert_eq!(column_names(&header), vec!["Timestamp", "Thread", "Type"]);
|
||||
assert!(header.tab_delimited);
|
||||
|
||||
// Two tabs in a row still qualify as a single separator (like a run of 2+ spaces), so this
|
||||
// still cleanly separates Timestamp/Thread/Type despite the doubled tab before "Default".
|
||||
let spaced = split_log_stream_row("2026-09-06 01:48:54\t0x4dfed2\t\tDefault trailing message", &header);
|
||||
let tabbed = split_log_stream_row("2026-09-06 01:48:54\t0x4dfed2\tDefault trailing message", &header);
|
||||
assert_eq!(spaced[0], "2026-09-06 01:48:54");
|
||||
assert_eq!(spaced[1], "0x4dfed2");
|
||||
assert_eq!(spaced[2], "Default trailing message");
|
||||
assert_eq!(tabbed[0], "2026-09-06 01:48:54");
|
||||
assert_eq!(tabbed[1], "0x4dfed2");
|
||||
assert_eq!(tabbed[2], "Default trailing message");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_and_message_patterns_capture_the_same_fields_split_log_stream_row_finds() {
|
||||
let header_line = LOG_STREAM_FIXTURE.lines().next().unwrap();
|
||||
let header = detect_log_stream_header(header_line).unwrap();
|
||||
let line = "2026-09-06 01:48:54.518117+0100 0x4dfed2 Default 0x0 606 0 WindowServer: (HSTouchHIDService) something happened";
|
||||
|
||||
let type_re = Regex::new(&log_stream_column_pattern(&header, 2, "Type")).unwrap();
|
||||
assert_eq!(type_re.captures(line).unwrap().name("Type").unwrap().as_str(), "Default");
|
||||
|
||||
let pid_re = Regex::new(&log_stream_column_pattern(&header, 4, "PID")).unwrap();
|
||||
assert_eq!(pid_re.captures(line).unwrap().name("PID").unwrap().as_str(), "606");
|
||||
|
||||
let message_re = Regex::new(&log_stream_message_pattern(&header, "message")).unwrap();
|
||||
assert_eq!(
|
||||
message_re.captures(line).unwrap().name("message").unwrap().as_str(),
|
||||
"WindowServer: (HSTouchHIDService) something happened"
|
||||
);
|
||||
|
||||
// Also exercise the Timestamp column specifically -- its own value contains one internal
|
||||
// space, so a naive content-only pattern would previously over-consume into the next column.
|
||||
let timestamp_re = Regex::new(&log_stream_column_pattern(&header, 0, "Timestamp")).unwrap();
|
||||
assert_eq!(
|
||||
timestamp_re.captures(line).unwrap().name("Timestamp").unwrap().as_str(),
|
||||
"2026-09-06 01:48:54.518117+0100"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wifi_stat_dump_line_is_still_recognized_as_log_stream_despite_embedded_key_value_pairs() {
|
||||
let line = LOG_STREAM_FIXTURE
|
||||
.lines()
|
||||
.find(|line| line.contains("txRTSFrm="))
|
||||
.expect("fixture must contain the WiFi stat dump line");
|
||||
assert_eq!(detect_format(line), "log_stream");
|
||||
// The embedded key=value pairs are still there for auto_declare_params to see.
|
||||
assert!(!extract_pairs(line).is_empty());
|
||||
}
|
||||
|
||||
const GO_LOG_FIXTURE: &str = include_str!("fixtures/go_log_sample.txt");
|
||||
const PY_LOG_FIXTURE: &str = include_str!("fixtures/py_log_sample.txt");
|
||||
|
||||
#[test]
|
||||
fn detects_go_log_shaped_lines() {
|
||||
for line in GO_LOG_FIXTURE.lines() {
|
||||
assert!(is_go_log_line(line), "expected go-log line to match: {line}");
|
||||
assert_eq!(detect_format(line), "go-log");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_py_log_shaped_lines() {
|
||||
for line in PY_LOG_FIXTURE.lines() {
|
||||
assert!(is_py_log_line(line), "expected py-log line to match: {line}");
|
||||
assert_eq!(detect_format(line), "py-log");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_generic_json_object_is_not_detected_as_go_log() {
|
||||
let line = r#"{"level":"error","code":500}"#;
|
||||
assert!(!is_go_log_line(line));
|
||||
assert_eq!(detect_format(line), "json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_generic_logfmt_line_is_not_detected_as_py_log() {
|
||||
let line = r#"ts=1788648105773 msg="entity_moved" x1=-4"#;
|
||||
assert!(!is_py_log_line(line));
|
||||
assert_eq!(detect_format(line), "logfmt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_log_prefix_extracts_time_level_and_msg() {
|
||||
let (rest, (time, level, msg)) =
|
||||
go_log_prefix(r#"{"time":"2026-09-06T10:35:34.499921+01:00","level":"WARN","msg":"eventC","labelC":1}"#)
|
||||
.unwrap();
|
||||
assert_eq!(time, "2026-09-06T10:35:34.499921+01:00");
|
||||
assert_eq!(level, "WARN");
|
||||
assert_eq!(msg, "eventC");
|
||||
assert_eq!(rest, r#","labelC":1}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn py_log_prefix_extracts_date_time_level_and_logger() {
|
||||
let (rest, (date, time, level, logger)) =
|
||||
py_log_prefix("2026-09-06 10:35:55,217 INFO delete-py-log event=eventC labelC=1").unwrap();
|
||||
assert_eq!(date, "2026-09-06");
|
||||
assert_eq!(time, "10:35:55,217");
|
||||
assert_eq!(level, "INFO");
|
||||
assert_eq!(logger, "delete-py-log");
|
||||
assert_eq!(rest, "event=eventC labelC=1");
|
||||
}
|
||||
}
|
||||
+112
-22
@@ -7,20 +7,23 @@ mod filter_datetime;
|
||||
mod filter_parser;
|
||||
mod index_key;
|
||||
mod ingest;
|
||||
mod logfmt;
|
||||
mod mcp_server;
|
||||
mod metric_primitive;
|
||||
mod profile;
|
||||
mod promql;
|
||||
mod rate;
|
||||
mod sessions;
|
||||
mod settings;
|
||||
mod timestamp;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
|
||||
use clap::Parser;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use profile::ProfileStore;
|
||||
use settings::Settings;
|
||||
|
||||
/// The resolved default settings path, rendered into `--help` so `--settings-path`'s help text
|
||||
@@ -37,6 +40,9 @@ static SETTINGS_PATH_HELP: LazyLock<String> = LazyLock::new(|| {
|
||||
/// Observability dashboard from terminal output.
|
||||
#[derive(Parser, Debug)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Option<Command>,
|
||||
|
||||
/// Port to serve the API and webapp on. Defaults to 0, letting the OS assign
|
||||
/// an ephemeral free port.
|
||||
#[arg(long, default_value_t = 0)]
|
||||
@@ -52,6 +58,30 @@ struct Args {
|
||||
/// URL of a Prometheus/OpenMetrics-compatible endpoint to poll for metrics, in addition to log ingestion.
|
||||
#[arg(long)]
|
||||
metrics_url: Option<String>,
|
||||
|
||||
/// Name to register this session under, so `ottyl mcp --session <name>` or `ottyl sessions`
|
||||
/// can find it later. Defaults to a random human-readable name (e.g. "comic-gazelle").
|
||||
/// Starting a second instance under the same name as one still running overwrites its
|
||||
/// registration (the older process keeps running, just without a discoverable session entry).
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Command {
|
||||
/// Start an MCP (Model Context Protocol) server over stdio, proxying tool calls to a running
|
||||
/// ottyl instance's HTTP API. Register the printed command with an MCP client (e.g. Claude
|
||||
/// Code's `.mcp.json`).
|
||||
Mcp {
|
||||
/// Name of a running ottyl session to connect to (see `ottyl sessions`).
|
||||
#[arg(long)]
|
||||
session: Option<String>,
|
||||
/// Port of a running ottyl instance to connect to, instead of resolving `--session` by name.
|
||||
#[arg(long)]
|
||||
port: Option<u16>,
|
||||
},
|
||||
/// List known ottyl sessions and the ports they're serving on.
|
||||
Sessions,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -62,6 +92,19 @@ async fn main() {
|
||||
settings::default_settings_path()
|
||||
.expect("could not determine a default settings path; pass --settings-path")
|
||||
});
|
||||
|
||||
match args.command {
|
||||
Some(Command::Mcp { session, port }) => {
|
||||
run_mcp(&settings_path, session, port).await;
|
||||
return;
|
||||
}
|
||||
Some(Command::Sessions) => {
|
||||
print_sessions(&settings_path);
|
||||
return;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
let settings = Arc::new(Mutex::new(Settings::load(&settings_path)));
|
||||
|
||||
let (engine_tx, engine_rx) = mpsc::channel(1024);
|
||||
@@ -74,28 +117,28 @@ async fn main() {
|
||||
// Reload the active profile's declarations before ingest starts: there's no way to
|
||||
// reprocess early lines later, so declaring after ingest starts could miss lines that raced ahead.
|
||||
{
|
||||
let (active_profile_name, profiles_dir) = {
|
||||
let (active_profile_name, active_profile) = {
|
||||
let mut settings_guard = settings.lock().unwrap();
|
||||
let profiles_dir = profile::resolve_profiles_dir(&settings_guard, &settings_path);
|
||||
let mut store = ProfileStore::load(&profile::profiles_store_path(&profiles_dir));
|
||||
store
|
||||
.ensure_default(&profile::profiles_store_path(&profiles_dir), &mut settings_guard, &settings_path)
|
||||
settings_guard
|
||||
.ensure_default_profile(&settings_path)
|
||||
.expect("failed to ensure the Default profile exists");
|
||||
(settings_guard.profiles.active_profile.clone(), profiles_dir)
|
||||
let active_profile_name = settings_guard.active_profile.clone();
|
||||
let active_profile = active_profile_name
|
||||
.as_deref()
|
||||
.and_then(|name| settings_guard.profile(name).cloned());
|
||||
(active_profile_name, active_profile)
|
||||
};
|
||||
if let Some(profile_name) = active_profile_name {
|
||||
let store = ProfileStore::load(&profile::profiles_store_path(&profiles_dir));
|
||||
match store.get(&profile_name) {
|
||||
Some(active_profile) => {
|
||||
profile::load_into_engine(&engine_tx, active_profile).await;
|
||||
match (active_profile_name, active_profile) {
|
||||
(Some(_), Some(active_profile)) => {
|
||||
profile::load_into_engine(&engine_tx, &active_profile).await;
|
||||
}
|
||||
None => {
|
||||
(Some(profile_name), None) => {
|
||||
eprintln!(
|
||||
"warning: active profile '{profile_name}' not found under {}; skipping",
|
||||
profiles_dir.display()
|
||||
"warning: active profile '{profile_name}' not found in {}; skipping",
|
||||
settings_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
(None, _) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +155,7 @@ async fn main() {
|
||||
tokio::spawn(ingest::run_metrics_poll(engine_tx.clone(), metrics_url));
|
||||
}
|
||||
|
||||
let app = api::router(engine_tx, settings, settings_path);
|
||||
let app = api::router(engine_tx, settings, settings_path.clone());
|
||||
let listener = TcpListener::bind(("0.0.0.0", args.port))
|
||||
.await
|
||||
.expect("failed to bind listener");
|
||||
@@ -125,16 +168,63 @@ async fn main() {
|
||||
Some(path) => format!("tailing {}", path.display()),
|
||||
None => "reading logs from stdin".to_string(),
|
||||
};
|
||||
eprintln!(
|
||||
"ottyl serving on http://localhost:{} ({})",
|
||||
bound_port, source
|
||||
);
|
||||
let session_name = args.name.clone().unwrap_or_else(sessions::generate_name);
|
||||
match sessions::register_session(&settings_path, &session_name, bound_port) {
|
||||
Ok(_) => eprintln!(
|
||||
"ottyl serving on http://localhost:{} ({}), session name: {}",
|
||||
bound_port, source, session_name
|
||||
),
|
||||
Err(err) => eprintln!(
|
||||
"ottyl serving on http://localhost:{} ({}); warning: failed to register session '{}': {}",
|
||||
bound_port, source, session_name, err
|
||||
),
|
||||
}
|
||||
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.expect("server error");
|
||||
}
|
||||
|
||||
/// Resolves `--session`/`--port` into a target base URL and runs the MCP server over stdio until
|
||||
/// the client disconnects.
|
||||
async fn run_mcp(settings_path: &std::path::Path, session: Option<String>, port: Option<u16>) {
|
||||
use rmcp::ServiceExt;
|
||||
|
||||
let port = match (port, session) {
|
||||
(Some(port), _) => port,
|
||||
(None, Some(name)) => match sessions::resolve_session(settings_path, &name) {
|
||||
Some(info) => info.port,
|
||||
None => {
|
||||
eprintln!("error: no running ottyl session named '{name}'");
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
(None, None) => {
|
||||
eprintln!("error: `ottyl mcp` requires --session <name> or --port <port>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let base_url = format!("http://localhost:{port}");
|
||||
let service = mcp_server::OttylMcp::new(base_url)
|
||||
.serve(rmcp::transport::stdio())
|
||||
.await
|
||||
.expect("failed to start MCP server");
|
||||
service.waiting().await.expect("MCP server error");
|
||||
}
|
||||
|
||||
fn print_sessions(settings_path: &std::path::Path) {
|
||||
let dir = sessions::sessions_dir(settings_path);
|
||||
let sessions = sessions::list_sessions(&dir);
|
||||
if sessions.is_empty() {
|
||||
println!("no known ottyl sessions");
|
||||
return;
|
||||
}
|
||||
for session in sessions {
|
||||
println!("{}\tport={}\tpid={}", session.name, session.port, session.pid);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
//! MCP (Model Context Protocol) server for agents (ticket 067): a short-lived process, started
|
||||
//! via `ottyl mcp --session <name>` (see `crate::sessions`), that speaks MCP over stdio and
|
||||
//! proxies each tool call to a *running* ottyl instance's existing HTTP API. Every tool here is a
|
||||
//! thin translation onto `api.rs` routes/DTOs — no query logic is duplicated outside `engine.rs`.
|
||||
//!
|
||||
//! Uses the official `rmcp` SDK (https://github.com/modelcontextprotocol/rust-sdk) over the
|
||||
//! `transport-io` (stdio) transport rather than hand-rolling the JSON-RPC/MCP handshake, since a
|
||||
//! maintained implementation of the protocol's framing and lifecycle is worth the extra
|
||||
//! dependency.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use rmcp::handler::server::router::tool::ToolRouter;
|
||||
use rmcp::handler::server::wrapper::Parameters;
|
||||
use rmcp::model::{
|
||||
CallToolResult, ContentBlock, Implementation, ProtocolVersion, ServerCapabilities, ServerInfo,
|
||||
};
|
||||
use rmcp::{ErrorData as McpError, ServerHandler, schemars, tool, tool_handler, tool_router};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct QueryLogsParams {
|
||||
/// FilterBar query string, e.g. `regex("error") and ottyl_seq<5`. Omitted matches every log.
|
||||
#[serde(default)]
|
||||
pub filter: Option<String>,
|
||||
/// If true, return only how many logs matched instead of the log lines themselves.
|
||||
#[serde(default)]
|
||||
pub count_only: bool,
|
||||
/// Maximum number of log lines to return. Ignored when `count_only` is set.
|
||||
#[serde(default)]
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
|
||||
pub struct DeclareParamParams {
|
||||
pub name: String,
|
||||
/// Regex matched against each log line; `capture_group` extracts this param's value from it.
|
||||
pub message_match_regex: String,
|
||||
pub capture_group: String,
|
||||
/// Dotted path into a JSON-structured log line to extract from instead of `capture_group`.
|
||||
#[serde(default)]
|
||||
pub json_field_path: Option<String>,
|
||||
/// One of: "string", "number", "timestamp", "duration", "boolean".
|
||||
pub kind: String,
|
||||
/// Extract the elapsed time since this param was last seen, instead of parsing captured text.
|
||||
#[serde(default)]
|
||||
pub elapsed_since_last: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
|
||||
pub struct DeclareMetricParams {
|
||||
pub name: String,
|
||||
pub message_match_regex: String,
|
||||
/// Names of already-declared params whose values become this metric's bucket labels.
|
||||
#[serde(default)]
|
||||
pub label_params: Vec<String>,
|
||||
/// Name of an already-declared param whose value becomes this metric's per-event bucket value.
|
||||
#[serde(default)]
|
||||
pub value_param: Option<String>,
|
||||
/// Fixed labels layered on top of `label_params` for matching lines.
|
||||
#[serde(default)]
|
||||
pub matched_labels: Option<BTreeMap<String, String>>,
|
||||
/// Fixed labels recorded once for a non-matching line, instead of it being invisible to this metric.
|
||||
#[serde(default)]
|
||||
pub unmatched_labels: Option<BTreeMap<String, String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct CreateDashboardParams {
|
||||
/// Profile to create the dashboard in. Defaults to the currently active profile.
|
||||
#[serde(default)]
|
||||
pub profile: Option<String>,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct AddPanelParams {
|
||||
/// Profile the dashboard lives in. Defaults to the currently active profile.
|
||||
#[serde(default)]
|
||||
pub profile: Option<String>,
|
||||
pub dashboard_id: String,
|
||||
/// Id of an existing panel to update in place; omit to add a new panel.
|
||||
#[serde(default)]
|
||||
pub panel_id: Option<String>,
|
||||
pub title: String,
|
||||
/// PromQL-style query backing this panel (see `crate::promql`).
|
||||
pub promql: String,
|
||||
/// One of: "bar", "line", "pie". Defaults to "line".
|
||||
#[serde(default = "default_chart_type")]
|
||||
pub chart_type: String,
|
||||
}
|
||||
|
||||
fn default_chart_type() -> String {
|
||||
"line".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct GetDashboardParams {
|
||||
/// Profile the dashboard lives in. Defaults to the currently active profile.
|
||||
#[serde(default)]
|
||||
pub profile: Option<String>,
|
||||
pub dashboard_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OttylMcp {
|
||||
client: reqwest::Client,
|
||||
base_url: String,
|
||||
// Constructed fresh per call by the `#[tool_handler]`-generated `ServerHandler` methods
|
||||
// (`Self::tool_router()`), not read through this field — kept only because `#[tool_router]`
|
||||
// requires a field of this name/type to exist on the struct.
|
||||
#[allow(dead_code)]
|
||||
tool_router: ToolRouter<OttylMcp>,
|
||||
}
|
||||
|
||||
fn mcp_error(context: &str, err: impl std::fmt::Display) -> McpError {
|
||||
McpError::internal_error(format!("{context}: {err}"), None)
|
||||
}
|
||||
|
||||
fn json_result(value: Value) -> Result<CallToolResult, McpError> {
|
||||
Ok(CallToolResult::success(vec![ContentBlock::text(
|
||||
serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string()),
|
||||
)]))
|
||||
}
|
||||
|
||||
#[tool_router]
|
||||
impl OttylMcp {
|
||||
pub fn new(base_url: String) -> Self {
|
||||
Self {
|
||||
client: reqwest::Client::new(),
|
||||
base_url,
|
||||
tool_router: Self::tool_router(),
|
||||
}
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> String {
|
||||
format!("{}{path}", self.base_url)
|
||||
}
|
||||
|
||||
/// Resolves the profile a dashboard tool call should operate on: the explicit `profile`
|
||||
/// argument if given, otherwise the target instance's currently active profile.
|
||||
async fn resolve_profile(&self, explicit: Option<String>) -> Result<String, McpError> {
|
||||
if let Some(profile) = explicit {
|
||||
return Ok(profile);
|
||||
}
|
||||
let settings: Value = self
|
||||
.client
|
||||
.get(self.url("/api/settings"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to reach ottyl", err))?
|
||||
.json()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to parse settings response", err))?;
|
||||
settings
|
||||
.get("active_profile")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| {
|
||||
McpError::invalid_params(
|
||||
"no active profile on the target instance; pass `profile` explicitly",
|
||||
None,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[tool(description = "Count or read log lines matching a FilterBar query against the running ottyl instance.")]
|
||||
async fn query_logs(
|
||||
&self,
|
||||
Parameters(params): Parameters<QueryLogsParams>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let mut query: Vec<(&str, String)> = Vec::new();
|
||||
if let Some(filter) = ¶ms.filter {
|
||||
query.push(("filter", filter.clone()));
|
||||
}
|
||||
if let Some(limit) = params.limit {
|
||||
query.push(("limit", limit.to_string()));
|
||||
}
|
||||
let response = self
|
||||
.client
|
||||
.get(self.url("/api/logs"))
|
||||
.query(&query)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to reach ottyl", err))?;
|
||||
if !response.status().is_success() {
|
||||
return Err(mcp_error("ottyl rejected the query", response.status()));
|
||||
}
|
||||
let logs: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to parse logs response", err))?;
|
||||
if params.count_only {
|
||||
let count = logs.as_array().map(Vec::len).unwrap_or(0);
|
||||
json_result(serde_json::json!({ "count": count }))
|
||||
} else {
|
||||
json_result(logs)
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "Declare a param: a named value extracted from matching log lines.")]
|
||||
async fn declare_param(
|
||||
&self,
|
||||
Parameters(params): Parameters<DeclareParamParams>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let response = self
|
||||
.client
|
||||
.post(self.url("/api/params"))
|
||||
.json(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to reach ottyl", err))?;
|
||||
let status = response.status();
|
||||
let body: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to parse declare_param response", err))?;
|
||||
if !status.is_success() {
|
||||
return Err(mcp_error("ottyl rejected the param declaration", body));
|
||||
}
|
||||
json_result(body)
|
||||
}
|
||||
|
||||
#[tool(description = "Declare a metric: named event counts/values bucketed by label, derived from matching log lines.")]
|
||||
async fn declare_metric(
|
||||
&self,
|
||||
Parameters(params): Parameters<DeclareMetricParams>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let response = self
|
||||
.client
|
||||
.post(self.url("/api/metrics"))
|
||||
.json(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to reach ottyl", err))?;
|
||||
let status = response.status();
|
||||
let body: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to parse declare_metric response", err))?;
|
||||
if !status.is_success() {
|
||||
return Err(mcp_error("ottyl rejected the metric declaration", body));
|
||||
}
|
||||
json_result(body)
|
||||
}
|
||||
|
||||
#[tool(description = "Create a new, empty dashboard in a profile.")]
|
||||
async fn create_dashboard(
|
||||
&self,
|
||||
Parameters(params): Parameters<CreateDashboardParams>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let profile = self.resolve_profile(params.profile).await?;
|
||||
let response = self
|
||||
.client
|
||||
.post(self.url(&format!(
|
||||
"/api/settings/profiles/{}/dashboards",
|
||||
urlencoding_path(&profile)
|
||||
)))
|
||||
.json(&serde_json::json!({ "name": params.name }))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to reach ottyl", err))?;
|
||||
let status = response.status();
|
||||
let body: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to parse create_dashboard response", err))?;
|
||||
if !status.is_success() {
|
||||
return Err(mcp_error("ottyl rejected the dashboard creation", body));
|
||||
}
|
||||
json_result(body)
|
||||
}
|
||||
|
||||
#[tool(description = "Read a dashboard's current panels.")]
|
||||
async fn get_dashboard(
|
||||
&self,
|
||||
Parameters(params): Parameters<GetDashboardParams>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let profile = self.resolve_profile(params.profile).await?;
|
||||
let body = self.fetch_dashboard(&profile, ¶ms.dashboard_id).await?;
|
||||
json_result(body)
|
||||
}
|
||||
|
||||
// Read-modify-write over HTTP (no dashboard-level locking): two concurrent `add_panel` calls
|
||||
// against the same dashboard can race and one's addition can be lost, same as two browser tabs
|
||||
// saving the same dashboard through the webapp today. Not addressed here — out of scope for a
|
||||
// thin proxy over the existing `save_dashboard` endpoint.
|
||||
#[tool(description = "Add a new panel to a dashboard, or update an existing one by panel_id.")]
|
||||
async fn add_panel(
|
||||
&self,
|
||||
Parameters(params): Parameters<AddPanelParams>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let profile = self.resolve_profile(params.profile).await?;
|
||||
let mut dashboard = self.fetch_dashboard(&profile, ¶ms.dashboard_id).await?;
|
||||
|
||||
let panel_id = params
|
||||
.panel_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| generate_panel_id(¶ms.title, ¶ms.promql));
|
||||
let new_panel = serde_json::json!({
|
||||
"id": panel_id,
|
||||
"title": params.title,
|
||||
"promql": params.promql,
|
||||
"chart_type": params.chart_type,
|
||||
});
|
||||
|
||||
let panels = dashboard
|
||||
.get_mut("panels")
|
||||
.and_then(Value::as_array_mut)
|
||||
.ok_or_else(|| mcp_error("unexpected dashboard shape", "missing panels array"))?;
|
||||
match panels.iter().position(|panel| panel.get("id") == new_panel.get("id")) {
|
||||
Some(index) => panels[index] = new_panel,
|
||||
None => panels.push(new_panel),
|
||||
}
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.put(self.url(&format!(
|
||||
"/api/settings/profiles/{}/dashboards/{}",
|
||||
urlencoding_path(&profile),
|
||||
urlencoding_path(¶ms.dashboard_id)
|
||||
)))
|
||||
.json(&serde_json::json!({ "name": dashboard.get("name"), "panels": dashboard.get("panels") }))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to reach ottyl", err))?;
|
||||
let status = response.status();
|
||||
let body: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to parse add_panel response", err))?;
|
||||
if !status.is_success() {
|
||||
return Err(mcp_error("ottyl rejected the panel update", body));
|
||||
}
|
||||
json_result(body)
|
||||
}
|
||||
|
||||
async fn fetch_dashboard(&self, profile: &str, dashboard_id: &str) -> Result<Value, McpError> {
|
||||
let response = self
|
||||
.client
|
||||
.get(self.url(&format!(
|
||||
"/api/settings/profiles/{}/dashboards/{}",
|
||||
urlencoding_path(profile),
|
||||
urlencoding_path(dashboard_id)
|
||||
)))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to reach ottyl", err))?;
|
||||
let status = response.status();
|
||||
let body: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|err| mcp_error("failed to parse dashboard response", err))?;
|
||||
if !status.is_success() {
|
||||
return Err(mcp_error("ottyl rejected the dashboard lookup", body));
|
||||
}
|
||||
Ok(body)
|
||||
}
|
||||
}
|
||||
|
||||
/// Percent-encodes a single path segment (profile/dashboard names/ids can contain characters that
|
||||
/// aren't valid unescaped in a URL path).
|
||||
fn urlencoding_path(segment: &str) -> String {
|
||||
segment
|
||||
.bytes()
|
||||
.map(|b| match b {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||
(b as char).to_string()
|
||||
}
|
||||
_ => format!("%{b:02X}"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generates a new panel id by hashing title/promql/time, avoiding a `uuid` dependency (matches
|
||||
/// `settings::generate_imported_css_id`'s approach for the same reason).
|
||||
fn generate_panel_id(title: &str, promql: &str) -> String {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
title.hash(&mut hasher);
|
||||
promql.hash(&mut hasher);
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0);
|
||||
nanos.hash(&mut hasher);
|
||||
format!("panel-{:016x}", hasher.finish())
|
||||
}
|
||||
|
||||
#[tool_handler]
|
||||
impl ServerHandler for OttylMcp {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(
|
||||
ServerCapabilities::builder().enable_tools().build(),
|
||||
)
|
||||
.with_server_info(Implementation::from_build_env())
|
||||
.with_protocol_version(ProtocolVersion::V_2024_11_05)
|
||||
.with_instructions(
|
||||
"Proxies to a running ottyl instance's HTTP API. Tools: query_logs, declare_param, \
|
||||
declare_metric, create_dashboard, add_panel, get_dashboard. See the embedded \
|
||||
SKILL.md (GET /SKILL.md on the target instance) for examples."
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+755
-319
File diff suppressed because it is too large
Load Diff
+55
-4
@@ -1,6 +1,7 @@
|
||||
//! Hand-rolled parser for a small PromQL subset: metric name + label matchers, an optional `[5m]` range, and a top-level `sum()`/`avg()`/`rate()`/`histogram()`/`gauge()` wrapper.
|
||||
//! Hand-rolled parser for a small PromQL subset: metric name + label matchers, an optional `[5m]` range, and a top-level `sum()`/`avg()`/`count()`/`rate()`/`histogram()`/`gauge()` wrapper.
|
||||
//! Only exact-match (`=`) label matchers are supported; `rate()` requires a duration range selector, while `histogram(metric[width])` reuses that `[...]` slot for a bare bucket width instead.
|
||||
//! `sum()`/`avg()` may additionally wrap a `rate(...)` call directly (e.g. `sum(rate(requests[5m]))`), and either may be followed by a `by (label, ...)` clause to preserve one series per distinct value of the named labels instead of collapsing to a single series.
|
||||
//! `count()` is a plain vector selector wrapper only (no `rate()` nesting) — it counts occurrences per group regardless of the metric's value, which is the deliberate opt-in for occurrence counting that `sum()`/`avg()` no longer fall back to for a `String`-kind metric (see `crate::engine::evaluate_promql`).
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -8,6 +9,9 @@ use std::time::Duration;
|
||||
pub enum AggOp {
|
||||
Sum,
|
||||
Avg,
|
||||
/// Occurrence count per group, ignoring the metric's stored value entirely — the explicit,
|
||||
/// opt-in-by-name alternative to summing a `String`-kind metric's coerced-to-1.0 values.
|
||||
Count,
|
||||
Rate,
|
||||
/// Buckets counter values into fixed-width buckets; requires `bucket_width`.
|
||||
Histogram,
|
||||
@@ -278,12 +282,17 @@ impl<'a> Parser<'a> {
|
||||
let agg = match ident.as_str() {
|
||||
"sum" => Some(AggOp::Sum),
|
||||
"avg" => Some(AggOp::Avg),
|
||||
"count" => Some(AggOp::Count),
|
||||
"rate" => Some(AggOp::Rate),
|
||||
"histogram" => Some(AggOp::Histogram),
|
||||
"gauge" => Some(AggOp::Gauge),
|
||||
_ => None,
|
||||
};
|
||||
self.skip_whitespace();
|
||||
// `sum by (label) (metric)`: a `by (...)` clause may also precede the aggregation's
|
||||
// parenthesized argument, not just follow it.
|
||||
let prefix_by_labels = if agg.is_some() { self.parse_by_clause()? } else { Vec::new() };
|
||||
self.skip_whitespace();
|
||||
if agg.is_some() && self.peek() == Some('(') {
|
||||
self.expect_char('(')?;
|
||||
self.skip_whitespace();
|
||||
@@ -318,7 +327,15 @@ impl<'a> Parser<'a> {
|
||||
};
|
||||
self.expect_char(')')?;
|
||||
inner.agg = agg;
|
||||
inner.by_labels = self.parse_by_clause()?;
|
||||
let trailing_by_labels = self.parse_by_clause()?;
|
||||
if !prefix_by_labels.is_empty() && !trailing_by_labels.is_empty() {
|
||||
return Err(
|
||||
"by (...) clause may only appear once, either before or after the aggregation's argument"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
inner.by_labels =
|
||||
if prefix_by_labels.is_empty() { trailing_by_labels } else { prefix_by_labels };
|
||||
Ok(inner)
|
||||
} else {
|
||||
// Rewind and parse as a plain vector selector, covering metric names that collide with an agg_op keyword.
|
||||
@@ -415,11 +432,26 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_aggregation() {
|
||||
// `count` isn't a supported agg_op, so this parses as a bare metric name and trips on the trailing `(...)`.
|
||||
let err = parse_query("count(requests)").unwrap_err();
|
||||
// `median` isn't a supported agg_op, so this parses as a bare metric name and trips on the trailing `(...)`.
|
||||
let err = parse_query("median(requests)").unwrap_err();
|
||||
assert!(err.contains("unexpected trailing input"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_count_aggregation() {
|
||||
let query = parse_query("count(requests)").unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Count));
|
||||
assert_eq!(query.metric_name, "requests");
|
||||
assert!(!query.inner_rate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_count_with_by_clause() {
|
||||
let query = parse_query("count(requests) by (path)").unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Count));
|
||||
assert_eq!(query.by_labels, vec!["path".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_regex_label_matcher() {
|
||||
let err = parse_query(r#"requests{path=~"/a"}"#).unwrap_err();
|
||||
@@ -527,4 +559,23 @@ mod tests {
|
||||
fn rejects_unterminated_by_clause() {
|
||||
assert!(parse_query("sum(requests) by (path").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_sum_with_prefix_by_clause() {
|
||||
let query = parse_query("sum by (path) (requests)").unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Sum));
|
||||
assert_eq!(query.by_labels, vec!["path".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_sum_with_prefix_by_clause_multiple_labels() {
|
||||
let query = parse_query("sum by (path, method) (requests)").unwrap();
|
||||
assert_eq!(query.by_labels, vec!["path".to_string(), "method".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_by_clause_both_before_and_after_aggregation() {
|
||||
let err = parse_query("sum by (path) (requests) by (method)").unwrap_err();
|
||||
assert!(err.contains("only appear once"));
|
||||
}
|
||||
}
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
//! Ticket 040: the single windowed-rate/count implementation in the codebase, shared by
|
||||
//! PromQL's `rate()` (`crate::engine::evaluate_promql`, via the `windowed_rate_series` adapter
|
||||
//! there) and alert `Threshold`/`Ratio` conditions (`crate::alerts`). Previously each computed
|
||||
//! its own trailing window independently — PromQL scanning indexed bucket data, alerts scanning
|
||||
//! raw log-line matches with a hand-rolled `VecDeque` — and could disagree for equivalent-looking
|
||||
//! rules simply because the window boundary was implemented twice.
|
||||
//!
|
||||
//! `count_in_window` is the one place that decides what "falls in the trailing window" means;
|
||||
//! everything else in this module is built on top of it, either by batch-rescanning a full point
|
||||
//! series (`rate_at`/`windowed_rate_series`, fine for PromQL's historical/one-shot evaluation) or
|
||||
//! incrementally (`RateWindow`, for alerts, which see one point at a time and need cost
|
||||
//! proportional to the window rather than a full rescan per ingested line).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::timestamp::Timestamp;
|
||||
|
||||
/// How many `points` fall in the trailing `(now - window_ms, now]` window. Points are occurrence
|
||||
/// markers rather than a continuous reading, so only their timestamps matter — the boundary is
|
||||
/// exclusive on the old end and inclusive of `now` itself, matching the original
|
||||
/// `windowed_rate_series` behavior this replaces.
|
||||
fn count_in_window(points: &[(Timestamp, f64)], now: Timestamp, window_ms: i64) -> usize {
|
||||
let now_ms = now.as_millis();
|
||||
let window_start = now_ms - window_ms;
|
||||
points
|
||||
.iter()
|
||||
.filter(|(ts, _)| {
|
||||
let ms = ts.as_millis();
|
||||
ms > window_start && ms <= now_ms
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Trailing-window occurrence rate (points per second) ending at `now`: how many `points` fall in
|
||||
/// `(now - window_ms, now]`, divided by the window width in seconds.
|
||||
pub fn rate_at(points: &[(Timestamp, f64)], now: Timestamp, window_ms: i64) -> f64 {
|
||||
let window_seconds = window_ms as f64 / 1000.0;
|
||||
count_in_window(points, now, window_ms) as f64 / window_seconds
|
||||
}
|
||||
|
||||
/// Evaluates `rate_at` at every one of `points`' own timestamps, turning an occurrence series
|
||||
/// into a windowed rate series — used by PromQL's `rate()` so it plots as a real line rather than
|
||||
/// one trailing point.
|
||||
pub fn windowed_rate_series(points: &[(Timestamp, f64)], window_ms: i64) -> Vec<(Timestamp, f64)> {
|
||||
points.iter().map(|(ts, _)| (*ts, rate_at(points, *ts, window_ms))).collect()
|
||||
}
|
||||
|
||||
/// Incrementally maintained trailing window: for a caller that sees points arrive one at a time
|
||||
/// (alert `Threshold`/`Ratio` conditions, evaluated per ingested line) and needs cost proportional
|
||||
/// to the window rather than `count_in_window`'s full rescan per call. Assumes points are pushed
|
||||
/// in non-decreasing timestamp order, true for ingested log lines.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct RateWindow {
|
||||
timestamps: VecDeque<Timestamp>,
|
||||
}
|
||||
|
||||
impl RateWindow {
|
||||
/// Builds a window pre-populated from already-seen history (e.g. currently-ingested logs
|
||||
/// matching an alert's query), so a freshly-created window starts with whatever of that
|
||||
/// history already falls in the trailing window instead of empty.
|
||||
pub fn backfill(timestamps: impl IntoIterator<Item = Timestamp>, now: Timestamp, window_ms: i64) -> Self {
|
||||
let mut window = Self::default();
|
||||
for ts in timestamps {
|
||||
window.push(ts, window_ms);
|
||||
}
|
||||
window.prune(now, window_ms);
|
||||
window
|
||||
}
|
||||
|
||||
/// Records one occurrence at `timestamp` and drops anything that has aged out of the
|
||||
/// resulting trailing window.
|
||||
pub fn push(&mut self, timestamp: Timestamp, window_ms: i64) {
|
||||
self.timestamps.push_back(timestamp);
|
||||
self.prune(timestamp, window_ms);
|
||||
}
|
||||
|
||||
/// Same boundary as `count_in_window`: drops entries at or before `now - window_ms`.
|
||||
fn prune(&mut self, now: Timestamp, window_ms: i64) {
|
||||
let window_start = now.as_millis() - window_ms;
|
||||
while let Some(&front) = self.timestamps.front() {
|
||||
if front.as_millis() <= window_start {
|
||||
self.timestamps.pop_front();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
self.timestamps.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rate_at_matches_hand_computed_window() {
|
||||
let points = vec![
|
||||
(Timestamp::from_millis(0), 1.0),
|
||||
(Timestamp::from_millis(1000), 1.0),
|
||||
(Timestamp::from_millis(2000), 1.0),
|
||||
];
|
||||
// (1000, 2000]: only the point at 2000.
|
||||
assert_eq!(rate_at(&points, Timestamp::from_millis(2000), 1000), 1.0);
|
||||
// (0, 2000]: points at 1000 and 2000 -> 2 points / 2s.
|
||||
assert_eq!(rate_at(&points, Timestamp::from_millis(2000), 2000), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windowed_rate_series_matches_rate_at_at_each_point() {
|
||||
let points = vec![
|
||||
(Timestamp::from_millis(0), 1.0),
|
||||
(Timestamp::from_millis(500), 1.0),
|
||||
(Timestamp::from_millis(1000), 1.0),
|
||||
];
|
||||
let series = windowed_rate_series(&points, 1000);
|
||||
for (ts, rate) in &series {
|
||||
assert_eq!(*rate, rate_at(&points, *ts, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rate_window_incremental_count_matches_batch_count_in_window() {
|
||||
let mut window = RateWindow::default();
|
||||
let millis = [0, 200, 400, 1200];
|
||||
for ms in millis {
|
||||
window.push(Timestamp::from_millis(ms), 1000);
|
||||
}
|
||||
let points: Vec<(Timestamp, f64)> =
|
||||
millis.iter().map(|ms| (Timestamp::from_millis(*ms), 1.0)).collect();
|
||||
assert_eq!(window.count(), count_in_window(&points, Timestamp::from_millis(1200), 1000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rate_window_backfill_seeds_from_history_within_the_window() {
|
||||
let history = [0, 500, 2000, 2500].map(Timestamp::from_millis);
|
||||
// now=2500ms, window=1000ms -> window (1500, 2500]: only 2000 and 2500 qualify.
|
||||
let window = RateWindow::backfill(history, Timestamp::from_millis(2500), 1000);
|
||||
assert_eq!(window.count(), 2);
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
//! Named session registry (ticket 068): each running `ottyl` process registers itself under a
|
||||
//! name by writing a small file to `<settings dir>/sessions/<name>.json`, so a second CLI
|
||||
//! invocation (e.g. `ottyl mcp --session <name>`, ticket 067) can resolve a name to a port without
|
||||
//! the user copying one around by hand.
|
||||
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// On-disk shape of a `sessions/<name>.json` file.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SessionInfo {
|
||||
pub name: String,
|
||||
pub port: u16,
|
||||
pub pid: u32,
|
||||
}
|
||||
|
||||
/// The `sessions/` directory for a given settings file, following the same
|
||||
/// sibling-directory convention as `profiles/` (see `settings::migrate_legacy_profiles_file`).
|
||||
pub fn sessions_dir(settings_path: &Path) -> PathBuf {
|
||||
settings_path
|
||||
.parent()
|
||||
.map(|parent| parent.join("sessions"))
|
||||
.unwrap_or_else(|| PathBuf::from("sessions"))
|
||||
}
|
||||
|
||||
fn session_file_path(dir: &Path, name: &str) -> PathBuf {
|
||||
dir.join(format!("{name}.json"))
|
||||
}
|
||||
|
||||
/// Best-effort liveness check for a PID. Shells out to `kill -0` on unix (no signal is delivered,
|
||||
/// only permission/existence is checked); assumed alive on other platforms, which just means a
|
||||
/// stale session file there waits for an explicit name collision to be overwritten instead of
|
||||
/// being proactively cleaned up.
|
||||
#[cfg(unix)]
|
||||
fn is_pid_alive(pid: u32) -> bool {
|
||||
process::Command::new("kill")
|
||||
.args(["-0", &pid.to_string()])
|
||||
.stdout(process::Stdio::null())
|
||||
.stderr(process::Stdio::null())
|
||||
.status()
|
||||
.map(|status| status.success())
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn is_pid_alive(_pid: u32) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Removes session files whose owning process is no longer running. Best-effort: there's no
|
||||
/// shutdown hook to unregister a session on graceful exit, so this runs on every startup instead.
|
||||
/// Any I/O error reading a single entry is ignored — a future call gets another chance.
|
||||
pub fn cleanup_stale(dir: &Path) {
|
||||
let Ok(entries) = fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
|
||||
continue;
|
||||
}
|
||||
let Ok(contents) = fs::read_to_string(&path) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(info) = serde_json::from_str::<SessionInfo>(&contents) else {
|
||||
continue;
|
||||
};
|
||||
if !is_pid_alive(info.pid) {
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a random human-readable session name (e.g. `comic-gazelle`) for when `--name` isn't
|
||||
/// given explicitly.
|
||||
pub fn generate_name() -> String {
|
||||
petname::petname(2, "-").unwrap_or_else(|| format!("session-{}", process::id()))
|
||||
}
|
||||
|
||||
/// Registers this process as session `name` on `port`: cleans up stale entries, then writes
|
||||
/// `sessions/<name>.json`, overwriting any existing file for that name (documented behavior — a
|
||||
/// second instance started under the same explicit `--name` as a still-running one simply
|
||||
/// replaces its registration; the older process keeps running, just without a discoverable entry).
|
||||
pub fn register_session(settings_path: &Path, name: &str, port: u16) -> io::Result<PathBuf> {
|
||||
let dir = sessions_dir(settings_path);
|
||||
fs::create_dir_all(&dir)?;
|
||||
cleanup_stale(&dir);
|
||||
let info = SessionInfo {
|
||||
name: name.to_string(),
|
||||
port,
|
||||
pid: process::id(),
|
||||
};
|
||||
let path = session_file_path(&dir, name);
|
||||
fs::write(&path, serde_json::to_string_pretty(&info)?)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Looks up a session by name. Returns `None` if no such file exists, it fails to parse, or its
|
||||
/// process is no longer alive (a stale entry left by a process that didn't exit through the normal
|
||||
/// next-`register_session` cleanup path).
|
||||
pub fn resolve_session(settings_path: &Path, name: &str) -> Option<SessionInfo> {
|
||||
let dir = sessions_dir(settings_path);
|
||||
let contents = fs::read_to_string(session_file_path(&dir, name)).ok()?;
|
||||
let info: SessionInfo = serde_json::from_str(&contents).ok()?;
|
||||
is_pid_alive(info.pid).then_some(info)
|
||||
}
|
||||
|
||||
/// Lists every session file under `dir`, live or stale (callers that only want live sessions
|
||||
/// should filter on `is_pid_alive`-backed `resolve_session` instead). Used by `ottyl sessions`.
|
||||
pub fn list_sessions(dir: &Path) -> Vec<SessionInfo> {
|
||||
let Ok(entries) = fs::read_dir(dir) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut sessions: Vec<SessionInfo> = entries
|
||||
.flatten()
|
||||
.filter(|entry| entry.path().extension().and_then(|ext| ext.to_str()) == Some("json"))
|
||||
.filter_map(|entry| fs::read_to_string(entry.path()).ok())
|
||||
.filter_map(|contents| serde_json::from_str(&contents).ok())
|
||||
.collect();
|
||||
sessions.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
sessions
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn register_session_writes_correct_port() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let settings_path = dir.path().join("settings.json");
|
||||
|
||||
let path = register_session(&settings_path, "foo", 4242).unwrap();
|
||||
assert!(path.exists());
|
||||
|
||||
let info = resolve_session(&settings_path, "foo").unwrap();
|
||||
assert_eq!(info.port, 4242);
|
||||
assert_eq!(info.name, "foo");
|
||||
assert_eq!(info.pid, process::id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_session_returns_none_for_unknown_name() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let settings_path = dir.path().join("settings.json");
|
||||
assert!(resolve_session(&settings_path, "does-not-exist").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_session_does_not_block_reregistration_under_same_name() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let settings_path = dir.path().join("settings.json");
|
||||
let sessions_dir_path = sessions_dir(&settings_path);
|
||||
fs::create_dir_all(&sessions_dir_path).unwrap();
|
||||
|
||||
// A PID essentially guaranteed not to be alive (PID 1 exists but a huge, unlikely-assigned
|
||||
// PID stands in as "not alive" without depending on system PID-reuse specifics).
|
||||
let stale = SessionInfo { name: "foo".to_string(), port: 1111, pid: 999_999 };
|
||||
fs::write(
|
||||
session_file_path(&sessions_dir_path, "foo"),
|
||||
serde_json::to_string(&stale).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Registering a live session under the same name succeeds and the stale cleanup pass (run
|
||||
// as part of registration) doesn't error out on the bogus pre-existing entry.
|
||||
register_session(&settings_path, "foo", 5555).unwrap();
|
||||
let info = resolve_session(&settings_path, "foo").unwrap();
|
||||
assert_eq!(info.port, 5555);
|
||||
assert_eq!(info.pid, process::id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_sessions_returns_all_entries_sorted_by_name() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let settings_path = dir.path().join("settings.json");
|
||||
register_session(&settings_path, "zeta", 1).unwrap();
|
||||
register_session(&settings_path, "alpha", 2).unwrap();
|
||||
|
||||
let sessions = list_sessions(&sessions_dir(&settings_path));
|
||||
let names: Vec<&str> = sessions.iter().map(|s| s.name.as_str()).collect();
|
||||
assert_eq!(names, vec!["alpha", "zeta"]);
|
||||
}
|
||||
}
|
||||
+254
-38
@@ -1,4 +1,5 @@
|
||||
//! Process-wide settings model: colour scheme, CSS entries, and profile pointers, persisted as JSON.
|
||||
//! Process-wide settings model: colour scheme, CSS entries, and profiles (ticket 049 folded
|
||||
//! profile storage inline here instead of a separate `profiles.json`), persisted as JSON.
|
||||
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::fmt;
|
||||
@@ -13,6 +14,7 @@ use directories::ProjectDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::alerts::AlertRule;
|
||||
use crate::profile::Profile;
|
||||
|
||||
/// Shared, process-wide settings handle. Cheap to clone.
|
||||
pub type SettingsHandle = Arc<Mutex<Settings>>;
|
||||
@@ -35,6 +37,24 @@ pub enum ColorScheme {
|
||||
Yellow,
|
||||
Pink,
|
||||
Orange,
|
||||
/// Brighter, light-surfaced counterpart to `Green` (ticket 048).
|
||||
#[serde(rename = "light_green")]
|
||||
LightGreen,
|
||||
/// Brighter, light-surfaced counterpart to `Red` (ticket 056).
|
||||
#[serde(rename = "light_red")]
|
||||
LightRed,
|
||||
/// Brighter, light-surfaced counterpart to `Blue` (ticket 056).
|
||||
#[serde(rename = "light_blue")]
|
||||
LightBlue,
|
||||
/// Brighter, light-surfaced counterpart to `Yellow` (ticket 056).
|
||||
#[serde(rename = "light_yellow")]
|
||||
LightYellow,
|
||||
/// Brighter, light-surfaced counterpart to `Pink` (ticket 056).
|
||||
#[serde(rename = "light_pink")]
|
||||
LightPink,
|
||||
/// Brighter, light-surfaced counterpart to `Orange` (ticket 056).
|
||||
#[serde(rename = "light_orange")]
|
||||
LightOrange,
|
||||
#[default]
|
||||
System,
|
||||
}
|
||||
@@ -51,27 +71,101 @@ pub enum CssEntry {
|
||||
Linked { path_or_url: String },
|
||||
}
|
||||
|
||||
/// Tracks known profile names and which one is active; the profile files themselves live elsewhere.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct Profiles {
|
||||
/// Names of known profiles.
|
||||
pub names: Vec<String>,
|
||||
/// Name of the currently active profile, if any.
|
||||
pub active_profile: Option<String>,
|
||||
/// Directory (relative to the settings file's directory, or absolute)
|
||||
/// where individual profile files are expected to live.
|
||||
pub profiles_dir: String,
|
||||
/// Legacy on-disk shape of the (pre-049) `Profiles` pointer struct: only names/active-profile/dir
|
||||
/// pointers, with the actual profile content living in a separate `profiles.json`. Kept only so an
|
||||
/// old `settings.json` still parses; see `ProfilesOnDisk` and `Settings::migrate_legacy_profiles_file`.
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
struct LegacyProfilesPointer {
|
||||
// `names` is intentionally not captured: it was always redundant with the legacy
|
||||
// `profiles.json`'s actual profile list, which is what migration reads instead.
|
||||
#[serde(default)]
|
||||
active_profile: Option<String>,
|
||||
/// Directory (relative to the settings file's directory, or absolute) the legacy `profiles.json`
|
||||
/// lived under. Every version of this codebase ever wrote `"profiles"` here (no API ever let a
|
||||
/// user change it), so migration doesn't bother re-deriving a custom value from this field.
|
||||
#[serde(default)]
|
||||
#[allow(dead_code)]
|
||||
profiles_dir: Option<String>,
|
||||
}
|
||||
|
||||
/// On-disk shape of the `profiles` field: either the new inline `Vec<Profile>` (ticket 049) or the
|
||||
/// old pointer-struct shape, so a `settings.json` written before this ticket still deserializes.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ProfilesOnDisk {
|
||||
Inline(Vec<Profile>),
|
||||
Legacy(LegacyProfilesPointer),
|
||||
}
|
||||
|
||||
impl Default for ProfilesOnDisk {
|
||||
fn default() -> Self {
|
||||
ProfilesOnDisk::Inline(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw on-disk shape of `Settings`, used only to support deserializing an old `settings.json` whose
|
||||
/// `profiles` field was the pointer struct and had no top-level `active_profile` — see `Settings`'s
|
||||
/// `#[serde(from = ...)]`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct SettingsOnDisk {
|
||||
color_scheme: ColorScheme,
|
||||
#[serde(default)]
|
||||
css_entries: Vec<CssEntry>,
|
||||
#[serde(default)]
|
||||
custom_theme_id: Option<String>,
|
||||
#[serde(default)]
|
||||
profiles: ProfilesOnDisk,
|
||||
#[serde(default)]
|
||||
active_profile: Option<String>,
|
||||
#[serde(default)]
|
||||
max_logs: Option<usize>,
|
||||
#[serde(default)]
|
||||
alert_rules: Vec<AlertRule>,
|
||||
#[serde(default)]
|
||||
ntfy_topic: Option<String>,
|
||||
#[serde(default)]
|
||||
ntfy_server: Option<String>,
|
||||
}
|
||||
|
||||
impl From<SettingsOnDisk> for Settings {
|
||||
fn from(on_disk: SettingsOnDisk) -> Self {
|
||||
let (profiles, legacy_active_profile) = match on_disk.profiles {
|
||||
ProfilesOnDisk::Inline(profiles) => (profiles, None),
|
||||
ProfilesOnDisk::Legacy(pointer) => (Vec::new(), pointer.active_profile),
|
||||
};
|
||||
Settings {
|
||||
color_scheme: on_disk.color_scheme,
|
||||
css_entries: on_disk.css_entries,
|
||||
custom_theme_id: on_disk.custom_theme_id,
|
||||
profiles,
|
||||
active_profile: on_disk.active_profile.or(legacy_active_profile),
|
||||
max_logs: on_disk.max_logs,
|
||||
alert_rules: on_disk.alert_rules,
|
||||
ntfy_topic: on_disk.ntfy_topic,
|
||||
ntfy_server: on_disk.ntfy_server,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The single, process-wide settings instance. Only `PartialEq` (not `Eq`): `alert_rules` carries `f64` fields, which aren't `Eq`.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
///
|
||||
/// `profiles`/`active_profile` used to be a separate `Profiles` pointer struct referencing a
|
||||
/// standalone `profiles.json` (see `crate::profile::ProfileStore`, removed by ticket 049); profile
|
||||
/// data now lives inline here, as the single source of truth. `Deserialize` goes through
|
||||
/// `SettingsOnDisk` so a `settings.json` written before this ticket still loads (see
|
||||
/// `migrate_legacy_profiles_file`, which then imports the old `profiles.json`, if any, on `load`).
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(from = "SettingsOnDisk")]
|
||||
pub struct Settings {
|
||||
pub color_scheme: ColorScheme,
|
||||
pub css_entries: Vec<CssEntry>,
|
||||
/// Id of the selected custom theme entry, if any; `None` means `color_scheme` alone determines the active palette.
|
||||
#[serde(default)]
|
||||
pub custom_theme_id: Option<String>,
|
||||
pub profiles: Profiles,
|
||||
/// Every known profile, inline (ticket 049).
|
||||
pub profiles: Vec<Profile>,
|
||||
/// Name of the currently active profile, if any.
|
||||
pub active_profile: Option<String>,
|
||||
/// Maximum number of ingested logs kept in memory; `None` means unlimited.
|
||||
#[serde(default)]
|
||||
pub max_logs: Option<usize>,
|
||||
@@ -86,36 +180,21 @@ pub struct Settings {
|
||||
pub ntfy_server: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
color_scheme: ColorScheme::default(),
|
||||
css_entries: Vec::new(),
|
||||
custom_theme_id: None,
|
||||
profiles: Profiles {
|
||||
names: Vec::new(),
|
||||
active_profile: None,
|
||||
profiles_dir: "profiles".to_string(),
|
||||
},
|
||||
max_logs: None,
|
||||
alert_rules: Vec::new(),
|
||||
ntfy_topic: None,
|
||||
ntfy_server: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the default settings file path (`<config_dir>/settings.json`) from OS-conventional config directories; `None` if none could be determined.
|
||||
pub fn default_settings_path() -> Option<PathBuf> {
|
||||
ProjectDirs::from("", "", "ottyl").map(|dirs| dirs.config_dir().join("settings.json"))
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// Loads settings from `path`, creating and persisting defaults if missing. Any I/O or parse error on an existing file is logged and treated as defaults, without overwriting the unreadable file.
|
||||
/// Loads settings from `path`, creating and persisting defaults if missing. Any I/O or parse error on an existing file is logged and treated as defaults, without overwriting the unreadable file. Also runs the one-time legacy `profiles.json` migration (see `migrate_legacy_profiles_file`).
|
||||
pub fn load(path: &Path) -> Settings {
|
||||
match fs::read_to_string(path) {
|
||||
Ok(contents) => match serde_json::from_str(&contents) {
|
||||
Ok(settings) => settings,
|
||||
Ok(contents) => match serde_json::from_str::<Settings>(&contents) {
|
||||
Ok(mut settings) => {
|
||||
settings.migrate_legacy_profiles_file(path);
|
||||
settings.migrate_legacy_profile_shapes(path);
|
||||
settings
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"warning: failed to parse settings file at {}: {err}; using defaults",
|
||||
@@ -125,7 +204,8 @@ impl Settings {
|
||||
}
|
||||
},
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
let settings = Settings::default();
|
||||
let mut settings = Settings::default();
|
||||
settings.migrate_legacy_profiles_file(path);
|
||||
if let Err(save_err) = settings.save(path) {
|
||||
eprintln!(
|
||||
"warning: failed to create default settings file at {}: {save_err}",
|
||||
@@ -153,6 +233,60 @@ impl Settings {
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
|
||||
fs::write(path, contents)
|
||||
}
|
||||
|
||||
/// One-time migration (ticket 049): if `self.profiles` is empty (a fresh `Settings`, or one
|
||||
/// parsed from a pre-049 `settings.json` whose profiles lived in a separate file) and a legacy
|
||||
/// `<settings_path's parent>/profiles/profiles.json` exists, reads it in, merges it into
|
||||
/// `self.profiles`, persists `self`, and — only once that save succeeds — renames the old file
|
||||
/// (`.migrated` suffix) so it isn't re-imported on a later load. Every prior version of this
|
||||
/// codebase always used the fixed `"profiles"` subdirectory name (no API ever let it be
|
||||
/// reconfigured), so that's the only location checked. Best-effort: any failure is logged and
|
||||
/// left for a future load to retry rather than panicking.
|
||||
fn migrate_legacy_profiles_file(&mut self, settings_path: &Path) {
|
||||
if !self.profiles.is_empty() {
|
||||
return;
|
||||
}
|
||||
let legacy_path = settings_path
|
||||
.parent()
|
||||
.map(|parent| parent.join("profiles"))
|
||||
.unwrap_or_else(|| PathBuf::from("profiles"))
|
||||
.join("profiles.json");
|
||||
let Some(profiles) = crate::profile::load_legacy_profile_store(&legacy_path) else {
|
||||
return;
|
||||
};
|
||||
self.profiles = profiles;
|
||||
if let Err(err) = self.save(settings_path) {
|
||||
eprintln!(
|
||||
"warning: failed to persist profiles migrated from {}: {err}",
|
||||
legacy_path.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
let migrated_path = legacy_path.with_extension("json.migrated");
|
||||
if let Err(err) = fs::rename(&legacy_path, &migrated_path) {
|
||||
eprintln!(
|
||||
"warning: migrated profiles from {} into settings.json, but failed to rename the old file: {err}",
|
||||
legacy_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates any pre-037/pre-038 metric or param shapes still present in `self.profiles` into
|
||||
/// their current form (see `crate::profile::migrate_legacy_profile_shapes`), persisting `self`
|
||||
/// if anything changed. Runs on every load — previously done inside the now-removed
|
||||
/// `ProfileStore::load` (ticket 049 folded profile storage into `settings.json`) — so a profile
|
||||
/// saved by an older version of ottyl, whether loaded inline here or just merged in by
|
||||
/// `migrate_legacy_profiles_file`, still gets migrated forward.
|
||||
fn migrate_legacy_profile_shapes(&mut self, settings_path: &Path) {
|
||||
if crate::profile::migrate_legacy_profile_shapes(&mut self.profiles)
|
||||
&& let Err(err) = self.save(settings_path)
|
||||
{
|
||||
eprintln!(
|
||||
"warning: failed to persist migrated profile shapes to {}: {err}",
|
||||
settings_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned by CSS-entry mutations and lookups.
|
||||
@@ -306,6 +440,23 @@ mod tests {
|
||||
assert_eq!(reloaded, settings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_variant_color_schemes_round_trip_through_serde() {
|
||||
for (scheme, rename) in [
|
||||
(ColorScheme::LightGreen, "light_green"),
|
||||
(ColorScheme::LightRed, "light_red"),
|
||||
(ColorScheme::LightBlue, "light_blue"),
|
||||
(ColorScheme::LightYellow, "light_yellow"),
|
||||
(ColorScheme::LightPink, "light_pink"),
|
||||
(ColorScheme::LightOrange, "light_orange"),
|
||||
] {
|
||||
let json = serde_json::to_string(&scheme).unwrap();
|
||||
assert_eq!(json, format!("\"{rename}\""));
|
||||
let deserialized: ColorScheme = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deserialized, scheme);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_grey_color_scheme_deserializes_as_dark() {
|
||||
let settings: Settings = serde_json::from_str(
|
||||
@@ -315,6 +466,20 @@ mod tests {
|
||||
assert_eq!(settings.color_scheme, ColorScheme::Dark);
|
||||
}
|
||||
|
||||
/// A `settings.json` written before ticket 049 (nested `Profiles` pointer struct, no top-level
|
||||
/// `active_profile`) still parses: the pointer's `active_profile` carries over, and `profiles`
|
||||
/// starts empty (the real content lived in a separate `profiles.json`, migrated separately by
|
||||
/// `Settings::load`, not by plain deserialization).
|
||||
#[test]
|
||||
fn legacy_profiles_pointer_shape_deserializes_with_empty_inline_profiles() {
|
||||
let settings: Settings = serde_json::from_str(
|
||||
r#"{"color_scheme":"dark","css_entries":[],"custom_theme_id":null,"profiles":{"names":["web"],"active_profile":"web","profiles_dir":"profiles"},"max_logs":null,"alert_rules":[],"ntfy_topic":null,"ntfy_server":null}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(settings.profiles.is_empty());
|
||||
assert_eq!(settings.active_profile, Some("web".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trips_through_save_and_load() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
@@ -329,8 +494,8 @@ mod tests {
|
||||
settings.css_entries.push(CssEntry::Linked {
|
||||
path_or_url: "https://example.com/style.css".to_string(),
|
||||
});
|
||||
settings.profiles.names.push("default".to_string());
|
||||
settings.profiles.active_profile = Some("default".to_string());
|
||||
settings.profiles.push(crate::profile::Profile::new("default".to_string()));
|
||||
settings.active_profile = Some("default".to_string());
|
||||
settings.alert_rules.push(crate::alerts::AlertRule {
|
||||
id: "rule1".to_string(),
|
||||
name: "error spike".to_string(),
|
||||
@@ -458,4 +623,55 @@ mod tests {
|
||||
let result = settings.css_content(&css_dir, &linked_id);
|
||||
assert!(matches!(result, Err(CssError::NotImported)));
|
||||
}
|
||||
|
||||
/// Acceptance criterion (ticket 049): a pre-existing legacy `profiles_dir/profiles.json` is
|
||||
/// merged into `settings.json` on the next load, and the old file is renamed (not re-imported on
|
||||
/// a subsequent load).
|
||||
#[test]
|
||||
fn load_migrates_legacy_profiles_file_into_inline_settings() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let settings_path = dir.path().join("settings.json");
|
||||
let profiles_dir = dir.path().join("profiles");
|
||||
fs::create_dir_all(&profiles_dir).unwrap();
|
||||
let legacy_path = profiles_dir.join("profiles.json");
|
||||
fs::write(
|
||||
&legacy_path,
|
||||
r#"[{"name":"web","params":[],"metrics":[],"dashboards":[]}]"#,
|
||||
)
|
||||
.unwrap();
|
||||
// No settings.json yet: simulates upgrading a pre-049 install that never wrote one either,
|
||||
// or one that did but whose nested pointer already carried an active_profile name.
|
||||
fs::write(
|
||||
&settings_path,
|
||||
r#"{"color_scheme":"system","css_entries":[],"custom_theme_id":null,"profiles":{"names":["web"],"active_profile":"web","profiles_dir":"profiles"},"max_logs":null,"alert_rules":[],"ntfy_topic":null,"ntfy_server":null}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let settings = Settings::load(&settings_path);
|
||||
assert_eq!(settings.profiles.len(), 1);
|
||||
assert_eq!(settings.profiles[0].name, "web");
|
||||
assert_eq!(settings.active_profile, Some("web".to_string()));
|
||||
|
||||
// The legacy file is gone (renamed), so it isn't re-imported.
|
||||
assert!(!legacy_path.exists());
|
||||
let migrated_path = legacy_path.with_extension("json.migrated");
|
||||
assert!(migrated_path.exists());
|
||||
|
||||
// Reload from disk: the migrated settings.json now carries profiles inline directly.
|
||||
let reloaded = Settings::load(&settings_path);
|
||||
assert_eq!(reloaded.profiles.len(), 1);
|
||||
assert_eq!(reloaded.profiles[0].name, "web");
|
||||
}
|
||||
|
||||
/// A fresh install (no legacy file at all) never creates `profiles.json` — it's not even looked
|
||||
/// for beyond the one migration check, and `profiles` simply stays empty.
|
||||
#[test]
|
||||
fn load_without_legacy_file_leaves_profiles_empty() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let settings_path = dir.path().join("settings.json");
|
||||
|
||||
let settings = Settings::load(&settings_path);
|
||||
assert!(settings.profiles.is_empty());
|
||||
assert!(!dir.path().join("profiles").exists());
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>webapp</title>
|
||||
<title>ottyl</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 669 B |
+76
-7
@@ -6,6 +6,9 @@ import {
|
||||
TIME_RANGE_PRESETS,
|
||||
computeBucketMs,
|
||||
computePresetRange,
|
||||
loadLogSortSettings,
|
||||
saveLogSortSettings,
|
||||
type LogSortSettings,
|
||||
type TimeRange,
|
||||
type TimeRangePresetId,
|
||||
} from './components/LogTimeline'
|
||||
@@ -251,6 +254,28 @@ function StandaloneRanged({
|
||||
)
|
||||
}
|
||||
|
||||
/// Ticket 058: only replaces `params` state when the declaration list actually changed, so a param
|
||||
/// list refetched on every poll tick doesn't force `Params.tsx` to re-render when nothing changed.
|
||||
function updateParamsIfChanged(
|
||||
setParams: (updater: (current: ParamDeclaration[]) => ParamDeclaration[]) => void,
|
||||
next: ParamDeclaration[],
|
||||
) {
|
||||
setParams((current) => (JSON.stringify(current) === JSON.stringify(next) ? current : next))
|
||||
}
|
||||
|
||||
/// Ticket 066: text shown under the filter bar when the user hasn't typed a manual filter, so an active preset/sort order is still visible somewhere near the filter input instead of leaving it looking empty/stale.
|
||||
function describeActiveState(activePreset: TimeRangePresetId | null, sortSettings: LogSortSettings): string | null {
|
||||
const parts: string[] = []
|
||||
if (activePreset && activePreset !== 'all') {
|
||||
const preset = TIME_RANGE_PRESETS.find((p) => p.id === activePreset)
|
||||
parts.push(`last ${preset?.label ?? activePreset}`)
|
||||
}
|
||||
if (sortSettings.sortBy) {
|
||||
parts.push(`sorted by ${sortSettings.sortBy}`)
|
||||
}
|
||||
return parts.length > 0 ? parts.join(', ') : null
|
||||
}
|
||||
|
||||
/// The log viewer has its own `LogTimeline`, so it needs its own copy of App's log/timeline polling state instead of a carried-over range.
|
||||
function StandaloneLog() {
|
||||
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||
@@ -260,6 +285,12 @@ function StandaloneLog() {
|
||||
const [activePreset, setActivePreset] = useState<TimeRangePresetId | null>('all')
|
||||
const [filter, setFilter] = useState('')
|
||||
const [filterError, setFilterError] = useState<string | null>(null)
|
||||
const [sortSettings, setSortSettings] = useState<LogSortSettings>(loadLogSortSettings)
|
||||
|
||||
function handleSortSettingsChange(next: LogSortSettings) {
|
||||
setSortSettings(next)
|
||||
saveLogSortSettings(next)
|
||||
}
|
||||
|
||||
const rangeRef = useRef(range)
|
||||
useEffect(() => {
|
||||
@@ -302,7 +333,7 @@ function StandaloneLog() {
|
||||
)
|
||||
}
|
||||
try {
|
||||
const [nextBuckets, nextLogs] = await Promise.all([
|
||||
const [nextBuckets, nextLogs, nextParams] = await Promise.all([
|
||||
fetchTimeline({
|
||||
startMs: effectiveRange?.startMs,
|
||||
endMs: effectiveRange?.endMs,
|
||||
@@ -310,11 +341,21 @@ function StandaloneLog() {
|
||||
bucketMs: effectiveRange ? computeBucketMs(effectiveRange.endMs - effectiveRange.startMs) : undefined,
|
||||
filter: filter || undefined,
|
||||
}),
|
||||
fetchLogs({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, limit: 1000, filter: filter || undefined }),
|
||||
fetchLogs({
|
||||
startMs: effectiveRange?.startMs,
|
||||
endMs: effectiveRange?.endMs,
|
||||
limit: 1000,
|
||||
filter: filter || undefined,
|
||||
sortBy: sortSettings.sortBy ?? undefined,
|
||||
sortMissingFallback: sortSettings.sortBy ? sortSettings.sortMissingFallback : undefined,
|
||||
}),
|
||||
// Ticket 058: params can get auto-declared from freshly ingested lines mid-run, not just from a user's own edit.
|
||||
fetchParamDeclarations(),
|
||||
])
|
||||
if (!cancelled) {
|
||||
setBuckets(nextBuckets)
|
||||
setLogs(nextLogs)
|
||||
updateParamsIfChanged(setParams, nextParams)
|
||||
setFilterError(null)
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -331,12 +372,17 @@ function StandaloneLog() {
|
||||
clearInterval(id)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filter, activePreset])
|
||||
}, [filter, activePreset, sortSettings])
|
||||
|
||||
return (
|
||||
<StandalonePage>
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<FilterBar value={filter} onChange={setFilter} error={filterError} />
|
||||
<FilterBar
|
||||
value={filter}
|
||||
onChange={setFilter}
|
||||
error={filterError}
|
||||
activeSummary={describeActiveState(activePreset, sortSettings)}
|
||||
/>
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<LogTimeline
|
||||
buckets={buckets}
|
||||
@@ -344,6 +390,9 @@ function StandaloneLog() {
|
||||
onSelectRange={handleManualRangeSelect}
|
||||
activePreset={activePreset}
|
||||
onSelectPreset={selectPreset}
|
||||
paramNames={params.map((p) => p.name)}
|
||||
sortSettings={sortSettings}
|
||||
onSortSettingsChange={handleSortSettingsChange}
|
||||
/>
|
||||
<LogPreview logs={logs} params={params} />
|
||||
</div>
|
||||
@@ -373,6 +422,12 @@ function FullApp() {
|
||||
const [filterError, setFilterError] = useState<string | null>(null)
|
||||
const [logViewerCollapsed, setLogViewerCollapsed] = useSectionCollapsed('log-viewer')
|
||||
const [logViewerHeight, setLogViewerHeight] = useState(() => loadLogViewerHeight())
|
||||
const [sortSettings, setSortSettings] = useState<LogSortSettings>(loadLogSortSettings)
|
||||
|
||||
function handleSortSettingsChange(next: LogSortSettings) {
|
||||
setSortSettings(next)
|
||||
saveLogSortSettings(next)
|
||||
}
|
||||
|
||||
function handleLogViewerResize(deltaY: number) {
|
||||
setLogViewerHeight((current) => clampLogViewerHeight(current + deltaY))
|
||||
@@ -433,7 +488,7 @@ function FullApp() {
|
||||
)
|
||||
}
|
||||
try {
|
||||
const [nextStats, nextBuckets, nextLogs] = await Promise.all([
|
||||
const [nextStats, nextBuckets, nextLogs, nextParams] = await Promise.all([
|
||||
fetchStats(),
|
||||
fetchTimeline({
|
||||
startMs: effectiveRange?.startMs,
|
||||
@@ -442,12 +497,22 @@ function FullApp() {
|
||||
bucketMs: effectiveRange ? computeBucketMs(effectiveRange.endMs - effectiveRange.startMs) : undefined,
|
||||
filter: filter || undefined,
|
||||
}),
|
||||
fetchLogs({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, limit: 1000, filter: filter || undefined }),
|
||||
fetchLogs({
|
||||
startMs: effectiveRange?.startMs,
|
||||
endMs: effectiveRange?.endMs,
|
||||
limit: 1000,
|
||||
filter: filter || undefined,
|
||||
sortBy: sortSettings.sortBy ?? undefined,
|
||||
sortMissingFallback: sortSettings.sortBy ? sortSettings.sortMissingFallback : undefined,
|
||||
}),
|
||||
// Ticket 058: params can get auto-declared from freshly ingested lines mid-run, not just from a user's own edit.
|
||||
fetchParamDeclarations(),
|
||||
])
|
||||
if (!cancelled) {
|
||||
setStats(nextStats)
|
||||
setBuckets(nextBuckets)
|
||||
setLogs(nextLogs)
|
||||
updateParamsIfChanged(setParams, nextParams)
|
||||
setFilterError(null)
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -465,7 +530,7 @@ function FullApp() {
|
||||
}
|
||||
// Intentionally excludes `range` to avoid restarting this interval on every rolling-preset tick.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filter, activePreset])
|
||||
}, [filter, activePreset, sortSettings])
|
||||
|
||||
return (
|
||||
<DocsNavProvider>
|
||||
@@ -476,6 +541,7 @@ function FullApp() {
|
||||
value={filter}
|
||||
onChange={setFilter}
|
||||
error={filterError}
|
||||
activeSummary={describeActiveState(activePreset, sortSettings)}
|
||||
collapseToggle={
|
||||
<>
|
||||
<CollapseToggle collapsed={logViewerCollapsed} onToggle={() => setLogViewerCollapsed(!logViewerCollapsed)} />
|
||||
@@ -497,6 +563,9 @@ function FullApp() {
|
||||
onSelectRange={handleManualRangeSelect}
|
||||
activePreset={activePreset}
|
||||
onSelectPreset={selectPreset}
|
||||
paramNames={params.map((p) => p.name)}
|
||||
sortSettings={sortSettings}
|
||||
onSortSettingsChange={handleSortSettingsChange}
|
||||
/>
|
||||
<LogPreview logs={logs} params={params} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
stroke-width: 9px;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2 {
|
||||
stroke: #000;
|
||||
stroke-miterlimit: 10;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: none;
|
||||
stroke-width: 8px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="cls-1" x="10" y="10" width="80" height="80" rx="18.79" ry="18.79"/>
|
||||
<polyline class="cls-2" points="25,33 42,50 25,67"/>
|
||||
<line class="cls-2" x1="46" y1="66" x2="75" y2="37"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 669 B |
@@ -96,7 +96,7 @@ function PanelPreview({ panel, range }: { panel: Panel; range: TimeRange | null
|
||||
if (loading && series.length === 0) {
|
||||
return <div className="flex h-48 items-center justify-center text-xs text-text-faint">Loading…</div>
|
||||
}
|
||||
return <PanelChart series={series} chartType={panel.chart_type} />
|
||||
return <PanelChart series={series} chartType={panel.chart_type} range={range} />
|
||||
}
|
||||
|
||||
function AutosaveStatus({ status, error }: { status: SaveStatus; error: string | null }) {
|
||||
@@ -153,6 +153,9 @@ export function Dashboards({ range, activePreset }: { range: TimeRange | null; a
|
||||
const [saveError, setSaveError] = useState<string | null>(null)
|
||||
// Last-persisted shape for the current draft, so the autosave effect can skip when nothing actually changed.
|
||||
const lastSavedRef = useRef<string | null>(null)
|
||||
// Ticket 064: latest `draft`, read inside the autosave timeout's async callback so a save response can tell
|
||||
// whether the user kept typing after the request went out, instead of unconditionally overwriting `draft`.
|
||||
const draftRef = useRef<Dashboard | null>(draft)
|
||||
const [collapsed, setCollapsed] = useSectionCollapsed('dashboards')
|
||||
|
||||
async function loadDashboards(profileName: string, selectAfterLoad?: string) {
|
||||
@@ -181,11 +184,21 @@ export function Dashboards({ range, activePreset }: { range: TimeRange | null; a
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
draftRef.current = draft
|
||||
}, [draft])
|
||||
|
||||
// Deliberately keyed only on `selectedId`, not `dashboards` — this effect's job is to load a *newly selected*
|
||||
// dashboard's content into `draft`. Keying it on `dashboards` too meant every autosave's own `setDashboards`
|
||||
// call (below) re-ran this effect and clobbered whatever the user had typed since the save was kicked off
|
||||
// (ticket 064). Reading `dashboards` from the closure here still sees the just-loaded array, since
|
||||
// `setSelectedId`/`setDashboards` from `loadDashboards` land in the same render.
|
||||
useEffect(() => {
|
||||
const found = dashboards.find((d) => d.id === selectedId) ?? null
|
||||
setDraft(found)
|
||||
lastSavedRef.current = found ? JSON.stringify(dashboardSaveShape(found)) : null
|
||||
}, [selectedId, dashboards])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedId])
|
||||
|
||||
// Skips entirely once the draft matches what's already persisted — covers both re-selecting an unchanged dashboard and this effect's own post-save re-render.
|
||||
useEffect(() => {
|
||||
@@ -198,9 +211,14 @@ export function Dashboards({ range, activePreset }: { range: TimeRange | null; a
|
||||
setSaveStatus('saving')
|
||||
try {
|
||||
const saved = await saveDashboard(activeProfile, dashboardId, dashboardSaveShape(draft))
|
||||
lastSavedRef.current = JSON.stringify(dashboardSaveShape(saved))
|
||||
lastSavedRef.current = snapshot
|
||||
setDashboards((prev) => prev.map((d) => (d.id === saved.id ? saved : d)))
|
||||
// Only reconcile `saved` into `draft` if nothing newer was typed while the request was in flight —
|
||||
// otherwise this stale response would overwrite those in-progress keystrokes (ticket 064).
|
||||
const current = draftRef.current
|
||||
if (current && current.id === dashboardId && JSON.stringify(dashboardSaveShape(current)) === snapshot) {
|
||||
setDraft(saved)
|
||||
}
|
||||
setSaveStatus('saved')
|
||||
setSaveError(null)
|
||||
} catch (err) {
|
||||
|
||||
@@ -50,9 +50,10 @@ const SECTIONS: DocSection[] = [
|
||||
</ul>
|
||||
<p className="font-semibold text-text">Settings</p>
|
||||
<p>
|
||||
One <em>settings</em> file per machine holds the colour scheme, imported/linked CSS, the ntfy sink, and
|
||||
which profile is active. Settings only tracks profile pointers — the profiles themselves (params, metrics,
|
||||
dashboards) are stored separately, next to the settings file by default.
|
||||
One <em>settings</em> file per machine holds the active style (a built-in colour scheme or an
|
||||
imported/linked stylesheet — see below), the ntfy sink, and which profile is active. Settings only tracks
|
||||
profile pointers — the profiles themselves (params, metrics, dashboards) are stored separately, next to the
|
||||
settings file by default.
|
||||
</p>
|
||||
<p className="font-semibold text-text">Profiles</p>
|
||||
<p>
|
||||
@@ -87,19 +88,28 @@ const SECTIONS: DocSection[] = [
|
||||
<Code>{'regex("error")'}</Code> — lines containing "error".
|
||||
</li>
|
||||
<li>
|
||||
<Code>{'regex("error") and sequence<5'}</Code> — lines containing "error" with sequence under 5.
|
||||
<Code>{'regex("error") and ottyl_seq<5'}</Code> — lines containing "error" with sequence under 5.
|
||||
</li>
|
||||
<li>
|
||||
<Code>{'requests[duration]>100'}</Code> — the <Code>requests</Code> metric's captured{' '}
|
||||
<Code>duration</Code> param over 100.
|
||||
</li>
|
||||
<li>
|
||||
<Code>{'timestamp>-5m'}</Code> — lines from the last 5 minutes.
|
||||
<Code>{'ottyl_ts>-5m'}</Code> — lines from the last 5 minutes.
|
||||
</li>
|
||||
<li>
|
||||
<Code>{'ottyl_fmt==json'}</Code> — lines auto-detected as JSON.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<Code>sequence</Code> and <Code>timestamp</Code> are reserved metric names present on every log line, so
|
||||
they're always filterable without declaring anything.
|
||||
<Code>ottyl_seq</Code>, <Code>ottyl_ts</Code>, and <Code>ottyl_fmt</Code> are reserved metric names present
|
||||
on every log line, so they're always filterable without declaring anything. <Code>ottyl_fmt</Code> holds the
|
||||
auto-detected format of the line's text — <Code>log_stream</Code> (macOS <Code>log stream</Code> output),{' '}
|
||||
<Code>json</Code>, <Code>logfmt</Code>, or <Code>none</Code>. A <Code>log_stream</Code> line also gets{' '}
|
||||
<Code>log_type</Code>, <Code>process</Code>, and <Code>pid</Code> params derived automatically from its
|
||||
fixed-column header. All three reserved names also appear (flagged <Code>reserved</Code>) in{' '}
|
||||
<Code>GET /api/params</Code> for discoverability, though there's no UI to edit or remove them — see also{' '}
|
||||
<em>Dashboards</em> below for using <Code>ottyl_seq</Code>/<Code>ottyl_ts</Code> in a panel query.
|
||||
</p>
|
||||
<p className="font-semibold text-text">Date/time formats</p>
|
||||
<p>Timestamp comparisons accept three forms:</p>
|
||||
@@ -121,7 +131,7 @@ const SECTIONS: DocSection[] = [
|
||||
<ul className="list-disc pl-5">
|
||||
<li>
|
||||
Only lines mentioning "timeout" in the last 15 minutes:{' '}
|
||||
<Code>{'regex("timeout") and timestamp>-15m'}</Code>
|
||||
<Code>{'regex("timeout") and ottyl_ts>-15m'}</Code>
|
||||
</li>
|
||||
<li>
|
||||
Requests slower than 200ms, once a <Code>requests</Code> metric has a <Code>value</Code>-role{' '}
|
||||
@@ -134,12 +144,71 @@ const SECTIONS: DocSection[] = [
|
||||
<Code>{'requests[path]==/checkout and regex("5[0-9]{2}")'}</Code>
|
||||
</li>
|
||||
<li>
|
||||
Everything before noon on a specific date: <Code>{'timestamp<2024-06-15T12:00:00Z'}</Code>
|
||||
Everything before noon on a specific date: <Code>{'ottyl_ts<2024-06-15T12:00:00Z'}</Code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'auto-formats',
|
||||
title: 'Auto-detected formats',
|
||||
body: (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>
|
||||
Every ingested line is classified into one of four formats, exposed as the reserved{' '}
|
||||
<Code>ottyl_fmt</Code> param (see <em>Filtering</em> above). The format also decides what — if anything —
|
||||
gets auto-declared as a param without you doing anything:
|
||||
</p>
|
||||
<ul className="list-disc pl-5">
|
||||
<li>
|
||||
<span className="font-semibold text-text">
|
||||
<Code>log_stream</Code>
|
||||
</span>{' '}
|
||||
— macOS <Code>{'log stream'}</Code> output's fixed-column shape (timestamp, thread, type, activity id,
|
||||
pid, ttl, then <Code>process: message</Code>). The first such line seen auto-declares{' '}
|
||||
<Code>log_type</Code>, <Code>process</Code>, and <Code>pid</Code> as params, parsed straight out of those
|
||||
columns.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-semibold text-text">
|
||||
<Code>json</Code>
|
||||
</span>{' '}
|
||||
— the line parses as a JSON object (a bare array or scalar doesn't count). Every leaf scalar field
|
||||
(string/number/bool) auto-declares a param named after its dot-separated path, extracted by JSON field
|
||||
path rather than regex — a nested field like <Code>{'{"request":{"path":"/x"}}'}</Code> becomes a param
|
||||
named <Code>request.path</Code>. Nested objects are walked into automatically; arrays and <Code>null</Code>{' '}
|
||||
fields are skipped since there's no single scalar value to declare a param against.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-semibold text-text">
|
||||
<Code>logfmt</Code>
|
||||
</span>{' '}
|
||||
— the line contains at least one <Code>key=value</Code>/<Code>{'key="value"'}</Code> pair anywhere in its
|
||||
text (it doesn't need to be logfmt end-to-end — a free-text prefix or suffix around the pairs is fine).
|
||||
Every distinct key auto-declares a param of the same name, type-inferred as <Code>number</Code> if its
|
||||
first-seen value parses as one, else <Code>string</Code>.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-semibold text-text">
|
||||
<Code>none</Code>
|
||||
</span>{' '}
|
||||
— none of the above; plain prose with no structure ottyl can key off of. Nothing is auto-declared, though
|
||||
the line is still fully searchable via <Code>regex(...)</Code> in the filter bar.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Auto-declared params are flagged <Code>auto_derived</Code> in <Code>GET /api/params</Code> and show up as
|
||||
log-table columns immediately, same as a manually declared param — but they can't be edited or removed
|
||||
individually, and they're never persisted to a profile (so they're rediscovered fresh each run rather than
|
||||
traveling with a profile like manual declarations do). A manual param declaration for a name always takes
|
||||
precedence and is never overwritten by auto-detection. To keep one unusual line from flooding the params
|
||||
list, auto-detection is capped at 64 auto-derived params total and 8 new ones per line — once either cap is
|
||||
hit, further would-be auto-declarations from new keys/fields are simply skipped.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'parameters',
|
||||
title: 'Parameters',
|
||||
@@ -330,6 +399,14 @@ const SECTIONS: DocSection[] = [
|
||||
Dashboards are saved per profile, so they travel with it when you switch or copy it — an active profile is
|
||||
required to create or save one; pick or create one from the settings menu first.
|
||||
</p>
|
||||
<p>
|
||||
The reserved <Code>ottyl_seq</Code>/<Code>ottyl_ts</Code> names (see <em>Filtering</em> above) work in panel
|
||||
queries the same as any declared metric — e.g. <Code>count(ottyl_seq)</Code> for log volume, or{' '}
|
||||
<Code>rate(ottyl_ts[1m])</Code> for ingest rate — since every log line carries them without a metric
|
||||
declaration. <Code>ottyl_fmt</Code> is categorical (<Code>json</Code>/<Code>logfmt</Code>/<Code>none</Code>
|
||||
), not a number, so it's rejected in a panel query with an explicit error rather than silently charting
|
||||
nothing — filter on it in the filter bar instead.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -367,8 +444,9 @@ const SECTIONS: DocSection[] = [
|
||||
</ul>
|
||||
<p>
|
||||
Query-based conditions (Immediate/Threshold/Ratio) reuse the filter bar's syntax, restricted to{' '}
|
||||
<Code>regex(...)</Code>, <Code>sequence</Code>, <Code>timestamp</Code>, and <Code>and</Code>/<Code>or</Code>
|
||||
. A <Code>metric_name[param]</Code> comparison isn't supported in an alert query.
|
||||
<Code>regex(...)</Code>, <Code>ottyl_seq</Code>, <Code>ottyl_ts</Code>, <Code>ottyl_fmt</Code>, and{' '}
|
||||
<Code>and</Code>/<Code>or</Code>. A <Code>metric_name[param]</Code> comparison isn't supported in an alert
|
||||
query.
|
||||
</p>
|
||||
<p>
|
||||
A window accepts a plain number of milliseconds or a humantime-style duration: <Code>30s</Code>,{' '}
|
||||
@@ -419,9 +497,10 @@ const SECTIONS: DocSection[] = [
|
||||
body: (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>
|
||||
The Settings panel controls process-wide state: colour scheme, the ntfy sink alert rules deliver to, the
|
||||
max number of logs kept in memory (blank means unlimited), and imported/linked CSS. Changes apply
|
||||
immediately and persist to the settings file on disk. There is no save button.
|
||||
The Settings panel controls process-wide state: the active style — one unified list of built-in colour
|
||||
schemes and any imported/linked stylesheets, any of which can be a simple palette or a full layout
|
||||
redesign — the ntfy sink alert rules deliver to, and the max number of logs kept in memory (blank means
|
||||
unlimited). Changes apply immediately and persist to the settings file on disk. There is no save button.
|
||||
</p>
|
||||
<p className="font-semibold text-text">Worked example</p>
|
||||
<p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { HelpIcon } from './HelpIcon'
|
||||
import { RegexHelpIcon } from './RegexHelpIcon'
|
||||
|
||||
@@ -13,14 +13,22 @@ export function FilterBar({
|
||||
onChange,
|
||||
error,
|
||||
collapseToggle,
|
||||
activeSummary,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
error?: string | null
|
||||
collapseToggle?: ReactNode
|
||||
/** Ticket 066: a read-only textual summary of the active time-range preset / sort order, shown below the input when the user hasn't typed a manual filter — the filter text itself stays the single source of truth for what's actually sent to the server. */
|
||||
activeSummary?: string | null
|
||||
}) {
|
||||
const [draft, setDraft] = useState(value)
|
||||
|
||||
// Resync when `value` changes externally (e.g. a programmatic clear) rather than only reading it once on mount.
|
||||
useEffect(() => {
|
||||
setDraft(value)
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-col gap-1 border-b border-border px-3 py-2"
|
||||
@@ -30,15 +38,21 @@ export function FilterBar({
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpIcon sectionId="filtering" label="Help: Filtering" />
|
||||
<RegexHelpIcon label="Help: regex(...) syntax" />
|
||||
<span className="flex items-center gap-1">
|
||||
<HelpIcon sectionId="filtering" label="Help: filter syntax" />
|
||||
<span className="text-xs text-text-subtle">Filter syntax</span>
|
||||
</span>
|
||||
{collapseToggle}
|
||||
<input
|
||||
className={queryInputClassName}
|
||||
placeholder='regex("error") and sequence<5 or timestamp>12:00'
|
||||
placeholder='ottyl_ts>-15m and requests[duration]>200 or regex("error")'
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
/>
|
||||
<span className="flex items-center gap-1" title="Cheat sheet for regex(...) clauses specifically">
|
||||
<RegexHelpIcon label="Help: regex(...) syntax" />
|
||||
<span className="text-xs text-text-faint">regex(...)</span>
|
||||
</span>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded border border-border-soft px-3 py-1 text-sm text-text-secondary hover:bg-surface-2"
|
||||
@@ -47,6 +61,7 @@ export function FilterBar({
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="text-xs text-danger-text">{error}</p>}
|
||||
{!error && !draft && activeSummary && <p className="text-xs text-text-subtle">Active: {activeSummary}</p>}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -209,7 +209,14 @@ function useScrollAnchor(containerRef: React.RefObject<HTMLDivElement | null>, d
|
||||
}, deps)
|
||||
}
|
||||
|
||||
export function LogPreview({ logs, params }: { logs: LogEntry[]; params: ParamDeclaration[] }) {
|
||||
export function LogPreview({
|
||||
logs,
|
||||
params,
|
||||
}: {
|
||||
logs: LogEntry[]
|
||||
/** Ticket 045/053: already sorted by the caller's `fetchLogs` call per its own sort setting (owned and rendered up in `LogTimeline`'s header now) — this component just renders whatever order it's given. */
|
||||
params: ParamDeclaration[]
|
||||
}) {
|
||||
const paramNames = params.map((d) => d.name)
|
||||
const [visibleParamColumns, setVisibleParamColumns] = useState<string[]>(loadVisibleParamColumns)
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
@@ -237,8 +244,10 @@ export function LogPreview({ logs, params }: { logs: LogEntry[]; params: ParamDe
|
||||
Log preview
|
||||
<HelpIcon sectionId="log-preview" label="Help: Log preview" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<ColumnPicker paramNames={paramNames} visible={visible} onToggle={toggleColumn} />
|
||||
</div>
|
||||
</div>
|
||||
<div ref={scrollContainerRef} className="flex-1 overflow-auto">
|
||||
<table className="w-full border-collapse text-left text-xs">
|
||||
<thead className="sticky top-0 bg-surface-0 text-text-subtle">
|
||||
|
||||
@@ -41,18 +41,116 @@ function formatTimestamp(ms: number): string {
|
||||
return new Date(ms).toLocaleTimeString()
|
||||
}
|
||||
|
||||
// Ticket 053: moved here from LogPreview.tsx, along with the `SortPicker` control below — the
|
||||
// setting now renders in the Timeline's header, but it still drives `fetchLogs` up in App.tsx the
|
||||
// same way it always did. Renamed from the old `ottyl.logPreview.sort` key since the control no
|
||||
// longer lives in "log preview"; `loadLogSortSettings` still falls back to the old key so an
|
||||
// existing choice survives this move instead of silently resetting on first load.
|
||||
const LOG_SORT_STORAGE_KEY = 'ottyl.logTimeline.sort'
|
||||
const LEGACY_LOG_SORT_STORAGE_KEY = 'ottyl.logPreview.sort'
|
||||
|
||||
export interface LogSortSettings {
|
||||
/** An already-declared param name to order log lines by, or `null` for the default (`ottyl_ts`-tracking) order. */
|
||||
sortBy: string | null
|
||||
/** Only meaningful when `sortBy` is set. `true` (default) orders lines missing that param by `ottyl_seq`, appended after the sorted lines; `false` drops them from view. */
|
||||
sortMissingFallback: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_LOG_SORT_SETTINGS: LogSortSettings = { sortBy: null, sortMissingFallback: true }
|
||||
|
||||
function parseLogSortSettings(raw: string | null): LogSortSettings | null {
|
||||
if (!raw) return null
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!parsed || typeof parsed !== 'object') return null
|
||||
const sortBy = typeof parsed.sortBy === 'string' ? parsed.sortBy : null
|
||||
const sortMissingFallback = typeof parsed.sortMissingFallback === 'boolean' ? parsed.sortMissingFallback : true
|
||||
return { sortBy, sortMissingFallback }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function loadLogSortSettings(): LogSortSettings {
|
||||
try {
|
||||
return (
|
||||
parseLogSortSettings(localStorage.getItem(LOG_SORT_STORAGE_KEY)) ??
|
||||
parseLogSortSettings(localStorage.getItem(LEGACY_LOG_SORT_STORAGE_KEY)) ??
|
||||
DEFAULT_LOG_SORT_SETTINGS
|
||||
)
|
||||
} catch {
|
||||
return DEFAULT_LOG_SORT_SETTINGS
|
||||
}
|
||||
}
|
||||
|
||||
export function saveLogSortSettings(settings: LogSortSettings) {
|
||||
try {
|
||||
localStorage.setItem(LOG_SORT_STORAGE_KEY, JSON.stringify(settings))
|
||||
} catch {
|
||||
// best-effort — private browsing or full storage just means the selection won't persist across reloads
|
||||
}
|
||||
}
|
||||
|
||||
function SortPicker({
|
||||
paramNames,
|
||||
settings,
|
||||
onChange,
|
||||
}: {
|
||||
paramNames: string[]
|
||||
settings: LogSortSettings
|
||||
onChange: (next: LogSortSettings) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs text-text-secondary">
|
||||
<label className="flex items-center gap-1">
|
||||
Sort by
|
||||
<select
|
||||
value={settings.sortBy ?? ''}
|
||||
onChange={(e) => onChange({ ...settings, sortBy: e.target.value || null })}
|
||||
className="rounded border border-border-soft bg-surface-0 px-1 py-0.5 text-text"
|
||||
>
|
||||
<option value="">ottyl_ts (default)</option>
|
||||
{paramNames.map((name) => (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{settings.sortBy && (
|
||||
<label className="flex cursor-pointer items-center gap-1" title="How lines missing the sort param are handled">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.sortMissingFallback}
|
||||
onChange={(e) => onChange({ ...settings, sortMissingFallback: e.target.checked })}
|
||||
className="accent-text-muted"
|
||||
/>
|
||||
Fall back to ottyl_seq
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function LogTimeline({
|
||||
buckets,
|
||||
range,
|
||||
onSelectRange,
|
||||
activePreset,
|
||||
onSelectPreset,
|
||||
paramNames,
|
||||
sortSettings,
|
||||
onSortSettingsChange,
|
||||
}: {
|
||||
buckets: TimelineBucket[]
|
||||
range: TimeRange | null
|
||||
onSelectRange: (range: TimeRange | null) => void
|
||||
activePreset: TimeRangePresetId | null
|
||||
onSelectPreset: (id: TimeRangePresetId) => void
|
||||
/** Ticket 045/053: current ordering choice, owned by the caller (it also feeds `fetchLogs`). Omitted by callers with no log list to sort (e.g. the standalone metrics/dashboards popout). */
|
||||
paramNames?: string[]
|
||||
sortSettings?: LogSortSettings
|
||||
onSortSettingsChange?: (next: LogSortSettings) => void
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [dragStartX, setDragStartX] = useState<number | null>(null)
|
||||
@@ -122,7 +220,11 @@ export function LogTimeline({
|
||||
Timeline
|
||||
<HelpIcon sectionId="timeline" label="Help: Timeline" />
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 text-xs text-text-subtle">
|
||||
<div className="flex items-center gap-3 text-xs text-text-subtle">
|
||||
{sortSettings && onSortSettingsChange && (
|
||||
<SortPicker paramNames={paramNames ?? []} settings={sortSettings} onChange={onSortSettingsChange} />
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1">
|
||||
{TIME_RANGE_PRESETS.map((preset) => (
|
||||
<button
|
||||
@@ -147,6 +249,7 @@ export function LogTimeline({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<div
|
||||
ref={containerRef}
|
||||
|
||||
@@ -3,13 +3,15 @@ import type { TimeRange, TimeRangePresetId } from './LogTimeline'
|
||||
import {
|
||||
ApiError,
|
||||
declareMetric,
|
||||
deleteMetric,
|
||||
fetchMetricBucketValues,
|
||||
fetchMetricDeclarations,
|
||||
fetchParamDeclarations,
|
||||
metricPrimitiveToDisplayString,
|
||||
updateMetric,
|
||||
type BucketValue,
|
||||
type MetricDeclaration,
|
||||
type MetricParamMatcher,
|
||||
type ParamDeclaration,
|
||||
} from '../lib/api'
|
||||
import {
|
||||
CollapseToggle,
|
||||
@@ -22,8 +24,6 @@ import {
|
||||
import { HelpIcon } from './HelpIcon'
|
||||
import { RegexHelpIcon } from './RegexHelpIcon'
|
||||
|
||||
const EMPTY_MATCHER: MetricParamMatcher = { param_name: '', capture_group: '', kind: 'string' }
|
||||
|
||||
// One row in the labels -> occurrence count table; `values` keeps the raw points for the sparkline rendering.
|
||||
interface LabelGroup {
|
||||
key: string
|
||||
@@ -80,12 +80,16 @@ function sparkline(values: BucketValue[], range: TimeRange | null): string {
|
||||
|
||||
export function Metrics({ range, activePreset }: { range: TimeRange | null; activePreset?: TimeRangePresetId | null }) {
|
||||
const [metrics, setMetrics] = useState<MetricDeclaration[]>([])
|
||||
const [params, setParams] = useState<ParamDeclaration[]>([])
|
||||
const [selected, setSelected] = useState<string | null>(null)
|
||||
const [bucketValues, setBucketValues] = useState<BucketValue[]>([])
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [regex, setRegex] = useState('')
|
||||
const [matchers, setMatchers] = useState<MetricParamMatcher[]>([{ ...EMPTY_MATCHER }])
|
||||
// A metric picks its label/value params from the params already declared in the Params panel
|
||||
// (ticket 037) rather than re-entering a regex/capture-group/kind per param.
|
||||
const [labelParams, setLabelParams] = useState<string[]>([])
|
||||
const [valueParam, setValueParam] = useState<string>('')
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [collapsed, setCollapsed] = useSectionCollapsed('metrics')
|
||||
@@ -101,8 +105,17 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
}
|
||||
}
|
||||
|
||||
async function loadParams() {
|
||||
try {
|
||||
setParams(await fetchParamDeclarations())
|
||||
} catch {
|
||||
// engine not reachable yet; leave the previous list in place
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadMetrics()
|
||||
loadParams()
|
||||
}, [])
|
||||
|
||||
// Refetch-on-change rather than continuous polling: bucket values only move when the selected metric or range changes, so no interval is needed here like `App.tsx`'s log/timeline polling.
|
||||
@@ -124,23 +137,18 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
}
|
||||
}, [selected, range])
|
||||
|
||||
function updateMatcher(index: number, patch: Partial<MetricParamMatcher>) {
|
||||
setMatchers((prev) => prev.map((m, i) => (i === index ? { ...m, ...patch } : m)))
|
||||
}
|
||||
|
||||
function addMatcher() {
|
||||
setMatchers((prev) => [...prev, { ...EMPTY_MATCHER }])
|
||||
}
|
||||
|
||||
function removeMatcher(index: number) {
|
||||
setMatchers((prev) => prev.filter((_, i) => i !== index))
|
||||
function toggleLabelParam(paramName: string) {
|
||||
setLabelParams((prev) =>
|
||||
prev.includes(paramName) ? prev.filter((name) => name !== paramName) : [...prev, paramName],
|
||||
)
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setEditingName(null)
|
||||
setName('')
|
||||
setRegex('')
|
||||
setMatchers([{ ...EMPTY_MATCHER }])
|
||||
setLabelParams([])
|
||||
setValueParam('')
|
||||
setSubmitError(null)
|
||||
}
|
||||
|
||||
@@ -149,7 +157,8 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
setEditingName(metric.name)
|
||||
setName(metric.name)
|
||||
setRegex(metric.message_match_regex)
|
||||
setMatchers(metric.params.length > 0 ? metric.params.map((p) => ({ ...p })) : [{ ...EMPTY_MATCHER }])
|
||||
setLabelParams(metric.label_params)
|
||||
setValueParam(metric.value_param ?? '')
|
||||
setSubmitError(null)
|
||||
}
|
||||
|
||||
@@ -158,6 +167,19 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
editMetric(metric)
|
||||
}
|
||||
|
||||
// No confirm-before-delete dialog — matches the rest of this settings UI (e.g. CSS entry / profile
|
||||
// deletion in NavMenu.tsx), which also deletes directly on click rather than prompting first.
|
||||
async function handleDelete(metricName: string) {
|
||||
try {
|
||||
await deleteMetric(metricName)
|
||||
if (selected === metricName) setSelected(null)
|
||||
if (editingName === metricName) resetForm()
|
||||
await loadMetrics()
|
||||
} catch {
|
||||
// best-effort; the declared-metrics list simply keeps showing it if the delete failed
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setSubmitError(null)
|
||||
@@ -166,7 +188,8 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
const request: MetricDeclaration = {
|
||||
name,
|
||||
message_match_regex: regex,
|
||||
params: matchers.filter((m) => m.param_name && m.capture_group),
|
||||
label_params: labelParams,
|
||||
value_param: valueParam || null,
|
||||
}
|
||||
const saved = editingName ? await updateMetric(editingName, request) : await declareMetric(request)
|
||||
resetForm()
|
||||
@@ -227,61 +250,49 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 flex items-center justify-between text-xs text-text-subtle">
|
||||
<span>Params</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addMatcher}
|
||||
className="rounded border border-border-soft px-2 py-0.5 text-text-secondary hover:bg-surface-2"
|
||||
>
|
||||
+ Add
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{matchers.map((matcher, i) => (
|
||||
<div key={i} className="flex items-center gap-1">
|
||||
<label className="mb-1 block text-xs text-text-subtle">
|
||||
Label params
|
||||
{params.length === 0 && (
|
||||
<span className="ml-1 text-text-faint">(declare a param in the Params panel below first)</span>
|
||||
)}
|
||||
</label>
|
||||
<div className="max-h-32 space-y-0.5 overflow-auto rounded border border-border-soft bg-surface-1 p-1.5">
|
||||
{params.length === 0 ? (
|
||||
<span className="text-xs text-text-faint">No params declared yet.</span>
|
||||
) : (
|
||||
params.map((p) => (
|
||||
<label key={p.name} className="flex items-center gap-1.5 text-xs text-text-secondary">
|
||||
<input
|
||||
className="w-24 min-w-0 flex-1 rounded border border-border-soft bg-surface-1 px-1.5 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
value={matcher.param_name}
|
||||
onChange={(e) => updateMatcher(i, { param_name: e.target.value })}
|
||||
placeholder="param_name"
|
||||
type="checkbox"
|
||||
checked={labelParams.includes(p.name)}
|
||||
onChange={() => toggleLabelParam(p.name)}
|
||||
/>
|
||||
<input
|
||||
className="w-24 min-w-0 flex-1 rounded border border-border-soft bg-surface-1 px-1.5 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
value={matcher.capture_group}
|
||||
onChange={(e) => updateMatcher(i, { capture_group: e.target.value })}
|
||||
placeholder="capture_group"
|
||||
/>
|
||||
<select
|
||||
className="rounded border border-border-soft bg-surface-1 px-1 py-1 text-xs text-text focus:border-border-strong focus:outline-none"
|
||||
value={matcher.kind}
|
||||
onChange={(e) => updateMatcher(i, { kind: e.target.value as MetricParamMatcher['kind'] })}
|
||||
>
|
||||
<option value="string">string</option>
|
||||
<option value="number">number</option>
|
||||
<option value="timestamp">timestamp</option>
|
||||
<option value="duration">duration</option>
|
||||
</select>
|
||||
<select
|
||||
className="rounded border border-border-soft bg-surface-1 px-1 py-1 text-xs text-text focus:border-border-strong focus:outline-none"
|
||||
value={matcher.role ?? 'label'}
|
||||
onChange={(e) => updateMatcher(i, { role: e.target.value as MetricParamMatcher['role'] })}
|
||||
title="Label params identify a bucket; at most one Value param becomes the counter's per-event captured value (used by histogram()/gauge()). A Value param of kind 'duration' is derived automatically — elapsed time since the previous match with the same labels — rather than parsed from the capture group."
|
||||
>
|
||||
<option value="label">label</option>
|
||||
<option value="value">value</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeMatcher(i)}
|
||||
disabled={matchers.length === 1}
|
||||
className="rounded border border-border-soft px-1.5 py-1 text-xs text-text-muted hover:bg-surface-2 disabled:opacity-30"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<span className="font-mono">{p.name}</span>
|
||||
<span className="text-text-faint">({p.elapsed_since_last ? 'elapsed since previous' : p.kind})</span>
|
||||
</label>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
className="mb-1 block text-xs text-text-subtle"
|
||||
title="At most one param becomes the counter's per-event captured value (used by histogram()/gauge()) instead of a label. An 'elapsed since previous event' param (declared in the Params panel) is computed automatically — elapsed time since the previous match with the same labels — rather than parsed from its capture group."
|
||||
>
|
||||
Value param
|
||||
</label>
|
||||
<select
|
||||
className="w-full rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text focus:border-border-strong focus:outline-none"
|
||||
value={valueParam}
|
||||
onChange={(e) => setValueParam(e.target.value)}
|
||||
>
|
||||
<option value="">(none — occurrence count)</option>
|
||||
{params.map((p) => (
|
||||
<option key={p.name} value={p.name}>
|
||||
{p.name} ({p.elapsed_since_last ? 'elapsed since previous' : p.kind})
|
||||
</option>
|
||||
))}
|
||||
</div>
|
||||
</select>
|
||||
</div>
|
||||
{editingName && (
|
||||
<div className="rounded border border-accent bg-accent/5 px-2 py-1 text-xs text-accent-text">
|
||||
@@ -315,18 +326,27 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
<div className="mb-3 flex flex-wrap gap-1">
|
||||
{metrics.length === 0 && <span className="text-xs text-text-faint">No metrics declared yet.</span>}
|
||||
{metrics.map((m) => (
|
||||
<button
|
||||
<div
|
||||
key={m.name}
|
||||
onClick={() => selectMetric(m)}
|
||||
className={`rounded border px-2 py-1 text-xs ${
|
||||
className={`flex items-center gap-0.5 rounded border pl-2 pr-1 py-1 text-xs ${
|
||||
selected === m.name
|
||||
? 'border-accent bg-accent/10 text-accent-text'
|
||||
: 'border-border-soft text-text-secondary hover:bg-surface-2'
|
||||
}`}
|
||||
title={m.message_match_regex}
|
||||
>
|
||||
{m.name} ({m.params.map((p) => p.param_name).join(', ') || 'no params'})
|
||||
<button type="button" onClick={() => selectMetric(m)} title={m.message_match_regex}>
|
||||
{m.name} ({m.label_params.join(', ') || 'no params'})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDelete(m.name)}
|
||||
className="rounded px-1 text-text-faint hover:bg-danger-bg/50 hover:text-danger-text"
|
||||
aria-label={`Delete metric ${m.name}`}
|
||||
title={`Delete metric "${m.name}"`}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -342,17 +362,17 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
|
||||
<span className="font-mono">{selectedMetric.message_match_regex}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-subtle">Params: </span>
|
||||
{selectedMetric.params.length === 0 ? (
|
||||
<span className="text-text-subtle">Label params: </span>
|
||||
{selectedMetric.label_params.length === 0 ? (
|
||||
<span className="text-text-faint">none</span>
|
||||
) : (
|
||||
<span className="font-mono">
|
||||
{selectedMetric.params
|
||||
.map((p) => `${p.param_name}←${p.capture_group} (${p.kind}, ${p.role ?? 'label'})`)
|
||||
.join(', ')}
|
||||
</span>
|
||||
<span className="font-mono">{selectedMetric.label_params.join(', ')}</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-subtle">Value param: </span>
|
||||
<span className="font-mono">{selectedMetric.value_param ?? <span className="text-text-faint">none</span>}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{groups.length === 0 ? (
|
||||
|
||||
+135
-115
@@ -6,6 +6,7 @@ import { useDocsNav, type DocsTarget } from '../lib/docsNav'
|
||||
import { DocsPanel } from './Docs'
|
||||
import { HelpIcon } from './HelpIcon'
|
||||
import { BUILTIN_THEMES, downloadThemeFile, generateThemeCss, parseThemeCss } from '../lib/themes'
|
||||
import ottylIcon from '../assets/ottyl-icon.svg'
|
||||
|
||||
type PanelName = 'settings' | 'docs' | null
|
||||
|
||||
@@ -27,8 +28,19 @@ const COLOR_SCHEMES: ColorScheme[] = [
|
||||
'yellow',
|
||||
'pink',
|
||||
'orange',
|
||||
'light_green',
|
||||
'light_red',
|
||||
'light_blue',
|
||||
'light_yellow',
|
||||
'light_pink',
|
||||
'light_orange',
|
||||
]
|
||||
|
||||
/** Display label for a `ColorScheme`: underscores become spaces so `capitalize` (which titles each word) renders e.g. "light_green" as "Light Green". */
|
||||
function schemeLabel(scheme: ColorScheme): string {
|
||||
return scheme.replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
export function NavMenu({ stats }: { stats: Stats | null }) {
|
||||
const [openPanel, setOpenPanel] = useState<PanelName>(null)
|
||||
const panel = useSettingsPanel()
|
||||
@@ -56,7 +68,10 @@ export function NavMenu({ stats }: { stats: Stats | null }) {
|
||||
<div className="flex h-full">
|
||||
<div className="flex h-full w-48 shrink-0 flex-col justify-between border-r border-border p-3">
|
||||
<div>
|
||||
<div className="mb-4 text-lg font-semibold text-text">ottyl</div>
|
||||
<div className="mb-4 flex items-center gap-2 text-lg font-semibold text-text">
|
||||
<img src={ottylIcon} alt="" className="h-5 w-5" />
|
||||
ottyl
|
||||
</div>
|
||||
<PresetsDropdown panel={panel} />
|
||||
<nav className="mt-2 flex flex-col gap-1 text-sm text-text-muted">
|
||||
<button
|
||||
@@ -252,8 +267,10 @@ function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }
|
||||
setCssUploadError(null)
|
||||
}
|
||||
|
||||
// Every imported/linked stylesheet is always a selectable theme (ticket 020), so it's always
|
||||
// validated against the canonical `--c-*` variable set before it's sent.
|
||||
// Every imported/linked stylesheet is always a selectable style, listed alongside the built-in
|
||||
// colour schemes (ticket 020/047), so it's always validated against the canonical `--c-*`
|
||||
// variable set before it's sent — that's a floor, not a ceiling: the stylesheet is free to also
|
||||
// carry rules for any other selector, up to and including a full layout redesign.
|
||||
async function handleCssImport(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setCssUploadError(null)
|
||||
@@ -294,8 +311,14 @@ function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }
|
||||
return (
|
||||
<div className="flex flex-col gap-5 text-sm text-text-secondary">
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-text-subtle">Colour scheme</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="mb-2 flex items-center gap-1.5">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-text-subtle">Style</h3>
|
||||
<span className="text-[11px] text-text-faint">
|
||||
one list of interchangeable styles — a built-in palette or your own imported/linked stylesheet. Any of
|
||||
them can be a simple colour swap or a full layout redesign; only one is active at a time.
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-3 flex flex-wrap gap-1">
|
||||
{COLOR_SCHEMES.map((scheme) => (
|
||||
<div key={scheme} className="flex items-center">
|
||||
<button
|
||||
@@ -307,14 +330,14 @@ function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }
|
||||
: 'border-border-soft text-text-muted hover:bg-surface-2'
|
||||
}`}
|
||||
>
|
||||
{scheme}
|
||||
{schemeLabel(scheme)}
|
||||
</button>
|
||||
{scheme !== 'system' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => downloadBuiltinTheme(scheme)}
|
||||
className="rounded-r border border-l-0 border-border-soft px-1.5 py-1 text-xs text-text-faint hover:bg-surface-2 hover:text-text"
|
||||
aria-label={`Download ${scheme} theme`}
|
||||
aria-label={`Download ${schemeLabel(scheme)} theme`}
|
||||
title="Download as .css"
|
||||
>
|
||||
↓
|
||||
@@ -323,6 +346,111 @@ function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{panel.cssEntries.length === 0 ? (
|
||||
<div className="mb-2 text-xs text-text-faint">No styles imported or linked yet.</div>
|
||||
) : (
|
||||
<ul className="mb-2 flex flex-col gap-1">
|
||||
{panel.cssEntries.map((entry) => (
|
||||
<li
|
||||
key={entry.id}
|
||||
className="flex items-center justify-between rounded border border-border px-2 py-1 text-xs"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => panel.setCustomTheme(entry.id)}
|
||||
className={`block w-full truncate text-left capitalize ${
|
||||
panel.activeCustomTheme?.id === entry.id ? 'text-text' : 'text-text-muted hover:text-text'
|
||||
}`}
|
||||
>
|
||||
{entry.filename ?? entry.path_or_url}
|
||||
</button>
|
||||
<div className="text-[10px] uppercase text-text-faint">{entry.kind}</div>
|
||||
</div>
|
||||
<div className="ml-2 flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => downloadCssEntry(entry)}
|
||||
className="rounded px-1.5 text-text-subtle hover:bg-surface-2 hover:text-text"
|
||||
aria-label={`Download ${entry.filename ?? entry.path_or_url}`}
|
||||
title="Download as .css"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
{entry.kind === 'imported' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => panel.removeCssEntry(entry.id)}
|
||||
className="rounded px-1.5 text-text-subtle hover:bg-surface-2 hover:text-danger-text"
|
||||
aria-label={`Delete ${entry.filename}`}
|
||||
>
|
||||
delete
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-text-faintest" title="Linked CSS isn't deletable here">
|
||||
—
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<p className="mb-2 text-[11px] text-text-faint">
|
||||
Add your own style below — a stylesheet, uploaded or linked by URL/path. It must at least define every{' '}
|
||||
<code>--c-*</code> variable the built-ins do, but nothing stops it from also targeting any other selector
|
||||
in the app (layout, spacing, fonts, anything) — it isn't limited to palette overrides.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleCssImport} className="flex flex-col gap-2">
|
||||
<input
|
||||
type="file"
|
||||
accept=".css,text/css"
|
||||
onChange={handleCssFileChange}
|
||||
className="text-xs text-text-muted file:mr-2 file:rounded file:border file:border-border-soft file:bg-surface-2 file:px-2 file:py-1 file:text-text"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="enter name"
|
||||
value={cssFilename}
|
||||
onChange={(e) => setCssFilename(e.target.value)}
|
||||
className="rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Paste CSS content here"
|
||||
value={cssContent}
|
||||
onChange={(e) => setCssContent(e.target.value)}
|
||||
rows={4}
|
||||
className="resize-y rounded border border-border-soft bg-surface-1 px-2 py-1 font-mono text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
{cssUploadError && <div className="text-xs text-danger-text-strong">{cssUploadError}</div>}
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded border border-border-soft px-2 py-1 text-xs text-text hover:bg-surface-2"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form onSubmit={handleCssLink} className="mt-2 flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://example.com/style.css"
|
||||
value={cssLinkUrl}
|
||||
onChange={(e) => setCssLinkUrl(e.target.value)}
|
||||
className="min-w-0 flex-1 rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="shrink-0 rounded border border-border-soft px-2 py-1 text-xs text-text hover:bg-surface-2"
|
||||
>
|
||||
Link
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@@ -378,114 +506,6 @@ function SettingsPanel({ panel }: { panel: ReturnType<typeof useSettingsPanel> }
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="mb-2 flex items-center gap-1.5">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-text-subtle">CSS</h3>
|
||||
<span className="text-[11px] text-text-faint">
|
||||
imported/linked stylesheets — each one becomes a selectable alternative to the built-in colour schemes
|
||||
above
|
||||
</span>
|
||||
</div>
|
||||
{panel.cssEntries.length === 0 ? (
|
||||
<div className="mb-2 text-xs text-text-faint">No CSS configured.</div>
|
||||
) : (
|
||||
<ul className="mb-2 flex flex-col gap-1">
|
||||
{panel.cssEntries.map((entry) => (
|
||||
<li
|
||||
key={entry.id}
|
||||
className="flex items-center justify-between rounded border border-border px-2 py-1 text-xs"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => panel.setCustomTheme(entry.id)}
|
||||
className={`block w-full truncate text-left capitalize ${
|
||||
panel.activeCustomTheme?.id === entry.id ? 'text-text' : 'text-text-muted hover:text-text'
|
||||
}`}
|
||||
>
|
||||
{entry.filename ?? entry.path_or_url}
|
||||
</button>
|
||||
<div className="text-[10px] uppercase text-text-faint">{entry.kind}</div>
|
||||
</div>
|
||||
<div className="ml-2 flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => downloadCssEntry(entry)}
|
||||
className="rounded px-1.5 text-text-subtle hover:bg-surface-2 hover:text-text"
|
||||
aria-label={`Download ${entry.filename ?? entry.path_or_url}`}
|
||||
title="Download as .css"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
{entry.kind === 'imported' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => panel.removeCssEntry(entry.id)}
|
||||
className="rounded px-1.5 text-text-subtle hover:bg-surface-2 hover:text-danger-text"
|
||||
aria-label={`Delete ${entry.filename}`}
|
||||
>
|
||||
delete
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-text-faintest" title="Linked CSS isn't deletable here">
|
||||
—
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleCssImport} className="flex flex-col gap-2">
|
||||
<input
|
||||
type="file"
|
||||
accept=".css,text/css"
|
||||
onChange={handleCssFileChange}
|
||||
className="text-xs text-text-muted file:mr-2 file:rounded file:border file:border-border-soft file:bg-surface-2 file:px-2 file:py-1 file:text-text"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="enter name"
|
||||
value={cssFilename}
|
||||
onChange={(e) => setCssFilename(e.target.value)}
|
||||
className="rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Paste CSS content here"
|
||||
value={cssContent}
|
||||
onChange={(e) => setCssContent(e.target.value)}
|
||||
rows={4}
|
||||
className="resize-y rounded border border-border-soft bg-surface-1 px-2 py-1 font-mono text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
{cssUploadError && <div className="text-xs text-danger-text-strong">{cssUploadError}</div>}
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded border border-border-soft px-2 py-1 text-xs text-text hover:bg-surface-2"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form onSubmit={handleCssLink} className="mt-2 flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://example.com/style.css"
|
||||
value={cssLinkUrl}
|
||||
onChange={(e) => setCssLinkUrl(e.target.value)}
|
||||
className="min-w-0 flex-1 rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="shrink-0 rounded border border-border-soft px-2 py-1 text-xs text-text hover:bg-surface-2"
|
||||
>
|
||||
Link
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
YAxis,
|
||||
} from 'recharts'
|
||||
import type { ChartType, PromqlSeries } from '../lib/api'
|
||||
import type { TimeRange } from './LogTimeline'
|
||||
|
||||
/// Renders a label combination as a series key/legend string, e.g. `{"path":"/a"}` -> `path=/a`; no labels renders as `value`.
|
||||
export function seriesLabel(labels: Record<string, string>): string {
|
||||
@@ -22,8 +23,38 @@ export function seriesLabel(labels: Record<string, string>): string {
|
||||
return entries.map(([k, v]) => `${k}=${v}`).join(', ')
|
||||
}
|
||||
|
||||
// Fixed, small categorical palette to avoid a colour-scale dependency; cycles if there are more series than colours.
|
||||
const COLORS = ['#38bdf8', '#f472b6', '#a78bfa', '#4ade80', '#fbbf24', '#fb7185', '#2dd4bf', '#f97316']
|
||||
// Categorical palette drawn from the active theme's accent set (ticket 048: every built-in theme
|
||||
// exposes --c-accent/--c-accent-2/--c-accent-3, not just one accent hue) instead of a hardcoded,
|
||||
// theme-independent hex list. `color-mix` tints/shades those three base accents into a fuller
|
||||
// 8-colour cycle (matching the previous fixed palette's size, so MAX_KEPT_SERIES/squashing below is
|
||||
// unaffected) without any JS colour math or a getComputedStyle read — the browser resolves the CSS
|
||||
// variables per-theme at paint time, same as `stroke="var(--c-border)"` elsewhere in this file.
|
||||
const COLORS = [
|
||||
'var(--c-accent)',
|
||||
'var(--c-accent-2)',
|
||||
'var(--c-accent-3)',
|
||||
'color-mix(in srgb, var(--c-accent) 60%, white)',
|
||||
'color-mix(in srgb, var(--c-accent-2) 60%, white)',
|
||||
'color-mix(in srgb, var(--c-accent-3) 60%, white)',
|
||||
'color-mix(in srgb, var(--c-accent) 55%, black)',
|
||||
'color-mix(in srgb, var(--c-accent-2) 55%, black)',
|
||||
]
|
||||
|
||||
// One less than the palette size: above this many series, keep the top MAX_KEPT_SERIES (by value) and squash the
|
||||
// rest into a single "Other" bucket, so the squashed range never exceeds COLORS.length and never repeats a colour.
|
||||
const MAX_KEPT_SERIES = COLORS.length - 1
|
||||
|
||||
// Sorts rows by value descending and, if there are more than MAX_KEPT_SERIES, keeps the top MAX_KEPT_SERIES and
|
||||
// collapses the remainder into a single "Other" row summing their values. Below the threshold, rows are returned
|
||||
// unchanged so panels with few series keep their existing bar/slice order and colour assignment.
|
||||
function squashRows(rows: { name: string; value: number }[]): { name: string; value: number }[] {
|
||||
if (rows.length <= MAX_KEPT_SERIES) return rows
|
||||
const sorted = [...rows].sort((a, b) => b.value - a.value)
|
||||
const kept = sorted.slice(0, MAX_KEPT_SERIES)
|
||||
const rest = sorted.slice(MAX_KEPT_SERIES)
|
||||
const otherValue = rest.reduce((sum, r) => sum + r.value, 0)
|
||||
return [...kept, { name: `Other (${rest.length})`, value: otherValue }]
|
||||
}
|
||||
|
||||
// Merges series into one row-per-timestamp dataset (recharts wants a single array keyed by series name); a timestamp missing from a series is left undefined there, which recharts skips when drawing that line.
|
||||
function toLineData(series: PromqlSeries[]): { rows: Record<string, number | string>[]; keys: string[] } {
|
||||
@@ -61,19 +92,36 @@ function sumValue(series: PromqlSeries): number {
|
||||
return series.points.reduce((total, [, value]) => total + value, 0)
|
||||
}
|
||||
|
||||
export function PanelChart({ series, chartType }: { series: PromqlSeries[]; chartType: ChartType }) {
|
||||
export function PanelChart({
|
||||
series,
|
||||
chartType,
|
||||
range,
|
||||
}: {
|
||||
series: PromqlSeries[]
|
||||
chartType: ChartType
|
||||
range?: TimeRange | null
|
||||
}) {
|
||||
if (series.length === 0) {
|
||||
return <div className="flex h-48 items-center justify-center text-xs text-text-faint">No data in range.</div>
|
||||
}
|
||||
|
||||
if (chartType === 'line') {
|
||||
const { rows, keys } = toLineData(series)
|
||||
// Ticket 057: without an explicit numeric domain, recharts auto-scales the x-axis to the
|
||||
// min/max timestamp actually present in `rows` — so a metric whose occurrences only cover a
|
||||
// sliver of the queried range (a burst followed by silence) silently renders as if that
|
||||
// sliver *were* the full range, with no visual sign the axis got narrowed. Pinning the domain
|
||||
// to the panel's actual queried range keeps the axis honest; `type="number"` is required for
|
||||
// a `domain` tuple to be interpreted as a value range rather than a category filter.
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<LineChart data={rows} margin={{ top: 8, right: 16, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--c-border)" />
|
||||
<XAxis
|
||||
dataKey="timestamp_ms"
|
||||
type="number"
|
||||
domain={range ? [range.startMs, range.endMs] : ['dataMin', 'dataMax']}
|
||||
allowDataOverflow
|
||||
tickFormatter={formatTimestamp}
|
||||
stroke="var(--c-text-subtle)"
|
||||
tick={{ fontSize: 11, fill: 'var(--c-text-muted)' }}
|
||||
@@ -101,7 +149,7 @@ export function PanelChart({ series, chartType }: { series: PromqlSeries[]; char
|
||||
}
|
||||
|
||||
if (chartType === 'bar') {
|
||||
const rows = series.map((s) => ({ name: seriesLabel(s.labels), value: latestValue(s) }))
|
||||
const rows = squashRows(series.map((s) => ({ name: seriesLabel(s.labels), value: latestValue(s) })))
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<BarChart data={rows} margin={{ top: 8, right: 16, left: 0, bottom: 0 }}>
|
||||
@@ -119,7 +167,7 @@ export function PanelChart({ series, chartType }: { series: PromqlSeries[]; char
|
||||
)
|
||||
}
|
||||
|
||||
const rows = series.map((s) => ({ name: seriesLabel(s.labels), value: sumValue(s) }))
|
||||
const rows = squashRows(series.map((s) => ({ name: seriesLabel(s.labels), value: sumValue(s) })))
|
||||
const total = rows.reduce((sum, r) => sum + r.value, 0)
|
||||
if (total === 0) {
|
||||
return <div className="flex h-48 items-center justify-center text-xs text-text-faint">No non-zero values in range.</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { ApiError, declareParam, type ParamDeclaration } from '../lib/api'
|
||||
import { ApiError, clearParams, declareParam, type ParamDeclaration } from '../lib/api'
|
||||
import {
|
||||
CollapseToggle,
|
||||
handleSectionHeaderKeyDown,
|
||||
@@ -13,7 +13,11 @@ import { RegexHelpIcon } from './RegexHelpIcon'
|
||||
|
||||
const EMPTY_KIND: ParamDeclaration['kind'] = 'string'
|
||||
|
||||
type ExtractionSource = 'regex' | 'json_field'
|
||||
// 'elapsed_since_last' is always offered, regardless of what the message match regex looks like —
|
||||
// unlike 'json_field' (which only makes sense once the sample line looks like a JSON object), this
|
||||
// param's value is never extracted from text at all: it's elapsed time since the previous event
|
||||
// sharing the same label set (ticket 038).
|
||||
type ExtractionSource = 'regex' | 'json_field' | 'elapsed_since_last'
|
||||
|
||||
/// A JSON *object* (not an array/scalar) parses cleanly and pretty-prints with braces — the same
|
||||
/// shape a JSON field path can be read from, so this is the "line looks like JSON" gate that
|
||||
@@ -39,40 +43,116 @@ export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onD
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [collapsed, setCollapsed] = useSectionCollapsed('params')
|
||||
// Name of the declaration currently loaded into the form for editing, or `null` when the form is
|
||||
// declaring a brand-new param. The name field is locked while editing — `declareParam` overwrites
|
||||
// in place by name, so changing it here would silently create a second param and orphan the
|
||||
// original (and anything referencing it by name) rather than renaming it.
|
||||
const [editingName, setEditingName] = useState<string | null>(null)
|
||||
const [clearing, setClearing] = useState(false)
|
||||
const [clearResult, setClearResult] = useState<string | null>(null)
|
||||
const [clearError, setClearError] = useState<string | null>(null)
|
||||
|
||||
// `reserved` params (`ottyl_seq`/`ottyl_ts`/`ottyl_fmt`) are synthesized server-side purely for
|
||||
// discoverability (see `GET /api/params`) — they aren't real declarations, so they're kept out of
|
||||
// the editable/clickable list and shown separately, non-interactively.
|
||||
const declaredParams = useMemo(() => params.filter((p) => !p.reserved), [params])
|
||||
const reservedParams = useMemo(() => params.filter((p) => p.reserved), [params])
|
||||
|
||||
const isBoolean = kind === 'boolean'
|
||||
const isElapsedSinceLast = source === 'elapsed_since_last'
|
||||
// The message match regex doubles as a sample line here so a pasted JSON log line unlocks the
|
||||
// "JSON field" extraction option — regex-based params are unaffected either way.
|
||||
// "JSON field" extraction option — regex-based params are unaffected either way. Not consulted at
|
||||
// all for "elapsed since previous event", which is always offered.
|
||||
const detectedJson = useMemo(() => looksLikeJsonObject(regex), [regex])
|
||||
const effectiveSource: ExtractionSource = detectedJson && !isBoolean ? source : 'regex'
|
||||
const effectiveSource: ExtractionSource = isElapsedSinceLast
|
||||
? 'elapsed_since_last'
|
||||
: detectedJson && !isBoolean
|
||||
? source
|
||||
: 'regex'
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setSubmitError(null)
|
||||
setSubmitting(true)
|
||||
try {
|
||||
// A boolean-kind param never uses a capture group — its value is just whether the regex matched.
|
||||
await declareParam({
|
||||
name,
|
||||
message_match_regex: regex,
|
||||
capture_group: isBoolean || effectiveSource === 'json_field' ? '' : captureGroup,
|
||||
json_field_path: effectiveSource === 'json_field' ? jsonFieldPath : null,
|
||||
kind,
|
||||
})
|
||||
function handleSourceChange(next: ExtractionSource) {
|
||||
setSource(next)
|
||||
// "Elapsed since previous event" always produces a Duration value — never text-extracted — so
|
||||
// there's no separate kind to pick; force it rather than leaving a stale/mismatched kind selected.
|
||||
if (next === 'elapsed_since_last') setKind('duration')
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
setEditingName(null)
|
||||
setName('')
|
||||
setRegex('')
|
||||
setCaptureGroup('')
|
||||
setJsonFieldPath('')
|
||||
setSource('regex')
|
||||
setKind(EMPTY_KIND)
|
||||
setSubmitError(null)
|
||||
}
|
||||
|
||||
// Loads `param`'s current values into the declaration form for editing — a click handler for a
|
||||
// declared-param badge.
|
||||
function editParam(param: ParamDeclaration) {
|
||||
setEditingName(param.name)
|
||||
setName(param.name)
|
||||
setRegex(param.message_match_regex)
|
||||
setCaptureGroup(param.capture_group)
|
||||
setJsonFieldPath(param.json_field_path ?? '')
|
||||
setSource(param.elapsed_since_last ? 'elapsed_since_last' : param.json_field_path ? 'json_field' : 'regex')
|
||||
setKind(param.kind)
|
||||
setSubmitError(null)
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setSubmitError(null)
|
||||
setSubmitting(true)
|
||||
try {
|
||||
// A boolean-kind param never uses a capture group — its value is just whether the regex
|
||||
// matched. An elapsed-since-last param never uses a capture group, JSON field path, or even
|
||||
// its own message match regex — its value is computed, not extracted from text at all.
|
||||
await declareParam({
|
||||
name,
|
||||
message_match_regex: effectiveSource === 'elapsed_since_last' ? '' : regex,
|
||||
capture_group: isBoolean || effectiveSource !== 'regex' ? '' : captureGroup,
|
||||
json_field_path: effectiveSource === 'json_field' ? jsonFieldPath : null,
|
||||
kind: effectiveSource === 'elapsed_since_last' ? 'duration' : kind,
|
||||
elapsed_since_last: effectiveSource === 'elapsed_since_last',
|
||||
})
|
||||
resetForm()
|
||||
onDeclared()
|
||||
} catch (err) {
|
||||
setSubmitError(err instanceof ApiError ? err.message : 'Failed to declare param')
|
||||
setSubmitError(err instanceof ApiError ? err.message : `Failed to ${editingName ? 'update' : 'declare'} param`)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Clears every declared param (manual and auto-derived) in one action, then immediately
|
||||
// re-derives whatever the engine can still find in retained log text (ticket 052) — a single
|
||||
// confirmed click rather than removing params one at a time. `logs_considered` in the response
|
||||
// is surfaced in the status line so it's clear the re-extraction only covers logs the engine
|
||||
// still holds, not ones already evicted by a max-logs cap.
|
||||
async function handleClearAll() {
|
||||
if (params.length === 0) return
|
||||
if (!window.confirm(`Clear all ${params.length} declared param(s)? Params will immediately be re-derived from log lines still held in memory; lines already evicted by a log cap can't regain their params.`)) {
|
||||
return
|
||||
}
|
||||
setClearing(true)
|
||||
setClearError(null)
|
||||
setClearResult(null)
|
||||
try {
|
||||
const result = await clearParams()
|
||||
setClearResult(
|
||||
`Cleared ${result.removed}, re-derived ${result.reextracted} from ${result.logs_considered} retained log line(s).`,
|
||||
)
|
||||
resetForm()
|
||||
onDeclared()
|
||||
} catch (err) {
|
||||
setClearError(err instanceof ApiError ? err.message : 'Failed to clear params')
|
||||
} finally {
|
||||
setClearing(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b border-border text-sm">
|
||||
<div
|
||||
@@ -95,15 +175,19 @@ export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onD
|
||||
<div className="flex gap-4 p-3" hidden={collapsed}>
|
||||
<form onSubmit={handleSubmit} className="w-80 shrink-0 space-y-2">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-text-subtle">Name</label>
|
||||
<label className="mb-1 block text-xs text-text-subtle">
|
||||
Name{editingName && ' (locked while editing)'}
|
||||
</label>
|
||||
<input
|
||||
className="w-full rounded border border-border-soft bg-surface-1 px-2 py-1 text-sm text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none"
|
||||
className="w-full rounded border border-border-soft bg-surface-1 px-2 py-1 text-sm text-text placeholder:text-text-subtle focus:border-border-strong focus:outline-none disabled:opacity-50"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="path"
|
||||
required
|
||||
disabled={editingName !== null}
|
||||
/>
|
||||
</div>
|
||||
{!isElapsedSinceLast && (
|
||||
<div>
|
||||
<label className="mb-1 flex items-center gap-1 text-xs text-text-subtle">
|
||||
Message match regex
|
||||
@@ -117,20 +201,25 @@ export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onD
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{detectedJson && !isBoolean && (
|
||||
)}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-text-subtle">Extraction</label>
|
||||
<select
|
||||
className="w-full rounded border border-border-soft bg-surface-1 px-2 py-1 text-xs text-text focus:border-border-strong focus:outline-none"
|
||||
value={source}
|
||||
onChange={(e) => setSource(e.target.value as ExtractionSource)}
|
||||
onChange={(e) => handleSourceChange(e.target.value as ExtractionSource)}
|
||||
>
|
||||
<option value="regex">Regex capture group</option>
|
||||
<option value="json_field">JSON field</option>
|
||||
{detectedJson && !isBoolean && <option value="json_field">JSON field</option>}
|
||||
<option value="elapsed_since_last">Elapsed since previous event</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{effectiveSource === 'json_field' ? (
|
||||
{isElapsedSinceLast ? (
|
||||
<p className="text-xs text-text-faint">
|
||||
Computed automatically — elapsed time since the previous event that shares the same
|
||||
labels wherever this param is used, not parsed from the log line.
|
||||
</p>
|
||||
) : effectiveSource === 'json_field' ? (
|
||||
<div className="flex items-end gap-1">
|
||||
<div className="min-w-0 flex-1">
|
||||
<label className="mb-1 block text-xs text-text-subtle">JSON field path</label>
|
||||
@@ -181,34 +270,96 @@ export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onD
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{editingName && (
|
||||
<div className="rounded border border-accent bg-accent/5 px-2 py-1 text-xs text-accent-text">
|
||||
Editing "{editingName}"
|
||||
</div>
|
||||
)}
|
||||
{submitError && <div className="rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text">{submitError}</div>}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="rounded border border-border-soft px-3 py-1 text-sm text-text-secondary hover:bg-surface-2 disabled:opacity-50"
|
||||
>
|
||||
{submitting ? 'Declaring…' : 'Declare param'}
|
||||
{submitting ? (editingName ? 'Saving…' : 'Declaring…') : editingName ? 'Save changes' : 'Declare param'}
|
||||
</button>
|
||||
{editingName && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetForm}
|
||||
disabled={submitting}
|
||||
className="rounded border border-border-soft px-3 py-1 text-sm text-text-secondary hover:bg-surface-2 disabled:opacity-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 text-xs text-text-subtle">Declared params</div>
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<span className="text-xs text-text-subtle">Declared params</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearAll}
|
||||
disabled={clearing || params.length === 0}
|
||||
className="rounded border border-border-soft px-2 py-0.5 text-xs text-text-secondary hover:bg-surface-2 disabled:opacity-50"
|
||||
title="Clear every declared param, then re-derive params from log lines still held in memory"
|
||||
>
|
||||
{clearing ? 'Clearing…' : 'Clear all'}
|
||||
</button>
|
||||
</div>
|
||||
{clearResult && <div className="mb-1 text-xs text-text-subtle">{clearResult}</div>}
|
||||
{clearError && <div className="mb-1 text-xs text-danger-text">{clearError}</div>}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{params.length === 0 && <span className="text-xs text-text-faint">No params declared yet.</span>}
|
||||
{params.map((p) => (
|
||||
<span
|
||||
{declaredParams.length === 0 && (
|
||||
<span className="text-xs text-text-faint">No params declared yet.</span>
|
||||
)}
|
||||
{declaredParams.map((p) => (
|
||||
<button
|
||||
key={p.name}
|
||||
className="rounded border border-border-soft px-2 py-1 text-xs text-text-secondary"
|
||||
title={p.message_match_regex}
|
||||
type="button"
|
||||
onClick={() => editParam(p)}
|
||||
className={`rounded border px-2 py-1 text-xs ${
|
||||
editingName === p.name
|
||||
? 'border-accent bg-accent/10 text-accent-text'
|
||||
: 'border-border-soft text-text-secondary hover:bg-surface-2'
|
||||
}`}
|
||||
title={p.elapsed_since_last ? undefined : p.message_match_regex}
|
||||
>
|
||||
{p.name} ({p.kind}
|
||||
{p.kind === 'boolean'
|
||||
{p.elapsed_since_last
|
||||
? ', elapsed since previous'
|
||||
: p.kind === 'boolean'
|
||||
? ''
|
||||
: `, ${p.json_field_path ? `json field ${p.json_field_path}` : `group ${p.capture_group}`}`}
|
||||
: p.json_field_path
|
||||
? `, json field ${p.json_field_path}`
|
||||
: p.capture_group !== p.name
|
||||
? `, group ${p.capture_group}`
|
||||
: ''}
|
||||
)
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{reservedParams.length > 0 && (
|
||||
<>
|
||||
<div className="mb-1 mt-2 text-xs text-text-subtle">
|
||||
Reserved (built in — present on every line, not editable)
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{reservedParams.map((p) => (
|
||||
<span
|
||||
key={p.name}
|
||||
className="rounded border border-dashed border-border-soft px-2 py-1 text-xs text-text-faint"
|
||||
title={`${p.name} (${p.kind}, reserved)`}
|
||||
>
|
||||
{p.name} ({p.kind})
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,14 @@
|
||||
* composite K = H+150 (mod 360), then fill --c-surface-, --c-border-,
|
||||
* --c-text- as hsl(H, ...) at the saturation/lightness steps below, accent
|
||||
* + accent-text as hsl(C, ...), and accent-soft as hsl(K, ...).
|
||||
*
|
||||
* --c-accent (base look, unchanged above) is joined by two more accent
|
||||
* colours (ticket 048) so charts/highlights can draw from a 3-colour set
|
||||
* instead of one hue: --c-accent-2 sits at H+90 and --c-accent-3 at H+270 —
|
||||
* a quadrature pair straddling the base hue, distinct from both the
|
||||
* complement (accent) and the split-complement (accent-soft). These are
|
||||
* purely additive: nothing that already reads --c-accent/-soft/-text
|
||||
* changes look.
|
||||
*/
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
@@ -62,6 +70,8 @@
|
||||
--c-accent: #0ea5e9;
|
||||
--c-accent-soft: #0369a1;
|
||||
--c-accent-text: #7dd3fc;
|
||||
--c-accent-2: #a78bfa;
|
||||
--c-accent-3: #4ade80;
|
||||
}
|
||||
|
||||
/* Exact lightness mirror of :root's neutral-950..50 scale (Tailwind neutral,
|
||||
@@ -91,6 +101,8 @@ html.light {
|
||||
--c-accent: #0284c7;
|
||||
--c-accent-soft: #7dd3fc;
|
||||
--c-accent-text: #0369a1;
|
||||
--c-accent-2: #7c3aed;
|
||||
--c-accent-3: #16a34a;
|
||||
}
|
||||
|
||||
/* base hue 0 (red): surfaces/borders/text tinted red; accent complement 180
|
||||
@@ -115,6 +127,8 @@ html.red {
|
||||
--c-accent: hsl(180 70% 62%);
|
||||
--c-accent-soft: hsl(150 55% 34%);
|
||||
--c-accent-text: hsl(180 75% 80%);
|
||||
--c-accent-2: hsl(90 65% 58%);
|
||||
--c-accent-3: hsl(270 65% 68%);
|
||||
}
|
||||
|
||||
/* base hue 140 (green): surfaces/borders/text tinted green; accent complement
|
||||
@@ -139,6 +153,8 @@ html.green {
|
||||
--c-accent: hsl(320 70% 62%);
|
||||
--c-accent-soft: hsl(290 55% 34%);
|
||||
--c-accent-text: hsl(320 75% 80%);
|
||||
--c-accent-2: hsl(230 65% 62%);
|
||||
--c-accent-3: hsl(50 65% 58%);
|
||||
}
|
||||
|
||||
/* base hue 210 (blue): surfaces/borders/text tinted blue; accent complement
|
||||
@@ -163,6 +179,8 @@ html.blue {
|
||||
--c-accent: hsl(30 70% 62%);
|
||||
--c-accent-soft: hsl(0 55% 34%);
|
||||
--c-accent-text: hsl(30 75% 80%);
|
||||
--c-accent-2: hsl(300 65% 68%);
|
||||
--c-accent-3: hsl(120 65% 55%);
|
||||
}
|
||||
|
||||
/* base hue 50 (yellow): surfaces/borders/text tinted yellow; accent
|
||||
@@ -187,6 +205,8 @@ html.yellow {
|
||||
--c-accent: hsl(230 70% 62%);
|
||||
--c-accent-soft: hsl(200 55% 34%);
|
||||
--c-accent-text: hsl(230 75% 80%);
|
||||
--c-accent-2: hsl(140 65% 55%);
|
||||
--c-accent-3: hsl(320 65% 68%);
|
||||
}
|
||||
|
||||
/* base hue 330 (pink): surfaces/borders/text tinted pink; accent complement
|
||||
@@ -211,6 +231,8 @@ html.pink {
|
||||
--c-accent: hsl(150 70% 62%);
|
||||
--c-accent-soft: hsl(120 55% 34%);
|
||||
--c-accent-text: hsl(150 75% 80%);
|
||||
--c-accent-2: hsl(60 65% 58%);
|
||||
--c-accent-3: hsl(240 65% 68%);
|
||||
}
|
||||
|
||||
/* base hue 30 (orange): surfaces/borders/text tinted orange; accent
|
||||
@@ -235,6 +257,203 @@ html.orange {
|
||||
--c-accent: hsl(210 70% 62%);
|
||||
--c-accent-soft: hsl(180 55% 34%);
|
||||
--c-accent-text: hsl(210 75% 80%);
|
||||
--c-accent-2: hsl(120 65% 55%);
|
||||
--c-accent-3: hsl(300 65% 68%);
|
||||
}
|
||||
|
||||
/* Brighter, light-surfaced counterpart to `green` (ticket 048): same base
|
||||
hue (140) and accent hues as `green`, but with a `light`-style inverted
|
||||
surface/border/text scale (high lightness instead of low) so it reads as
|
||||
a distinctly brighter theme, not a re-tint of the dark `green`. Danger
|
||||
colours are `light`'s (hue-neutral), matching how `light` itself doesn't
|
||||
tint danger either. */
|
||||
html.light_green {
|
||||
--c-surface-0: hsl(140 40% 96%);
|
||||
--c-surface-1: hsl(140 35% 92%);
|
||||
--c-surface-2: hsl(140 30% 85%);
|
||||
|
||||
--c-border: hsl(140 25% 80%);
|
||||
--c-border-soft: hsl(140 22% 70%);
|
||||
--c-border-faint: hsl(140 35% 92%);
|
||||
--c-border-strong: hsl(140 20% 40%);
|
||||
|
||||
--c-text: hsl(140 30% 12%);
|
||||
--c-text-secondary: hsl(140 20% 25%);
|
||||
--c-text-muted: hsl(140 15% 40%);
|
||||
--c-text-subtle: hsl(140 10% 50%);
|
||||
--c-text-faint: hsl(140 10% 65%);
|
||||
--c-text-faintest: hsl(140 12% 80%);
|
||||
|
||||
--c-danger-bg: #fee2e2;
|
||||
--c-danger-border: #fca5a5;
|
||||
--c-danger-text: #b91c1c;
|
||||
--c-danger-text-strong: #991b1b;
|
||||
|
||||
--c-accent: hsl(320 70% 40%);
|
||||
--c-accent-soft: hsl(290 60% 85%);
|
||||
--c-accent-text: hsl(320 75% 28%);
|
||||
--c-accent-2: hsl(230 70% 45%);
|
||||
--c-accent-3: hsl(50 70% 38%);
|
||||
}
|
||||
|
||||
/* Brighter, light-surfaced counterpart to `red` (ticket 056): same base hue
|
||||
(0) and accent hues as `red`, with `light`'s inverted surface/border/text
|
||||
scale and `light`'s hue-neutral danger colours. */
|
||||
html.light_red {
|
||||
--c-surface-0: hsl(0 40% 96%);
|
||||
--c-surface-1: hsl(0 35% 92%);
|
||||
--c-surface-2: hsl(0 30% 85%);
|
||||
|
||||
--c-border: hsl(0 25% 80%);
|
||||
--c-border-soft: hsl(0 22% 70%);
|
||||
--c-border-faint: hsl(0 35% 92%);
|
||||
--c-border-strong: hsl(0 20% 40%);
|
||||
|
||||
--c-text: hsl(0 30% 12%);
|
||||
--c-text-secondary: hsl(0 20% 25%);
|
||||
--c-text-muted: hsl(0 15% 40%);
|
||||
--c-text-subtle: hsl(0 10% 50%);
|
||||
--c-text-faint: hsl(0 10% 65%);
|
||||
--c-text-faintest: hsl(0 12% 80%);
|
||||
|
||||
--c-danger-bg: #fee2e2;
|
||||
--c-danger-border: #fca5a5;
|
||||
--c-danger-text: #b91c1c;
|
||||
--c-danger-text-strong: #991b1b;
|
||||
|
||||
--c-accent: hsl(180 70% 40%);
|
||||
--c-accent-soft: hsl(150 60% 85%);
|
||||
--c-accent-text: hsl(180 75% 28%);
|
||||
--c-accent-2: hsl(90 70% 45%);
|
||||
--c-accent-3: hsl(270 70% 38%);
|
||||
}
|
||||
|
||||
/* Brighter, light-surfaced counterpart to `blue` (ticket 056): same base hue
|
||||
(210) and accent hues as `blue`, with `light`'s inverted surface/border/text
|
||||
scale and `light`'s hue-neutral danger colours. */
|
||||
html.light_blue {
|
||||
--c-surface-0: hsl(210 40% 96%);
|
||||
--c-surface-1: hsl(210 35% 92%);
|
||||
--c-surface-2: hsl(210 30% 85%);
|
||||
|
||||
--c-border: hsl(210 25% 80%);
|
||||
--c-border-soft: hsl(210 22% 70%);
|
||||
--c-border-faint: hsl(210 35% 92%);
|
||||
--c-border-strong: hsl(210 20% 40%);
|
||||
|
||||
--c-text: hsl(210 30% 12%);
|
||||
--c-text-secondary: hsl(210 20% 25%);
|
||||
--c-text-muted: hsl(210 15% 40%);
|
||||
--c-text-subtle: hsl(210 10% 50%);
|
||||
--c-text-faint: hsl(210 10% 65%);
|
||||
--c-text-faintest: hsl(210 12% 80%);
|
||||
|
||||
--c-danger-bg: #fee2e2;
|
||||
--c-danger-border: #fca5a5;
|
||||
--c-danger-text: #b91c1c;
|
||||
--c-danger-text-strong: #991b1b;
|
||||
|
||||
--c-accent: hsl(30 70% 40%);
|
||||
--c-accent-soft: hsl(0 60% 85%);
|
||||
--c-accent-text: hsl(30 75% 28%);
|
||||
--c-accent-2: hsl(300 70% 45%);
|
||||
--c-accent-3: hsl(120 70% 38%);
|
||||
}
|
||||
|
||||
/* Brighter, light-surfaced counterpart to `yellow` (ticket 056): same base
|
||||
hue (50) and accent hues as `yellow`, with `light`'s inverted
|
||||
surface/border/text scale and `light`'s hue-neutral danger colours. */
|
||||
html.light_yellow {
|
||||
--c-surface-0: hsl(50 40% 96%);
|
||||
--c-surface-1: hsl(50 35% 92%);
|
||||
--c-surface-2: hsl(50 30% 85%);
|
||||
|
||||
--c-border: hsl(50 25% 80%);
|
||||
--c-border-soft: hsl(50 22% 70%);
|
||||
--c-border-faint: hsl(50 35% 92%);
|
||||
--c-border-strong: hsl(50 20% 40%);
|
||||
|
||||
--c-text: hsl(50 30% 12%);
|
||||
--c-text-secondary: hsl(50 20% 25%);
|
||||
--c-text-muted: hsl(50 15% 40%);
|
||||
--c-text-subtle: hsl(50 10% 50%);
|
||||
--c-text-faint: hsl(50 10% 65%);
|
||||
--c-text-faintest: hsl(50 12% 80%);
|
||||
|
||||
--c-danger-bg: #fee2e2;
|
||||
--c-danger-border: #fca5a5;
|
||||
--c-danger-text: #b91c1c;
|
||||
--c-danger-text-strong: #991b1b;
|
||||
|
||||
--c-accent: hsl(230 70% 40%);
|
||||
--c-accent-soft: hsl(200 60% 85%);
|
||||
--c-accent-text: hsl(230 75% 28%);
|
||||
--c-accent-2: hsl(140 70% 45%);
|
||||
--c-accent-3: hsl(320 70% 38%);
|
||||
}
|
||||
|
||||
/* Brighter, light-surfaced counterpart to `pink` (ticket 056): same base hue
|
||||
(330) and accent hues as `pink`, with `light`'s inverted surface/border/text
|
||||
scale and `light`'s hue-neutral danger colours. */
|
||||
html.light_pink {
|
||||
--c-surface-0: hsl(330 40% 96%);
|
||||
--c-surface-1: hsl(330 35% 92%);
|
||||
--c-surface-2: hsl(330 30% 85%);
|
||||
|
||||
--c-border: hsl(330 25% 80%);
|
||||
--c-border-soft: hsl(330 22% 70%);
|
||||
--c-border-faint: hsl(330 35% 92%);
|
||||
--c-border-strong: hsl(330 20% 40%);
|
||||
|
||||
--c-text: hsl(330 30% 12%);
|
||||
--c-text-secondary: hsl(330 20% 25%);
|
||||
--c-text-muted: hsl(330 15% 40%);
|
||||
--c-text-subtle: hsl(330 10% 50%);
|
||||
--c-text-faint: hsl(330 10% 65%);
|
||||
--c-text-faintest: hsl(330 12% 80%);
|
||||
|
||||
--c-danger-bg: #fee2e2;
|
||||
--c-danger-border: #fca5a5;
|
||||
--c-danger-text: #b91c1c;
|
||||
--c-danger-text-strong: #991b1b;
|
||||
|
||||
--c-accent: hsl(150 70% 40%);
|
||||
--c-accent-soft: hsl(120 60% 85%);
|
||||
--c-accent-text: hsl(150 75% 28%);
|
||||
--c-accent-2: hsl(60 70% 45%);
|
||||
--c-accent-3: hsl(240 70% 38%);
|
||||
}
|
||||
|
||||
/* Brighter, light-surfaced counterpart to `orange` (ticket 056): same base
|
||||
hue (30) and accent hues as `orange`, with `light`'s inverted
|
||||
surface/border/text scale and `light`'s hue-neutral danger colours. */
|
||||
html.light_orange {
|
||||
--c-surface-0: hsl(30 40% 96%);
|
||||
--c-surface-1: hsl(30 35% 92%);
|
||||
--c-surface-2: hsl(30 30% 85%);
|
||||
|
||||
--c-border: hsl(30 25% 80%);
|
||||
--c-border-soft: hsl(30 22% 70%);
|
||||
--c-border-faint: hsl(30 35% 92%);
|
||||
--c-border-strong: hsl(30 20% 40%);
|
||||
|
||||
--c-text: hsl(30 30% 12%);
|
||||
--c-text-secondary: hsl(30 20% 25%);
|
||||
--c-text-muted: hsl(30 15% 40%);
|
||||
--c-text-subtle: hsl(30 10% 50%);
|
||||
--c-text-faint: hsl(30 10% 65%);
|
||||
--c-text-faintest: hsl(30 12% 80%);
|
||||
|
||||
--c-danger-bg: #fee2e2;
|
||||
--c-danger-border: #fca5a5;
|
||||
--c-danger-text: #b91c1c;
|
||||
--c-danger-text-strong: #991b1b;
|
||||
|
||||
--c-accent: hsl(210 70% 40%);
|
||||
--c-accent-soft: hsl(180 60% 85%);
|
||||
--c-accent-text: hsl(210 75% 28%);
|
||||
--c-accent-2: hsl(120 70% 45%);
|
||||
--c-accent-3: hsl(300 70% 38%);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -262,6 +481,8 @@ html.orange {
|
||||
--color-accent: var(--c-accent);
|
||||
--color-accent-soft: var(--c-accent-soft);
|
||||
--color-accent-text: var(--c-accent-text);
|
||||
--color-accent-2: var(--c-accent-2);
|
||||
--color-accent-3: var(--c-accent-3);
|
||||
}
|
||||
|
||||
html,
|
||||
|
||||
+39
-13
@@ -30,6 +30,10 @@ export interface ParamDeclaration {
|
||||
/** Alternative to `capture_group`: a dot-separated JSON field path (e.g. "foo.bar"), read from lines that parse as a JSON object. When set (non-empty), takes precedence over `capture_group`. */
|
||||
json_field_path?: string | null
|
||||
kind: 'string' | 'number' | 'timestamp' | 'duration' | 'boolean'
|
||||
/** A third extraction source, distinct from `capture_group`/`json_field_path`: when true, this param's value is never parsed from text — it's computed as elapsed time since the previous event sharing the same label set. Only meaningful paired with `kind: 'duration'`. */
|
||||
elapsed_since_last?: boolean
|
||||
/** Set only for the built-in `ottyl_seq`/`ottyl_ts`/`ottyl_fmt` names, synthesized into `GET /api/params` for discoverability — they're usable in the filter bar (and `ottyl_seq`/`ottyl_ts` in dashboard PromQL queries) without ever being declared, and can't be edited or removed. */
|
||||
reserved?: boolean
|
||||
}
|
||||
|
||||
export interface TimelineBucket {
|
||||
@@ -47,22 +51,13 @@ export interface Stats {
|
||||
approx_bytes_total: number
|
||||
}
|
||||
|
||||
export type ParamRole = 'label' | 'value'
|
||||
|
||||
export interface MetricParamMatcher {
|
||||
param_name: string
|
||||
capture_group: string
|
||||
/** See `ParamDeclaration.json_field_path`. */
|
||||
json_field_path?: string | null
|
||||
kind: 'string' | 'number' | 'timestamp' | 'duration'
|
||||
/** Defaults to 'label' server-side when omitted. */
|
||||
role?: ParamRole
|
||||
}
|
||||
|
||||
export interface MetricDeclaration {
|
||||
name: string
|
||||
message_match_regex: string
|
||||
params: MetricParamMatcher[]
|
||||
/** Names of already-declared `ParamDeclaration`s (see the Params panel) whose values become this metric's bucket labels. */
|
||||
label_params: string[]
|
||||
/** Name of an already-declared param (if any) whose value becomes this metric's per-event bucket value instead of a label. */
|
||||
value_param?: string | null
|
||||
/** Fixed labels applied on a match/non-match, on top of any captured params. */
|
||||
matched_labels?: Record<string, string> | null
|
||||
unmatched_labels?: Record<string, string> | null
|
||||
@@ -97,6 +92,12 @@ export type ColorScheme =
|
||||
| 'yellow'
|
||||
| 'pink'
|
||||
| 'orange'
|
||||
| 'light_green'
|
||||
| 'light_red'
|
||||
| 'light_blue'
|
||||
| 'light_yellow'
|
||||
| 'light_pink'
|
||||
| 'light_orange'
|
||||
|
||||
export interface Settings {
|
||||
color_scheme: ColorScheme
|
||||
@@ -251,6 +252,10 @@ export function fetchLogs(params: {
|
||||
endMs?: number
|
||||
limit?: number
|
||||
filter?: string
|
||||
/** Ticket 045: an already-declared param name to order entries by, instead of the default (`ottyl_ts`-tracking) order. */
|
||||
sortBy?: string
|
||||
/** Ticket 045: only meaningful alongside `sortBy`. `true` (default) orders lines missing that param by `ottyl_seq`, appended after the sorted lines; `false` drops them. */
|
||||
sortMissingFallback?: boolean
|
||||
}): Promise<LogEntry[]> {
|
||||
const search = new URLSearchParams()
|
||||
if (params.sinceSequence !== undefined) search.set('since_sequence', String(params.sinceSequence))
|
||||
@@ -258,6 +263,8 @@ export function fetchLogs(params: {
|
||||
if (params.endMs !== undefined) search.set('end_ms', String(params.endMs))
|
||||
if (params.limit !== undefined) search.set('limit', String(params.limit))
|
||||
if (params.filter) search.set('filter', params.filter)
|
||||
if (params.sortBy) search.set('sort_by', params.sortBy)
|
||||
if (params.sortMissingFallback !== undefined) search.set('sort_missing_fallback', String(params.sortMissingFallback))
|
||||
return getJson(`/api/logs?${search.toString()}`)
|
||||
}
|
||||
|
||||
@@ -287,6 +294,21 @@ export function declareParam(request: ParamDeclaration): Promise<ParamDeclaratio
|
||||
return postJson('/api/params', request)
|
||||
}
|
||||
|
||||
export interface ClearParamsResult {
|
||||
/** How many param declarations (manual + auto-derived) were removed. */
|
||||
removed: number
|
||||
/** How many params were immediately re-derived from log text still retained by the engine. */
|
||||
reextracted: number
|
||||
/** How many retained log lines were re-scanned — lines already evicted by `max_logs` before the clear can't regain their params, so this can be smaller than the total lines ever ingested. */
|
||||
logs_considered: number
|
||||
}
|
||||
|
||||
// Clears every declared param (ticket 052) and re-derives whatever the engine's still-retained log
|
||||
// text supports — see `ClearParamsResult`'s fields for what came back.
|
||||
export function clearParams(): Promise<ClearParamsResult> {
|
||||
return sendJson('/api/params', 'DELETE')
|
||||
}
|
||||
|
||||
export function fetchMetricDeclarations(): Promise<MetricDeclaration[]> {
|
||||
return getJson('/api/metrics')
|
||||
}
|
||||
@@ -300,6 +322,10 @@ export function updateMetric(existingName: string, request: MetricDeclaration):
|
||||
return putJson(`/api/metrics/${encodeURIComponent(existingName)}`, request)
|
||||
}
|
||||
|
||||
export function deleteMetric(name: string): Promise<void> {
|
||||
return sendJson(`/api/metrics/${encodeURIComponent(name)}`, 'DELETE')
|
||||
}
|
||||
|
||||
export function fetchMetricBucketValues(
|
||||
name: string,
|
||||
params: {
|
||||
|
||||
+169
-1
@@ -1,4 +1,10 @@
|
||||
// A theme file is a CSS file with one `:root { --c-*: ...; }` block defining every variable in `THEME_CSS_VARS`; `useSettingsPanel` applies the selected one as an unscoped stylesheet only while no built-in `html.<scheme>` class is present, so it wins the cascade without rewriting the selector.
|
||||
// A user-supplied style is any CSS file that defines every variable in `THEME_CSS_VARS` somewhere in
|
||||
// a `:root { --c-*: ...; }` block (ticket 020/047: styles are selectable on equal footing with the
|
||||
// built-ins, and there's no requirement that this block be the *only* thing the file contains — a
|
||||
// style is free to also target any other selector, e.g. to restyle layout, spacing, or fonts, not
|
||||
// just the palette). `useSettingsPanel` applies the selected one as an unscoped stylesheet only
|
||||
// while no built-in `html.<scheme>` class is present, so it wins the cascade without rewriting the
|
||||
// selector.
|
||||
|
||||
/** Canonical list of every `--c-*` variable every theme in `index.css` defines. */
|
||||
export const THEME_CSS_VARS = [
|
||||
@@ -22,6 +28,8 @@ export const THEME_CSS_VARS = [
|
||||
'--c-accent',
|
||||
'--c-accent-soft',
|
||||
'--c-accent-text',
|
||||
'--c-accent-2',
|
||||
'--c-accent-3',
|
||||
] as const
|
||||
|
||||
export type ThemeVars = Record<(typeof THEME_CSS_VARS)[number], string>
|
||||
@@ -49,6 +57,8 @@ export const BUILTIN_THEMES: Record<string, ThemeVars> = {
|
||||
'--c-accent': '#0ea5e9',
|
||||
'--c-accent-soft': '#0369a1',
|
||||
'--c-accent-text': '#7dd3fc',
|
||||
'--c-accent-2': '#a78bfa',
|
||||
'--c-accent-3': '#4ade80',
|
||||
},
|
||||
light: {
|
||||
'--c-surface-0': '#fafafa',
|
||||
@@ -71,6 +81,8 @@ export const BUILTIN_THEMES: Record<string, ThemeVars> = {
|
||||
'--c-accent': '#0284c7',
|
||||
'--c-accent-soft': '#7dd3fc',
|
||||
'--c-accent-text': '#0369a1',
|
||||
'--c-accent-2': '#7c3aed',
|
||||
'--c-accent-3': '#16a34a',
|
||||
},
|
||||
red: {
|
||||
'--c-surface-0': 'hsl(0 32% 10%)',
|
||||
@@ -93,6 +105,8 @@ export const BUILTIN_THEMES: Record<string, ThemeVars> = {
|
||||
'--c-accent': 'hsl(180 70% 62%)',
|
||||
'--c-accent-soft': 'hsl(150 55% 34%)',
|
||||
'--c-accent-text': 'hsl(180 75% 80%)',
|
||||
'--c-accent-2': 'hsl(90 65% 58%)',
|
||||
'--c-accent-3': 'hsl(270 65% 68%)',
|
||||
},
|
||||
green: {
|
||||
'--c-surface-0': 'hsl(140 32% 10%)',
|
||||
@@ -115,6 +129,8 @@ export const BUILTIN_THEMES: Record<string, ThemeVars> = {
|
||||
'--c-accent': 'hsl(320 70% 62%)',
|
||||
'--c-accent-soft': 'hsl(290 55% 34%)',
|
||||
'--c-accent-text': 'hsl(320 75% 80%)',
|
||||
'--c-accent-2': 'hsl(230 65% 62%)',
|
||||
'--c-accent-3': 'hsl(50 65% 58%)',
|
||||
},
|
||||
blue: {
|
||||
'--c-surface-0': 'hsl(210 32% 10%)',
|
||||
@@ -137,6 +153,8 @@ export const BUILTIN_THEMES: Record<string, ThemeVars> = {
|
||||
'--c-accent': 'hsl(30 70% 62%)',
|
||||
'--c-accent-soft': 'hsl(0 55% 34%)',
|
||||
'--c-accent-text': 'hsl(30 75% 80%)',
|
||||
'--c-accent-2': 'hsl(300 65% 68%)',
|
||||
'--c-accent-3': 'hsl(120 65% 55%)',
|
||||
},
|
||||
yellow: {
|
||||
'--c-surface-0': 'hsl(50 32% 10%)',
|
||||
@@ -159,6 +177,8 @@ export const BUILTIN_THEMES: Record<string, ThemeVars> = {
|
||||
'--c-accent': 'hsl(230 70% 62%)',
|
||||
'--c-accent-soft': 'hsl(200 55% 34%)',
|
||||
'--c-accent-text': 'hsl(230 75% 80%)',
|
||||
'--c-accent-2': 'hsl(140 65% 55%)',
|
||||
'--c-accent-3': 'hsl(320 65% 68%)',
|
||||
},
|
||||
pink: {
|
||||
'--c-surface-0': 'hsl(330 32% 10%)',
|
||||
@@ -181,6 +201,8 @@ export const BUILTIN_THEMES: Record<string, ThemeVars> = {
|
||||
'--c-accent': 'hsl(150 70% 62%)',
|
||||
'--c-accent-soft': 'hsl(120 55% 34%)',
|
||||
'--c-accent-text': 'hsl(150 75% 80%)',
|
||||
'--c-accent-2': 'hsl(60 65% 58%)',
|
||||
'--c-accent-3': 'hsl(240 65% 68%)',
|
||||
},
|
||||
orange: {
|
||||
'--c-surface-0': 'hsl(30 32% 10%)',
|
||||
@@ -203,6 +225,152 @@ export const BUILTIN_THEMES: Record<string, ThemeVars> = {
|
||||
'--c-accent': 'hsl(210 70% 62%)',
|
||||
'--c-accent-soft': 'hsl(180 55% 34%)',
|
||||
'--c-accent-text': 'hsl(210 75% 80%)',
|
||||
'--c-accent-2': 'hsl(120 65% 55%)',
|
||||
'--c-accent-3': 'hsl(300 65% 68%)',
|
||||
},
|
||||
light_green: {
|
||||
'--c-surface-0': 'hsl(140 40% 96%)',
|
||||
'--c-surface-1': 'hsl(140 35% 92%)',
|
||||
'--c-surface-2': 'hsl(140 30% 85%)',
|
||||
'--c-border': 'hsl(140 25% 80%)',
|
||||
'--c-border-soft': 'hsl(140 22% 70%)',
|
||||
'--c-border-faint': 'hsl(140 35% 92%)',
|
||||
'--c-border-strong': 'hsl(140 20% 40%)',
|
||||
'--c-text': 'hsl(140 30% 12%)',
|
||||
'--c-text-secondary': 'hsl(140 20% 25%)',
|
||||
'--c-text-muted': 'hsl(140 15% 40%)',
|
||||
'--c-text-subtle': 'hsl(140 10% 50%)',
|
||||
'--c-text-faint': 'hsl(140 10% 65%)',
|
||||
'--c-text-faintest': 'hsl(140 12% 80%)',
|
||||
'--c-danger-bg': '#fee2e2',
|
||||
'--c-danger-border': '#fca5a5',
|
||||
'--c-danger-text': '#b91c1c',
|
||||
'--c-danger-text-strong': '#991b1b',
|
||||
'--c-accent': 'hsl(320 70% 40%)',
|
||||
'--c-accent-soft': 'hsl(290 60% 85%)',
|
||||
'--c-accent-text': 'hsl(320 75% 28%)',
|
||||
'--c-accent-2': 'hsl(230 70% 45%)',
|
||||
'--c-accent-3': 'hsl(50 70% 38%)',
|
||||
},
|
||||
light_red: {
|
||||
'--c-surface-0': 'hsl(0 40% 96%)',
|
||||
'--c-surface-1': 'hsl(0 35% 92%)',
|
||||
'--c-surface-2': 'hsl(0 30% 85%)',
|
||||
'--c-border': 'hsl(0 25% 80%)',
|
||||
'--c-border-soft': 'hsl(0 22% 70%)',
|
||||
'--c-border-faint': 'hsl(0 35% 92%)',
|
||||
'--c-border-strong': 'hsl(0 20% 40%)',
|
||||
'--c-text': 'hsl(0 30% 12%)',
|
||||
'--c-text-secondary': 'hsl(0 20% 25%)',
|
||||
'--c-text-muted': 'hsl(0 15% 40%)',
|
||||
'--c-text-subtle': 'hsl(0 10% 50%)',
|
||||
'--c-text-faint': 'hsl(0 10% 65%)',
|
||||
'--c-text-faintest': 'hsl(0 12% 80%)',
|
||||
'--c-danger-bg': '#fee2e2',
|
||||
'--c-danger-border': '#fca5a5',
|
||||
'--c-danger-text': '#b91c1c',
|
||||
'--c-danger-text-strong': '#991b1b',
|
||||
'--c-accent': 'hsl(180 70% 40%)',
|
||||
'--c-accent-soft': 'hsl(150 60% 85%)',
|
||||
'--c-accent-text': 'hsl(180 75% 28%)',
|
||||
'--c-accent-2': 'hsl(90 70% 45%)',
|
||||
'--c-accent-3': 'hsl(270 70% 38%)',
|
||||
},
|
||||
light_blue: {
|
||||
'--c-surface-0': 'hsl(210 40% 96%)',
|
||||
'--c-surface-1': 'hsl(210 35% 92%)',
|
||||
'--c-surface-2': 'hsl(210 30% 85%)',
|
||||
'--c-border': 'hsl(210 25% 80%)',
|
||||
'--c-border-soft': 'hsl(210 22% 70%)',
|
||||
'--c-border-faint': 'hsl(210 35% 92%)',
|
||||
'--c-border-strong': 'hsl(210 20% 40%)',
|
||||
'--c-text': 'hsl(210 30% 12%)',
|
||||
'--c-text-secondary': 'hsl(210 20% 25%)',
|
||||
'--c-text-muted': 'hsl(210 15% 40%)',
|
||||
'--c-text-subtle': 'hsl(210 10% 50%)',
|
||||
'--c-text-faint': 'hsl(210 10% 65%)',
|
||||
'--c-text-faintest': 'hsl(210 12% 80%)',
|
||||
'--c-danger-bg': '#fee2e2',
|
||||
'--c-danger-border': '#fca5a5',
|
||||
'--c-danger-text': '#b91c1c',
|
||||
'--c-danger-text-strong': '#991b1b',
|
||||
'--c-accent': 'hsl(30 70% 40%)',
|
||||
'--c-accent-soft': 'hsl(0 60% 85%)',
|
||||
'--c-accent-text': 'hsl(30 75% 28%)',
|
||||
'--c-accent-2': 'hsl(300 70% 45%)',
|
||||
'--c-accent-3': 'hsl(120 70% 38%)',
|
||||
},
|
||||
light_yellow: {
|
||||
'--c-surface-0': 'hsl(50 40% 96%)',
|
||||
'--c-surface-1': 'hsl(50 35% 92%)',
|
||||
'--c-surface-2': 'hsl(50 30% 85%)',
|
||||
'--c-border': 'hsl(50 25% 80%)',
|
||||
'--c-border-soft': 'hsl(50 22% 70%)',
|
||||
'--c-border-faint': 'hsl(50 35% 92%)',
|
||||
'--c-border-strong': 'hsl(50 20% 40%)',
|
||||
'--c-text': 'hsl(50 30% 12%)',
|
||||
'--c-text-secondary': 'hsl(50 20% 25%)',
|
||||
'--c-text-muted': 'hsl(50 15% 40%)',
|
||||
'--c-text-subtle': 'hsl(50 10% 50%)',
|
||||
'--c-text-faint': 'hsl(50 10% 65%)',
|
||||
'--c-text-faintest': 'hsl(50 12% 80%)',
|
||||
'--c-danger-bg': '#fee2e2',
|
||||
'--c-danger-border': '#fca5a5',
|
||||
'--c-danger-text': '#b91c1c',
|
||||
'--c-danger-text-strong': '#991b1b',
|
||||
'--c-accent': 'hsl(230 70% 40%)',
|
||||
'--c-accent-soft': 'hsl(200 60% 85%)',
|
||||
'--c-accent-text': 'hsl(230 75% 28%)',
|
||||
'--c-accent-2': 'hsl(140 70% 45%)',
|
||||
'--c-accent-3': 'hsl(320 70% 38%)',
|
||||
},
|
||||
light_pink: {
|
||||
'--c-surface-0': 'hsl(330 40% 96%)',
|
||||
'--c-surface-1': 'hsl(330 35% 92%)',
|
||||
'--c-surface-2': 'hsl(330 30% 85%)',
|
||||
'--c-border': 'hsl(330 25% 80%)',
|
||||
'--c-border-soft': 'hsl(330 22% 70%)',
|
||||
'--c-border-faint': 'hsl(330 35% 92%)',
|
||||
'--c-border-strong': 'hsl(330 20% 40%)',
|
||||
'--c-text': 'hsl(330 30% 12%)',
|
||||
'--c-text-secondary': 'hsl(330 20% 25%)',
|
||||
'--c-text-muted': 'hsl(330 15% 40%)',
|
||||
'--c-text-subtle': 'hsl(330 10% 50%)',
|
||||
'--c-text-faint': 'hsl(330 10% 65%)',
|
||||
'--c-text-faintest': 'hsl(330 12% 80%)',
|
||||
'--c-danger-bg': '#fee2e2',
|
||||
'--c-danger-border': '#fca5a5',
|
||||
'--c-danger-text': '#b91c1c',
|
||||
'--c-danger-text-strong': '#991b1b',
|
||||
'--c-accent': 'hsl(150 70% 40%)',
|
||||
'--c-accent-soft': 'hsl(120 60% 85%)',
|
||||
'--c-accent-text': 'hsl(150 75% 28%)',
|
||||
'--c-accent-2': 'hsl(60 70% 45%)',
|
||||
'--c-accent-3': 'hsl(240 70% 38%)',
|
||||
},
|
||||
light_orange: {
|
||||
'--c-surface-0': 'hsl(30 40% 96%)',
|
||||
'--c-surface-1': 'hsl(30 35% 92%)',
|
||||
'--c-surface-2': 'hsl(30 30% 85%)',
|
||||
'--c-border': 'hsl(30 25% 80%)',
|
||||
'--c-border-soft': 'hsl(30 22% 70%)',
|
||||
'--c-border-faint': 'hsl(30 35% 92%)',
|
||||
'--c-border-strong': 'hsl(30 20% 40%)',
|
||||
'--c-text': 'hsl(30 30% 12%)',
|
||||
'--c-text-secondary': 'hsl(30 20% 25%)',
|
||||
'--c-text-muted': 'hsl(30 15% 40%)',
|
||||
'--c-text-subtle': 'hsl(30 10% 50%)',
|
||||
'--c-text-faint': 'hsl(30 10% 65%)',
|
||||
'--c-text-faintest': 'hsl(30 12% 80%)',
|
||||
'--c-danger-bg': '#fee2e2',
|
||||
'--c-danger-border': '#fca5a5',
|
||||
'--c-danger-text': '#b91c1c',
|
||||
'--c-danger-text-strong': '#991b1b',
|
||||
'--c-accent': 'hsl(210 70% 40%)',
|
||||
'--c-accent-soft': 'hsl(180 60% 85%)',
|
||||
'--c-accent-text': 'hsl(210 75% 28%)',
|
||||
'--c-accent-2': 'hsl(120 70% 45%)',
|
||||
'--c-accent-3': 'hsl(300 70% 38%)',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,22 @@ import {
|
||||
const INJECTED_THEME_ATTR = 'data-ottyl-theme-css-id'
|
||||
|
||||
// Themes selectable independently of `system`, each backed by a `html.<name>` block in `index.css`.
|
||||
const THEME_CLASSES = ['light', 'dark', 'red', 'green', 'blue', 'yellow', 'pink', 'orange'] as const
|
||||
const THEME_CLASSES = [
|
||||
'light',
|
||||
'dark',
|
||||
'red',
|
||||
'green',
|
||||
'blue',
|
||||
'yellow',
|
||||
'pink',
|
||||
'orange',
|
||||
'light_green',
|
||||
'light_red',
|
||||
'light_blue',
|
||||
'light_yellow',
|
||||
'light_pink',
|
||||
'light_orange',
|
||||
] as const
|
||||
|
||||
// Reflects `colorScheme` onto `<html>` as a class + `data-color-scheme`; when a custom theme is active, no built-in class is added at all, since a class selector would out-specificity its injected `:root` override (see `applyCustomThemeCss`) and mask it.
|
||||
function applyColorScheme(colorScheme: ColorScheme, customThemeActive: boolean) {
|
||||
@@ -143,7 +158,8 @@ export function useSettingsPanel() {
|
||||
[guarded],
|
||||
)
|
||||
|
||||
// Every entry is always a selectable theme (ticket 020), so there's a single import path.
|
||||
// Every entry is always a selectable style, on equal footing with the built-in colour schemes
|
||||
// (ticket 020 unified the data model; ticket 047 unified the UI), so there's a single import path.
|
||||
const importCssEntry = useCallback(
|
||||
(filename: string, content: string) =>
|
||||
guarded(async () => {
|
||||
|
||||
Reference in New Issue
Block a user