Semi Structured Logs in Cairo: Complete Solution & Deep Dive Guide
Cairo Enums: The Complete Guide to Building Semi-Structured Logs
Cairo enums, or enumerations, are a powerful type for defining a set of named variants, such as Info, Warning, and Error. This guide demonstrates how to leverage enums with match expressions to create robust, semi-structured log messages for clearer and more auditable smart contracts.
Have you ever found yourself lost in a chaotic sea of debugging messages? Scrambling to decipher cryptic, unstructured text printed from a complex smart contract transaction can be a developer's nightmare. This lack of structure not only slows down debugging but also makes production monitoring and automated alerting nearly impossible. The core problem is the absence of consistency and machine-readability in your output.
This is where Cairo's powerful type system comes to the rescue. By embracing enums, you can impose a clear, predictable structure on your application's logs. In this comprehensive guide, we'll take you from the fundamental theory of enums to building a practical, semi-structured logging system from scratch. You'll learn how to transform messy debugging into an elegant, type-safe, and easily parsable information stream, a critical skill for any serious Cairo developer.
What Exactly Are Enums in Cairo?
Enums, short for enumerations, are a fundamental concept in modern programming languages, and Cairo is no exception. An enum is a custom type that allows you to define a set of possible values, known as variants. Think of it as creating a new category where a variable of that type can only be one of a few specific things you've predefined.
For instance, if you were modeling traffic light states, you could create an enum called TrafficLight with variants Red, Yellow, and Green. A variable of type TrafficLight could only ever hold one of those three values, nothing else. This provides immense type safety, as the Cairo compiler will prevent you from accidentally assigning an invalid state, like "Blue".
In Cairo, defining an enum is straightforward and clean, borrowing its elegant syntax from Rust.
#[derive(Drop, Clone, PartialEq, Debug)]
pub enum LogLevel {
Info,
Warning,
Error,
Debug,
}
In this snippet from the kodikra.com learning module, we define a LogLevel enum. A variable of this type can only be LogLevel::Info, LogLevel::Warning, LogLevel::Error, or LogLevel::Debug. This compile-time guarantee is the foundation of building robust and error-resistant systems.
Key Attributes: The #[derive] Macro
You'll often see the #[derive(...)] attribute above enum and struct definitions. This is a powerful macro that tells the Cairo compiler to automatically generate boilerplate code for certain common traits.
Drop: This is crucial. It tells the compiler how to handle the memory of this type when it goes out of scope. For most custom types in Cairo, making them "droppable" is a necessity.Clone: Allows you to create a deep copy of an enum instance.PartialEq: Generates the code needed to compare two instances of the enum for equality (e.g.,level1 == level2).Debug: Allows the enum to be printed in a developer-friendly format, which is invaluable for debugging.
Why Are Enums the Perfect Tool for Logging?
Using simple strings or the print! macro for logging is tempting due to its simplicity, but it quickly becomes a technical debt. Structured logging is a practice where logs are emitted in a consistent, predictable format, often resembling JSON or key-value pairs. Enums are the first step toward achieving this in Cairo.
The primary advantage is consistency. By forcing every log message to be categorized under a specific LogLevel variant, you guarantee that every log entry will have a clearly defined severity. This consistency is a gift to both human operators and automated tools.
Pros and Cons: Enum-Based Logging vs. Simple Text
To truly understand the benefits, let's compare the enum-based approach with traditional, unstructured text logging.
| Feature | Enum-Based Logging (Our Approach) | Simple print! Logging |
|---|---|---|
| Structure | High. Each message has a guaranteed format like [LEVEL]: message. |
None. Format is arbitrary and can change with every call. |
| Machine Readability | Excellent. Easy to parse with scripts to filter, aggregate, or trigger alerts. | Poor. Requires complex and brittle regular expressions (regex) to parse. |
| Filterability | Trivial. You can easily build logic to only process or display Error logs. |
Very difficult. You must parse the string content to guess the log's severity. |
| Type Safety | High. The compiler prevents you from using an invalid log level like LogLevel::Critical if it's not defined. |
None. A typo like "ERORR" instead of "ERROR" goes unnoticed by the compiler. |
| Maintainability | Excellent. Adding a new log level requires updating one enum definition and the `match` statement (which the compiler will enforce). | Poor. Ensuring all developers use the same string format across a large codebase is a nightmare. |
| Gas Cost | Slightly higher due to `match` logic, but the benefits often outweigh the minimal cost. | Potentially lower for a single call, but less useful in practice. |
How to Implement a Semi-Structured Logger in Cairo
Now, let's dive into the practical implementation based on the exclusive kodikra.com curriculum. We'll build a simple yet powerful logging system by combining an enum, helper functions, and a central dispatcher function that uses a match expression.
The Overall Logic Flow
Before looking at the code, let's visualize the high-level process. The goal is to take a log level (our enum) and a message (a ByteArray) and produce a single, formatted ByteArray.
● Start (level: LogLevel, message: ByteArray)
│
▼
◆ Match on `level` variant
│
├─ is it `LogLevel::Info`? ───► Format as "[INFO]: {message}"
│
├─ is it `LogLevel::Warning`? ► Format as "[WARNING]: {message}"
│
├─ is it `LogLevel::Error`? ──► Format as "[ERROR]: {message}"
│
└─ is it `LogLevel::Debug`? ──► Format as "[DEBUG]: {message}"
│
│
▼
● End (Return the single formatted ByteArray)
Step-by-Step Code Walkthrough
Let's break down the complete solution code. We'll analyze each function to understand its role in the system.
#[derive(Drop)]
pub enum LogLevel {
Info,
Warning,
Error,
Debug,
}
pub fn log(level: LogLevel, message: ByteArray) -> ByteArray {
match level {
LogLevel::Info => info(message),
LogLevel::Warning => warn(message),
LogLevel::Error => error(message),
LogLevel::Debug => format!("[DEBUG]: {message}"),
}
}
pub fn info(message: ByteArray) -> ByteArray {
format!("[INFO]: {message}")
}
pub fn warn(message: ByteArray) -> ByteArray {
format!("[WARNING]: {message}")
}
pub fn error(message: ByteArray) -> ByteArray {
format!("[ERROR]: {message}")
}
1. The LogLevel Enum Definition
pub enum LogLevel { ... }
This is the heart of our system. We define a public (pub) enum named LogLevel. It establishes the four and only four valid levels for our logs. The #[derive(Drop)] attribute is included to ensure Cairo knows how to manage its memory.
2. The Helper Functions: info, warn, and error
pub fn info(message: ByteArray) -> ByteArray { ... }
These functions are responsible for the actual formatting. Each function takes a ByteArray (a dynamic string type in Cairo) as input and returns a new, formatted ByteArray.
- They use the
format!macro, which is a powerful tool for string interpolation in Cairo. - The syntax
{message}inside the string literal injects the value of themessagevariable directly into the string. - This separation of concerns is good practice. Each function has a single, clear responsibility: to format a message for its specific log level.
3. The Central Dispatcher: The log Function
pub fn log(level: LogLevel, message: ByteArray) -> ByteArray { ... }
This is the main entry point for our logging system. It takes a LogLevel enum and a message. Its job is to decide *which* helper function to call based on the provided level.
match level { ... }
The magic happens here with the match expression. A match statement in Cairo is a powerful control flow operator that compares a value against a series of patterns and executes code based on which pattern matches. It's like a super-powered `if-else if-else` chain.
- Exhaustiveness Checking: The Cairo compiler forces
matchstatements to be exhaustive. This means you *must* provide a branch for every single variant of theLogLevelenum. If you were to add a `LogLevel::Trace` variant later and forget to update this `match` statement, your code would fail to compile. This is an incredible safety feature that prevents entire classes of bugs. - Pattern Matching: Each line like
LogLevel::Info => info(message),is a "match arm".LogLevel::Infois the pattern.- The
=>is the "fat arrow" operator, separating the pattern from the code to execute. info(message)is the expression that runs if the pattern matches. The value of this expression is what the entirematchblock returns.
In this specific solution, the Debug case is handled inline with format! directly inside the `match` arm, while the others call their respective helper functions. Both approaches are valid and demonstrate the flexibility of the language.
Visualizing the match Logic
Here's a more detailed flow diagram illustrating how the log function dispatches calls.
● Start: log(level, message)
│
▼
┌─────────────────┐
│ Read `level` │
└────────┬────────┘
│
▼
◆ Condition?
╱ │ ╲
╱ │ ╲
╱ │ ╲
▼ ▼ ▼
`Info` `Warning` `Error` ...
│ │ │
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Call │ │ Call │ │ Call │
│ info() │ │ warn() │ │ error()│
└────────┘ └────────┘ └────────┘
│ │ │
└─────────┼─────────┘
│
▼
┌──────────────────┐
│ Return Formatted │
│ ByteArray │
└──────────────────┘
│
▼
● End
Where Else Can You Use Enums in Cairo?
Enums are far from being a one-trick pony limited to logging. Their ability to model a state that can be one of a few distinct possibilities makes them indispensable across many programming patterns, especially in smart contracts.
State Machines
Smart contracts often represent complex state machines. For example, a contract managing a crowdfunding campaign could have several distinct states.
#[derive(Drop, Serde)]
enum CampaignStatus {
Funding,
Successful,
Failed,
Closed,
}
Using an enum here makes the contract logic much clearer and safer. Functions that should only run during the Funding phase can use a match statement or an `if` condition to ensure they are not called when the campaign is, for example, Closed.
Error Handling
Instead of returning generic error codes (like a simple felt252), you can define a custom error enum that provides more context about what went wrong. This makes off-chain error interpretation significantly easier.
#[derive(Drop, Debug)]
enum TransferError {
InsufficientBalance,
InvalidRecipient,
AmountIsZero,
}
Representing Distinct Choices
Enums are perfect for representing configuration options or different types of assets within a single system. For instance, in a decentralized exchange (DEX) contract, you might represent different order types.
enum OrderType {
Market,
Limit,
StopLoss,
}
This pattern ensures that any function handling orders can robustly check the order type and apply the correct logic, preventing dangerous ambiguity.
Frequently Asked Questions (FAQ)
- What is the main difference between an enum and a struct in Cairo?
-
A
structis used to group related data together, where all fields exist simultaneously. For example, aUserstruct might have anameand anaddress. Anenumdefines a type that can be one of several different variants, but only one at a time. You have eitherLogLevel::InfoORLogLevel::Error, never both at once. - Why is
#[derive(Drop)]so important for enums? -
In Cairo's memory model, the compiler needs to know how to clean up (or "drop") a value when it's no longer needed. For simple types like
u32orfelt252, this is built-in. For custom types like structs and enums, you must explicitly provide this information, and#[derive(Drop)]is the easiest way to have the compiler generate this cleanup logic for you. - Can Cairo enums hold data like in Rust?
-
Yes, absolutely. This is one of their most powerful features. You can define enum variants that carry associated data. For example, you could enhance our error enum:
enum TransferError { InsufficientBalance(u256), InvalidRecipient(ContractAddress) }. This allows you to create highly expressive and data-rich types. - What happens if my
matchstatement doesn't cover all enum variants? -
The Cairo compiler will raise a compilation error. This feature, known as exhaustiveness checking, is a major safety benefit. It forces you to handle every possible case, preventing runtime errors that could occur if a new enum variant was added but the logic to handle it was forgotten.
- Is
ByteArraythe most efficient type for log messages? -
It depends.
ByteArrayis very flexible for arbitrary-length strings. However, it can be more gas-intensive than other types. For very short, fixed messages, using afelt252could be more efficient. For complex data, you might serialize a struct into an array offelt252.ByteArrayis a great general-purpose choice for human-readable logs. - How can I view these logs from a Starknet smart contract?
-
In a real Starknet contract, you wouldn't just format a string. You would emit an event. An event is a special mechanism that writes data to the transaction receipt, making it accessible to off-chain services like indexers (e.g., Apibara, The Graph) and front-end applications. You would define an event struct and then emit it, often with the formatted log message as one of its fields.
- When should I choose an enum over a set of constants?
-
You should almost always prefer an enum. While you could define constants like
const INFO: u8 = 0;, you lose all type safety. A function expecting a log level could be passed any arbitraryu8value. With an enum, the function signature requires aLogLevel, and the compiler guarantees that only valid variants can be passed.
Conclusion: From Chaos to Clarity
We've journeyed from the chaos of unstructured text to the clarity and safety of enum-driven, semi-structured logs. By leveraging Cairo's powerful enum and match constructs, you can create logging systems that are not only easier to read but are also robust, maintainable, and machine-parsable. This pattern elevates your code quality, simplifies debugging, and lays the groundwork for sophisticated monitoring and alerting systems for your dApps.
The principles learned here—type safety, state modeling, and exhaustive pattern matching—are cornerstones of writing secure and reliable smart contracts. Mastering enums is a critical step in becoming a proficient Cairo developer, enabling you to build complex systems with confidence.
Technology Disclaimer: This guide and its code examples are based on Cairo version 2.x and the corresponding Starknet environment. The Cairo language is under active development, and syntax or features may evolve. Always consult the official documentation for the most current information.
Ready to tackle the next challenge? Continue your journey by exploring the next module in the kodikra Cairo Learning Path.
For a broader look at the language's capabilities, be sure to visit our complete guide to Cairo programming.
Published by Kodikra — Your trusted Cairo learning resource.
Post a Comment