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
32 changes: 30 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [0.2.3] - 2026-01-12

### Added

- **Error parameter support**: All log methods (`v`, `d`, `i`, `w`, `e`) now accept an optional `Error?` parameter
- **Error propagation**: Error objects are now correctly passed to all Tree implementations
- **Enhanced error handling**: Built-in Trees (DebugTree, CrashBufferTree, AsyncTree) now properly process and display error information
- **Error-specific tests**: Added 6 new test cases to verify error parameter functionality across all log levels and scenarios
- **Comprehensive error documentation**: Added "Logging with Errors" section to README (both English and Chinese)

### Fixed

- **Error parameter propagation**: Previously, `error` parameter was hardcoded to `nil` in internal `log()` method
- **SentryTree compatibility**: Error objects are now correctly captured and can be sent to Sentry's `captureException()` instead of just `captureMessage()`
- **CrashBufferTree error handling**: Error information is now included in buffered log messages

### Changed

- **Total test count**: Increased from 96 to 102 tests (6 new error-related tests)

### BREAKING CHANGES

- **None** - This release is fully backward compatible with 0.2.2
- All existing APIs without `error` parameter continue to work
- New `error` parameter is optional with default value `nil`

---

## [0.2.2] - 2026-01-12

### Fixed
Expand All @@ -18,7 +46,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### BREAKING CHANGES

- **None** - This release is fully backward compatible with 0.2.1

---

## [0.2.1] - 2026-01-12
Expand Down Expand Up @@ -129,7 +156,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

| Version | Date | Status |
|---------|------|--------|
| [0.2.2] | 2026-01-12 | **Current Release** - Fix DebugTree accessibility |
| [0.2.3] | 2026-01-12 | **Current Release** - Error parameter support |
| [0.2.2] | 2026-01-12 | Fix DebugTree accessibility |
| [0.2.1] | 2026-01-12 | Update source URL to HTTPS |
| [0.2.0] | 2026-01-09 | Stability & Security Improvements |
| [0.1.0] | 2026-01-08 | Initial release |
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ All tests must pass before merging.

1. Update version in `Package.swift`
2. Update CHANGELOG.md with new version
3. Create git tag (`git tag v0.2.2`)
4. Push tag (`git push origin v0.2.2`)
3. Create git tag (`git tag v0.2.3`)
4. Push tag (`git push origin v0.2.3`)

### Release Checklist

Expand All @@ -201,7 +201,7 @@ All tests must pass before merging.
### Release Notes Format

```markdown
## v0.2.2
## v0.2.3

### New Features
- Feature description
Expand Down
2 changes: 1 addition & 1 deletion Canopy.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'Canopy'
s.version = '0.2.2'
s.version = '0.2.3'
s.summary = 'A lightweight, high-performance logging framework for iOS'
s.description = <<-DESC
Canopy is a logging framework inspired by Android's Timber, using a Tree-based architecture.
Expand Down
129 changes: 127 additions & 2 deletions Canopy/Sources/Canopy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,108 @@ public struct TaggedTreeProxy {
public func e(_ message: @autoclosure () -> String, _ args: CVarArg..., file: StaticString = #file, function: StaticString = #function, line: UInt = #line) {
Canopy.log(LogLevel.error, message(), args, file: file, function: function, line: line, withTag: tag)
}

// MARK: - Log Methods with Error

public func v(
_ message: @autoclosure () -> String,
error: Error?,
_ args: CVarArg...,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line
) {
Canopy.log(
LogLevel.verbose,
message(),
args,
file: file,
function: function,
line: line,
withTag: tag,
error: error
)
}

public func d(
_ message: @autoclosure () -> String,
error: Error?,
_ args: CVarArg...,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line
) {
Canopy.log(
LogLevel.debug,
message(),
args,
file: file,
function: function,
line: line,
withTag: tag,
error: error
)
}

public func i(
_ message: @autoclosure () -> String,
error: Error?,
_ args: CVarArg...,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line
) {
Canopy.log(
LogLevel.info,
message(),
args,
file: file,
function: function,
line: line,
withTag: tag,
error: error
)
}

public func w(
_ message: @autoclosure () -> String,
error: Error?,
_ args: CVarArg...,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line
) {
Canopy.log(
LogLevel.warning,
message(),
args,
file: file,
function: function,
line: line,
withTag: tag,
error: error
)
}

public func e(
_ message: @autoclosure () -> String,
error: Error?,
_ args: CVarArg...,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line
) {
Canopy.log(
LogLevel.error,
message(),
args,
file: file,
function: function,
line: line,
withTag: tag,
error: error
)
}
}

/// The main entry point for the Canopy logging framework.
Expand Down Expand Up @@ -87,6 +189,28 @@ public enum Canopy {
log(LogLevel.error, message(), args, file: file, function: function, line: line, withTag: tag)
}

// MARK: - Log Methods with Error

public static func v(_ message: @autoclosure () -> String, error: Error?, _ args: CVarArg..., tag: String? = nil, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) {
log(LogLevel.verbose, message(), args, file: file, function: function, line: line, withTag: tag, error: error)
}

public static func d(_ message: @autoclosure () -> String, error: Error?, _ args: CVarArg..., tag: String? = nil, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) {
log(LogLevel.debug, message(), args, file: file, function: function, line: line, withTag: tag, error: error)
}

public static func i(_ message: @autoclosure () -> String, error: Error?, _ args: CVarArg..., tag: String? = nil, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) {
log(LogLevel.info, message(), args, file: file, function: function, line: line, withTag: tag, error: error)
}

public static func w(_ message: @autoclosure () -> String, error: Error?, _ args: CVarArg..., tag: String? = nil, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) {
log(LogLevel.warning, message(), args, file: file, function: function, line: line, withTag: tag, error: error)
}

public static func e(_ message: @autoclosure () -> String, error: Error?, _ args: CVarArg..., tag: String? = nil, file: StaticString = #file, function: StaticString = #function, line: UInt = #line) {
log(LogLevel.error, message(), args, file: file, function: function, line: line, withTag: tag, error: error)
}

// MARK: - Internal Helpers

private static func hasNonDebugTrees() -> Bool {
Expand All @@ -106,7 +230,8 @@ public enum Canopy {
file: StaticString,
function: StaticString,
line: UInt,
withTag tag: String?
withTag tag: String?,
error: Error? = nil
) {
#if !DEBUG
guard hasNonDebugTrees() else { return }
Expand All @@ -126,7 +251,7 @@ public enum Canopy {
tag: nil,
message: message,
arguments: args,
error: nil,
error: error,
file: file,
function: function,
line: line
Expand Down
10 changes: 9 additions & 1 deletion Canopy/Sources/CrashBufferTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,21 @@ public final class CrashBufferTree: Tree, @unchecked Sendable {
explicitTag = nil

let tagString = effectiveTag ?? ""
let msg = tagString.isEmpty ? "[\(priority)] : \(message())" : "[\(priority)] : \(tagString): \(message())"
let fullMessage = buildFullMessage(message(), error: error)
let msg = tagString.isEmpty ? "[\(priority)] : \(fullMessage)" : "[\(priority)] : \(tagString): \(fullMessage)"
lock.lock()
buffer.append(msg)
if buffer.count > maxSize { buffer.removeFirst() }
lock.unlock()
}

nonisolated private func buildFullMessage(_ message: String, error: Error?) -> String {
if let err = error {
return "\(message) | Error: \(err.localizedDescription)"
}
return message
}

nonisolated func flush() {
lock.lock()
defer { lock.unlock() }
Expand Down
Loading