Skip to content
Open
32 changes: 32 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,38 @@ jobs:
if: failure()
run: ./scripts/ci-diagnostics.sh

# This will be replaced once #6945 is merged.
cocoalumberjack-integration-unit-tests:
name: SentryCocoaLumberjack Unit Tests
if: needs.files-changed.outputs.run_unit_tests_for_prs == 'true'
needs: files-changed
runs-on: macos-15
steps:
- uses: actions/checkout@v6

- name: Select Xcode
run: ./scripts/ci-select-xcode.sh 16.4

- name: Setup local sentry-cocoa dependency
working-directory: 3rd-party-integrations/SentryCocoaLumberjack
run: swift package edit sentry-cocoa --path ../..

- name: Run CocoaLumberjack tests
working-directory: 3rd-party-integrations/SentryCocoaLumberjack
run: swift test

- name: Archiving Raw Logs
uses: actions/upload-artifact@v5
if: ${{ failure() || cancelled() }}
with:
name: raw-output-cocoalumberjack-integration
path: |
3rd-party-integrations/SentryCocoaLumberjack/.build/**/*.log

- name: Run CI Diagnostics
if: failure()
run: ./scripts/ci-diagnostics.sh

# This check validates that either all unit tests passed or were skipped, which allows us
# to make unit tests a required check with only running the unit tests when required.
# So, we don't have to run unit tests, for example, for Changelog or ReadMe changes.
Expand Down
100 changes: 100 additions & 0 deletions 3rd-party-integrations/SentryCocoaLumberjack/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# --- macOS ---

# General
.DS_Store
__MACOSX/
.AppleDouble
.LSOverride
Icon[]

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

# --- Swift ---

# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## User settings
xcuserdata/

## Obj-C/Swift specific
*.hmap

## App packaging
*.ipa
*.dSYM.zip
*.dSYM

## Playgrounds
timeline.xctimeline
playground.xcworkspace

# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm

.build/

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build/

# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

# --- Xcode ---

## User settings
xcuserdata/

# Archive
*.xcarchive
34 changes: 34 additions & 0 deletions 3rd-party-integrations/SentryCocoaLumberjack/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// swift-tools-version:6.0
import PackageDescription

let package = Package(
name: "SentryCocoaLumberjack",
platforms: [.iOS(.v15), .macOS(.v10_14), .tvOS(.v15), .watchOS(.v8), .visionOS(.v1)],
products: [
.library(
name: "SentryCocoaLumberjack",
targets: ["SentryCocoaLumberjack"]
)
],
dependencies: [
.package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack", from: "3.8.0"),
.package(url: "https://github.com/getsentry/sentry-cocoa", from: "9.0.0")
],
targets: [
.target(
name: "SentryCocoaLumberjack",
dependencies: [
.product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"),
.product(name: "Sentry", package: "sentry-cocoa")
]
),
.testTarget(
name: "SentryCocoaLumberjackTests",
dependencies: [
"SentryCocoaLumberjack",
.product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"),
.product(name: "Sentry", package: "sentry-cocoa")
]
)
]
)
87 changes: 87 additions & 0 deletions 3rd-party-integrations/SentryCocoaLumberjack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Sentry CocoaLumberjack Integration

A [CocoaLumberjack](https://github.com/CocoaLumberjack/CocoaLumberjack) logger that forwards log entries to Sentry's structured logging system, automatically capturing application logs with full context including source location, thread information, and log levels.

> [!NOTE]
> This repo is a mirror of [github.com/getsentry/sentry-cocoa](https://github.com/getsentry/sentry-cocoa). The source code lives in `3rd-party-integrations/SentryCocoaLumberjack/`. This allows users to import only what they need via SPM while keeping all integration code in the main repository.

## Installation

### Swift Package Manager

Add the following dependencies to your `Package.swift` or Xcode package dependencies:

```swift
dependencies: [
.package(url: "https://github.com/getsentry/sentry-cocoa-cocoalumberjack", from: "1.0.0")
]
```

## Quick Start

```swift
import Sentry
import SentryCocoaLumberjack
import CocoaLumberjackSwift

SentrySDK.start { options in
options.dsn = "YOUR_DSN"
options.enableLogs = true
}

DDLog.add(SentryCocoaLumberjackLogger(), with: .info)

DDLogInfo("User logged in")
DDLogError("Payment failed")
DDLogWarn("API rate limit approaching")
DDLogDebug("Processing request")
DDLogVerbose("Detailed trace information")
```

## Configuration

### Log Level Threshold

Use CocoaLumberjack's built-in filtering API when adding the logger to set the minimum log level for messages to be sent to Sentry:

```swift
// Only send error logs to Sentry
DDLog.add(SentryCocoaLumberjackLogger(), with: .error)
```

## Log Level Mapping

CocoaLumberjack log levels are automatically mapped to Sentry log levels:

| CocoaLumberjack Level | Sentry Log Level |
| --------------------- | ---------------- |
| `.error` | `.error` |
| `.warning` | `.warn` |
| `.info` | `.info` |
| `.debug` | `.debug` |
| `.verbose` | `.trace` |

## Automatic Attributes

The logger automatically includes the following attributes with every log entry:

- `sentry.origin`: `"auto.logging.cocoalumberjack"`
- `cocoalumberjack.level`: The original CocoaLumberjack log level (error, warning, info, debug, verbose)
- `cocoalumberjack.file`: The source file name
- `cocoalumberjack.function`: The function name
- `cocoalumberjack.line`: The line number
- `cocoalumberjack.context`: The log context (integer)
- `cocoalumberjack.timestamp`: The log timestamp
- `cocoalumberjack.threadID`: The thread ID
- `cocoalumberjack.threadName`: The thread name (if available)
- `cocoalumberjack.queueLabel`: The dispatch queue label (if available)

## Documentation

- [Sentry Cocoa SDK Documentation](https://docs.sentry.io/platforms/apple/)
- [Sentry Logs Documentation](https://docs.sentry.io/platforms/apple/logs/)
- [CocoaLumberjack Repo](https://github.com/CocoaLumberjack/CocoaLumberjack)

## License

This integration follows the same license as the Sentry Cocoa SDK. See the [LICENSE](https://github.com/getsentry/sentry-cocoa/blob/main/LICENSE.md) file for details.
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import CocoaLumberjackSwift
import Sentry

/// A CocoaLumberjack logger that forwards log entries to Sentry's structured logging system.
///
/// `SentryCocoaLumberjackLogger` implements CocoaLumberjack's `DDAbstractLogger` protocol, allowing you
/// to integrate Sentry's structured logging capabilities with CocoaLumberjack. This enables you to capture
/// application logs from CocoaLumberjack and send them to Sentry for analysis and monitoring.
///
/// ## Level Filtering
/// Use CocoaLumberjack's built-in filtering API when adding the logger:
/// ```swift
/// DDLog.add(SentryCocoaLumberjackLogger(), with: .info)
/// ```
/// This ensures only logs at or above the specified level are sent to Sentry.
///
/// ## Level Mapping
/// CocoaLumberjack log levels are mapped to Sentry log levels:
/// - `.error` → `.error`
/// - `.warning` → `.warn`
/// - `.info` → `.info`
/// - `.debug` → `.debug`
/// - `.verbose` → `.trace`
///
/// ## Usage
/// ```swift
/// import CocoaLumberjackSwift
/// import Sentry
/// import SentryCocoaLumberjack
///
/// // Initialize Sentry SDK
/// SentrySDK.start { options in
/// options.dsn = "YOUR_DSN"
/// }
///
/// // Add SentryCocoaLumberjackLogger to CocoaLumberjack
/// // Only logs at .info level and above will be sent to Sentry
/// DDLog.add(SentryCocoaLumberjackLogger(), with: .info)
///
/// // Use CocoaLumberjack as usual
/// DDLogInfo("User logged in")
/// DDLogError("Payment failed")
/// ```
///
/// - Note: Sentry Logs is currently in Beta. See the [Sentry Logs Documentation](https://docs.sentry.io/platforms/apple/logs/).
/// - Warning: This logger requires Sentry SDK to be initialized before use.
public class SentryCocoaLumberjackLogger: DDAbstractLogger {

/// Creates a new SentryCocoaLumberjackLogger instance.
///
/// - Note: Make sure to initialize the Sentry SDK before creating this logger.
/// Use `DDLog.add(_:with:)` to configure log level filtering.
public override init() {
super.init()
}

/// Logs a message from CocoaLumberjack to Sentry.
///
/// - Parameter logMessage: The log message from CocoaLumberjack containing the message, level, and metadata.
public override func log(message logMessage: DDLogMessage) {
var attributes: [String: Any] = [:]
attributes["sentry.origin"] = "auto.logging.cocoalumberjack"

attributes["cocoalumberjack.level"] = logFlagToString(logMessage.flag)
attributes["cocoalumberjack.file"] = logMessage.file
attributes["cocoalumberjack.function"] = logMessage.function ?? ""
attributes["cocoalumberjack.line"] = String(logMessage.line)
attributes["cocoalumberjack.context"] = String(logMessage.context)
attributes["cocoalumberjack.timestamp"] = logMessage.timestamp.timeIntervalSince1970
attributes["cocoalumberjack.threadID"] = String(logMessage.threadID)

if let threadName = logMessage.threadName, !threadName.isEmpty {
attributes["cocoalumberjack.threadName"] = threadName
}

if !logMessage.queueLabel.isEmpty {
attributes["cocoalumberjack.queueLabel"] = logMessage.queueLabel
}

forwardToSentry(message: logMessage.message, flag: logMessage.flag, attributes: attributes)
}

private func forwardToSentry(message: String, flag: DDLogFlag, attributes: [String: Any]) {
if flag.contains(.error) {
SentrySDK.logger.error(message, attributes: attributes)
} else if flag.contains(.warning) {
SentrySDK.logger.warn(message, attributes: attributes)
} else if flag.contains(.info) {
SentrySDK.logger.info(message, attributes: attributes)
} else if flag.contains(.debug) {
SentrySDK.logger.debug(message, attributes: attributes)
} else if flag.contains(.verbose) {
SentrySDK.logger.trace(message, attributes: attributes)
} else {
SentrySDK.logger.info(message, attributes: attributes)
}
}

private func logFlagToString(_ flag: DDLogFlag) -> String {
if flag.contains(.error) {
return "error"
} else if flag.contains(.warning) {
return "warning"
} else if flag.contains(.info) {
return "info"
} else if flag.contains(.debug) {
return "debug"
} else if flag.contains(.verbose) {
return "verbose"
} else {
return "unknown"
}
}
}
Loading
Loading