Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The built-in Event Viewer can answer most of these questions, eventually. This o
the two things that actually take the time: narrowing fifty thousand records to the dozen that
matter, and seeing when they happened.

![The Events page reading System and Application](docs/events.png)

- **Events** — a virtualised table over up to 50 000 records. Per-column filters that fit each
column: tick lists with a search box and a count for level, provider, task, channel and computer;
a from/to range for the time; an expression for the ID (`41, 6008`, `>7000`, `7000-7040`,
Expand Down
Binary file added docs/events.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openeventviewer-ui",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "openeventviewer"
version = "0.1.0"
version = "1.0.0"
description = "Read the Windows event logs and filter them down to what matters"
authors = ["thorstenalpers"]
repository = "https://github.com/thorstenalpers/OpenEventViewer"
Expand Down
8 changes: 4 additions & 4 deletions src-tauri/app-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/128x128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/128x128@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/64x64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/Square107x107Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/Square142x142Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/Square150x150Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/Square284x284Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/Square30x30Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/Square310x310Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/Square44x44Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/Square71x71Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/Square89x89Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/StoreLogo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src-tauri/icons/icon.icns
Binary file not shown.
Binary file modified src-tauri/icons/icon.ico
Binary file not shown.
Binary file modified src-tauri/icons/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 55 additions & 5 deletions src-tauri/src/eventlog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ use windows::Win32::Foundation::{
ERROR_TIMEOUT, WIN32_ERROR,
};
use windows::Win32::System::EventLog::{
EvtClose, EvtFormatMessage, EvtFormatMessageEvent, EvtFormatMessageTask, EvtNext,
EvtNextChannelPath, EvtOpenChannelEnum, EvtOpenPublisherMetadata, EvtQuery,
EvtQueryChannelPath, EvtQueryReverseDirection, EvtQueryTolerateQueryErrors, EvtRender,
EvtRenderEventXml, EVT_HANDLE,
EvtChannelConfigType, EvtChannelTypeAnalytic, EvtChannelTypeDebug, EvtClose, EvtFormatMessage,
EvtFormatMessageEvent, EvtFormatMessageTask, EvtGetChannelConfigProperty, EvtNext,
EvtNextChannelPath, EvtOpenChannelConfig, EvtOpenChannelEnum, EvtOpenPublisherMetadata,
EvtQuery, EvtQueryChannelPath, EvtQueryReverseDirection, EvtQueryTolerateQueryErrors,
EvtRender, EvtRenderEventXml, EVT_HANDLE, EVT_VARIANT,
};

use crate::error::{AppError, AppResult};
Expand Down Expand Up @@ -170,13 +171,48 @@ pub fn list_channels() -> AppResult<Vec<String>> {
let mut buffer = vec![0u16; used as usize];
unsafe { EvtNextChannelPath(enumerator.0, Some(&mut buffer), &mut used) }
.map_err(|error| describe(&error, "listing the channels"))?;
channels.push(from_wide(&buffer));
let channel = from_wide(&buffer);
if queryable(&channel) {
channels.push(channel);
}
}

channels.sort_unstable();
Ok(channels)
}

/// Whether `EvtQuery` will take this channel at all.
///
/// Analytic and Debug channels answer every query with ERROR_NOT_SUPPORTED, so offering them in
/// the picker sells a query that can only fail. A channel whose type will not say stays offered —
/// wrongly hiding a readable channel is the worse failure.
fn queryable(channel: &str) -> bool {
let path = wide(channel);
let Ok(config) = (unsafe { EvtOpenChannelConfig(None, PCWSTR(path.as_ptr()), 0) }) else {
return true;
};
let config = Handle(config);

let mut variant: EVT_VARIANT = unsafe { std::mem::zeroed() };
let mut used = 0u32;
let read = unsafe {
EvtGetChannelConfigProperty(
config.0,
EvtChannelConfigType,
0,
std::mem::size_of::<EVT_VARIANT>() as u32,
Some(&mut variant),
&mut used,
)
};
if read.is_err() {
return true;
}

let kind = unsafe { variant.Anonymous.UInt32Val } as i32;
kind != EvtChannelTypeAnalytic.0 && kind != EvtChannelTypeDebug.0
}

pub fn query(filter: &Filter) -> AppResult<QueryResult> {
let started = std::time::Instant::now();
let wanted = filter.max.clamp(1, MAX_EVENTS);
Expand Down Expand Up @@ -1061,6 +1097,20 @@ mod tests {

assert!(channels.iter().any(|name| name == "System"));
assert!(channels.iter().any(|name| name == "Application"));
// A Debug or Analytic name is only a convention — some such channels are declared
// Operational and answer fine. What must hold is that everything still on offer answers.
for name in channels
.iter()
.filter(|name| name.ends_with("/Debug") || name.ends_with("/Analytic"))
{
let filter = Filter {
channels: vec![name.clone()],
max: 1,
..Filter::default()
};
query(&filter)
.unwrap_or_else(|error| panic!("{name} is offered but refuses a query: {error:?}"));
}
println!("{} channels", channels.len());
}
}
12 changes: 6 additions & 6 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.52 0.22 262);
--primary-foreground: oklch(0.99 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
Expand All @@ -24,7 +24,7 @@
--warning: oklch(0.72 0.16 75);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.52 0.22 262);
--ring: oklch(0.708 0 0);
--radius: 0.75rem;

--sidebar: oklch(0.985 0 0);
Expand All @@ -44,8 +44,8 @@
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.66 0.19 262);
--primary-foreground: oklch(0.15 0.02 264);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
Expand All @@ -58,7 +58,7 @@
--warning: oklch(0.8 0.16 80);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.66 0.19 262);
--ring: oklch(0.556 0 0);

--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
Expand Down
119 changes: 0 additions & 119 deletions src/lib/components/app-menu.svelte

This file was deleted.

25 changes: 16 additions & 9 deletions src/lib/components/events-histogram.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

interface Props {
events: EventRecord[];
/** Pins the axis to the time filter's window; an open end falls back to the events. */
span?: { from?: number; to?: number };
class?: string;
}

let { events, class: className }: Props = $props();
let { events, span = {}, class: className }: Props = $props();

const t = $derived(i18n.t);

Expand All @@ -19,7 +21,7 @@
let width = $state(0);
// One bar per ~9 px, so the chart is denser on a wide window rather than stretched.
const columns = $derived(Math.max(12, Math.min(160, Math.floor(width / 9) || 60)));
const chart = $derived(histogram(events, columns));
const chart = $derived(histogram(events, columns, span));
const hovered = $state<{ index: number | null; x: number; y: number }>({
index: null,
x: 0,
Expand Down Expand Up @@ -49,22 +51,26 @@
}

/**
* How tall one bar stands, and how much of it is red.
* How tall one bar stands, and how much of it is red and amber.
*
* The bar is scaled by a square root — one spike of ten thousand would otherwise flatten every
* other bar to a single pixel, and the quiet stretches are the interesting part. The split
* inside it is then a straight proportion: scaling the two segments separately would draw four
* inside it is then a straight proportion: scaling the segments separately would draw four
* errors out of ten as two thirds of the bar.
*/
function bar(entry: Bucket): { total: number; errors: number } {
if (chart.peak === 0 || entry.total === 0) return { total: 0, errors: 0 };
function bar(entry: Bucket): { total: number; errors: number; warnings: number } {
if (chart.peak === 0 || entry.total === 0) return { total: 0, errors: 0, warnings: 0 };
const total = Math.max(
2,
Math.round((Math.sqrt(entry.total) / Math.sqrt(chart.peak)) * HEIGHT)
);
const errors =
entry.errors === 0 ? 0 : Math.max(1, Math.round((entry.errors / entry.total) * total));
return { total, errors };
const warnings =
entry.warnings === 0
? 0
: Math.min(total - errors, Math.max(1, Math.round((entry.warnings / entry.total) * total)));
return { total, errors, warnings };
}

function spanOf(entry: Bucket): string {
Expand Down Expand Up @@ -130,8 +136,9 @@
>
<div
class="w-full rounded-t-[1px] bg-primary/45"
style:height="{size.total - size.errors}px"
style:height="{size.total - size.errors - size.warnings}px"
></div>
<div class="w-full bg-warning" style:height="{size.warnings}px"></div>
<div class="w-full bg-destructive" style:height="{size.errors}px"></div>
</div>
{/each}
Expand All @@ -158,7 +165,7 @@
>
<p class="pb-1 text-[11px] text-muted-foreground tabular-nums">{spanOf(bucket)}</p>
<p class="font-medium">
{t.events.bucketCount(bucket.total, bucket.errors)}
{t.events.bucketCount(bucket.total, bucket.errors, bucket.warnings)}
</p>
{#if tally.length}
<ul class="flex flex-col gap-0.5 pt-1.5">
Expand Down
13 changes: 13 additions & 0 deletions src/lib/components/filters/time-filter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,21 @@
: t.events.filters.before(short(range.to))
);

/** Now, in the wall-clock shape `datetime-local` speaks. */
function nowLocal(): string {
const now = new Date();
const pad = (part: number) => String(part).padStart(2, '0');
return (
`${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` +
`T${pad(now.getHours())}:${pad(now.getMinutes())}`
);
}

function set(edge: 'from' | 'to', value: string) {
const next: TimeRange = { ...range, [edge]: value };
// A lower bound alone almost always means "since then until now" — write the now down, so
// the reader sees the window they got and can move its end if they meant something else.
if (edge === 'from' && value && !next.to) next.to = nowLocal();
onChange(isEmptyTimeRange(next) ? undefined : next);
}
</script>
Expand Down
Loading
Loading