diff --git a/.github/file-filters.yml b/.github/file-filters.yml index 473d7ddfae..cc3609bcef 100644 --- a/.github/file-filters.yml +++ b/.github/file-filters.yml @@ -25,6 +25,7 @@ run_unit_tests_for_prs: &run_unit_tests_for_prs - "SentryTestUtils/**" - "SentryTestUtilsDynamic/**" - "SentryTestUtilsTests/**" + - "3rd-party-integrations/**" # GH Actions - ".github/workflows/test.yml" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f3dafea4d..e6a967d269 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -369,6 +369,38 @@ jobs: device: "Apple TV" scheme: "Sentry" + # This will be replaced once #6945 is merged. + swiftlog-integration-unit-tests: + name: SentrySwiftLog 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/SentrySwiftLog + run: swift package edit sentry-cocoa --path ../.. + + - name: Run SwiftLog tests + working-directory: 3rd-party-integrations/SentrySwiftLog + run: swift test + + - name: Archiving Raw Logs + uses: actions/upload-artifact@v5 + if: ${{ failure() || cancelled() }} + with: + name: raw-output-swiftlog-integration + path: | + 3rd-party-integrations/SentrySwiftLog/.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. diff --git a/3rd-party-integrations/SentrySwiftLog/.gitignore b/3rd-party-integrations/SentrySwiftLog/.gitignore new file mode 100644 index 0000000000..cffaa2c670 --- /dev/null +++ b/3rd-party-integrations/SentrySwiftLog/.gitignore @@ -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 diff --git a/3rd-party-integrations/SentrySwiftLog/Package.swift b/3rd-party-integrations/SentrySwiftLog/Package.swift new file mode 100644 index 0000000000..56822f20c3 --- /dev/null +++ b/3rd-party-integrations/SentrySwiftLog/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:6.0 +import PackageDescription + +let package = Package( + name: "SentrySwiftLog", + platforms: [.iOS(.v15), .macOS(.v10_14), .tvOS(.v15), .watchOS(.v8), .visionOS(.v1)], + products: [ + .library( + name: "SentrySwiftLog", + targets: ["SentrySwiftLog"] + ) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-log", from: "1.5.0"), + .package(url: "https://github.com/getsentry/sentry-cocoa", from: "9.0.0") + ], + targets: [ + .target( + name: "SentrySwiftLog", + dependencies: [ + .product(name: "Logging", package: "swift-log"), + .product(name: "Sentry", package: "sentry-cocoa") + ] + ), + .testTarget( + name: "SentrySwiftLogTests", + dependencies: [ + "SentrySwiftLog", + .product(name: "Logging", package: "swift-log"), + .product(name: "Sentry", package: "sentry-cocoa") + ] + ) + ] +) diff --git a/3rd-party-integrations/SentrySwiftLog/README.md b/3rd-party-integrations/SentrySwiftLog/README.md new file mode 100644 index 0000000000..6e46fe88e9 --- /dev/null +++ b/3rd-party-integrations/SentrySwiftLog/README.md @@ -0,0 +1,113 @@ +# Sentry SwiftLog Integration + +A [SwiftLog](https://github.com/apple/swift-log) handler that forwards log entries to Sentry's structured logging system, automatically capturing application logs with full context including metadata, source location, 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/SentrySwiftLog/`. 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-swiftlog", from: "1.0.0") +] +``` + +## Quick Start + +```swift +import Sentry +import Logging + +SentrySDK.start { options in + options.dsn = "YOUR_DSN" + options.enableLogs = true +} + +var handler = SentryLogHandler(logLevel: .info) +handler.metadata["app_version"] = "1.0.0" +handler.metadata["environment"] = "production" + +LoggingSystem.bootstrap { _ in handler } + +let logger = Logger(label: "com.example.app") + +logger.info("User logged in", metadata: ["userId": "12345"]) +logger.error("Payment failed", metadata: ["errorCode": 500]) +logger.warning("API rate limit approaching", metadata: ["remaining": 10]) + +logger.info("User action", metadata: [ + "userId": "12345", + "action": "purchase" +]) +``` + +## Configuration + +### Log Level Threshold + +Set the minimum log level for messages to be sent to Sentry. Messages below the configured threshold will be filtered out and not sent to Sentry. + +```swift +let handler = SentryLogHandler(logLevel: .trace) +``` + +### Handler Metadata + +Add metadata that will be included with all log entries. Handler metadata is merged with call-site metadata, with call-site metadata taking precedence. + +```swift +var handler = SentryLogHandler(logLevel: .info) +handler.metadata["app_version"] = "1.0.0" +handler.metadata["environment"] = "production" +``` + +You can also access metadata using subscript syntax: + +```swift +handler[metadataKey: "app_version"] = "1.0.0" +let version = handler[metadataKey: "app_version"] +``` + +## Log Level Mapping + +`swift-log` levels are automatically mapped to Sentry log levels: + +| swift-log Level | Sentry Log Level | +| --------------- | ---------------- | +| `.trace` | `.trace` | +| `.debug` | `.debug` | +| `.info` | `.info` | +| `.notice` | `.info` | +| `.warning` | `.warn` | +| `.error` | `.error` | +| `.critical` | `.fatal` | + +## Metadata Handling + +The handler supports all `swift-log` metadata types including strings, dictionaries, arrays, and string convertible types (numbers, booleans, etc.). All metadata is automatically prefixed with `swift-log.` in Sentry attributes (e.g., `swift-log.userId`, `swift-log.user.id`). See the Quick Start section above for examples of each metadata type. + +## Automatic Attributes + +The handler automatically includes the following attributes with every log entry: + +- `sentry.origin`: `"auto.logging.swift-log"` +- `swift-log.level`: The original swift-log level +- `swift-log.source`: The log source +- `swift-log.file`: The source file name +- `swift-log.function`: The function name +- `swift-log.line`: The line number + +## Documentation + +- [Sentry Cocoa SDK Documentation](https://docs.sentry.io/platforms/apple/) +- [Sentry Logs Documentation](https://docs.sentry.io/platforms/apple/logs/) +- [SwiftLog Repo](https://github.com/apple/swift-log) + +## 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. diff --git a/3rd-party-integrations/SentrySwiftLog/Sources/SentryLogHandler.swift b/3rd-party-integrations/SentrySwiftLog/Sources/SentryLogHandler.swift new file mode 100644 index 0000000000..f734a208f1 --- /dev/null +++ b/3rd-party-integrations/SentrySwiftLog/Sources/SentryLogHandler.swift @@ -0,0 +1,125 @@ +import Logging +import Sentry + +/// A `swift-log` handler that forwards log entries to Sentry's structured logging system. +/// +/// `SentryLogHandler` implements the `swift-log` `LogHandler` protocol, allowing you to integrate +/// Sentry's structured logging capabilities with Swift's standard logging framework. This enables +/// you to capture application logs and send them to Sentry for analysis and monitoring. +/// +/// ## Level Mapping +/// `swift-log` levels are mapped to Sentry log levels: +/// - `.trace` → `.trace` +/// - `.debug` → `.debug` +/// - `.info` → `.info` +/// - `.notice` → `.info` (notice maps to info as SentryLog doesn't have notice) +/// - `.warning` → `.warn` +/// - `.error` → `.error` +/// - `.critical` → `.fatal` +/// +/// ## Usage +/// ```swift +/// import Logging +/// import Sentry +/// +/// // Initialize Sentry SDK +/// SentrySDK.start { options in +/// options.dsn = "YOUR_DSN" +/// } +/// +/// // Register SentryLogHandler +/// LoggingSystem.bootstrap { _ in +/// return SentryLogHandler(logLevel: .trace) +/// } +/// +/// // Create & use the logger +/// let logger = Logger(label: "com.example.app") +/// logger.info("User logged in", metadata: ["userId": "12345"]) +/// logger.error("Payment failed", metadata: ["errorCode": 500]) +/// ``` +/// +/// - Note: Sentry Logs is currently in Beta. See the [Sentry Logs Documentation](https://docs.sentry.io/platforms/apple/logs/). +/// - Warning: This handler requires Sentry SDK to be initialized before use. +public struct SentryLogHandler: LogHandler { + + /// Logger metadata that will be included with all log entries. + /// + /// This metadata is merged with any metadata provided at the call site, + /// with call-site metadata taking precedence over handler metadata. + public var metadata = Logger.Metadata() + + /// The minimum log level for messages to be sent to Sentry. + /// + /// Messages below this level will be filtered out and not sent to Sentry. + /// Defaults to `.info`. + public var logLevel: Logger.Level + + /// Creates a new SentryLogHandler with the specified log level. + /// + /// - Parameter logLevel: The minimum log level for messages to be sent to Sentry. + /// Defaults to `.info`. + public init(logLevel: Logger.Level = .info) { + self.logLevel = logLevel + } + + public func log( + level: Logger.Level, + message: Logger.Message, + metadata: Logger.Metadata?, + source: String, + file: String, + function: String, + line: UInt + ) { + // Filter out messages below the configured log level threshold + guard level >= self.logLevel else { + return + } + + var attributes: [String: Any] = [:] + attributes["sentry.origin"] = "auto.logging.swift-log" + attributes["swift-log.level"] = level.rawValue + attributes["swift-log.source"] = source + attributes["swift-log.file"] = file + attributes["swift-log.function"] = function + attributes["swift-log.line"] = String(line) + + let allMetadata = self.metadata.merging(metadata ?? [:]) { _, new in + new + } + for (key, value) in allMetadata { + attributes["swift-log.\(key)"] = "\(value)" + } + + // Call the appropriate SentryLog method based on level + let messageString = String(describing: message) + let logger = SentrySDK.logger + + switch level { + case .trace: + logger.trace(messageString, attributes: attributes) + case .debug: + logger.debug(messageString, attributes: attributes) + case .info: + logger.info(messageString, attributes: attributes) + case .notice: + // Map notice to info as SentryLog doesn't have notice + logger.info(messageString, attributes: attributes) + case .warning: + logger.warn(messageString, attributes: attributes) + case .error: + logger.error(messageString, attributes: attributes) + case .critical: + logger.fatal(messageString, attributes: attributes) + } + } + + public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { + get { + metadata[metadataKey] + } + set(newValue) { + metadata[metadataKey] = newValue + } + } +} diff --git a/3rd-party-integrations/SentrySwiftLog/Tests/SentryLogHandlerTests.swift b/3rd-party-integrations/SentrySwiftLog/Tests/SentryLogHandlerTests.swift new file mode 100644 index 0000000000..45896a69ef --- /dev/null +++ b/3rd-party-integrations/SentrySwiftLog/Tests/SentryLogHandlerTests.swift @@ -0,0 +1,296 @@ +import Logging +import Sentry +@testable import SentrySwiftLog +import XCTest + +final class SentryLogHandlerTests: XCTestCase { + + private var capturedLogs: [SentryLog] = [] + + override func setUp() { + super.setUp() + capturedLogs = [] + SentrySDK.start { options in + options.dsn = "https://test@test.ingest.sentry.io/123456" + options.enableLogs = true + options.beforeSendLog = { [weak self] log in + self?.capturedLogs.append(log) + return nil + } + } + } + + override func tearDown() { + super.tearDown() + SentrySDK.close() + capturedLogs = [] + } + + // MARK: - Basic Logging Tests + + func testLog_WithInfoLevel() throws { + let sut = SentryLogHandler(logLevel: .info) + sut.log(level: .info, message: "Test info message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 42) + + XCTAssertEqual(capturedLogs.count, 1, "Expected exactly one log to be captured") + let log = try XCTUnwrap(capturedLogs.first) + XCTAssertEqual(log.level, .info) + XCTAssertEqual(log.body, "Test info message") + XCTAssertEqual(log.attributes["sentry.origin"]?.value as? String, "auto.logging.swift-log") + XCTAssertEqual(log.attributes["swift-log.level"]?.value as? String, "info") + XCTAssertEqual(log.attributes["swift-log.source"]?.value as? String, "test") + XCTAssertEqual(log.attributes["swift-log.file"]?.value as? String, "TestFile.swift") + XCTAssertEqual(log.attributes["swift-log.function"]?.value as? String, "testFunction") + XCTAssertEqual(log.attributes["swift-log.line"]?.value as? String, "42") + } + + func testLog_WithErrorLevel() throws { + let sut = SentryLogHandler(logLevel: .info) + sut.log(level: .error, message: "Test error message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 100) + + XCTAssertEqual(capturedLogs.count, 1) + let log = try XCTUnwrap(capturedLogs.first) + XCTAssertEqual(log.level, .error) + XCTAssertEqual(log.body, "Test error message") + XCTAssertEqual(log.attributes["swift-log.level"]?.value as? String, "error") + } + + func testLog_WithTraceLevel() throws { + let sut = SentryLogHandler(logLevel: .trace) + sut.log(level: .trace, message: "Test trace message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 1) + + XCTAssertEqual(capturedLogs.count, 1) + let log = try XCTUnwrap(capturedLogs.first) + XCTAssertEqual(log.level, .trace) + XCTAssertEqual(log.body, "Test trace message") + XCTAssertEqual(log.attributes["swift-log.level"]?.value as? String, "trace") + } + + func testLog_WithDebugLevel() throws { + let sut = SentryLogHandler(logLevel: .debug) + sut.log(level: .debug, message: "Test debug message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 50) + + XCTAssertEqual(capturedLogs.count, 1) + let log = try XCTUnwrap(capturedLogs.first) + XCTAssertEqual(log.level, .debug) + XCTAssertEqual(log.body, "Test debug message") + XCTAssertEqual(log.attributes["swift-log.level"]?.value as? String, "debug") + } + + func testLog_WithWarningLevel() throws { + let sut = SentryLogHandler(logLevel: .info) + sut.log(level: .warning, message: "Test warning message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 75) + + XCTAssertEqual(capturedLogs.count, 1) + let log = try XCTUnwrap(capturedLogs.first) + XCTAssertEqual(log.level, .warn) + XCTAssertEqual(log.body, "Test warning message") + XCTAssertEqual(log.attributes["swift-log.level"]?.value as? String, "warning") + } + + func testLog_WithCriticalLevel() throws { + let sut = SentryLogHandler(logLevel: .info) + sut.log(level: .critical, message: "Test critical message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 200) + + XCTAssertEqual(capturedLogs.count, 1) + let log = try XCTUnwrap(capturedLogs.first) + XCTAssertEqual(log.level, .fatal) + XCTAssertEqual(log.body, "Test critical message") + XCTAssertEqual(log.attributes["swift-log.level"]?.value as? String, "critical") + } + + func testLog_WithNoticeLevel() throws { + let sut = SentryLogHandler(logLevel: .info) + sut.log(level: .notice, message: "Test notice message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 150) + + // Notice should map to info + XCTAssertEqual(capturedLogs.count, 1) + let log = try XCTUnwrap(capturedLogs.first) + XCTAssertEqual(log.level, .info) + XCTAssertEqual(log.body, "Test notice message") + XCTAssertEqual(log.attributes["swift-log.level"]?.value as? String, "notice") + } + + // MARK: - Metadata Tests + + func testLog_WithStringMetadata() throws { + let sut = SentryLogHandler(logLevel: .info) + let metadata: Logger.Metadata = [ + "user_id": "12345", + "session_id": "abc-def-ghi" + ] + + sut.log(level: .info, message: "Test with metadata", metadata: metadata, source: "test", file: "TestFile.swift", function: "testFunction", line: 10) + + XCTAssertEqual(capturedLogs.count, 1) + let log = try XCTUnwrap(capturedLogs.first) + XCTAssertEqual(log.attributes["swift-log.user_id"]?.value as? String, "12345") + XCTAssertEqual(log.attributes["swift-log.session_id"]?.value as? String, "abc-def-ghi") + } + + func testLog_WithHandlerMetadata() throws { + var sut = SentryLogHandler(logLevel: .info) + sut.metadata["app_version"] = "1.0.0" + sut.metadata["environment"] = "test" + + sut.log(level: .info, message: "Test with handler metadata", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 20) + + XCTAssertEqual(capturedLogs.count, 1) + let log = try XCTUnwrap(capturedLogs.first) + XCTAssertEqual(log.attributes["swift-log.app_version"]?.value as? String, "1.0.0") + XCTAssertEqual(log.attributes["swift-log.environment"]?.value as? String, "test") + } + + func testLog_WithMergedMetadata() throws { + var sut = SentryLogHandler(logLevel: .info) + sut.metadata["app_version"] = "1.0.0" + + let logMetadata: Logger.Metadata = [ + "user_id": "12345", + "app_version": "2.0.0" // This should override handler metadata + ] + + sut.log(level: .info, message: "Test with merged metadata", metadata: logMetadata, source: "test", file: "TestFile.swift", function: "testFunction", line: 30) + + XCTAssertEqual(capturedLogs.count, 1) + let log = try XCTUnwrap(capturedLogs.first) + XCTAssertEqual(log.attributes["swift-log.user_id"]?.value as? String, "12345") + XCTAssertEqual(log.attributes["swift-log.app_version"]?.value as? String, "2.0.0") // Should be overridden + } + + func testLog_WithStringConvertibleMetadata() throws { + let sut = SentryLogHandler(logLevel: .info) + let metadata: Logger.Metadata = [ + "count": Logger.MetadataValue.stringConvertible(42), + "enabled": Logger.MetadataValue.stringConvertible(true), + "score": Logger.MetadataValue.stringConvertible(3.14159) + ] + + sut.log(level: .info, message: "Test with string convertible metadata", metadata: metadata, source: "test", file: "TestFile.swift", function: "testFunction", line: 40) + + XCTAssertEqual(capturedLogs.count, 1) + let log = try XCTUnwrap(capturedLogs.first) + XCTAssertEqual(log.attributes["swift-log.count"]?.value as? String, "42") + XCTAssertEqual(log.attributes["swift-log.enabled"]?.value as? String, "true") + XCTAssertEqual(log.attributes["swift-log.score"]?.value as? String, "3.14159") + } + + func testLog_WithDictionaryMetadata() throws { + let sut = SentryLogHandler(logLevel: .info) + let metadata: Logger.Metadata = [ + "user": Logger.MetadataValue.dictionary([ + "id": "12345", + "name": "John Doe" + ]) + ] + + sut.log(level: .info, message: "Test with dictionary metadata", metadata: metadata, source: "test", file: "TestFile.swift", function: "testFunction", line: 50) + + XCTAssertEqual(capturedLogs.count, 1) + let log = try XCTUnwrap(capturedLogs.first) + let userString = log.attributes["swift-log.user"]?.value as? String + XCTAssertNotNil(userString) + XCTAssertTrue(userString?.contains("\"id\": \"12345\"") ?? false) + XCTAssertTrue(userString?.contains("\"name\": \"John Doe\"") ?? false) + } + + func testLog_WithArrayMetadata() throws { + let sut = SentryLogHandler(logLevel: .info) + let metadata: Logger.Metadata = [ + "tags": Logger.MetadataValue.array([ + Logger.MetadataValue.string("production"), + Logger.MetadataValue.string("api"), + Logger.MetadataValue.stringConvertible(42) + ]) + ] + + sut.log(level: .info, message: "Test with array metadata", metadata: metadata, source: "test", file: "TestFile.swift", function: "testFunction", line: 60) + + XCTAssertEqual(capturedLogs.count, 1) + let log = try XCTUnwrap(capturedLogs.first) + let tagsString = log.attributes["swift-log.tags"]?.value as? String + XCTAssertNotNil(tagsString) + XCTAssertTrue(tagsString?.contains("\"production\"") ?? false) + XCTAssertTrue(tagsString?.contains("\"api\"") ?? false) + XCTAssertTrue(tagsString?.contains("\"42\"") ?? false) + } + + // MARK: - Log Level Configuration Tests + + func testLogLevelConfiguration() { + let sut = SentryLogHandler(logLevel: .info) + XCTAssertEqual(sut.logLevel, .info) + } + + func testLogLevelFiltering_InfoThreshold_FiltersTraceAndDebug() { + let sut = SentryLogHandler(logLevel: .info) + + sut.log(level: .trace, message: "Trace message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 1) + sut.log(level: .debug, message: "Debug message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 2) + + XCTAssertEqual(capturedLogs.count, 0, "Expected no logs to be captured") + } + + func testLogLevelFiltering_InfoThreshold_CapturesInfo() { + let sut = SentryLogHandler(logLevel: .info) + + sut.log(level: .info, message: "Info message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 3) + + XCTAssertEqual(capturedLogs.count, 1) + XCTAssertEqual(capturedLogs.first?.level, .info) + } + + func testLogLevelFiltering_InfoThreshold_CapturesNotice() { + let sut = SentryLogHandler(logLevel: .info) + + sut.log(level: .notice, message: "Notice message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 4) + + XCTAssertEqual(capturedLogs.count, 1) + XCTAssertEqual(capturedLogs.first?.level, .info) // Notice maps to info + } + + func testLogLevelFiltering_InfoThreshold_CapturesWarning() { + let sut = SentryLogHandler(logLevel: .info) + + sut.log(level: .warning, message: "Warning message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 5) + + XCTAssertEqual(capturedLogs.count, 1) + XCTAssertEqual(capturedLogs.first?.level, .warn) + } + + func testLogLevelFiltering_InfoThreshold_CapturesError() { + let sut = SentryLogHandler(logLevel: .info) + + sut.log(level: .error, message: "Error message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 6) + + XCTAssertEqual(capturedLogs.count, 1) + XCTAssertEqual(capturedLogs.first?.level, .error) + } + + func testLogLevelFiltering_InfoThreshold_CapturesCritical() { + let sut = SentryLogHandler(logLevel: .info) + + sut.log(level: .critical, message: "Critical message", metadata: nil, source: "test", file: "TestFile.swift", function: "testFunction", line: 7) + + XCTAssertEqual(capturedLogs.count, 1) + XCTAssertEqual(capturedLogs.first?.level, .fatal) + } + + func testMetadataSubscript() { + var sut = SentryLogHandler(logLevel: .info) + XCTAssertNil(sut.metadata["test_key"]) + XCTAssertNil(sut[metadataKey: "test_key_2"]) + + sut.metadata["test_key"] = "test_value" + XCTAssertEqual(sut.metadata["test_key"], .string("test_value")) + + sut[metadataKey: "test_key_2"] = "test_value_2" + XCTAssertEqual(sut[metadataKey: "test_key_2"], .string("test_value_2")) + + sut.metadata["test_key"] = nil + XCTAssertNil(sut.metadata["test_key"]) + + sut[metadataKey: "test_key_2"] = nil + XCTAssertNil(sut[metadataKey: "test_key_2"]) + } +}