oculi 0.1.0

Structured, Borsh-serialized Anchor events as a drop-in replacement for msg! in Solana programs.
Documentation
  • Coverage
  • 100%
    20 out of 20 items documented0 out of 9 items with examples
  • Size
  • Source code size: 69.83 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 860.23 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 37s Average build duration of successful builds.
  • all releases: 37s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Repository
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • shubhiscoding

Oculi

crates.io docs.rs license

Structured, Borsh-serialized Anchor events — a drop-in replacement for msg! that costs ~93% fewer compute units.

msg! formats strings on-chain. Logging a swap (an amount and a pubkey) costs roughly 11,000 CU per call. Oculi serializes a typed struct into an Anchor event instead — the same data for ~781 CU:

Standard msg!      11086 CU
Oculi log!           781 CU   (92.96% cheaper)

The event lands as a Program data: log in the transaction metadata and decodes from any Borsh-aware client (TypeScript, Python, Go). No string formatting, no runtime schema reflection, no dynamic dispatch.

Install

[dependencies]
oculi = "0.1"
anchor-lang = "0.30"

Tested against anchor-lang 0.30.x. MSRV: Rust 1.75.

Quick start

Define a payload struct and log it. The first argument is the event name; the second is any AnchorSerialize value.

use anchor_lang::prelude::*;
use oculi::log_info;

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct SwapExecuted {
    pub amount: u64,
    pub user: Pubkey,
}

pub fn swap(ctx: Context<Swap>, amount: u64) -> Result<()> {
    log_info!("swap_executed", SwapExecuted {
        amount,
        user: ctx.accounts.user.key(),
    });
    Ok(())
}

Log levels

Each level has a convenience macro that wraps the core log! with the level pre-set:

Macro Level Typical use
log_debug!(name, payload) Debug Verbose internal state snapshots
log_info!(name, payload) Info Milestones indexers should consume
log_warn!(name, payload) Warn Recoverable anomalies monitors should alert on
log_error!(name, payload) Error A sub-operation failed but the instruction recovered

For the most verbose Trace level, call the core macro directly:

use oculi::{log, LogLevel};

log!(LogLevel::Trace, "fn_enter", FnEnter { instruction: 1 });

Zero overhead in production: devnet-only

Enable the devnet-only feature in your mainnet build and every log! — including payload serialization — is compiled out to a no-op. The compiler eliminates the entire logging path, so you pay nothing for log statements you leave in the code.

# mainnet / production
oculi = { version = "0.1", features = ["devnet-only"] }

# devnet / test — logging active
oculi = "0.1"
Feature Default Effect
devnet-only off Compiles out all emit! calls. Enable for mainnet.
no-entrypoint off Forwards to anchor-lang/no-entrypoint for CPI dependents.

Serialization failures don't panic

If Borsh serialization of a payload ever fails, Oculi emits a sentinel event instead of panicking on-chain:

event_name = "__oculi_serialization_error"
payload    = []

The failure stays observable off-chain and no compute budget is wasted unwinding a panic.

Decoding events off-chain

Each event is Borsh-encoded with an 8-byte Anchor discriminator, then:

[discriminator: 8][level: 1][name_len: 4 LE][name: n][payload_len: 4 LE][payload: m]

Decoding in TypeScript:

import { PublicKey } from "@solana/web3.js";
import BN from "bn.js";

const logs = txDetails?.meta?.logMessages ?? [];
const dataLog = logs.find((l) => l.includes("Program data: "));
const buffer = Buffer.from(dataLog!.split("Program data: ")[1], "base64");

let offset = 8;                              // skip 8-byte Anchor discriminator
const level = buffer.readUInt8(offset);      offset += 1;
const nameLen = buffer.readUInt32LE(offset); offset += 4;
const eventName = buffer.subarray(offset, offset + nameLen).toString("utf8");
offset += nameLen;
const payloadLen = buffer.readUInt32LE(offset); offset += 4;
const payload = buffer.subarray(offset, offset + payloadLen);

// Decode your struct — e.g. SwapExecuted: u64 (8 bytes LE) + Pubkey (32 bytes)
const amount = new BN(payload.subarray(0, 8), "le").toNumber();
const user   = new PublicKey(payload.subarray(8, 40)).toBase58();

The LogLevel discriminants are stable: Trace=0, Debug=1, Info=2, Warn=3, Error=4. They are part of the public API and won't change without a major version bump.

API

Export Kind Description
log! macro Core macro; takes a LogLevel, an event name, and a payload
log_debug! / log_info! / log_warn! / log_error! macros Level-specific wrappers over log!
LogLevel enum Severity level; #[repr(u8)] with stable discriminants
OculiEvent struct The Anchor event type emitted by every log! call

Full documentation: docs.rs/oculi.

Contributing

Issues and PRs welcome at github.com/Oculi-logs/Oculi. Run cargo fmt and cargo clippy -- -D warnings before opening a PR — CI enforces both.

License

MIT — see LICENSE.