//! `atgc logs`: reading atgc's own local logs back out. //! //! Each log atgc keeps gets one reader here, named for the log it reads: //! [`mod@oauth`] is `atgc logs oauth`, [`mod@pds`] is `atgc logs pds`, //! [`mod@git`] is `atgc logs git`. They are three implementations of one //! thing, and [`mod@render`] is that thing: the filters, the palette, the //! invocation grouping and the follow loop, generic over what a log's events //! happen to be. A person who has learned to read one has learned to read the //! others, which is the whole reason the rendering is shared rather than //! merely similar. //! //! These readers are the halves of the logging that cannot write. The writers //! live beside the code they instrument: [`crate::logging::oauth`] under jacquard's //! OAuth transport, [`crate::logging::pds`] under the same transport a layer out, //! [`crate::logging::git`] under the one function that spawns a git process: //! for the reason those modules give: a writer's promise about what it never //! records is only auditable while the writer is small enough to read in one //! sitting, and several hundred lines of formatting and filtering would bury //! it. Nothing in this directory opens a file for writing. //! //! What both writers share is in [`crate::logging::file`]: the envelope, the size //! cap, the rotation, and the invocation id that lets a line from one log be //! laid beside a line from the other. pub(crate) mod git; pub(crate) mod oauth; pub(crate) mod pds; pub(crate) mod render; /// The `atgc logs` readers. #[derive(clap::Subcommand, Debug)] pub(crate) enum Command { /// Read the OAuth event log at ~/.config/atgc/oauth.jsonl (also: /// ATGC_OAUTH_LOG to change the path; ATGC_OAUTH_LOG=off disables logging) /// /// Events are grouped by invocation, newest last, and every row carries /// the invocation that wrote it and, on token events, the client_id /// fingerprint that actually went on the wire. Both generations of the /// log are read, so a rotation does not hide history. /// /// Examples: /// atgc logs oauth --failures --since 2h /// atgc logs oauth --inv 3f2a1b0c /// atgc logs oauth --event 'token_*' --json | jq .client_id.fp /// atgc logs oauth -f -n 0 /// atgc logs oauth --incident // Without this clap reflows the example block into one paragraph, and a // command line that has been wrapped is not an example of anything. #[command(verbatim_doc_comment)] Oauth { #[command(flatten)] args: OauthArgs, }, /// Read the PDS write log at ~/.config/atgc/pds.jsonl (also: ATGC_PDS_LOG /// to change the path; ATGC_PDS_LOG=off disables logging) /// /// One line per write leaving atgc and one per answer coming back: /// which collection and record key, the swapRecord/swapCommit /// precondition that was sent, and the at-uri and CID the PDS returned. /// Record content is never written here: a value appears only as an /// eight-character fingerprint, which is enough to tell two writes apart /// and not enough to reconstruct either. /// /// Same flags, same rendering and the same invocation ids as `logs /// oauth`, so a line from each can be read side by side. /// /// Examples: /// atgc logs pds --failures --since 2h /// atgc logs pds --event write_refused /// atgc logs pds --json | jq -c '{ts, op, cid}' /// atgc logs pds -f -n 0 #[command(verbatim_doc_comment)] Pds { #[command(flatten)] args: render::Args, }, /// Read the git subprocess log at ~/.config/atgc/git.jsonl (also: /// ATGC_GIT_LOG to change the path; ATGC_GIT_LOG=off disables logging) /// /// One line per git subprocess starting and one per exit: the argv as /// git received it, the directory, the exit status, and, for the /// commands that can move a ref: where HEAD stood before and after. /// This is what `git reflog` cannot say: that atgc was the thing that /// moved your branch, and with which arguments. Credentials in a remote /// URL, `-c` overrides and config values are fingerprinted, never /// written; stdin is recorded only as a length. /// /// A non-zero exit is often the answer rather than a fault here: /// `merge-base --is-ancestor` replies by exit status, so --failures /// selects everything that did not return 0. /// /// Examples: /// atgc logs git --since 30m /// atgc logs git --event exit --json | jq -c '{verb, before, after}' /// atgc logs git --inv 3f2a1b0c /// atgc logs git -f -n 0 #[command(verbatim_doc_comment)] Git { #[command(flatten)] args: render::Args, }, } /// The OAuth log's flags: the shared set, plus the one view only it has. /// /// `--incident` is not in [`render::Args`] because it answers a question /// about OAuth sessions specifically, and a flag the other reader would have /// to refuse is worse than a flag it never offers. #[derive(clap::Args, Debug)] pub(crate) struct OauthArgs { #[command(flatten)] pub common: render::Args, /// Compare client_ids across authorization and refresh, and look for /// overlapping invocations around a failure #[arg(long, conflicts_with_all = ["follow", "json"])] pub incident: bool, } /// Run whichever `logs` reader was parsed. /// /// Not `async`: both readers only open a local file, and `--follow` blocks on /// a sleep loop rather than on a socket. pub(crate) fn run(command: Command) -> anyhow::Result<()> { match command { Command::Oauth { args } => oauth::log(args.common, args.incident), Command::Pds { args } => pds::log(args), Command::Git { args } => git::log(args), } }