Oculi
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
[]
= "0.1"
= "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 *;
use log_info;
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 ;
log!;
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
= { = "0.1", = ["devnet-only"] }
# devnet / test — logging active
= "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.