diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 73a282bb09..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Bug report -about: Report a bug -title: '' -labels: bug -assignees: '' - ---- - -**Describe what happened** -Include any error message or stack trace if available. - -**Steps to reproduce the issue:** -๐Ÿ“ - -**Expected behaviour:** -๐Ÿ“ - -**Actual behaviour:** -๐Ÿ“ - -**Additional context** - - OS version and device model - - Datadog SDK version - - an explanation of what might cause the bug and/or how it can be fixed diff --git a/.github/ISSUE_TEMPLATE/compilation_issue.md b/.github/ISSUE_TEMPLATE/compilation_issue.md new file mode 100644 index 0000000000..840e566a2b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/compilation_issue.md @@ -0,0 +1,46 @@ +--- +name: Compilation Issue +about: Having a Cocoapods / Carthage / SPM problem when linking the SDK? +title: '' +labels: compilation issue +assignees: '' + +--- + +### The issue + +๐Ÿ“ Give us the error message you receive, describe the problem and answer the questions. + +--- + +#### Datadog SDK version: + +_Which version of the Datadog SDK causes this problem? e.g. `1.2.0`_ + +#### Last working Datadog SDK version: + +_What is the last Datadog SDK version where this problem didn't occur? e.g. `1.1.0`_ + +#### Dependency Manager: + +_Which dependency manager do you use? e.g. Cocoapods / Carthage / SPM / ..._ + +#### Other toolset: + +_Do you use additional tools with your dependency manager? e.g. [CarthageCache](https://github.com/Wolox/carthage_cache)_ + +#### Xcode version: + +_e.g. `Xcode 11.5 (11E608c)`_ + +#### Swift version: + +_e.g. `5.1`_ + +#### Deployment Target: + +_What is the Deployment Target of your app? e.g. `iOS 12`, `iPhone` + `iPad`_ + +#### macOS version: + +_e.g. `macOS Catalina 10.15.5 (19F96)`_ diff --git a/.github/ISSUE_TEMPLATE/crash_report.md b/.github/ISSUE_TEMPLATE/crash_report.md new file mode 100644 index 0000000000..f32d11f9a0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crash_report.md @@ -0,0 +1,42 @@ +--- +name: Crash +about: Noticed the SDK crash? +title: '' +labels: crash +assignees: '' + +--- + +### The crash + +๐Ÿ“ Give us the crash report or stack trace, describe the problem in details and answer the questions. + +--- + +#### Datadog SDK versions: + +_Which version(s) of the Datadog SDK you see this crash happening in?_ + +#### Last stable Datadog SDK version: + +_What is the last Datadog SDK version where this crash doesn't happen?_ + +#### Volume: + +_What % of your app sessions is impacted with this crash?_ + +#### OS version: + +_Which iOS versions does this crash happen on?_ + +#### Deployment Target: + +_What is the Deployment Target of your app? e.g. `iOS 12`, `iPhone` + `iPad`_ + +#### Device version: + +_Which devices does this crash happen on? e.g. `iPhone X` only or various iPads_ + +#### Environment: + +_Do you notice any environment correlation in crash reports? e.g. low battery, no internet connection, memory pressure_ diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index de0d3dd35d..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea or request a feature -title: '' -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe:** -๐Ÿ“ A clear and concise description of what the problem is. - -**Describe the solution you'd like:** -๐Ÿ“ A clear and concise description of what you want to happen. - -**Describe alternatives you've considered:** -๐Ÿ“ A clear and concise description of any alternative solutions or features you've considered. - -**Additional context:** -๐Ÿ“ Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/other.md b/.github/ISSUE_TEMPLATE/other.md new file mode 100644 index 0000000000..951afbf02c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/other.md @@ -0,0 +1,11 @@ +--- +name: Other +about: Noticed a bug, having a question or a feature request? +title: '' +assignees: '' + +--- + +### The thing + +Tell us the thing ๐Ÿ™‚ diff --git a/.github/workflows/all-platform-tests.yml b/.github/workflows/all-platform-tests.yml new file mode 100644 index 0000000000..9d701d7a09 --- /dev/null +++ b/.github/workflows/all-platform-tests.yml @@ -0,0 +1,50 @@ +name: Trigger all-platform tests + +on: + pull_request: + types: [opened] + issue_comment: + types: [created] + +jobs: + trigger-all-platform-tests: + + # Only certain repository members can run it + if: github.actor == 'ncreated' || github.actor == 'buranmert' || github.actor == 'nachoBonafonte' + + runs-on: ubuntu-latest + steps: + - name: Look for a keyword triggering the build on Bitrise + id: check-keyword-trigger + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: khan/pull-request-comment-trigger@014b821 + with: + trigger: '@test-all-platforms' + reaction: rocket + - name: Trigger Bitrise build + if: steps.check-keyword-trigger.outputs.triggered == 'true' + env: + BITRISE_APP_SLUG: ${{ secrets.BITRISE_APP_SLUG }} + BITRISE_TOKEN: ${{ secrets.BITRISE_TOKEN }} + CURRENT_PR_SOURCE_BRANCH: ${{ github.head_ref }} + COMMENT_BODY: ${{ github.event.comment.body }} + shell: bash + run: | + if [[ -z "${CURRENT_PR_SOURCE_BRANCH}" ]]; then + # when running on Pull Request comment (get the branch name from comment's body) + SANITIZED_COMMENT=${COMMENT_BODY//[^a-zA-Z0-9@ -\/]/} # sanitize the user input + BRANCH_NAME_REGEXP='[a-zA-Z0-9-]*\/[a-zA-Z0-9-]*' + CURRENT_BRANCH=$(echo "${SANITIZED_COMMENT}" | grep -oe '@test-all-platforms '$BRANCH_NAME_REGEXP | grep -oe $BRANCH_NAME_REGEXP) + else + # when running due to opening a Pull Request + CURRENT_BRANCH="${CURRENT_PR_SOURCE_BRANCH}" + fi + + echo "Calling Bitrise API to run build for branch: $CURRENT_BRANCH" + + curl -X POST "https://api.bitrise.io/v0.1/apps/${BITRISE_APP_SLUG}/builds" \ + -H "accept: application/json" \ + -H "Authorization: ${BITRISE_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ \"build_params\": { \"branch\": \"${CURRENT_BRANCH}\", \"workflow_id\": \"trigger_all_platform_tests\" }, \"hook_info\": { \"type\": \"bitrise\" }}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b867a5643f..6959ee72ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,13 +9,13 @@ To propose improvements, feel free to submit a PR or open an Issue. Many great ideas for new features come from the community, and we'd be happy to consider yours ๐Ÿ‘. -To share your idea or request, [open a GitHub Issue](https://github.com/DataDog/dd-sdk-ios/issues/new) using dedicated issue template. +To share your idea or request, [open a GitHub Issue](https://github.com/DataDog/dd-sdk-ios/issues/new/choose) using dedicated issue template. ## Found a bug? For any urgent matters (such as outages) or issues concerning the Datadog service or UI, contact our support team via https://docs.datadoghq.com/help/ for direct, faster assistance. -You may submit a bug report concerning the Datadog SDK for iOS by [opening a GitHub Issue](https://github.com/DataDog/dd-sdk-ios/issues/new). Use dedicated bug-issue template and provide all listed details to let us solve it better. +You may submit a bug report concerning the Datadog SDK for iOS by [opening a GitHub Issue](https://github.com/DataDog/dd-sdk-ios/issues/new/choose). Use appropriate template and provide all listed details to help us resolve the issue. ## Have a patch? @@ -40,11 +40,9 @@ $ make ### Repo structure -#### Datadog +#### `Datadog.xcworkspace` -1. Datadog.xcodeproj -2. DatadogPrivate _(can be moved to `Sources`)_ - * ObjC -> Swift bridge module +The workspace for SDK development and integration (tests, benchmarks, example app). #### Sources @@ -52,12 +50,8 @@ $ make #### Tests -`DatadogTests` (unit tests) and `DatadogIntegrationTests` source files +`DatadogTests` (unit tests), `DatadogIntegrationTests` (integration tests), and `DatadogBenchmarkTests` (benchmarks) source files #### Dependency manager tests Isolated example apps using `cocoapods`, `carthage` and `spm` to ensure SDK is well integrated with all supported dependency managers. - -#### Examples (to be removed) - -Example apps for different package managers diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 27641d6454..36f75aa302 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -49,10 +49,8 @@ 61133C492423990D00786299 /* DDLoggerBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C152423990D00786299 /* DDLoggerBuilderTests.swift */; }; 61133C4A2423990D00786299 /* DDConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C162423990D00786299 /* DDConfigurationTests.swift */; }; 61133C4B2423990D00786299 /* DDLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C172423990D00786299 /* DDLoggerTests.swift */; }; - 61133C4C2423990D00786299 /* LogsMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C1A2423990D00786299 /* LogsMocks.swift */; }; 61133C4D2423990D00786299 /* CoreTelephonyMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C1B2423990D00786299 /* CoreTelephonyMocks.swift */; }; 61133C4E2423990D00786299 /* UIKitMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C1C2423990D00786299 /* UIKitMocks.swift */; }; - 61133C512423990D00786299 /* DatadogMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C1F2423990D00786299 /* DatadogMocks.swift */; }; 61133C522423990D00786299 /* FoundationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C202423990D00786299 /* FoundationMocks.swift */; }; 61133C532423990D00786299 /* MobileDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C232423990D00786299 /* MobileDeviceTests.swift */; }; 61133C542423990D00786299 /* NetworkConnectionInfoProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C242423990D00786299 /* NetworkConnectionInfoProviderTests.swift */; }; @@ -82,66 +80,180 @@ 61133C6D2423990D00786299 /* TestsDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C462423990D00786299 /* TestsDirectory.swift */; }; 61133C6E2423990D00786299 /* DatadogExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C472423990D00786299 /* DatadogExtensions.swift */; }; 61133C702423993200786299 /* Datadog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* Datadog.framework */; }; + 61216276247D1CD700AC5D67 /* LoggingForTracingAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216275247D1CD700AC5D67 /* LoggingForTracingAdapter.swift */; }; + 6121627C247D220500AC5D67 /* LoggingForTracingAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61216279247D21FE00AC5D67 /* LoggingForTracingAdapterTests.swift */; }; 612983CD2449E62E00D4424B /* LoggingFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612983CC2449E62E00D4424B /* LoggingFeature.swift */; }; + 6132BF4224A38D2400D7BD17 /* OTTracer+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4124A38D2400D7BD17 /* OTTracer+objc.swift */; }; + 6132BF4424A3AAD700D7BD17 /* OTGlobal+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4324A3AAD700D7BD17 /* OTGlobal+objc.swift */; }; + 6132BF4724A498D800D7BD17 /* DDSpan+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4624A498D800D7BD17 /* DDSpan+objc.swift */; }; + 6132BF4924A49B6800D7BD17 /* DDSpanContext+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4824A49B6800D7BD17 /* DDSpanContext+objc.swift */; }; + 6132BF4C24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4B24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift */; }; + 6132BF4E24A49D5400D7BD17 /* OTNoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF4D24A49D5400D7BD17 /* OTNoop.swift */; }; + 6132BF5124A49F7400D7BD17 /* Casting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6132BF5024A49F7400D7BD17 /* Casting.swift */; }; 61345613244756E300E7DA6B /* PerformancePresetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61345612244756E300E7DA6B /* PerformancePresetTests.swift */; }; + 61441C0524616DE9003D8BB8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C0424616DE9003D8BB8 /* AppDelegate.swift */; }; + 61441C0C24616DE9003D8BB8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0A24616DE9003D8BB8 /* Main.storyboard */; }; + 61441C0E24616DEC003D8BB8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0D24616DEC003D8BB8 /* Assets.xcassets */; }; + 61441C1124616DEC003D8BB8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 61441C0F24616DEC003D8BB8 /* LaunchScreen.storyboard */; }; + 61441C4024617013003D8BB8 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C3B24617013003D8BB8 /* IntegrationTests.swift */; }; + 61441C4124617013003D8BB8 /* LoggingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C3C24617013003D8BB8 /* LoggingIntegrationTests.swift */; }; + 61441C44246174CE003D8BB8 /* HTTPServerMock in Frameworks */ = {isa = PBXBuildFile; productRef = 61441C43246174CE003D8BB8 /* HTTPServerMock */; }; + 61441C4924618052003D8BB8 /* JSONDataMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BE624519A3700F2C652 /* JSONDataMatcher.swift */; }; + 61441C4A24618052003D8BB8 /* LogMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C432423990D00786299 /* LogMatcher.swift */; }; + 61441C4B24618052003D8BB8 /* SpanMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45ED02451A8730061DAC7 /* SpanMatcher.swift */; }; + 61441C4E24619498003D8BB8 /* Datadog.framework in โš™๏ธ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* Datadog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 61441C6D24619FE4003D8BB8 /* Datadog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* Datadog.framework */; platformFilter = ios; }; + 61441C7A2461A204003D8BB8 /* LoggingBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C782461A204003D8BB8 /* LoggingBenchmarkTests.swift */; }; + 61441C7B2461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C792461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift */; }; + 61441C7C2461A244003D8BB8 /* TestsDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C462423990D00786299 /* TestsDirectory.swift */; }; + 61441C952461A649003D8BB8 /* ConsoleOutputInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C902461A648003D8BB8 /* ConsoleOutputInterceptor.swift */; }; + 61441C962461A649003D8BB8 /* UIButton+Disabling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C912461A648003D8BB8 /* UIButton+Disabling.swift */; }; + 61441C972461A649003D8BB8 /* UIViewController+KeyboardControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C922461A648003D8BB8 /* UIViewController+KeyboardControlling.swift */; }; + 61441C982461A649003D8BB8 /* DebugTracingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C932461A649003D8BB8 /* DebugTracingViewController.swift */; }; + 61441C992461A649003D8BB8 /* DebugLoggingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C942461A649003D8BB8 /* DebugLoggingViewController.swift */; }; + 61441C9D2461A796003D8BB8 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61441C9C2461A796003D8BB8 /* AppConfig.swift */; }; + 614872772485067300E3EBDB /* SpanTagsReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614872762485067300E3EBDB /* SpanTagsReducer.swift */; }; 614E9EB3244719FA007EE3E1 /* BundleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614E9EB2244719FA007EE3E1 /* BundleType.swift */; }; + 6152C83E24BE1C91006A1679 /* HTTPServerMock in Frameworks */ = {isa = PBXBuildFile; productRef = 6152C83D24BE1C91006A1679 /* HTTPServerMock */; }; + 6152C84024BE1CC8006A1679 /* DataUploaderBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6152C83F24BE1CC8006A1679 /* DataUploaderBenchmarkTests.swift */; }; + 61570005246AADFA00E96950 /* DatadogObjc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133BF0242397DA00786299 /* DatadogObjc.framework */; }; + 61570006246AAE5E00E96950 /* DatadogObjc.framework in โš™๏ธ Embed Framework Dependencies */ = {isa = PBXBuildFile; fileRef = 61133BF0242397DA00786299 /* DatadogObjc.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 61570007246AAED100E96950 /* DatadogObjc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133BF0242397DA00786299 /* DatadogObjc.framework */; }; + 615A4A8324A3431600233986 /* Tracer+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8224A3431600233986 /* Tracer+objc.swift */; }; + 615A4A8524A3445700233986 /* TracerConfiguration+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8424A3445700233986 /* TracerConfiguration+objc.swift */; }; + 615A4A8724A3452800233986 /* DDTracerConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8624A3452800233986 /* DDTracerConfigurationTests.swift */; }; + 615A4A8924A34FD700233986 /* DDTracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8824A34FD700233986 /* DDTracerTests.swift */; }; + 615A4A8B24A3568900233986 /* OTSpan+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8A24A3568900233986 /* OTSpan+objc.swift */; }; + 615A4A8D24A356A000233986 /* OTSpanContext+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8C24A356A000233986 /* OTSpanContext+objc.swift */; }; + 617CEB392456BC3A00AD4669 /* TracingUUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617CEB382456BC3A00AD4669 /* TracingUUID.swift */; }; + 618C365F248E85B400520CDE /* DateFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618C365E248E85B400520CDE /* DateFormattingTests.swift */; }; + 61AD4E182451C7FF006E34EA /* TracingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AD4E172451C7FF006E34EA /* TracingFeatureMocks.swift */; }; + 61AD4E3824531500006E34EA /* DataFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AD4E3724531500006E34EA /* DataFormat.swift */; }; + 61AD4E3A24534075006E34EA /* TracingFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AD4E3924534075006E34EA /* TracingFeatureTests.swift */; }; 61B558CF2469561C001460D3 /* LoggerBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B558CE2469561C001460D3 /* LoggerBuilderTests.swift */; }; + 61B558D42469CDD8001460D3 /* TracingUUIDGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B558D32469CDD8001460D3 /* TracingUUIDGeneratorTests.swift */; }; + 61B9ED1C2461E12000C0DCFF /* SendLogsFixtureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B9ED1A2461E12000C0DCFF /* SendLogsFixtureViewController.swift */; }; + 61B9ED1D2461E12000C0DCFF /* SendTracesFixtureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B9ED1B2461E12000C0DCFF /* SendTracesFixtureViewController.swift */; }; + 61B9ED1F2461E57700C0DCFF /* UITestsHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B9ED1E2461E57700C0DCFF /* UITestsHelpers.swift */; }; + 61B9ED212462089600C0DCFF /* TracingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B9ED202462089600C0DCFF /* TracingIntegrationTests.swift */; }; 61BB2B1B244A185D009F3F56 /* PerformancePreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61BB2B1A244A185D009F3F56 /* PerformancePreset.swift */; }; 61C363802436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3637F2436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift */; }; 61C3638324361BE200C4D4E6 /* DatadogPrivateMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */; }; 61C3638524361E9200C4D4E6 /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3638424361E9200C4D4E6 /* Globals.swift */; }; 61C36470243B5C8300C4D4E6 /* ServerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */; }; + 61C5A88424509A0C00DA608C /* DDSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87824509A0C00DA608C /* DDSpan.swift */; }; + 61C5A88524509A0C00DA608C /* DDNoOps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87924509A0C00DA608C /* DDNoOps.swift */; }; + 61C5A88624509A0C00DA608C /* TracingUUIDGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87B24509A0C00DA608C /* TracingUUIDGenerator.swift */; }; + 61C5A88724509A0C00DA608C /* Casting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87C24509A0C00DA608C /* Casting.swift */; }; + 61C5A88824509A0C00DA608C /* Warnings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87D24509A0C00DA608C /* Warnings.swift */; }; + 61C5A88924509A0C00DA608C /* DDSpanContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A87E24509A0C00DA608C /* DDSpanContext.swift */; }; + 61C5A88A24509A0C00DA608C /* SpanFileOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A88024509A0C00DA608C /* SpanFileOutput.swift */; }; + 61C5A88B24509A0C00DA608C /* SpanOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A88124509A0C00DA608C /* SpanOutput.swift */; }; + 61C5A88C24509A0C00DA608C /* HTTPHeadersWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A88324509A0C00DA608C /* HTTPHeadersWriter.swift */; }; + 61C5A88E24509A1F00DA608C /* Tracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A88D24509A1F00DA608C /* Tracer.swift */; }; + 61C5A89024509AA700DA608C /* TracingFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A88F24509AA700DA608C /* TracingFeature.swift */; }; + 61C5A89624509BF600DA608C /* TracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89524509BF600DA608C /* TracerTests.swift */; }; + 61C5A89D24509C1100DA608C /* DDSpanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89824509C1100DA608C /* DDSpanTests.swift */; }; + 61C5A89E24509C1100DA608C /* WarningsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89A24509C1100DA608C /* WarningsTests.swift */; }; + 61C5A89F24509C1100DA608C /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89B24509C1100DA608C /* UUID.swift */; }; + 61C5A8A024509C1100DA608C /* Casting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A89C24509C1100DA608C /* Casting.swift */; }; + 61C5A8A624509FAA00DA608C /* SpanEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A8A424509FAA00DA608C /* SpanEncoder.swift */; }; + 61C5A8A724509FAA00DA608C /* SpanBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C5A8A524509FAA00DA608C /* SpanBuilder.swift */; }; + 61D447E224917F8F00649287 /* DateFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D447E124917F8F00649287 /* DateFormatting.swift */; }; + 61E45BCF2450A6EC00F2C652 /* TracingUUIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BCE2450A6EC00F2C652 /* TracingUUIDTests.swift */; }; + 61E45BD22450F65B00F2C652 /* SpanBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BD12450F65B00F2C652 /* SpanBuilderTests.swift */; }; + 61E45BE5245196EA00F2C652 /* SpanFileOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BE4245196EA00F2C652 /* SpanFileOutputTests.swift */; }; + 61E45BE724519A3700F2C652 /* JSONDataMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BE624519A3700F2C652 /* JSONDataMatcher.swift */; }; + 61E45ED12451A8730061DAC7 /* SpanMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45ED02451A8730061DAC7 /* SpanMatcher.swift */; }; + 61E909ED24A24DD3005EA2DE /* OTSpan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E624A24DD3005EA2DE /* OTSpan.swift */; }; + 61E909EE24A24DD3005EA2DE /* OTFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E724A24DD3005EA2DE /* OTFormat.swift */; }; + 61E909EF24A24DD3005EA2DE /* OTGlobal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E824A24DD3005EA2DE /* OTGlobal.swift */; }; + 61E909F024A24DD3005EA2DE /* OTTracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909E924A24DD3005EA2DE /* OTTracer.swift */; }; + 61E909F124A24DD3005EA2DE /* OTReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909EA24A24DD3005EA2DE /* OTReference.swift */; }; + 61E909F224A24DD3005EA2DE /* OTConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909EB24A24DD3005EA2DE /* OTConstants.swift */; }; + 61E909F324A24DD3005EA2DE /* OTSpanContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909EC24A24DD3005EA2DE /* OTSpanContext.swift */; }; + 61E909F624A32D1C005EA2DE /* OTGlobalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E909F524A32D1C005EA2DE /* OTGlobalTests.swift */; }; + 61E917CF2464270500E6C631 /* EncodableValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E917CE2464270500E6C631 /* EncodableValueTests.swift */; }; + 61E917D12465423600E6C631 /* TracerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E917D02465423600E6C631 /* TracerConfiguration.swift */; }; + 61E917D3246546BF00E6C631 /* TracerConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E917D2246546BF00E6C631 /* TracerConfigurationTests.swift */; }; + 61F1A61A2498A51700075390 /* CoreMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F1A6192498A51700075390 /* CoreMocks.swift */; }; + 61F1A621249A45E400075390 /* DDSpanContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F1A620249A45E400075390 /* DDSpanContextTests.swift */; }; + 61F1A623249B811200075390 /* Encoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F1A622249B811200075390 /* Encoding.swift */; }; 61F8CC092469295500FE2908 /* DatadogConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F8CC082469295500FE2908 /* DatadogConfigurationTests.swift */; }; 61FB222D244A21ED00902D19 /* LoggingFeatureMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FB222C244A21ED00902D19 /* LoggingFeatureMocks.swift */; }; 61FB2230244E1BE900902D19 /* LoggingFeatureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FB222F244E1BE900902D19 /* LoggingFeatureTests.swift */; }; - 9E2FB2722447660E001C9B7B /* Datadog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133B82242393DE00786299 /* Datadog.framework */; }; + 9E330A8E24ADE1250031408E /* NSURLSessionBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E330A8D24ADE1250031408E /* NSURLSessionBridge.m */; }; 9E36D92224373EA700BFBDB7 /* SwiftExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */; }; - 9E58E8DF24615B89008E5063 /* ISO8601DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E58E8DE24615B89008E5063 /* ISO8601DateFormatter.swift */; }; + 9E493E1C249B7BAA005F95F5 /* TracingAutoInstrumentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E493E1B249B7BAA005F95F5 /* TracingAutoInstrumentationTests.swift */; }; + 9E50B2E424B49DDF00A2CB95 /* URLFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E50B2E324B49DDF00A2CB95 /* URLFilterTests.swift */; }; + 9E544A4D24752A8900E83072 /* URLSessionSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E544A4C24752A8900E83072 /* URLSessionSwizzlerTests.swift */; }; + 9E544A4F24753C6E00E83072 /* MethodSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E544A4E24753C6E00E83072 /* MethodSwizzler.swift */; }; + 9E544A5124753DDE00E83072 /* MethodSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E544A5024753DDE00E83072 /* MethodSwizzlerTests.swift */; }; 9E58E8E124615C75008E5063 /* JSONEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E58E8E024615C75008E5063 /* JSONEncoder.swift */; }; - 9E58E8E324615EDA008E5063 /* EncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E58E8E224615EDA008E5063 /* EncodingTests.swift */; }; + 9E58E8E324615EDA008E5063 /* JSONEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */; }; 9E68FB55244707FD0013A8AA /* ObjcExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */; }; 9E68FB56244707FD0013A8AA /* ObjcExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 9EA6A539244897A900621535 /* LoggingIOBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA6A538244897A900621535 /* LoggingIOBenchmarkTests.swift */; }; - 9EA6A53E24489DC800621535 /* TestsDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C462423990D00786299 /* TestsDirectory.swift */; }; - 9EF49F0F24476D0C004F2CA0 /* HTTPServerMock in Frameworks */ = {isa = PBXBuildFile; productRef = 9EF49F0E24476D0C004F2CA0 /* HTTPServerMock */; }; - 9EF49F1024476D96004F2CA0 /* BenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2FB27C24476707001C9B7B /* BenchmarkTests.swift */; }; - 9EF49F1124476D96004F2CA0 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2FB27D24476707001C9B7B /* IntegrationTests.swift */; }; - 9EF49F1224476D96004F2CA0 /* LoggingBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2FB27B24476707001C9B7B /* LoggingBenchmarkTests.swift */; }; - 9EF49F1324476D96004F2CA0 /* LoggingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2FB27A24476707001C9B7B /* LoggingIntegrationTests.swift */; }; - 9EF49F1424476DD6004F2CA0 /* LogMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61133C432423990D00786299 /* LogMatcher.swift */; }; + 9EB47B92247443FA004F90BE /* URLSessionSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EB47B91247443FA004F90BE /* URLSessionSwizzler.swift */; }; + 9ED583A32498C222004CFF2A /* TracingAutoInstrumentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED583A22498C222004CFF2A /* TracingAutoInstrumentation.swift */; }; + 9EFD112C24B32D29003A1A2B /* URLFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EFD112B24B32D29003A1A2B /* URLFilter.swift */; }; + E132727B24B333C700952F8B /* TracingBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E132727A24B333C700952F8B /* TracingBenchmarkTests.swift */; }; + E132727D24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E132727C24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift */; }; + E1D5AEA724B4D45B007F194B /* Versioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D5AEA624B4D45A007F194B /* Versioning.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 61133B8D242393DE00786299 /* PBXContainerItemProxy */ = { + 61133C722423993200786299 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 61133B79242393DE00786299 /* Project object */; proxyType = 1; remoteGlobalIDString = 61133B81242393DE00786299; remoteInfo = Datadog; }; - 61133C722423993200786299 /* PBXContainerItemProxy */ = { + 61441C2F24616F1D003D8BB8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 61133B79242393DE00786299 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 61441C0124616DE9003D8BB8; + remoteInfo = Example; + }; + 61441C4F24619499003D8BB8 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 61133B79242393DE00786299 /* Project object */; proxyType = 1; remoteGlobalIDString = 61133B81242393DE00786299; remoteInfo = Datadog; }; - 61133C752423993C00786299 /* PBXContainerItemProxy */ = { + 61441C5924619A08003D8BB8 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 61133B79242393DE00786299 /* Project object */; proxyType = 1; - remoteGlobalIDString = 61133BEF242397DA00786299; - remoteInfo = DatadogObjc; + remoteGlobalIDString = 61441C0124616DE9003D8BB8; + remoteInfo = Example; }; - 9E2FB2432447660E001C9B7B /* PBXContainerItemProxy */ = { + 61441C7424619FED003D8BB8 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 61133B79242393DE00786299 /* Project object */; proxyType = 1; - remoteGlobalIDString = 61133B81242393DE00786299; - remoteInfo = Datadog; + remoteGlobalIDString = 61441C0124616DE9003D8BB8; + remoteInfo = Example; }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 61441C5124619499003D8BB8 /* โš™๏ธ Embed Framework Dependencies */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 61441C4E24619498003D8BB8 /* Datadog.framework in โš™๏ธ Embed Framework Dependencies */, + 61570006246AAE5E00E96950 /* DatadogObjc.framework in โš™๏ธ Embed Framework Dependencies */, + ); + name = "โš™๏ธ Embed Framework Dependencies"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 61133B82242393DE00786299 /* Datadog.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Datadog.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 61133B85242393DE00786299 /* Datadog.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Datadog.h; sourceTree = ""; }; @@ -190,10 +302,8 @@ 61133C152423990D00786299 /* DDLoggerBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDLoggerBuilderTests.swift; sourceTree = ""; }; 61133C162423990D00786299 /* DDConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDConfigurationTests.swift; sourceTree = ""; }; 61133C172423990D00786299 /* DDLoggerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDLoggerTests.swift; sourceTree = ""; }; - 61133C1A2423990D00786299 /* LogsMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogsMocks.swift; sourceTree = ""; }; 61133C1B2423990D00786299 /* CoreTelephonyMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreTelephonyMocks.swift; sourceTree = ""; }; 61133C1C2423990D00786299 /* UIKitMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitMocks.swift; sourceTree = ""; }; - 61133C1F2423990D00786299 /* DatadogMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogMocks.swift; sourceTree = ""; }; 61133C202423990D00786299 /* FoundationMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationMocks.swift; sourceTree = ""; }; 61133C232423990D00786299 /* MobileDeviceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MobileDeviceTests.swift; sourceTree = ""; }; 61133C242423990D00786299 /* NetworkConnectionInfoProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkConnectionInfoProviderTests.swift; sourceTree = ""; }; @@ -222,67 +332,170 @@ 61133C452423990D00786299 /* SwiftExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftExtensions.swift; sourceTree = ""; }; 61133C462423990D00786299 /* TestsDirectory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestsDirectory.swift; sourceTree = ""; }; 61133C472423990D00786299 /* DatadogExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatadogExtensions.swift; sourceTree = ""; }; + 61216275247D1CD700AC5D67 /* LoggingForTracingAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingForTracingAdapter.swift; sourceTree = ""; }; + 61216279247D21FE00AC5D67 /* LoggingForTracingAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingForTracingAdapterTests.swift; sourceTree = ""; }; 612983CC2449E62E00D4424B /* LoggingFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingFeature.swift; sourceTree = ""; }; + 6132BF4124A38D2400D7BD17 /* OTTracer+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OTTracer+objc.swift"; sourceTree = ""; }; + 6132BF4324A3AAD700D7BD17 /* OTGlobal+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OTGlobal+objc.swift"; sourceTree = ""; }; + 6132BF4624A498D800D7BD17 /* DDSpan+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDSpan+objc.swift"; sourceTree = ""; }; + 6132BF4824A49B6800D7BD17 /* DDSpanContext+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DDSpanContext+objc.swift"; sourceTree = ""; }; + 6132BF4B24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPHeadersWriter+objc.swift"; sourceTree = ""; }; + 6132BF4D24A49D5400D7BD17 /* OTNoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTNoop.swift; sourceTree = ""; }; + 6132BF5024A49F7400D7BD17 /* Casting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Casting.swift; sourceTree = ""; }; 61345612244756E300E7DA6B /* PerformancePresetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformancePresetTests.swift; sourceTree = ""; }; + 61441C0224616DE9003D8BB8 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 61441C0424616DE9003D8BB8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 61441C0B24616DE9003D8BB8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 61441C0D24616DEC003D8BB8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 61441C1024616DEC003D8BB8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 61441C1224616DEC003D8BB8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 61441C2A24616F1D003D8BB8 /* DatadogIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatadogIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 61441C3B24617013003D8BB8 /* IntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; + 61441C3C24617013003D8BB8 /* LoggingIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingIntegrationTests.swift; sourceTree = ""; }; + 61441C6824619FE4003D8BB8 /* DatadogBenchmarkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatadogBenchmarkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 61441C6C24619FE4003D8BB8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 61441C782461A204003D8BB8 /* LoggingBenchmarkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingBenchmarkTests.swift; sourceTree = ""; }; + 61441C792461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingStorageBenchmarkTests.swift; sourceTree = ""; }; + 61441C902461A648003D8BB8 /* ConsoleOutputInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsoleOutputInterceptor.swift; sourceTree = ""; }; + 61441C912461A648003D8BB8 /* UIButton+Disabling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Disabling.swift"; sourceTree = ""; }; + 61441C922461A648003D8BB8 /* UIViewController+KeyboardControlling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+KeyboardControlling.swift"; sourceTree = ""; }; + 61441C932461A649003D8BB8 /* DebugTracingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugTracingViewController.swift; sourceTree = ""; }; + 61441C942461A649003D8BB8 /* DebugLoggingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugLoggingViewController.swift; sourceTree = ""; }; + 61441C9C2461A796003D8BB8 /* AppConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; }; + 614872762485067300E3EBDB /* SpanTagsReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanTagsReducer.swift; sourceTree = ""; }; 614E9EB2244719FA007EE3E1 /* BundleType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleType.swift; sourceTree = ""; }; + 6152C83F24BE1CC8006A1679 /* DataUploaderBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataUploaderBenchmarkTests.swift; sourceTree = ""; }; + 6152C84124BE1F47006A1679 /* DatadogBenchmarkTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogBenchmarkTests.xcconfig; sourceTree = ""; }; + 6152C84224BE2165006A1679 /* MockServerAddress.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = MockServerAddress.local.xcconfig; sourceTree = ""; }; + 615519252461BCE7002A85CF /* Datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.xcconfig; sourceTree = ""; }; + 615519262461BCE7002A85CF /* Datadog.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.local.xcconfig; sourceTree = ""; }; + 615A4A8224A3431600233986 /* Tracer+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tracer+objc.swift"; sourceTree = ""; }; + 615A4A8424A3445700233986 /* TracerConfiguration+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TracerConfiguration+objc.swift"; sourceTree = ""; }; + 615A4A8624A3452800233986 /* DDTracerConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDTracerConfigurationTests.swift; sourceTree = ""; }; + 615A4A8824A34FD700233986 /* DDTracerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDTracerTests.swift; sourceTree = ""; }; + 615A4A8A24A3568900233986 /* OTSpan+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OTSpan+objc.swift"; sourceTree = ""; }; + 615A4A8C24A356A000233986 /* OTSpanContext+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OTSpanContext+objc.swift"; sourceTree = ""; }; + 617CEB382456BC3A00AD4669 /* TracingUUID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingUUID.swift; sourceTree = ""; }; + 618C365E248E85B400520CDE /* DateFormattingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormattingTests.swift; sourceTree = ""; }; + 61AD4E172451C7FF006E34EA /* TracingFeatureMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingFeatureMocks.swift; sourceTree = ""; }; + 61AD4E3724531500006E34EA /* DataFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataFormat.swift; sourceTree = ""; }; + 61AD4E3924534075006E34EA /* TracingFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingFeatureTests.swift; sourceTree = ""; }; 61B558CE2469561C001460D3 /* LoggerBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerBuilderTests.swift; sourceTree = ""; }; + 61B558D32469CDD8001460D3 /* TracingUUIDGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingUUIDGeneratorTests.swift; sourceTree = ""; }; + 61B9ED1A2461E12000C0DCFF /* SendLogsFixtureViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendLogsFixtureViewController.swift; sourceTree = ""; }; + 61B9ED1B2461E12000C0DCFF /* SendTracesFixtureViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendTracesFixtureViewController.swift; sourceTree = ""; }; + 61B9ED1E2461E57700C0DCFF /* UITestsHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsHelpers.swift; sourceTree = ""; }; + 61B9ED202462089600C0DCFF /* TracingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingIntegrationTests.swift; sourceTree = ""; }; 61BB2B1A244A185D009F3F56 /* PerformancePreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformancePreset.swift; sourceTree = ""; }; 61C3637F2436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcExceptionHandlerTests.swift; sourceTree = ""; }; 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogPrivateMocks.swift; sourceTree = ""; }; 61C3638424361E9200C4D4E6 /* Globals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Globals.swift; sourceTree = ""; }; 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMock.swift; sourceTree = ""; }; + 61C5A87824509A0C00DA608C /* DDSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDSpan.swift; sourceTree = ""; }; + 61C5A87924509A0C00DA608C /* DDNoOps.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDNoOps.swift; sourceTree = ""; }; + 61C5A87B24509A0C00DA608C /* TracingUUIDGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TracingUUIDGenerator.swift; sourceTree = ""; }; + 61C5A87C24509A0C00DA608C /* Casting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Casting.swift; sourceTree = ""; }; + 61C5A87D24509A0C00DA608C /* Warnings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Warnings.swift; sourceTree = ""; }; + 61C5A87E24509A0C00DA608C /* DDSpanContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDSpanContext.swift; sourceTree = ""; }; + 61C5A88024509A0C00DA608C /* SpanFileOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpanFileOutput.swift; sourceTree = ""; }; + 61C5A88124509A0C00DA608C /* SpanOutput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpanOutput.swift; sourceTree = ""; }; + 61C5A88324509A0C00DA608C /* HTTPHeadersWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPHeadersWriter.swift; sourceTree = ""; }; + 61C5A88D24509A1F00DA608C /* Tracer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tracer.swift; sourceTree = ""; }; + 61C5A88F24509AA700DA608C /* TracingFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingFeature.swift; sourceTree = ""; }; + 61C5A89524509BF600DA608C /* TracerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TracerTests.swift; sourceTree = ""; }; + 61C5A89824509C1100DA608C /* DDSpanTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDSpanTests.swift; sourceTree = ""; }; + 61C5A89A24509C1100DA608C /* WarningsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WarningsTests.swift; sourceTree = ""; }; + 61C5A89B24509C1100DA608C /* UUID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UUID.swift; sourceTree = ""; }; + 61C5A89C24509C1100DA608C /* Casting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Casting.swift; sourceTree = ""; }; + 61C5A8A424509FAA00DA608C /* SpanEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpanEncoder.swift; sourceTree = ""; }; + 61C5A8A524509FAA00DA608C /* SpanBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpanBuilder.swift; sourceTree = ""; }; + 61D447E124917F8F00649287 /* DateFormatting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateFormatting.swift; sourceTree = ""; }; + 61E45BCE2450A6EC00F2C652 /* TracingUUIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingUUIDTests.swift; sourceTree = ""; }; + 61E45BD12450F65B00F2C652 /* SpanBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanBuilderTests.swift; sourceTree = ""; }; + 61E45BE4245196EA00F2C652 /* SpanFileOutputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanFileOutputTests.swift; sourceTree = ""; }; + 61E45BE624519A3700F2C652 /* JSONDataMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDataMatcher.swift; sourceTree = ""; }; + 61E45ED02451A8730061DAC7 /* SpanMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanMatcher.swift; sourceTree = ""; }; + 61E909E624A24DD3005EA2DE /* OTSpan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTSpan.swift; sourceTree = ""; }; + 61E909E724A24DD3005EA2DE /* OTFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTFormat.swift; sourceTree = ""; }; + 61E909E824A24DD3005EA2DE /* OTGlobal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTGlobal.swift; sourceTree = ""; }; + 61E909E924A24DD3005EA2DE /* OTTracer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTTracer.swift; sourceTree = ""; }; + 61E909EA24A24DD3005EA2DE /* OTReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTReference.swift; sourceTree = ""; }; + 61E909EB24A24DD3005EA2DE /* OTConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTConstants.swift; sourceTree = ""; }; + 61E909EC24A24DD3005EA2DE /* OTSpanContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTSpanContext.swift; sourceTree = ""; }; + 61E909F524A32D1C005EA2DE /* OTGlobalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTGlobalTests.swift; sourceTree = ""; }; + 61E917CE2464270500E6C631 /* EncodableValueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodableValueTests.swift; sourceTree = ""; }; + 61E917D02465423600E6C631 /* TracerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracerConfiguration.swift; sourceTree = ""; }; + 61E917D2246546BF00E6C631 /* TracerConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracerConfigurationTests.swift; sourceTree = ""; }; + 61F1A6192498A51700075390 /* CoreMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreMocks.swift; sourceTree = ""; }; + 61F1A620249A45E400075390 /* DDSpanContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDSpanContextTests.swift; sourceTree = ""; }; + 61F1A622249B811200075390 /* Encoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encoding.swift; sourceTree = ""; }; 61F8CC082469295500FE2908 /* DatadogConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogConfigurationTests.swift; sourceTree = ""; }; 61FB222C244A21ED00902D19 /* LoggingFeatureMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingFeatureMocks.swift; sourceTree = ""; }; 61FB222F244E1BE900902D19 /* LoggingFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingFeatureTests.swift; sourceTree = ""; }; - 9E2FB2772447660E001C9B7B /* DatadogIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatadogIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 9E2FB27A24476707001C9B7B /* LoggingIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingIntegrationTests.swift; sourceTree = ""; }; - 9E2FB27B24476707001C9B7B /* LoggingBenchmarkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingBenchmarkTests.swift; sourceTree = ""; }; - 9E2FB27C24476707001C9B7B /* BenchmarkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BenchmarkTests.swift; sourceTree = ""; }; - 9E2FB27D24476707001C9B7B /* IntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; + 9E330A8B24ADE1250031408E /* DatadogTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DatadogTests-Bridging-Header.h"; sourceTree = ""; }; + 9E330A8C24ADE1250031408E /* NSURLSessionBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSURLSessionBridge.h; sourceTree = ""; }; + 9E330A8D24ADE1250031408E /* NSURLSessionBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSURLSessionBridge.m; sourceTree = ""; }; 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftExtensionsTests.swift; sourceTree = ""; }; - 9E4195742449D739000AB0DB /* app-target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "app-target.xcconfig"; sourceTree = ""; }; - 9E4195752449D739000AB0DB /* unit-tests-target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "unit-tests-target.xcconfig"; sourceTree = ""; }; - 9E58E8DE24615B89008E5063 /* ISO8601DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ISO8601DateFormatter.swift; sourceTree = ""; }; + 9E493E1B249B7BAA005F95F5 /* TracingAutoInstrumentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingAutoInstrumentationTests.swift; sourceTree = ""; }; + 9E50B2E324B49DDF00A2CB95 /* URLFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLFilterTests.swift; sourceTree = ""; }; + 9E544A4C24752A8900E83072 /* URLSessionSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionSwizzlerTests.swift; sourceTree = ""; }; + 9E544A4E24753C6E00E83072 /* MethodSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSwizzler.swift; sourceTree = ""; }; + 9E544A5024753DDE00E83072 /* MethodSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSwizzlerTests.swift; sourceTree = ""; }; 9E58E8E024615C75008E5063 /* JSONEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONEncoder.swift; sourceTree = ""; }; - 9E58E8E224615EDA008E5063 /* EncodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodingTests.swift; sourceTree = ""; }; + 9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONEncoderTests.swift; sourceTree = ""; }; 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjcExceptionHandler.m; sourceTree = ""; }; 9E68FB54244707FD0013A8AA /* ObjcExceptionHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ObjcExceptionHandler.h; sourceTree = ""; }; 9E9EB37624468CE90002C80B /* Datadog.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = Datadog.modulemap; sourceTree = ""; }; - 9EA6A538244897A900621535 /* LoggingIOBenchmarkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingIOBenchmarkTests.swift; sourceTree = ""; }; + 9EB47B91247443FA004F90BE /* URLSessionSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionSwizzler.swift; sourceTree = ""; }; + 9ED583A22498C222004CFF2A /* TracingAutoInstrumentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingAutoInstrumentation.swift; sourceTree = ""; }; 9EF49F1624476FBD004F2CA0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9EF49F17244770AD004F2CA0 /* DatadogIntegrationTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DatadogIntegrationTests.xcconfig; sourceTree = ""; }; + 9EFD112B24B32D29003A1A2B /* URLFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLFilter.swift; sourceTree = ""; }; + E132727A24B333C700952F8B /* TracingBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingBenchmarkTests.swift; sourceTree = ""; }; + E132727C24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingStorageBenchmarkTests.swift; sourceTree = ""; }; + E1D5AEA624B4D45A007F194B /* Versioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Versioning.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 61133B7F242393DE00786299 /* Frameworks */ = { + 61133B88242393DE00786299 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 61133B8C242393DE00786299 /* Datadog.framework in Frameworks */, + 61570005246AADFA00E96950 /* DatadogObjc.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 61133B88242393DE00786299 /* Frameworks */ = { + 61133BED242397DA00786299 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 61133B8C242393DE00786299 /* Datadog.framework in Frameworks */, + 61133C702423993200786299 /* Datadog.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 61133BED242397DA00786299 /* Frameworks */ = { + 61441BFF24616DE9003D8BB8 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 61133C702423993200786299 /* Datadog.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - 9E2FB2712447660E001C9B7B /* Frameworks */ = { + 61441C2724616F1D003D8BB8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 61441C44246174CE003D8BB8 /* HTTPServerMock in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61441C6524619FE4003D8BB8 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9E2FB2722447660E001C9B7B /* Datadog.framework in Frameworks */, - 9EF49F0F24476D0C004F2CA0 /* HTTPServerMock in Frameworks */, + 6152C83E24BE1C91006A1679 /* HTTPServerMock in Frameworks */, + 61441C6D24619FE4003D8BB8 /* Datadog.framework in Frameworks */, + 61570007246AAED100E96950 /* DatadogObjc.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -296,8 +509,10 @@ 61133C082423983800786299 /* DatadogObjc */, 9E68FB52244707FD0013A8AA /* _Datadog_Private */, 61133C122423990D00786299 /* DatadogTests */, - 9E2FB279244766CF001C9B7B /* DatadogIntegrationTests */, + 61441C772461A204003D8BB8 /* DatadogBenchmarkTests */, + 61441C3524617013003D8BB8 /* DatadogIntegrationTests */, 61133C07242397F200786299 /* TargetSupport */, + 61441C0324616DE9003D8BB8 /* Example */, 61133B83242393DE00786299 /* Products */, 61133C6F2423993200786299 /* Frameworks */, ); @@ -309,7 +524,9 @@ 61133B82242393DE00786299 /* Datadog.framework */, 61133B8B242393DE00786299 /* DatadogTests.xctest */, 61133BF0242397DA00786299 /* DatadogObjc.framework */, - 9E2FB2772447660E001C9B7B /* DatadogIntegrationTests.xctest */, + 61441C0224616DE9003D8BB8 /* Example.app */, + 61441C2A24616F1D003D8BB8 /* DatadogIntegrationTests.xctest */, + 61441C6824619FE4003D8BB8 /* DatadogBenchmarkTests.xctest */, ); name = Products; sourceTree = ""; @@ -327,6 +544,7 @@ isa = PBXGroup; children = ( 61133B92242393DE00786299 /* Info.plist */, + 9E330A8B24ADE1250031408E /* DatadogTests-Bridging-Header.h */, ); path = DatadogTests; sourceTree = ""; @@ -337,10 +555,16 @@ 9E9EB37624468CE90002C80B /* Datadog.modulemap */, 61133BBB2423979B00786299 /* Datadog.swift */, 61133BB62423979B00786299 /* Logger.swift */, + 61C5A88D24509A1F00DA608C /* Tracer.swift */, + 61E917D02465423600E6C631 /* TracerConfiguration.swift */, 61133BB52423979B00786299 /* DatadogConfiguration.swift */, + E1D5AEA624B4D45A007F194B /* Versioning.swift */, 61133B9E2423979B00786299 /* Core */, - 61133BBC2423979B00786299 /* Logs */, + 61133BBC2423979B00786299 /* Logging */, + 61C5A87724509A0C00DA608C /* Tracing */, + 61216277247D1F2100AC5D67 /* FeaturesIntegration */, 61133BB72423979B00786299 /* Utils */, + 61E909E524A24DD3005EA2DE /* OpenTracing */, ); name = Datadog; path = ../Sources/Datadog; @@ -351,6 +575,8 @@ children = ( 614E9EB2244719FA007EE3E1 /* BundleType.swift */, 61BB2B1A244A185D009F3F56 /* PerformancePreset.swift */, + 9E5D2D4A249137E900763FE4 /* AutoInstrumentation */, + 61133BBF2423979B00786299 /* Attributes */, 61133B9F2423979B00786299 /* Utils */, 61133BA12423979B00786299 /* System */, 61133BA62423979B00786299 /* Persistence */, @@ -362,8 +588,8 @@ 61133B9F2423979B00786299 /* Utils */ = { isa = PBXGroup; children = ( + 61D447E124917F8F00649287 /* DateFormatting.swift */, 61133BA02423979B00786299 /* EncodableValue.swift */, - 9E58E8DE24615B89008E5063 /* ISO8601DateFormatter.swift */, 9E58E8E024615C75008E5063 /* JSONEncoder.swift */, ); path = Utils; @@ -384,6 +610,7 @@ 61133BA62423979B00786299 /* Persistence */ = { isa = PBXGroup; children = ( + 61AD4E3724531500006E34EA /* DataFormat.swift */, 61133BA92423979B00786299 /* FilesOrchestrator.swift */, 61133BA72423979B00786299 /* FileWriter.swift */, 61133BAD2423979B00786299 /* FileReader.swift */, @@ -425,15 +652,14 @@ path = Utils; sourceTree = ""; }; - 61133BBC2423979B00786299 /* Logs */ = { + 61133BBC2423979B00786299 /* Logging */ = { isa = PBXGroup; children = ( 612983CC2449E62E00D4424B /* LoggingFeature.swift */, - 61133BBF2423979B00786299 /* Attributes */, 61133BC12423979B00786299 /* Log */, 61133BC52423979B00786299 /* LogOutputs */, ); - path = Logs; + path = Logging; sourceTree = ""; }; 61133BBF2423979B00786299 /* Attributes */ = { @@ -477,11 +703,13 @@ 61133C07242397F200786299 /* TargetSupport */ = { isa = PBXGroup; children = ( - 9E4195732449D739000AB0DB /* xcconfigs */, + 615519242461BCE7002A85CF /* xcconfigs */, 61133B84242393DE00786299 /* Datadog */, 61133BF1242397DA00786299 /* DatadogObjc */, 61133B8F242393DE00786299 /* DatadogTests */, + 61441C762461A01D003D8BB8 /* DatadogBenchmarkTests */, 9EF49F1524476FBD004F2CA0 /* DatadogIntegrationTests */, + 61441C9E2461AF4D003D8BB8 /* Example */, ); path = TargetSupport; sourceTree = ""; @@ -490,9 +718,13 @@ isa = PBXGroup; children = ( 61133C092423983800786299 /* Datadog+objc.swift */, - 61133C0C2423983800786299 /* Logger+objc.swift */, 61133C0D2423983800786299 /* DatadogConfiguration+objc.swift */, + 61133C0C2423983800786299 /* Logger+objc.swift */, + 615A4A8224A3431600233986 /* Tracer+objc.swift */, + 615A4A8424A3445700233986 /* TracerConfiguration+objc.swift */, + 6132BF4524A498B400D7BD17 /* Tracing */, 61133C0A2423983800786299 /* ObjcIntercompatibility */, + 6132BF4024A38D0600D7BD17 /* OpenTracing */, ); name = DatadogObjc; path = ../Sources/DatadogObjc; @@ -512,6 +744,7 @@ 61133C182423990D00786299 /* Datadog */, 61133C132423990D00786299 /* DatadogObjc */, 61C3637E2436163400C4D4E6 /* DatadogPrivate */, + 61E909F424A32CF6005EA2DE /* OpenTracing */, 61133C422423990D00786299 /* Matchers */, 61133C442423990D00786299 /* Helpers */, ); @@ -523,9 +756,11 @@ isa = PBXGroup; children = ( 61133C142423990D00786299 /* DDDatadogTests.swift */, - 61133C152423990D00786299 /* DDLoggerBuilderTests.swift */, 61133C162423990D00786299 /* DDConfigurationTests.swift */, 61133C172423990D00786299 /* DDLoggerTests.swift */, + 61133C152423990D00786299 /* DDLoggerBuilderTests.swift */, + 615A4A8824A34FD700233986 /* DDTracerTests.swift */, + 615A4A8624A3452800233986 /* DDTracerConfigurationTests.swift */, ); path = DatadogObjc; sourceTree = ""; @@ -537,9 +772,13 @@ 61133C412423990D00786299 /* DatadogTests.swift */, 61F8CC082469295500FE2908 /* DatadogConfigurationTests.swift */, 61133C382423990D00786299 /* LoggerTests.swift */, + 61C5A89524509BF600DA608C /* TracerTests.swift */, + 61E917D2246546BF00E6C631 /* TracerConfigurationTests.swift */, 61B558CE2469561C001460D3 /* LoggerBuilderTests.swift */, 61133C212423990D00786299 /* Core */, - 61133C392423990D00786299 /* Logs */, + 61133C392423990D00786299 /* Logging */, + 61C5A89724509C1100DA608C /* Tracing */, + 61216278247D20D500AC5D67 /* FeaturesIntegration */, 61133C352423990D00786299 /* Utils */, ); path = Datadog; @@ -548,14 +787,12 @@ 61133C192423990D00786299 /* Mocks */ = { isa = PBXGroup; children = ( - 61133C1A2423990D00786299 /* LogsMocks.swift */, - 61133C1B2423990D00786299 /* CoreTelephonyMocks.swift */, - 61133C1C2423990D00786299 /* UIKitMocks.swift */, - 61133C1F2423990D00786299 /* DatadogMocks.swift */, - 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */, - 61133C202423990D00786299 /* FoundationMocks.swift */, 61C3646F243B5C8300C4D4E6 /* ServerMock.swift */, + 61F1A6192498A51700075390 /* CoreMocks.swift */, 61FB222C244A21ED00902D19 /* LoggingFeatureMocks.swift */, + 61AD4E172451C7FF006E34EA /* TracingFeatureMocks.swift */, + 61C3638224361BE200C4D4E6 /* DatadogPrivateMocks.swift */, + 61F1A61B2498AD2C00075390 /* SystemFrameworks */, ); path = Mocks; sourceTree = ""; @@ -564,9 +801,12 @@ isa = PBXGroup; children = ( 61345612244756E300E7DA6B /* PerformancePresetTests.swift */, + 618C365D248E858200520CDE /* Utils */, + 9E5D2D4B2491382800763FE4 /* AutoInstrumentation */, 61133C222423990D00786299 /* System */, 61133C272423990D00786299 /* Persistence */, 61133C2E2423990D00786299 /* Upload */, + 61E917CD246426E000E6C631 /* Utils */, ); path = Core; sourceTree = ""; @@ -620,19 +860,19 @@ children = ( 61133C362423990D00786299 /* InternalLoggersTests.swift */, 9E36D92124373EA700BFBDB7 /* SwiftExtensionsTests.swift */, - 9E58E8E224615EDA008E5063 /* EncodingTests.swift */, + 9E50B2E324B49DDF00A2CB95 /* URLFilterTests.swift */, ); path = Utils; sourceTree = ""; }; - 61133C392423990D00786299 /* Logs */ = { + 61133C392423990D00786299 /* Logging */ = { isa = PBXGroup; children = ( 61FB222F244E1BE900902D19 /* LoggingFeatureTests.swift */, 61133C3A2423990D00786299 /* Log */, 61133C3D2423990D00786299 /* LogOutputs */, ); - path = Logs; + path = Logging; sourceTree = ""; }; 61133C3A2423990D00786299 /* Log */ = { @@ -657,7 +897,9 @@ 61133C422423990D00786299 /* Matchers */ = { isa = PBXGroup; children = ( + 61E45BE624519A3700F2C652 /* JSONDataMatcher.swift */, 61133C432423990D00786299 /* LogMatcher.swift */, + 61E45ED02451A8730061DAC7 /* SpanMatcher.swift */, ); path = Matchers; sourceTree = ""; @@ -668,6 +910,7 @@ 61133C452423990D00786299 /* SwiftExtensions.swift */, 61133C462423990D00786299 /* TestsDirectory.swift */, 61133C472423990D00786299 /* DatadogExtensions.swift */, + 61F1A622249B811200075390 /* Encoding.swift */, ); path = Helpers; sourceTree = ""; @@ -679,32 +922,325 @@ name = Frameworks; sourceTree = ""; }; - 61C3637E2436163400C4D4E6 /* DatadogPrivate */ = { + 61216277247D1F2100AC5D67 /* FeaturesIntegration */ = { isa = PBXGroup; children = ( - 61C3637F2436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift */, + 61216275247D1CD700AC5D67 /* LoggingForTracingAdapter.swift */, ); - path = DatadogPrivate; + path = FeaturesIntegration; sourceTree = ""; }; - 9E2FB279244766CF001C9B7B /* DatadogIntegrationTests */ = { + 61216278247D20D500AC5D67 /* FeaturesIntegration */ = { isa = PBXGroup; children = ( - 9EA6A53A2448982800621535 /* Benchmark */, - 9EA6A53B2448983300621535 /* Integration */, + 61216279247D21FE00AC5D67 /* LoggingForTracingAdapterTests.swift */, + ); + path = FeaturesIntegration; + sourceTree = ""; + }; + 6132BF4024A38D0600D7BD17 /* OpenTracing */ = { + isa = PBXGroup; + children = ( + 6132BF4324A3AAD700D7BD17 /* OTGlobal+objc.swift */, + 6132BF4124A38D2400D7BD17 /* OTTracer+objc.swift */, + 615A4A8A24A3568900233986 /* OTSpan+objc.swift */, + 615A4A8C24A356A000233986 /* OTSpanContext+objc.swift */, + 6132BF4D24A49D5400D7BD17 /* OTNoop.swift */, + ); + path = OpenTracing; + sourceTree = ""; + }; + 6132BF4524A498B400D7BD17 /* Tracing */ = { + isa = PBXGroup; + children = ( + 6132BF4624A498D800D7BD17 /* DDSpan+objc.swift */, + 6132BF4824A49B6800D7BD17 /* DDSpanContext+objc.swift */, + 6132BF4A24A49C7200D7BD17 /* Propagation */, + 6132BF4F24A49F6400D7BD17 /* Utils */, + ); + path = Tracing; + sourceTree = ""; + }; + 6132BF4A24A49C7200D7BD17 /* Propagation */ = { + isa = PBXGroup; + children = ( + 6132BF4B24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift */, + ); + path = Propagation; + sourceTree = ""; + }; + 6132BF4F24A49F6400D7BD17 /* Utils */ = { + isa = PBXGroup; + children = ( + 6132BF5024A49F7400D7BD17 /* Casting.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 61441C0324616DE9003D8BB8 /* Example */ = { + isa = PBXGroup; + children = ( + 61441C9C2461A796003D8BB8 /* AppConfig.swift */, + 61441C0424616DE9003D8BB8 /* AppDelegate.swift */, + 61441C9A2461A64F003D8BB8 /* Debugging */, + 61B9ED142461DFEE00C0DCFF /* IntegrationTestFixtures */, + 61441C8F2461A648003D8BB8 /* Utils */, + 61441C0A24616DE9003D8BB8 /* Main.storyboard */, + 61441C0D24616DEC003D8BB8 /* Assets.xcassets */, + 61441C0F24616DEC003D8BB8 /* LaunchScreen.storyboard */, + ); + path = Example; + sourceTree = ""; + }; + 61441C3524617013003D8BB8 /* DatadogIntegrationTests */ = { + isa = PBXGroup; + children = ( + 61441C3B24617013003D8BB8 /* IntegrationTests.swift */, + 61441C3C24617013003D8BB8 /* LoggingIntegrationTests.swift */, + 61B9ED202462089600C0DCFF /* TracingIntegrationTests.swift */, + 61B9ED1E2461E57700C0DCFF /* UITestsHelpers.swift */, ); name = DatadogIntegrationTests; path = ../Tests/DatadogIntegrationTests; sourceTree = ""; }; - 9E4195732449D739000AB0DB /* xcconfigs */ = { + 61441C762461A01D003D8BB8 /* DatadogBenchmarkTests */ = { + isa = PBXGroup; + children = ( + 6152C84124BE1F47006A1679 /* DatadogBenchmarkTests.xcconfig */, + 61441C6C24619FE4003D8BB8 /* Info.plist */, + ); + path = DatadogBenchmarkTests; + sourceTree = ""; + }; + 61441C772461A204003D8BB8 /* DatadogBenchmarkTests */ = { + isa = PBXGroup; + children = ( + 6152C83F24BE1CC8006A1679 /* DataUploaderBenchmarkTests.swift */, + 61441C782461A204003D8BB8 /* LoggingBenchmarkTests.swift */, + 61441C792461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift */, + E132727A24B333C700952F8B /* TracingBenchmarkTests.swift */, + E132727C24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift */, + ); + name = DatadogBenchmarkTests; + path = ../Tests/DatadogBenchmarkTests; + sourceTree = ""; + }; + 61441C8F2461A648003D8BB8 /* Utils */ = { isa = PBXGroup; children = ( - 9E4195742449D739000AB0DB /* app-target.xcconfig */, - 9E4195752449D739000AB0DB /* unit-tests-target.xcconfig */, + 61441C902461A648003D8BB8 /* ConsoleOutputInterceptor.swift */, + 61441C912461A648003D8BB8 /* UIButton+Disabling.swift */, + 61441C922461A648003D8BB8 /* UIViewController+KeyboardControlling.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 61441C9A2461A64F003D8BB8 /* Debugging */ = { + isa = PBXGroup; + children = ( + 61441C942461A649003D8BB8 /* DebugLoggingViewController.swift */, + 61441C932461A649003D8BB8 /* DebugTracingViewController.swift */, + ); + path = Debugging; + sourceTree = ""; + }; + 61441C9E2461AF4D003D8BB8 /* Example */ = { + isa = PBXGroup; + children = ( + 61441C1224616DEC003D8BB8 /* Info.plist */, + ); + path = Example; + sourceTree = ""; + }; + 615519242461BCE7002A85CF /* xcconfigs */ = { + isa = PBXGroup; + children = ( + 6152C84224BE2165006A1679 /* MockServerAddress.local.xcconfig */, + 615519252461BCE7002A85CF /* Datadog.xcconfig */, + 615519262461BCE7002A85CF /* Datadog.local.xcconfig */, ); name = xcconfigs; - path = "../../dependency-manager-tests/xcconfigs"; + path = ../../xcconfigs; + sourceTree = ""; + }; + 617CEB372456BC2200AD4669 /* UUIDs */ = { + isa = PBXGroup; + children = ( + 61C5A87B24509A0C00DA608C /* TracingUUIDGenerator.swift */, + 617CEB382456BC3A00AD4669 /* TracingUUID.swift */, + ); + path = UUIDs; + sourceTree = ""; + }; + 617CEB3A2456BC8200AD4669 /* UUIDs */ = { + isa = PBXGroup; + children = ( + 61E45BCE2450A6EC00F2C652 /* TracingUUIDTests.swift */, + 61B558D32469CDD8001460D3 /* TracingUUIDGeneratorTests.swift */, + ); + path = UUIDs; + sourceTree = ""; + }; + 618C365D248E858200520CDE /* Utils */ = { + isa = PBXGroup; + children = ( + 618C365E248E85B400520CDE /* DateFormattingTests.swift */, + 9E58E8E224615EDA008E5063 /* JSONEncoderTests.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 61B9ED142461DFEE00C0DCFF /* IntegrationTestFixtures */ = { + isa = PBXGroup; + children = ( + 61B9ED1A2461E12000C0DCFF /* SendLogsFixtureViewController.swift */, + 61B9ED1B2461E12000C0DCFF /* SendTracesFixtureViewController.swift */, + ); + path = IntegrationTestFixtures; + sourceTree = ""; + }; + 61C3637E2436163400C4D4E6 /* DatadogPrivate */ = { + isa = PBXGroup; + children = ( + 61C3637F2436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift */, + ); + path = DatadogPrivate; + sourceTree = ""; + }; + 61C5A87724509A0C00DA608C /* Tracing */ = { + isa = PBXGroup; + children = ( + 61C5A88F24509AA700DA608C /* TracingFeature.swift */, + 61C5A87924509A0C00DA608C /* DDNoOps.swift */, + 61C5A87824509A0C00DA608C /* DDSpan.swift */, + 61C5A87E24509A0C00DA608C /* DDSpanContext.swift */, + 9ED583A22498C222004CFF2A /* TracingAutoInstrumentation.swift */, + 61C5A8A324509FAA00DA608C /* Span */, + 61C5A87F24509A0C00DA608C /* SpanOutputs */, + 61C5A88224509A0C00DA608C /* Propagation */, + 617CEB372456BC2200AD4669 /* UUIDs */, + 61C5A87A24509A0C00DA608C /* Utils */, + ); + path = Tracing; + sourceTree = ""; + }; + 61C5A87A24509A0C00DA608C /* Utils */ = { + isa = PBXGroup; + children = ( + 61C5A87C24509A0C00DA608C /* Casting.swift */, + 61C5A87D24509A0C00DA608C /* Warnings.swift */, + 9EFD112B24B32D29003A1A2B /* URLFilter.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 61C5A87F24509A0C00DA608C /* SpanOutputs */ = { + isa = PBXGroup; + children = ( + 61C5A88024509A0C00DA608C /* SpanFileOutput.swift */, + 61C5A88124509A0C00DA608C /* SpanOutput.swift */, + ); + path = SpanOutputs; + sourceTree = ""; + }; + 61C5A88224509A0C00DA608C /* Propagation */ = { + isa = PBXGroup; + children = ( + 61C5A88324509A0C00DA608C /* HTTPHeadersWriter.swift */, + ); + path = Propagation; + sourceTree = ""; + }; + 61C5A89724509C1100DA608C /* Tracing */ = { + isa = PBXGroup; + children = ( + 9E493E1B249B7BAA005F95F5 /* TracingAutoInstrumentationTests.swift */, + 61AD4E3924534075006E34EA /* TracingFeatureTests.swift */, + 61C5A89824509C1100DA608C /* DDSpanTests.swift */, + 61F1A620249A45E400075390 /* DDSpanContextTests.swift */, + 61E45BD02450F64100F2C652 /* Span */, + 61E45BE3245196D500F2C652 /* SpanOutputs */, + 617CEB3A2456BC8200AD4669 /* UUIDs */, + 61C5A89924509C1100DA608C /* Utils */, + ); + path = Tracing; + sourceTree = ""; + }; + 61C5A89924509C1100DA608C /* Utils */ = { + isa = PBXGroup; + children = ( + 61C5A89B24509C1100DA608C /* UUID.swift */, + 61C5A89A24509C1100DA608C /* WarningsTests.swift */, + 61C5A89C24509C1100DA608C /* Casting.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 61C5A8A324509FAA00DA608C /* Span */ = { + isa = PBXGroup; + children = ( + 61C5A8A424509FAA00DA608C /* SpanEncoder.swift */, + 61C5A8A524509FAA00DA608C /* SpanBuilder.swift */, + 614872762485067300E3EBDB /* SpanTagsReducer.swift */, + ); + path = Span; + sourceTree = ""; + }; + 61E45BD02450F64100F2C652 /* Span */ = { + isa = PBXGroup; + children = ( + 61E45BD12450F65B00F2C652 /* SpanBuilderTests.swift */, + ); + path = Span; + sourceTree = ""; + }; + 61E45BE3245196D500F2C652 /* SpanOutputs */ = { + isa = PBXGroup; + children = ( + 61E45BE4245196EA00F2C652 /* SpanFileOutputTests.swift */, + ); + path = SpanOutputs; + sourceTree = ""; + }; + 61E909E524A24DD3005EA2DE /* OpenTracing */ = { + isa = PBXGroup; + children = ( + 61E909E624A24DD3005EA2DE /* OTSpan.swift */, + 61E909E724A24DD3005EA2DE /* OTFormat.swift */, + 61E909E824A24DD3005EA2DE /* OTGlobal.swift */, + 61E909E924A24DD3005EA2DE /* OTTracer.swift */, + 61E909EA24A24DD3005EA2DE /* OTReference.swift */, + 61E909EB24A24DD3005EA2DE /* OTConstants.swift */, + 61E909EC24A24DD3005EA2DE /* OTSpanContext.swift */, + ); + path = OpenTracing; + sourceTree = ""; + }; + 61E909F424A32CF6005EA2DE /* OpenTracing */ = { + isa = PBXGroup; + children = ( + 61E909F524A32D1C005EA2DE /* OTGlobalTests.swift */, + ); + path = OpenTracing; + sourceTree = ""; + }; + 61E917CD246426E000E6C631 /* Utils */ = { + isa = PBXGroup; + children = ( + 61E917CE2464270500E6C631 /* EncodableValueTests.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 61F1A61B2498AD2C00075390 /* SystemFrameworks */ = { + isa = PBXGroup; + children = ( + 61133C202423990D00786299 /* FoundationMocks.swift */, + 61133C1C2423990D00786299 /* UIKitMocks.swift */, + 61133C1B2423990D00786299 /* CoreTelephonyMocks.swift */, + ); + path = SystemFrameworks; sourceTree = ""; }; 9E47010324471027000073A4 /* include */ = { @@ -715,33 +1251,34 @@ path = include; sourceTree = ""; }; - 9E68FB52244707FD0013A8AA /* _Datadog_Private */ = { + 9E5D2D4A249137E900763FE4 /* AutoInstrumentation */ = { isa = PBXGroup; children = ( - 9E47010324471027000073A4 /* include */, - 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */, + 9E544A4E24753C6E00E83072 /* MethodSwizzler.swift */, + 9EB47B91247443FA004F90BE /* URLSessionSwizzler.swift */, ); - name = _Datadog_Private; - path = ../Sources/_Datadog_Private; + path = AutoInstrumentation; sourceTree = ""; }; - 9EA6A53A2448982800621535 /* Benchmark */ = { + 9E5D2D4B2491382800763FE4 /* AutoInstrumentation */ = { isa = PBXGroup; children = ( - 9E2FB27C24476707001C9B7B /* BenchmarkTests.swift */, - 9E2FB27B24476707001C9B7B /* LoggingBenchmarkTests.swift */, - 9EA6A538244897A900621535 /* LoggingIOBenchmarkTests.swift */, + 9E544A5024753DDE00E83072 /* MethodSwizzlerTests.swift */, + 9E544A4C24752A8900E83072 /* URLSessionSwizzlerTests.swift */, + 9E330A8C24ADE1250031408E /* NSURLSessionBridge.h */, + 9E330A8D24ADE1250031408E /* NSURLSessionBridge.m */, ); - path = Benchmark; + path = AutoInstrumentation; sourceTree = ""; }; - 9EA6A53B2448983300621535 /* Integration */ = { + 9E68FB52244707FD0013A8AA /* _Datadog_Private */ = { isa = PBXGroup; children = ( - 9E2FB27D24476707001C9B7B /* IntegrationTests.swift */, - 9E2FB27A24476707001C9B7B /* LoggingIntegrationTests.swift */, + 9E47010324471027000073A4 /* include */, + 9E68FB53244707FD0013A8AA /* ObjcExceptionHandler.m */, ); - path = Integration; + name = _Datadog_Private; + path = ../Sources/_Datadog_Private; sourceTree = ""; }; 9EF49F1524476FBD004F2CA0 /* DatadogIntegrationTests */ = { @@ -782,7 +1319,6 @@ buildPhases = ( 61133B7D242393DE00786299 /* Headers */, 61133B7E242393DE00786299 /* Sources */, - 61133B7F242393DE00786299 /* Frameworks */, 61133B80242393DE00786299 /* Resources */, 61133C772423A4C300786299 /* โš™๏ธ Run linter */, ); @@ -807,8 +1343,7 @@ buildRules = ( ); dependencies = ( - 61133B8E242393DE00786299 /* PBXTargetDependency */, - 61133C762423993C00786299 /* PBXTargetDependency */, + 61441C5A24619A08003D8BB8 /* PBXTargetDependency */, ); name = DatadogTests; productName = DatadogTests; @@ -834,26 +1369,67 @@ productReference = 61133BF0242397DA00786299 /* DatadogObjc.framework */; productType = "com.apple.product-type.framework"; }; - 9E2FB2412447660E001C9B7B /* DatadogIntegrationTests */ = { + 61441C0124616DE9003D8BB8 /* Example */ = { isa = PBXNativeTarget; - buildConfigurationList = 9E2FB2742447660E001C9B7B /* Build configuration list for PBXNativeTarget "DatadogIntegrationTests" */; + buildConfigurationList = 61441C1324616DEC003D8BB8 /* Build configuration list for PBXNativeTarget "Example" */; buildPhases = ( - 9E2FB2462447660E001C9B7B /* Sources */, - 9E2FB2712447660E001C9B7B /* Frameworks */, - 9E2FB2732447660E001C9B7B /* Resources */, - 9EA6A53D24489AE800621535 /* โš™๏ธ Run linter */, + 61441BFE24616DE9003D8BB8 /* Sources */, + 61441BFF24616DE9003D8BB8 /* Frameworks */, + 61441C0024616DE9003D8BB8 /* Resources */, + 61441C5124619499003D8BB8 /* โš™๏ธ Embed Framework Dependencies */, ); buildRules = ( ); dependencies = ( - 9E2FB2422447660E001C9B7B /* PBXTargetDependency */, + 61441C5024619499003D8BB8 /* PBXTargetDependency */, + ); + name = Example; + packageProductDependencies = ( + ); + productName = Example; + productReference = 61441C0224616DE9003D8BB8 /* Example.app */; + productType = "com.apple.product-type.application"; + }; + 61441C2924616F1D003D8BB8 /* DatadogIntegrationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 61441C3124616F1D003D8BB8 /* Build configuration list for PBXNativeTarget "DatadogIntegrationTests" */; + buildPhases = ( + 61441C2624616F1D003D8BB8 /* Sources */, + 61441C2724616F1D003D8BB8 /* Frameworks */, + 61441C2824616F1D003D8BB8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 61441C3024616F1D003D8BB8 /* PBXTargetDependency */, ); name = DatadogIntegrationTests; packageProductDependencies = ( - 9EF49F0E24476D0C004F2CA0 /* HTTPServerMock */, + 61441C43246174CE003D8BB8 /* HTTPServerMock */, ); - productName = DatadogTests; - productReference = 9E2FB2772447660E001C9B7B /* DatadogIntegrationTests.xctest */; + productName = DatadogIntegrationTests; + productReference = 61441C2A24616F1D003D8BB8 /* DatadogIntegrationTests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 61441C6724619FE4003D8BB8 /* DatadogBenchmarkTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 61441C7024619FE4003D8BB8 /* Build configuration list for PBXNativeTarget "DatadogBenchmarkTests" */; + buildPhases = ( + 61441C6424619FE4003D8BB8 /* Sources */, + 61441C6524619FE4003D8BB8 /* Frameworks */, + 61441C6624619FE4003D8BB8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 61441C7524619FED003D8BB8 /* PBXTargetDependency */, + ); + name = DatadogBenchmarkTests; + packageProductDependencies = ( + 6152C83D24BE1C91006A1679 /* HTTPServerMock */, + ); + productName = DatadogBenchmarkTests; + productReference = 61441C6824619FE4003D8BB8 /* DatadogBenchmarkTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ @@ -862,7 +1438,7 @@ 61133B79242393DE00786299 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1130; + LastSwiftUpdateCheck = 1140; LastUpgradeCheck = 1130; ORGANIZATIONNAME = Datadog; TargetAttributes = { @@ -871,10 +1447,22 @@ }; 61133B8A242393DE00786299 = { CreatedOnToolsVersion = 11.3.1; + TestTargetID = 61441C0124616DE9003D8BB8; }; 61133BEF242397DA00786299 = { CreatedOnToolsVersion = 11.3.1; }; + 61441C0124616DE9003D8BB8 = { + CreatedOnToolsVersion = 11.4; + }; + 61441C2924616F1D003D8BB8 = { + CreatedOnToolsVersion = 11.4; + TestTargetID = 61441C0124616DE9003D8BB8; + }; + 61441C6724619FE4003D8BB8 = { + CreatedOnToolsVersion = 11.4; + TestTargetID = 61441C0124616DE9003D8BB8; + }; }; }; buildConfigurationList = 61133B7C242393DE00786299 /* Build configuration list for PBXProject "Datadog" */; @@ -893,7 +1481,9 @@ 61133B81242393DE00786299 /* Datadog */, 61133BEF242397DA00786299 /* DatadogObjc */, 61133B8A242393DE00786299 /* DatadogTests */, - 9E2FB2412447660E001C9B7B /* DatadogIntegrationTests */, + 61441C6724619FE4003D8BB8 /* DatadogBenchmarkTests */, + 61441C2924616F1D003D8BB8 /* DatadogIntegrationTests */, + 61441C0124616DE9003D8BB8 /* Example */, ); }; /* End PBXProject section */ @@ -920,36 +1510,34 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 9E2FB2732447660E001C9B7B /* Resources */ = { + 61441C0024616DE9003D8BB8 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 61441C1124616DEC003D8BB8 /* LaunchScreen.storyboard in Resources */, + 61441C0E24616DEC003D8BB8 /* Assets.xcassets in Resources */, + 61441C0C24616DE9003D8BB8 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 61133C772423A4C300786299 /* โš™๏ธ Run linter */ = { - isa = PBXShellScriptBuildPhase; + 61441C2824616F1D003D8BB8 /* Resources */ = { + isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "โš™๏ธ Run linter"; - outputFileListPaths = ( - ); - outputPaths = ( + runOnlyForDeploymentPostprocessing = 0; + }; + 61441C6624619FE4003D8BB8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\n cd ${SOURCE_ROOT}/..\n ./tools/lint/run-linter.sh\nfi\n"; - showEnvVarsInLog = 0; }; - 9EA6A53C24489AB100621535 /* โš™๏ธ Run linter */ = { +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 61133C772423A4C300786299 /* โš™๏ธ Run linter */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -968,7 +1556,7 @@ shellScript = "if which swiftlint >/dev/null; then\n cd ${SOURCE_ROOT}/..\n ./tools/lint/run-linter.sh\nfi\n"; showEnvVarsInLog = 0; }; - 9EA6A53D24489AE800621535 /* โš™๏ธ Run linter */ = { + 9EA6A53C24489AB100621535 /* โš™๏ธ Run linter */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -994,42 +1582,72 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 61E917D12465423600E6C631 /* TracerConfiguration.swift in Sources */, + 61E909ED24A24DD3005EA2DE /* OTSpan.swift in Sources */, 61133BDE2423979B00786299 /* CompilationConditions.swift in Sources */, + 61E909F324A24DD3005EA2DE /* OTSpanContext.swift in Sources */, + 61E909F024A24DD3005EA2DE /* OTTracer.swift in Sources */, + 61E909F124A24DD3005EA2DE /* OTReference.swift in Sources */, + 61216276247D1CD700AC5D67 /* LoggingForTracingAdapter.swift in Sources */, + 61E909EF24A24DD3005EA2DE /* OTGlobal.swift in Sources */, 61133BDD2423979B00786299 /* InternalLoggers.swift in Sources */, 61133BDC2423979B00786299 /* Logger.swift in Sources */, 61133BD02423979B00786299 /* DateProvider.swift in Sources */, + 614872772485067300E3EBDB /* SpanTagsReducer.swift in Sources */, 61133BCF2423979B00786299 /* FileWriter.swift in Sources */, + 61E909F224A24DD3005EA2DE /* OTConstants.swift in Sources */, 61133BCC2423979B00786299 /* MobileDevice.swift in Sources */, + 61C5A8A724509FAA00DA608C /* SpanBuilder.swift in Sources */, + 61AD4E3824531500006E34EA /* DataFormat.swift in Sources */, 9E58E8E124615C75008E5063 /* JSONEncoder.swift in Sources */, 61133BCA2423979B00786299 /* EncodableValue.swift in Sources */, 9E68FB55244707FD0013A8AA /* ObjcExceptionHandler.m in Sources */, + 9ED583A32498C222004CFF2A /* TracingAutoInstrumentation.swift in Sources */, + 617CEB392456BC3A00AD4669 /* TracingUUID.swift in Sources */, 61C3638524361E9200C4D4E6 /* Globals.swift in Sources */, + 61C5A88824509A0C00DA608C /* Warnings.swift in Sources */, + 9EFD112C24B32D29003A1A2B /* URLFilter.swift in Sources */, + 61C5A88924509A0C00DA608C /* DDSpanContext.swift in Sources */, 61133BE62423979B00786299 /* LogSanitizer.swift in Sources */, 61133BDF2423979B00786299 /* SwiftExtensions.swift in Sources */, + 9E544A4F24753C6E00E83072 /* MethodSwizzler.swift in Sources */, 61133BEA2423979B00786299 /* LogConsoleOutput.swift in Sources */, - 9E58E8DF24615B89008E5063 /* ISO8601DateFormatter.swift in Sources */, + 61C5A8A624509FAA00DA608C /* SpanEncoder.swift in Sources */, + 61C5A88E24509A1F00DA608C /* Tracer.swift in Sources */, + 61E909EE24A24DD3005EA2DE /* OTFormat.swift in Sources */, 61133BE32423979B00786299 /* UserInfo.swift in Sources */, 61133BE02423979B00786299 /* Datadog.swift in Sources */, 61133BCB2423979B00786299 /* CarrierInfoProvider.swift in Sources */, + 61C5A89024509AA700DA608C /* TracingFeature.swift in Sources */, 61133BD62423979B00786299 /* DataUploader.swift in Sources */, + 61C5A88724509A0C00DA608C /* Casting.swift in Sources */, 61133BE52423979B00786299 /* LogBuilder.swift in Sources */, 61133BD42423979B00786299 /* FileReader.swift in Sources */, + 61C5A88A24509A0C00DA608C /* SpanFileOutput.swift in Sources */, 61133BD32423979B00786299 /* File.swift in Sources */, + 61D447E224917F8F00649287 /* DateFormatting.swift in Sources */, 61133BE72423979B00786299 /* LogUtilityOutputs.swift in Sources */, 61133BDA2423979B00786299 /* HTTPHeaders.swift in Sources */, 61133BE82423979B00786299 /* LogFileOutput.swift in Sources */, 61133BD72423979B00786299 /* DataUploadWorker.swift in Sources */, 61133BD12423979B00786299 /* FilesOrchestrator.swift in Sources */, 61133BCD2423979B00786299 /* NetworkConnectionInfoProvider.swift in Sources */, + 61C5A88B24509A0C00DA608C /* SpanOutput.swift in Sources */, 61133BE42423979B00786299 /* LogEncoder.swift in Sources */, + 61C5A88424509A0C00DA608C /* DDSpan.swift in Sources */, + E1D5AEA724B4D45B007F194B /* Versioning.swift in Sources */, 61133BD82423979B00786299 /* HTTPClient.swift in Sources */, 61133BDB2423979B00786299 /* DatadogConfiguration.swift in Sources */, 614E9EB3244719FA007EE3E1 /* BundleType.swift in Sources */, 61133BCE2423979B00786299 /* BatteryStatusProvider.swift in Sources */, 61133BD52423979B00786299 /* DataUploadConditions.swift in Sources */, 612983CD2449E62E00D4424B /* LoggingFeature.swift in Sources */, + 9EB47B92247443FA004F90BE /* URLSessionSwizzler.swift in Sources */, 61133BE92423979B00786299 /* LogOutput.swift in Sources */, + 61C5A88524509A0C00DA608C /* DDNoOps.swift in Sources */, + 61C5A88624509A0C00DA608C /* TracingUUIDGenerator.swift in Sources */, 61133BD92423979B00786299 /* DataUploadDelay.swift in Sources */, + 61C5A88C24509A0C00DA608C /* HTTPHeadersWriter.swift in Sources */, 61BB2B1B244A185D009F3F56 /* PerformancePreset.swift in Sources */, 61133BD22423979B00786299 /* Directory.swift in Sources */, ); @@ -1039,21 +1657,34 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 61C5A8A024509C1100DA608C /* Casting.swift in Sources */, 61133C662423990D00786299 /* LogSanitizerTests.swift in Sources */, + 9E330A8E24ADE1250031408E /* NSURLSessionBridge.m in Sources */, + 9E493E1C249B7BAA005F95F5 /* TracingAutoInstrumentationTests.swift in Sources */, + 61E45ED12451A8730061DAC7 /* SpanMatcher.swift in Sources */, 61C36470243B5C8300C4D4E6 /* ServerMock.swift in Sources */, 61133C5D2423990D00786299 /* DataUploadConditionsTests.swift in Sources */, + 618C365F248E85B400520CDE /* DateFormattingTests.swift in Sources */, 61133C5A2423990D00786299 /* FileTests.swift in Sources */, - 61133C512423990D00786299 /* DatadogMocks.swift in Sources */, + 61AD4E3A24534075006E34EA /* TracingFeatureTests.swift in Sources */, 61133C6B2423990D00786299 /* LogMatcher.swift in Sources */, 61133C622423990D00786299 /* InternalLoggersTests.swift in Sources */, 61133C582423990D00786299 /* FileWriterTests.swift in Sources */, + 9E544A4D24752A8900E83072 /* URLSessionSwizzlerTests.swift in Sources */, + 61E917D3246546BF00E6C631 /* TracerConfigurationTests.swift in Sources */, + 61C5A89D24509C1100DA608C /* DDSpanTests.swift in Sources */, 61133C672423990D00786299 /* LogConsoleOutputTests.swift in Sources */, 61FB222D244A21ED00902D19 /* LoggingFeatureMocks.swift in Sources */, 61C3638324361BE200C4D4E6 /* DatadogPrivateMocks.swift in Sources */, - 61133C4C2423990D00786299 /* LogsMocks.swift in Sources */, + 61AD4E182451C7FF006E34EA /* TracingFeatureMocks.swift in Sources */, + 615A4A8924A34FD700233986 /* DDTracerTests.swift in Sources */, + 615A4A8724A3452800233986 /* DDTracerConfigurationTests.swift in Sources */, + 61F1A621249A45E400075390 /* DDSpanContextTests.swift in Sources */, + 61E917CF2464270500E6C631 /* EncodableValueTests.swift in Sources */, 61133C542423990D00786299 /* NetworkConnectionInfoProviderTests.swift in Sources */, 61B558CF2469561C001460D3 /* LoggerBuilderTests.swift in Sources */, 61133C4A2423990D00786299 /* DDConfigurationTests.swift in Sources */, + 61C5A89F24509C1100DA608C /* UUID.swift in Sources */, 61C363802436164B00C4D4E6 /* ObjcExceptionHandlerTests.swift in Sources */, 61133C602423990D00786299 /* HTTPHeadersTests.swift in Sources */, 61133C572423990D00786299 /* FileReaderTests.swift in Sources */, @@ -1062,27 +1693,40 @@ 61133C6A2423990D00786299 /* DatadogTests.swift in Sources */, 61133C5E2423990D00786299 /* LogsUploadDelayTests.swift in Sources */, 61133C5C2423990D00786299 /* DataUploadWorkerTests.swift in Sources */, - 9E58E8E324615EDA008E5063 /* EncodingTests.swift in Sources */, + 61E909F624A32D1C005EA2DE /* OTGlobalTests.swift in Sources */, + 9E58E8E324615EDA008E5063 /* JSONEncoderTests.swift in Sources */, 61133C692423990D00786299 /* LogFileOutputTests.swift in Sources */, 61133C682423990D00786299 /* LogUtilityOutputsTests.swift in Sources */, 61133C6E2423990D00786299 /* DatadogExtensions.swift in Sources */, + 61E45BE724519A3700F2C652 /* JSONDataMatcher.swift in Sources */, 61133C592423990D00786299 /* FilesOrchestratorTests.swift in Sources */, + 9E50B2E424B49DDF00A2CB95 /* URLFilterTests.swift in Sources */, + 61B558D42469CDD8001460D3 /* TracingUUIDGeneratorTests.swift in Sources */, + 9E544A5124753DDE00E83072 /* MethodSwizzlerTests.swift in Sources */, 61FB2230244E1BE900902D19 /* LoggingFeatureTests.swift in Sources */, 61133C6D2423990D00786299 /* TestsDirectory.swift in Sources */, 61133C6C2423990D00786299 /* SwiftExtensions.swift in Sources */, 61133C492423990D00786299 /* DDLoggerBuilderTests.swift in Sources */, 61133C4B2423990D00786299 /* DDLoggerTests.swift in Sources */, + 61C5A89624509BF600DA608C /* TracerTests.swift in Sources */, + 61F1A61A2498A51700075390 /* CoreMocks.swift in Sources */, + 61E45BD22450F65B00F2C652 /* SpanBuilderTests.swift in Sources */, + 61E45BCF2450A6EC00F2C652 /* TracingUUIDTests.swift in Sources */, 61133C482423990D00786299 /* DDDatadogTests.swift in Sources */, 61133C522423990D00786299 /* FoundationMocks.swift in Sources */, 61133C5B2423990D00786299 /* DirectoryTests.swift in Sources */, 61133C562423990D00786299 /* CarrierInfoProviderTests.swift in Sources */, + 61C5A89E24509C1100DA608C /* WarningsTests.swift in Sources */, 9E36D92224373EA700BFBDB7 /* SwiftExtensionsTests.swift in Sources */, 61133C652423990D00786299 /* LogBuilderTests.swift in Sources */, 61F8CC092469295500FE2908 /* DatadogConfigurationTests.swift in Sources */, + 61F1A623249B811200075390 /* Encoding.swift in Sources */, 61133C642423990D00786299 /* LoggerTests.swift in Sources */, + 61E45BE5245196EA00F2C652 /* SpanFileOutputTests.swift in Sources */, 61133C4E2423990D00786299 /* UIKitMocks.swift in Sources */, 61133C4D2423990D00786299 /* CoreTelephonyMocks.swift in Sources */, 61133C552423990D00786299 /* BatteryStatusProviderTests.swift in Sources */, + 6121627C247D220500AC5D67 /* LoggingForTracingAdapterTests.swift in Sources */, 61133C532423990D00786299 /* MobileDeviceTests.swift in Sources */, 61345613244756E300E7DA6B /* PerformancePresetTests.swift in Sources */, ); @@ -1093,51 +1737,115 @@ buildActionMask = 2147483647; files = ( 61133C0F2423983800786299 /* AnyEncodable.swift in Sources */, + 6132BF5124A49F7400D7BD17 /* Casting.swift in Sources */, + 6132BF4924A49B6800D7BD17 /* DDSpanContext+objc.swift in Sources */, + 6132BF4224A38D2400D7BD17 /* OTTracer+objc.swift in Sources */, + 615A4A8524A3445700233986 /* TracerConfiguration+objc.swift in Sources */, 61133C0E2423983800786299 /* Datadog+objc.swift in Sources */, 61133C102423983800786299 /* Logger+objc.swift in Sources */, + 615A4A8324A3431600233986 /* Tracer+objc.swift in Sources */, + 6132BF4C24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift in Sources */, + 6132BF4724A498D800D7BD17 /* DDSpan+objc.swift in Sources */, + 6132BF4E24A49D5400D7BD17 /* OTNoop.swift in Sources */, + 615A4A8B24A3568900233986 /* OTSpan+objc.swift in Sources */, + 6132BF4424A3AAD700D7BD17 /* OTGlobal+objc.swift in Sources */, + 615A4A8D24A356A000233986 /* OTSpanContext+objc.swift in Sources */, 61133C112423983800786299 /* DatadogConfiguration+objc.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 9E2FB2462447660E001C9B7B /* Sources */ = { + 61441BFE24616DE9003D8BB8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 61441C982461A649003D8BB8 /* DebugTracingViewController.swift in Sources */, + 61441C952461A649003D8BB8 /* ConsoleOutputInterceptor.swift in Sources */, + 61441C972461A649003D8BB8 /* UIViewController+KeyboardControlling.swift in Sources */, + 61B9ED1C2461E12000C0DCFF /* SendLogsFixtureViewController.swift in Sources */, + 61B9ED1D2461E12000C0DCFF /* SendTracesFixtureViewController.swift in Sources */, + 61441C9D2461A796003D8BB8 /* AppConfig.swift in Sources */, + 61441C0524616DE9003D8BB8 /* AppDelegate.swift in Sources */, + 61441C992461A649003D8BB8 /* DebugLoggingViewController.swift in Sources */, + 61441C962461A649003D8BB8 /* UIButton+Disabling.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61441C2624616F1D003D8BB8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 61441C4024617013003D8BB8 /* IntegrationTests.swift in Sources */, + 61B9ED212462089600C0DCFF /* TracingIntegrationTests.swift in Sources */, + 61B9ED1F2461E57700C0DCFF /* UITestsHelpers.swift in Sources */, + 61441C4124617013003D8BB8 /* LoggingIntegrationTests.swift in Sources */, + 61441C4B24618052003D8BB8 /* SpanMatcher.swift in Sources */, + 61441C4924618052003D8BB8 /* JSONDataMatcher.swift in Sources */, + 61441C4A24618052003D8BB8 /* LogMatcher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 61441C6424619FE4003D8BB8 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9EF49F1024476D96004F2CA0 /* BenchmarkTests.swift in Sources */, - 9EF49F1124476D96004F2CA0 /* IntegrationTests.swift in Sources */, - 9EF49F1224476D96004F2CA0 /* LoggingBenchmarkTests.swift in Sources */, - 9EA6A539244897A900621535 /* LoggingIOBenchmarkTests.swift in Sources */, - 9EF49F1324476D96004F2CA0 /* LoggingIntegrationTests.swift in Sources */, - 9EF49F1424476DD6004F2CA0 /* LogMatcher.swift in Sources */, - 9EA6A53E24489DC800621535 /* TestsDirectory.swift in Sources */, + 61441C7A2461A204003D8BB8 /* LoggingBenchmarkTests.swift in Sources */, + 61441C7B2461A204003D8BB8 /* LoggingStorageBenchmarkTests.swift in Sources */, + 6152C84024BE1CC8006A1679 /* DataUploaderBenchmarkTests.swift in Sources */, + E132727D24B35B5F00952F8B /* TracingStorageBenchmarkTests.swift in Sources */, + E132727B24B333C700952F8B /* TracingBenchmarkTests.swift in Sources */, + 61441C7C2461A244003D8BB8 /* TestsDirectory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 61133B8E242393DE00786299 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 61133B81242393DE00786299 /* Datadog */; - targetProxy = 61133B8D242393DE00786299 /* PBXContainerItemProxy */; - }; 61133C732423993200786299 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 61133B81242393DE00786299 /* Datadog */; targetProxy = 61133C722423993200786299 /* PBXContainerItemProxy */; }; - 61133C762423993C00786299 /* PBXTargetDependency */ = { + 61441C3024616F1D003D8BB8 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 61133BEF242397DA00786299 /* DatadogObjc */; - targetProxy = 61133C752423993C00786299 /* PBXContainerItemProxy */; + target = 61441C0124616DE9003D8BB8 /* Example */; + targetProxy = 61441C2F24616F1D003D8BB8 /* PBXContainerItemProxy */; }; - 9E2FB2422447660E001C9B7B /* PBXTargetDependency */ = { + 61441C5024619499003D8BB8 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 61133B81242393DE00786299 /* Datadog */; - targetProxy = 9E2FB2432447660E001C9B7B /* PBXContainerItemProxy */; + targetProxy = 61441C4F24619499003D8BB8 /* PBXContainerItemProxy */; + }; + 61441C5A24619A08003D8BB8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 61441C0124616DE9003D8BB8 /* Example */; + targetProxy = 61441C5924619A08003D8BB8 /* PBXContainerItemProxy */; + }; + 61441C7524619FED003D8BB8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 61441C0124616DE9003D8BB8 /* Example */; + targetProxy = 61441C7424619FED003D8BB8 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + 61441C0A24616DE9003D8BB8 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 61441C0B24616DE9003D8BB8 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 61441C0F24616DEC003D8BB8 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 61441C1024616DEC003D8BB8 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ 61133B94242393DE00786299 /* Debug */ = { isa = XCBuildConfiguration; @@ -1313,10 +2021,11 @@ }; 61133B9A242393DE00786299 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TargetSupport/DatadogTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1325,20 +2034,22 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogTests; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SUPPORTS_MACCATALYST = NO; + SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; }; name = Debug; }; 61133B9B242393DE00786299 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TargetSupport/DatadogTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1347,11 +2058,11 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogTests; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SUPPORTS_MACCATALYST = NO; + SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; }; name = Release; }; @@ -1403,12 +2114,63 @@ }; name = Release; }; - 9E2FB2752447660E001C9B7B /* Debug */ = { + 61441C1424616DEC003D8BB8 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 61441C1524616DEC003D8BB8 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 61441C1624616DEC003D8BB8 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Integration; + }; + 61441C3224616F1D003D8BB8 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9EF49F17244770AD004F2CA0 /* DatadogIntegrationTests.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TargetSupport/DatadogIntegrationTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1417,20 +2179,17 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogIntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; - SUPPORTS_MACCATALYST = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Example; }; name = Debug; }; - 9E2FB2762447660E001C9B7B /* Release */ = { + 61441C3324616F1D003D8BB8 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9EF49F17244770AD004F2CA0 /* DatadogIntegrationTests.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TargetSupport/DatadogIntegrationTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1439,14 +2198,84 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogIntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; - SUPPORTS_MACCATALYST = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Example; + }; + name = Release; + }; + 61441C3424616F1D003D8BB8 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9EF49F17244770AD004F2CA0 /* DatadogIntegrationTests.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/DatadogIntegrationTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogIntegrationTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Example; + }; + name = Integration; + }; + 61441C7124619FE4003D8BB8 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6152C84124BE1F47006A1679 /* DatadogBenchmarkTests.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/DatadogBenchmarkTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogBenchmarkTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + }; + name = Debug; + }; + 61441C7224619FE4003D8BB8 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6152C84124BE1F47006A1679 /* DatadogBenchmarkTests.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/DatadogBenchmarkTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogBenchmarkTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; }; name = Release; }; + 61441C7324619FE4003D8BB8 /* Integration */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6152C84124BE1F47006A1679 /* DatadogBenchmarkTests.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = TargetSupport/DatadogBenchmarkTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogBenchmarkTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + }; + name = Integration; + }; 9E2FB28224476765001C9B7B /* Integration */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1557,10 +2386,11 @@ }; 9E2FB28524476765001C9B7B /* Integration */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 615519252461BCE7002A85CF /* Datadog.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = TargetSupport/DatadogTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1569,34 +2399,11 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogTests; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; - SUPPORTS_MACCATALYST = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Integration; - }; - 9E2FB28624476765001C9B7B /* Integration */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9EF49F17244770AD004F2CA0 /* DatadogIntegrationTests.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = ""; - INFOPLIST_FILE = TargetSupport/DatadogIntegrationTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.DatadogIntegrationTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SUPPORTS_MACCATALYST = NO; + SWIFT_OBJC_BRIDGING_HEADER = "TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; }; name = Integration; }; @@ -1643,12 +2450,32 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 9E2FB2742447660E001C9B7B /* Build configuration list for PBXNativeTarget "DatadogIntegrationTests" */ = { + 61441C1324616DEC003D8BB8 /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 61441C1424616DEC003D8BB8 /* Debug */, + 61441C1524616DEC003D8BB8 /* Release */, + 61441C1624616DEC003D8BB8 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 61441C3124616F1D003D8BB8 /* Build configuration list for PBXNativeTarget "DatadogIntegrationTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 9E2FB2752447660E001C9B7B /* Debug */, - 9E2FB2762447660E001C9B7B /* Release */, - 9E2FB28624476765001C9B7B /* Integration */, + 61441C3224616F1D003D8BB8 /* Debug */, + 61441C3324616F1D003D8BB8 /* Release */, + 61441C3424616F1D003D8BB8 /* Integration */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 61441C7024619FE4003D8BB8 /* Build configuration list for PBXNativeTarget "DatadogBenchmarkTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 61441C7124619FE4003D8BB8 /* Debug */, + 61441C7224619FE4003D8BB8 /* Release */, + 61441C7324619FE4003D8BB8 /* Integration */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -1656,7 +2483,11 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - 9EF49F0E24476D0C004F2CA0 /* HTTPServerMock */ = { + 61441C43246174CE003D8BB8 /* HTTPServerMock */ = { + isa = XCSwiftPackageProductDependency; + productName = HTTPServerMock; + }; + 6152C83D24BE1C91006A1679 /* HTTPServerMock */ = { isa = XCSwiftPackageProductDependency; productName = HTTPServerMock; }; diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/7C373BCE-2B91-4706-AD99-F9FD342891D3.plist b/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/7C373BCE-2B91-4706-AD99-F9FD342891D3.plist new file mode 100644 index 0000000000..33953da047 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/7C373BCE-2B91-4706-AD99-F9FD342891D3.plist @@ -0,0 +1,75 @@ + + + + + classNames + + LoggingBenchmarkTests + + testCreatingOneLog() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 7.45e-05 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + testCreatingOneLogWithAttributes() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.000116 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + testCreatingOneLogWithTags() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 7.79e-05 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + + LoggingStorageBenchmarkTests + + testReadingLogsFromDisc() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.00161 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + testWrittingLogsOnDisc() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.00487 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/FE5A2FAD-6AE2-410D-A9A1-1E5F56CC9C52.plist b/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/FE5A2FAD-6AE2-410D-A9A1-1E5F56CC9C52.plist new file mode 100644 index 0000000000..ef3ae79519 --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/FE5A2FAD-6AE2-410D-A9A1-1E5F56CC9C52.plist @@ -0,0 +1,141 @@ + + + + + classNames + + LoggingBenchmarkTests + + testCreatingOneLog() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 7.5537e-05 + baselineIntegrationDisplayName + 7 Jul 2020 at 10:28:47 + maxPercentRelativeStandardDeviation + 30 + + + testCreatingOneLogWithAttributes() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 6.6391e-05 + baselineIntegrationDisplayName + 7 Jul 2020 at 10:28:47 + maxPercentRelativeStandardDeviation + 30 + + + testCreatingOneLogWithTags() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 7.2e-05 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + + LoggingStorageBenchmarkTests + + testReadingLogsFromDisc() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.00151 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + testWrittingLogsOnDisc() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.00376 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + + TracingBenchmarkTests + + testCreatingAndEndingOneSpan() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 5.81e-05 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + testCreatingOneSpanWithBaggageItems() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 6.89e-05 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + testCreatingOneSpanWithTags() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 6.48e-05 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + + TracingStorageBenchmarkTests + + testReadingSpansFromDisc() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.00139 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + testWrittingSpansOnDisc() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.00352 + baselineIntegrationDisplayName + Local Baseline + maxPercentRelativeStandardDeviation + 30 + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/Info.plist b/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/Info.plist new file mode 100644 index 0000000000..91bcc1a3ea --- /dev/null +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcbaselines/61441C6724619FE4003D8BB8.xcbaseline/Info.plist @@ -0,0 +1,33 @@ + + + + + runDestinationsByUUID + + 7C373BCE-2B91-4706-AD99-F9FD342891D3 + + targetArchitecture + arm64 + targetDevice + + modelCode + iPhone10,6 + platformIdentifier + com.apple.platform.iphoneos + + + FE5A2FAD-6AE2-410D-A9A1-1E5F56CC9C52 + + targetArchitecture + arm64e + targetDevice + + modelCode + iPhone12,1 + platformIdentifier + com.apple.platform.iphoneos + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Datadog.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Datadog.xcscheme index 19a8de90d5..40e0b2299f 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Datadog.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/Datadog.xcscheme @@ -26,10 +26,16 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" + shouldUseLaunchSchemeArgsEnv = "NO" enableThreadSanitizer = "YES" codeCoverageEnabled = "YES" onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + skipped = "NO" + testExecutionOrdering = "random"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogIntegrationTests.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogIntegrationTests.xcscheme index e3c2e39149..8dc1b17f7d 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogIntegrationTests.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogIntegrationTests.xcscheme @@ -14,7 +14,7 @@ @@ -25,10 +25,12 @@ + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> @@ -38,7 +40,7 @@ @@ -56,7 +58,7 @@ @@ -65,12 +67,28 @@ + + + + + + @@ -79,7 +97,7 @@ + + + + + + + + diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc.xcscheme index 5013baa631..e5ae68ed69 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogObjc.xcscheme @@ -26,9 +26,15 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" + shouldUseLaunchSchemeArgsEnv = "NO" codeCoverageEnabled = "YES" onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Example/AppConfig.swift b/Datadog/Example/AppConfig.swift new file mode 100644 index 0000000000..3a753f4e1c --- /dev/null +++ b/Datadog/Example/AppConfig.swift @@ -0,0 +1,89 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation +import Datadog + +protocol AppConfig { + /// Service name used for logs and traces. + var serviceName: String { get } + /// SDK configuration + var datadogConfiguration: Datadog.Configuration { get } + /// Endpoints for arbitrary network requests + var arbitraryNetworkURL: URL { get } + var arbitraryNetworkRequest: URLRequest { get } +} + +struct ExampleAppConfig: AppConfig { + /// Service name used for logs and traces. + let serviceName = "ios-sdk-example-app" + /// Configuration for uploading logs to Datadog servers + let datadogConfiguration: Datadog.Configuration + + let arbitraryNetworkURL = URL(string: "https://status.datadoghq.com")! + let arbitraryNetworkRequest: URLRequest = { + var request = URLRequest(url: URL(string: "https://status.datadoghq.com/bad/path")!) + request.httpMethod = "POST" + request.addValue("dataTaskWithRequest", forHTTPHeaderField: "creation-method") + return request + }() + + init() { + guard let clientToken = Bundle.main.infoDictionary?["DatadogClientToken"] as? String, !clientToken.isEmpty else { + fatalError(""" + โœ‹โ›”๏ธ Cannot read `DATADOG_CLIENT_TOKEN` from `Info.plist` dictionary. + Please update `Datadog.xcconfig` in the repository root with your own + client token obtained on datadoghq.com. + You might need to run `Product > Clean Build Folder` before retrying. + """) + } + + self.datadogConfiguration = Datadog.Configuration + .builderUsing(clientToken: clientToken, environment: "tests") + .set(tracedHosts: [arbitraryNetworkURL.host!, "foo.bar"]) + .build() + } +} + +struct UITestAppConfig: AppConfig { + /// Mocked service name for UITests + let serviceName = "ui-tests-service-name" + /// Configuration for uploading logs to mock servers + let datadogConfiguration: Datadog.Configuration + let arbitraryNetworkURL: URL + let arbitraryNetworkRequest: URLRequest + + init() { + let mockLogsEndpoint = ProcessInfo.processInfo.environment["DD_MOCK_LOGS_ENDPOINT_URL"]! + let mockTracesEndpoint = ProcessInfo.processInfo.environment["DD_MOCK_TRACES_ENDPOINT_URL"]! + let sourceEndpoint = ProcessInfo.processInfo.environment["DD_MOCK_SOURCE_ENDPOINT_URL"]! + let tracedhost = URL(string: sourceEndpoint)!.host! + self.datadogConfiguration = Datadog.Configuration + .builderUsing(clientToken: "ui-tests-client-token", environment: "integration") + .set(logsEndpoint: .custom(url: mockLogsEndpoint)) + .set(tracesEndpoint: .custom(url: mockTracesEndpoint)) + .set(tracedHosts: [tracedhost, "foo.bar"]) + .build() + + let url = URL(string: sourceEndpoint)! + self.arbitraryNetworkURL = URL(string: url.deletingLastPathComponent().absoluteString + "inspect")! + self.arbitraryNetworkRequest = { + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.addValue("dataTaskWithRequest", forHTTPHeaderField: "creation-method") + return request + }() + } +} + +/// Returns different `AppConfig` when running in UI Tests or launching directly. +func currentAppConfig() -> AppConfig { + if ProcessInfo.processInfo.arguments.contains("IS_RUNNING_UI_TESTS") { + return UITestAppConfig() + } else { + return ExampleAppConfig() + } +} diff --git a/Datadog/Example/AppDelegate.swift b/Datadog/Example/AppDelegate.swift new file mode 100644 index 0000000000..8bb0e6a73d --- /dev/null +++ b/Datadog/Example/AppDelegate.swift @@ -0,0 +1,102 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-2020 Datadog, Inc. +*/ + +import UIKit +import Datadog + +var logger: Logger! +var tracer: OTTracer { Global.sharedTracer } + +let appConfig: AppConfig = currentAppConfig() + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + if isRunningUnitTests() { + window = nil + return false + } + + if isRunningUITests() { + deletePersistedSDKData() + } + + // Initialize Datadog SDK + Datadog.initialize( + appContext: .init(), + configuration: appConfig.datadogConfiguration + ) + + // Set user information + Datadog.setUserInfo(id: "abcd-1234", name: "foo", email: "foo@example.com") + + // Create logger instance + logger = Logger.builder + .set(serviceName: appConfig.serviceName) + .set(loggerName: "logger-name") + .sendNetworkInfo(true) + .printLogsToConsole(true, usingFormat: .shortWith(prefix: "[iOS App] ")) + .build() + + // Register global tracer + Global.sharedTracer = Tracer.initialize( + configuration: Tracer.Configuration( + serviceName: appConfig.serviceName, + sendNetworkInfo: true + ) + ) + + // Set highest verbosity level to see internal actions made in SDK + Datadog.verbosityLevel = .debug + + // Add attributes + logger.addAttribute(forKey: "device-model", value: UIDevice.current.model) + + // Add tags + #if DEBUG + logger.addTag(withKey: "build_configuration", value: "debug") + #else + logger.addTag(withKey: "build_configuration", value: "release") + #endif + + return true + } + + func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + installConsoleOutputInterceptor() + return true + } +} + +private func isRunningUnitTests() -> Bool { + return ProcessInfo.processInfo.arguments.contains("IS_RUNNING_UNIT_TESTS") +} + +private func isRunningUITests() -> Bool { + return appConfig is UITestAppConfig +} + +private func deletePersistedSDKData() { + guard let cachesDirectoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { + return + } + + do { + let dataDirectories = try FileManager.default + .contentsOfDirectory(at: cachesDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey, .canonicalPathKey]) + .filter { $0.absoluteString.contains("com.datadoghq") } + + try dataDirectories.forEach { url in + try FileManager.default.removeItem(at: url) + print("๐Ÿงน Deleted SDK data directory: \(url)") + } + } catch { + print("๐Ÿ”ฅ Failed to delete SDK data directory: \(error)") + } +} diff --git a/Datadog/Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/Datadog/Example/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Datadog/Example/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Datadog/Example/Assets.xcassets/Contents.json b/Datadog/Example/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Datadog/Example/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Datadog/Example/Base.lproj/LaunchScreen.storyboard b/Datadog/Example/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..865e9329f3 --- /dev/null +++ b/Datadog/Example/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Example/Base.lproj/Main.storyboard b/Datadog/Example/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..8b4f827307 --- /dev/null +++ b/Datadog/Example/Base.lproj/Main.storyboard @@ -0,0 +1,724 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Datadog/Example/Debugging/DebugLoggingViewController.swift b/Datadog/Example/Debugging/DebugLoggingViewController.swift new file mode 100644 index 0000000000..18b3fefa24 --- /dev/null +++ b/Datadog/Example/Debugging/DebugLoggingViewController.swift @@ -0,0 +1,63 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import UIKit + +class DebugLoggingViewController: UIViewController { + @IBOutlet weak var logLevelSegmentedControl: UISegmentedControl! + @IBOutlet weak var logMessageTextField: UITextField! + @IBOutlet weak var logServiceNameTextField: UITextField! + @IBOutlet weak var sendOnceButton: UIButton! + @IBOutlet weak var send10xButton: UIButton! + @IBOutlet weak var consoleTextView: UITextView! + + enum LogLevelSegment: Int { + case debug = 0, info, notice, warn, error, critical + } + + override func viewDidLoad() { + super.viewDidLoad() + logServiceNameTextField.text = appConfig.serviceName + hideKeyboardWhenTapOutside() + startDisplayingDebugInfo(in: consoleTextView) + } + + private var message: String { + logMessageTextField.text!.isEmpty ? "message" : logMessageTextField.text! + } + + @IBAction func didTapSendSingleLog(_ sender: Any) { + sendOnceButton.disableFor(seconds: 0.5) + + switch LogLevelSegment(rawValue: logLevelSegmentedControl.selectedSegmentIndex) { + case .debug: logger.debug(message) + case .info: logger.info(message) + case .notice: logger.notice(message) + case .warn: logger.warn(message) + case .error: logger.error(message) + case .critical: logger.critical(message) + default: assertionFailure("Unsupported `.selectedSegmentIndex` value: \(logLevelSegmentedControl.selectedSegmentIndex)") + } + } + + @IBAction func didTapSend10Logs(_ sender: Any) { + send10xButton.disableFor(seconds: 0.5) + + switch LogLevelSegment(rawValue: logLevelSegmentedControl.selectedSegmentIndex) { + case .debug: repeat10x { logger.debug(message) } + case .info: repeat10x { logger.info(message) } + case .notice: repeat10x { logger.notice(message) } + case .warn: repeat10x { logger.warn(message) } + case .error: repeat10x { logger.error(message) } + case .critical: repeat10x { logger.critical(message) } + default: assertionFailure("Unsupported `.selectedSegmentIndex` value: \(logLevelSegmentedControl.selectedSegmentIndex)") + } + } + + private func repeat10x(block: () -> Void) { + (0..<10).forEach { _ in block() } + } +} diff --git a/Datadog/Example/Debugging/DebugTracingViewController.swift b/Datadog/Example/Debugging/DebugTracingViewController.swift new file mode 100644 index 0000000000..393c5c6ce7 --- /dev/null +++ b/Datadog/Example/Debugging/DebugTracingViewController.swift @@ -0,0 +1,121 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import UIKit +import Datadog + +class DebugTracingViewController: UIViewController { + @IBOutlet weak var serviceNameTextField: UITextField! + @IBOutlet weak var isErrorSegmentedControl: UISegmentedControl! + @IBOutlet weak var singleSpanOperationNameTextField: UITextField! + @IBOutlet weak var singleSpanResourceNameTextField: UITextField! + @IBOutlet weak var sendSingleSpanButton: UIButton! + @IBOutlet weak var complexSpanOperationNameTextField: UITextField! + @IBOutlet weak var sendComplexSpanButton: UIButton! + @IBOutlet weak var consoleTextView: UITextView! + + private let queue1 = DispatchQueue(label: "com.datadoghq.debug-tracing1") + private let queue2 = DispatchQueue(label: "com.datadoghq.debug-tracing2") + private let queue3 = DispatchQueue(label: "com.datadoghq.debug-tracing3") + + override func viewDidLoad() { + super.viewDidLoad() + serviceNameTextField.text = appConfig.serviceName + hideKeyboardWhenTapOutside() + startDisplayingDebugInfo(in: consoleTextView) + } + + private var isError: Bool { + isErrorSegmentedControl.selectedSegmentIndex == 1 + } + + // MARK: - Sending single span + + private var singleSpanOperationName: String { + singleSpanOperationNameTextField.text!.isEmpty ? "single span" : singleSpanOperationNameTextField.text! + } + + private var singleSpanResourceName: String? { + singleSpanResourceNameTextField.text!.isEmpty ? nil : singleSpanResourceNameTextField.text! + } + + @IBAction func didTapSendSingleSpan(_ sender: Any) { + sendSingleSpanButton.disableFor(seconds: 0.5) + + let spanName = singleSpanOperationName + let resourceName = singleSpanResourceName + let isError = self.isError + + queue1.async { + let span = Global.sharedTracer.startSpan(operationName: spanName) + if let resourceName = resourceName { + span.setTag(key: DDTags.resource, value: resourceName) + } + if isError { + // To only mark the span as an error, use the Open Tracing `error` tag: + // span.setTag(key: "error", value: true) + + // If you want more error information to be digested and attached to the span by Datadog, + // send a log containing Open Tracing log fields: + span.log( + fields: [ + OTLogFields.event: "error", + OTLogFields.errorKind: "Simulated error", + OTLogFields.message: "Describe what happened", + OTLogFields.stack: "Foo.swift:42", + ] + ) + } + wait(seconds: 1) + span.finish() + } + } + + // MARK: - Sending complex span + + private var complexSpanOperationName: String { + complexSpanOperationNameTextField.text!.isEmpty ? "complex span" : complexSpanOperationNameTextField.text! + } + + @IBAction func didTapSendComplexSpan(_ sender: Any) { + sendComplexSpanButton.disableFor(seconds: 0.5) + + let spanName = complexSpanOperationName + + queue1.async { [weak self] in + guard let self = self else { return } + + let rootSpan = Global.sharedTracer.startSpan(operationName: spanName) + wait(seconds: 0.5) + + self.queue2.sync { + let child1 = Global.sharedTracer.startSpan(operationName: "child operation 1", childOf: rootSpan.context) + wait(seconds: 0.5) + child1.finish() + + wait(seconds: 0.1) + + let child2 = Global.sharedTracer.startSpan(operationName: "child operation 2", childOf: rootSpan.context) + wait(seconds: 0.5) + + self.queue3.sync { + let grandChild = Global.sharedTracer.startSpan(operationName: "grandchild operation", childOf: child2.context) + wait(seconds: 1) + grandChild.finish() + } + + child2.finish() + } + + wait(seconds: 0.5) + rootSpan.finish() + } + } +} + +private func wait(seconds: TimeInterval) { + Thread.sleep(forTimeInterval: 0.5) +} diff --git a/Datadog/Example/IntegrationTestFixtures/SendLogsFixtureViewController.swift b/Datadog/Example/IntegrationTestFixtures/SendLogsFixtureViewController.swift new file mode 100644 index 0000000000..2f4e6e398c --- /dev/null +++ b/Datadog/Example/IntegrationTestFixtures/SendLogsFixtureViewController.swift @@ -0,0 +1,28 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import UIKit + +internal class SendLogsFixtureViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + + // Send logs + logger.addTag(withKey: "tag1", value: "tag-value") + logger.add(tag: "tag2") + + logger.addAttribute(forKey: "logger-attribute1", value: "string value") + logger.addAttribute(forKey: "logger-attribute2", value: 1_000) + logger.addAttribute(forKey: "some-url", value: URL(string: "https://example.com/image.png")!) + + logger.debug("debug message", attributes: ["attribute": "value"]) + logger.info("info message", attributes: ["attribute": "value"]) + logger.notice("notice message", attributes: ["attribute": "value"]) + logger.warn("warn message", attributes: ["attribute": "value"]) + logger.error("error message", attributes: ["attribute": "value"]) + logger.critical("critical message", attributes: ["attribute": "value"]) + } +} diff --git a/Datadog/Example/IntegrationTestFixtures/SendTracesFixtureViewController.swift b/Datadog/Example/IntegrationTestFixtures/SendTracesFixtureViewController.swift new file mode 100644 index 0000000000..77e840f642 --- /dev/null +++ b/Datadog/Example/IntegrationTestFixtures/SendTracesFixtureViewController.swift @@ -0,0 +1,86 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import UIKit +import Datadog + +internal class SendTracesFixtureViewController: UIViewController { + private let backgroundQueue = DispatchQueue(label: "background-queue") + + /// Traces view appearing + private var viewAppearingSpan: OTSpan! + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + viewAppearingSpan = tracer.startSpan(operationName: "view appearing") + + // Set `class: SendTracesFixtureViewController` baggage item on the root span, so it will be propagated to all child spans. + viewAppearingSpan.setBaggageItem(key: "class", value: "\(type(of: self))") + + let dataDownloadingSpan = tracer.startSpan( + operationName: "data downloading", + childOf: viewAppearingSpan.context + ) + dataDownloadingSpan.setTag(key: "data.kind", value: "image") + dataDownloadingSpan.setTag(key: "data.url", value: URL(string: "https://example.com/image.png")!) + dataDownloadingSpan.setTag(key: DDTags.resource, value: "GET /image.png") + + // Step #1: Manual tracing with complex hierarchy + downloadSomeData { [weak self] data in + // Simulate logging download progress + dataDownloadingSpan.log( + fields: [ + OTLogFields.message: "download progress", + "progress": 0.99 + ] + ) + + dataDownloadingSpan.finish() + guard let self = self else { return } + + let dataPresentationSpan = tracer.startSpan( + operationName: "data presentation", + childOf: self.viewAppearingSpan.context + ) + self.present(data: data) + dataPresentationSpan.setTag(key: OTTags.error, value: true) + dataPresentationSpan.finish() + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + viewAppearingSpan.finish() + + // Send requests which will be automatically traced as tracing auto-instrumentation is enabled + let url = currentAppConfig().arbitraryNetworkURL + let request = currentAppConfig().arbitraryNetworkRequest + let dnsErrorURL = URL(string: "https://foo.bar")! + // Step #2: Auto-instrumentated request with URL to succeed + URLSession.shared.dataTask(with: url) { _, _, _ in + // Step #3: Auto-instrumentated request with Request to fail + URLSession.shared.dataTask(with: request) { _, _, _ in + // Step #4: Auto-instrumentated request to return NSError + URLSession.shared.dataTask(with: dnsErrorURL) { _, _, _ in }.resume() + }.resume() + }.resume() + } + + /// Simulates doing an asynchronous work with completion. + private func downloadSomeData(completion: @escaping (Data) -> Void) { + backgroundQueue.async { + Thread.sleep(forTimeInterval: 0.3) + DispatchQueue.main.async { completion(Data()) } + } + } + + /// Simulates presenting some data. + private func present(data: Data) { + Thread.sleep(forTimeInterval: 0.06) + } +} diff --git a/Shopist/Shopist/Utils/ConsoleOutputInterceptor.swift b/Datadog/Example/Utils/ConsoleOutputInterceptor.swift similarity index 84% rename from Shopist/Shopist/Utils/ConsoleOutputInterceptor.swift rename to Datadog/Example/Utils/ConsoleOutputInterceptor.swift index fd7a5e2cef..3086f56c7a 100644 --- a/Shopist/Shopist/Utils/ConsoleOutputInterceptor.swift +++ b/Datadog/Example/Utils/ConsoleOutputInterceptor.swift @@ -15,7 +15,7 @@ class ConsoleOutputInterceptor { /// Max length of output log to notify. Content exceeding this length will be truncated at beginning. private let maxContentsLength = 2_048 - private var contents: String = "" + private(set) var contents: String = "" var notifyContentsChange: ((String) -> Void)? @@ -32,8 +32,7 @@ class ConsoleOutputInterceptor { let newContents = contents + "\n" + newLog contents = String(newContents.suffix(maxContentsLength)) DispatchQueue.main.async { - let reversedContents = self.contents.split(separator: "\n").reversed().joined(separator: "\n") - self.notifyContentsChange?(reversedContents) + self.notifyContentsChange?(reverseLinesOrder(self.contents)) } } } @@ -46,11 +45,17 @@ func installConsoleOutputInterceptor() { } func startDisplayingDebugInfo(in textView: UITextView) { + textView.text = reverseLinesOrder(consoleOutput.contents) + consoleOutput.notifyContentsChange = { [weak textView] newContents in textView?.text = newContents } } +private func reverseLinesOrder(_ string: String) -> String { + return string.split(separator: "\n").reversed().joined(separator: "\n") +} + #else import UIKit.UITextView diff --git a/Datadog/Example/Utils/UIButton+Disabling.swift b/Datadog/Example/Utils/UIButton+Disabling.swift new file mode 100644 index 0000000000..415019d067 --- /dev/null +++ b/Datadog/Example/Utils/UIButton+Disabling.swift @@ -0,0 +1,25 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import UIKit + +extension UIButton { + func disableFor(seconds: TimeInterval) { + let originalBackgroundColor = self.backgroundColor + + self.isEnabled = false + if #available(iOS 13.0, *) { + self.backgroundColor = .systemGray4 + } else { + self.backgroundColor = .systemGray + } + + DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { [weak self] in + self?.isEnabled = true + self?.backgroundColor = originalBackgroundColor + } + } +} diff --git a/Shopist/Shopist/Utils/UIViewController+KeyboardControlling.swift b/Datadog/Example/Utils/UIViewController+KeyboardControlling.swift similarity index 100% rename from Shopist/Shopist/Utils/UIViewController+KeyboardControlling.swift rename to Datadog/Example/Utils/UIViewController+KeyboardControlling.swift diff --git a/Datadog/TargetSupport/DatadogBenchmarkTests/DatadogBenchmarkTests.xcconfig b/Datadog/TargetSupport/DatadogBenchmarkTests/DatadogBenchmarkTests.xcconfig new file mode 100644 index 0000000000..067d020ba6 --- /dev/null +++ b/Datadog/TargetSupport/DatadogBenchmarkTests/DatadogBenchmarkTests.xcconfig @@ -0,0 +1,5 @@ +// Get common settings from Datadog.xcconfig +#include "../xcconfigs/Datadog.xcconfig" + +// This file is auto generated by pre-build action +#include "../xcconfigs/MockServerAddress.local.xcconfig" diff --git a/Datadog/TargetSupport/DatadogBenchmarkTests/Info.plist b/Datadog/TargetSupport/DatadogBenchmarkTests/Info.plist new file mode 100644 index 0000000000..e004e59be4 --- /dev/null +++ b/Datadog/TargetSupport/DatadogBenchmarkTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + MockServerAddress + $(MOCK_SERVER_ADDRESS) + + diff --git a/Datadog/TargetSupport/DatadogIntegrationTests/DatadogIntegrationTests.xcconfig b/Datadog/TargetSupport/DatadogIntegrationTests/DatadogIntegrationTests.xcconfig index 94c2b20dc9..067d020ba6 100644 --- a/Datadog/TargetSupport/DatadogIntegrationTests/DatadogIntegrationTests.xcconfig +++ b/Datadog/TargetSupport/DatadogIntegrationTests/DatadogIntegrationTests.xcconfig @@ -1,5 +1,5 @@ -// Get codesign settings from app-target.xcconfig -#include "../dependency-manager-tests/xcconfigs/app-target.xcconfig" +// Get common settings from Datadog.xcconfig +#include "../xcconfigs/Datadog.xcconfig" // This file is auto generated by pre-build action -#include "MockServerAddress.local.xcconfig" +#include "../xcconfigs/MockServerAddress.local.xcconfig" diff --git a/Datadog/TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h b/Datadog/TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h new file mode 100644 index 0000000000..7be31d06b4 --- /dev/null +++ b/Datadog/TargetSupport/DatadogTests/DatadogTests-Bridging-Header.h @@ -0,0 +1,11 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-2020 Datadog, Inc. +*/ + +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "NSURLSessionBridge.h" diff --git a/Datadog/TargetSupport/Example/Info.plist b/Datadog/TargetSupport/Example/Info.plist new file mode 100644 index 0000000000..e7d2d49731 --- /dev/null +++ b/Datadog/TargetSupport/Example/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + DatadogClientToken + $(DATADOG_CLIENT_TOKEN) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/DatadogSDK.podspec b/DatadogSDK.podspec index ea7a3fa6cd..9876073fad 100644 --- a/DatadogSDK.podspec +++ b/DatadogSDK.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDK" s.module_name = "Datadog" - s.version = "1.2.4" + s.version = "1.3.0-beta3" s.summary = "Official Datadog Swift SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogSDK.podspec.src b/DatadogSDK.podspec.src new file mode 100644 index 0000000000..0cc0854f7d --- /dev/null +++ b/DatadogSDK.podspec.src @@ -0,0 +1,27 @@ +Pod::Spec.new do |s| + s.name = "DatadogSDK" + s.module_name = "Datadog" + s.version = "__DATADOG_VERSION__" + s.summary = "Official Datadog Swift SDK for iOS." + + s.homepage = "https://www.datadoghq.com" + s.social_media_url = "https://twitter.com/datadoghq" + + s.license = { :type => "Apache", :file => 'LICENSE' } + s.authors = { + "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", + "Mert Buran" => "mert.buran@datadoghq.com" + } + + s.swift_version = '5.1' + s.ios.deployment_target = '11.0' + + s.source = { :git => "https://github.com/DataDog/dd-sdk-ios.git", :tag => s.version.to_s } + + s.source_files = ["Sources/Datadog/**/*.swift", + "Sources/_Datadog_Private/**/*.{h,m}", + "Datadog/TargetSupport/Datadog/Datadog.h"] + s.public_header_files = "Datadog/TargetSupport/Datadog/Datadog.h" + s.private_header_files = "Sources/_Datadog_Private/include/*.h" + s.module_map = "Sources/Datadog/Datadog.modulemap" +end diff --git a/DatadogSDKObjc.podspec b/DatadogSDKObjc.podspec index c5c5dd8187..8e27365e82 100644 --- a/DatadogSDKObjc.podspec +++ b/DatadogSDKObjc.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "DatadogSDKObjc" s.module_name = "DatadogObjc" - s.version = "1.2.4" + s.version = "1.3.0-beta3" s.summary = "Official Datadog Objective-C SDK for iOS." s.homepage = "https://www.datadoghq.com" diff --git a/DatadogSDKObjc.podspec.src b/DatadogSDKObjc.podspec.src new file mode 100644 index 0000000000..6d08a75e60 --- /dev/null +++ b/DatadogSDKObjc.podspec.src @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = "DatadogSDKObjc" + s.module_name = "DatadogObjc" + s.version = "__DATADOG_VERSION__" + s.summary = "Official Datadog Objective-C SDK for iOS." + + s.homepage = "https://www.datadoghq.com" + s.social_media_url = "https://twitter.com/datadoghq" + + s.license = { :type => "Apache", :file => 'LICENSE' } + s.authors = { + "Maciek Grzybowski" => "maciek.grzybowski@datadoghq.com", + "Mert Buran" => "mert.buran@datadoghq.com" + } + + s.swift_version = '5.1' + s.ios.deployment_target = '11.0' + + s.source = { :git => 'https://github.com/DataDog/dd-sdk-ios.git', :tag => s.version.to_s } + + s.source_files = "Sources/DatadogObjc/**/*.swift" + s.dependency 'DatadogSDK' +end diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv new file mode 100644 index 0000000000..b2b779d47d --- /dev/null +++ b/LICENSE-3rdparty.csv @@ -0,0 +1,4 @@ +Component,Origin,License,Copyright +import,io.opentracing,MIT,Copyright 2018 LightStep +import (tools),https://github.com/jpsim/SourceKitten,MIT,Copyright (c) 2014 JP Simard +import (tools),https://github.com/apple/swift-argument-parser,Apache-2.0,(c) 2020 Apple Inc. and the Swift project authors \ No newline at end of file diff --git a/Makefile b/Makefile index a1ccd7aaa1..1dfa559a65 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ all: tools dependencies xcodeproj-httpservermock templates -.PHONY : examples tools +.PHONY : tools tools: @echo "โš™๏ธ Installing tools..." @@ -7,13 +7,7 @@ tools: @echo "OK ๐Ÿ‘Œ" dependencies: -ifneq ("$(wildcard ./Cartfile)","") - @echo "โš™๏ธ Cartfile found, bootstrapping..." - @carthage bootstrap --platform iOS -else - @echo "โš™๏ธ Cartfile not found, ignoring." - @echo "OK ๐Ÿ‘Œ" -endif + @echo "โš™๏ธ No dependencies required, skipping..." xcodeproj-httpservermock: @echo "โš™๏ธ Generating 'HTTPServerMock.xcodeproj'..." @@ -36,3 +30,26 @@ test-carthage: # Tests if current branch ships a valid Cocoapods project. test-cocoapods: @cd dependency-manager-tests/cocoapods && $(MAKE) + +# Generate api-surface files for Datadog and DatadogObjc. +api-surface: + @cd tools/api-surface/ && swift build --configuration release + @echo "Generating api-surface-swift" + ./tools/api-surface/.build/x86_64-apple-macosx/release/api-surface workspace --workspace-name Datadog.xcworkspace --scheme Datadog --path . > api-surface-swift + @echo "Generating api-surface-objc" + ./tools/api-surface/.build/x86_64-apple-macosx/release/api-surface workspace --workspace-name Datadog.xcworkspace --scheme DatadogObjc --path . > api-surface-objc + +bump: + @read -p "Enter version number: " version; \ + echo "// GENERATED FILE: Do not edit directly\n\ninternal let sdkVersion = \"$$version\"" > Sources/Datadog/Versioning.swift; \ + sed "s/__DATADOG_VERSION__/$$version/g" DatadogSDK.podspec.src > DatadogSDK.podspec; \ + sed "s/__DATADOG_VERSION__/$$version/g" DatadogSDKObjc.podspec.src > DatadogSDKObjc.podspec; \ + git add . ; \ + git commit -m "Bumped version to $$version"; \ + echo Bumped version to $$version + +ship: + pod spec lint DatadogSDK.podspec + pod spec lint DatadogSDKObjc.podspec + pod trunk push DatadogSDK.podspec + pod trunk push DatadogSDKObjc.podspec diff --git a/Package.swift b/Package.swift index a97b03115f..f01fb582f3 100644 --- a/Package.swift +++ b/Package.swift @@ -17,8 +17,7 @@ let package = Package( type: .dynamic, targets: ["DatadogObjc"]), ], - dependencies: [ - ], + dependencies: [], targets: [ .target( name: "Datadog", diff --git a/README.md b/README.md index 60d3d4e4bc..f1fcc3060d 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,17 @@ ## Getting Started -See the dedicated [Datadog iOS log collection](https://docs.datadoghq.com/logs/log_collection/ios/?tab=us) documentation to learn how to send logs from your iOS application to Datadog. +### Log Collection +See the dedicated [Datadog iOS Log Collection](https://docs.datadoghq.com/logs/log_collection/ios/?tab=us) documentation to learn how to send logs from your iOS application to Datadog. -## Example Projects +![Datadog iOS Log Collection](docs/images/logging.png) -This repository contains example projects showing SDK features (see `examples/` folder). To send logs to Datadog, you must configure `examples/examples-secret.xcconfig` file with your own client token obtained on Datadog website. Use `make examples` tool to have the file template generated for you: +### Trace Collection (beta) -```xml -DATADOG_CLIENT_TOKEN=your-own-token-generated-on-datadog-website -``` +This feature is currently in beta. See [Datadog iOS Trace Collection](https://docs.datadoghq.com/tracing/setup/ios/?tab=us) documentation to try it out. + +![Datadog iOS Log Collection](docs/images/tracing.png) ## Contributing diff --git a/Shopist/Makefile b/Shopist/Makefile deleted file mode 100644 index 5f3fcfea73..0000000000 --- a/Shopist/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -all: secret - -LOCAL_XCCONFIG="./Shopist/shopist-secrets.local.xcconfig" -ifeq ($(secret),) - secret="my-secret-token" -endif - -secret: -ifeq (,$(wildcard ./Shopist/shopist-secrets.local.xcconfig)) - @echo "Creating shopist-secrets.local.xcconfig..." ; - @echo DATADOG_CLIENT_TOKEN="$(secret)" >> $(LOCAL_XCCONFIG) - @echo "๐Ÿ”‘ Your token is set: $(secret)" - @echo "๐Ÿ›  You can change it in $(LOCAL_XCCONFIG)" -endif - @echo "All good ๐Ÿ‘Œ" diff --git a/Shopist/Shopist.xcodeproj/project.pbxproj b/Shopist/Shopist.xcodeproj/project.pbxproj index 59c983ce2f..425444d733 100644 --- a/Shopist/Shopist.xcodeproj/project.pbxproj +++ b/Shopist/Shopist.xcodeproj/project.pbxproj @@ -8,21 +8,31 @@ /* Begin PBXBuildFile section */ 9E6ABAD3244F43BD003AE249 /* ExampleAppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6ABACC244F43BC003AE249 /* ExampleAppConfig.swift */; }; - 9E6ABAD4244F43BD003AE249 /* ConsoleOutputInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6ABACE244F43BD003AE249 /* ConsoleOutputInterceptor.swift */; }; - 9E6ABAD5244F43BD003AE249 /* UIViewController+KeyboardControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6ABACF244F43BD003AE249 /* UIViewController+KeyboardControlling.swift */; }; - 9E6ABAD6244F43BD003AE249 /* SendMessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6ABAD0244F43BD003AE249 /* SendMessageViewController.swift */; }; 9E6ABAD7244F43BD003AE249 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6ABAD1244F43BD003AE249 /* AppDelegate.swift */; }; 9E6ABAD8244F43BD003AE249 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6ABAD2244F43BD003AE249 /* SceneDelegate.swift */; }; 9E6ABAE1244F4450003AE249 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9E6ABADF244F43E2003AE249 /* Assets.xcassets */; }; 9EE0E1B0244F65900030DC52 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9E6ABADC244F43E2003AE249 /* LaunchScreen.storyboard */; }; 9EE0E1B1244F65900030DC52 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9E6ABADE244F43E2003AE249 /* Main.storyboard */; }; - 9EE0E1B7244F65B30030DC52 /* Shopist.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9EE0E1B6244F65B30030DC52 /* Shopist.xcconfig */; }; 9EE2AA14244F38E000A2C252 /* ShopistUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EE2AA13244F38E000A2C252 /* ShopistUITests.swift */; }; 9EE2AA31244F3A4500A2C252 /* Datadog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9EE2AA29244F3A3300A2C252 /* Datadog.framework */; }; 9EE2AA32244F3A4500A2C252 /* Datadog.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9EE2AA29244F3A3300A2C252 /* Datadog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 61441C8B2461A531003D8BB8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9EE2AA21244F3A3300A2C252 /* Datadog.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 61441C6824619FE4003D8BB8; + remoteInfo = DatadogBenchmarkTests; + }; + 61441C8D2461A531003D8BB8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9EE2AA21244F3A3300A2C252 /* Datadog.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 61441C0224616DE9003D8BB8; + remoteInfo = Example; + }; 9EE2AA10244F38E000A2C252 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9EE2A9E6244F38DE00A2C252 /* Project object */; @@ -75,17 +85,16 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 611E133E2461D75D007B7552 /* Datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.xcconfig; sourceTree = ""; }; + 611E133F2461D75D007B7552 /* Datadog.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.local.xcconfig; sourceTree = ""; }; + 617CEB3C245719E200AD4669 /* OpenTracing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenTracing.framework; path = ../Carthage/Build/iOS/OpenTracing.framework; sourceTree = ""; }; 9E6ABACC244F43BC003AE249 /* ExampleAppConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleAppConfig.swift; sourceTree = ""; }; - 9E6ABACE244F43BD003AE249 /* ConsoleOutputInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsoleOutputInterceptor.swift; sourceTree = ""; }; - 9E6ABACF244F43BD003AE249 /* UIViewController+KeyboardControlling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+KeyboardControlling.swift"; sourceTree = ""; }; - 9E6ABAD0244F43BD003AE249 /* SendMessageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendMessageViewController.swift; sourceTree = ""; }; 9E6ABAD1244F43BD003AE249 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 9E6ABAD2244F43BD003AE249 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 9E6ABAD9244F43D4003AE249 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9E6ABADC244F43E2003AE249 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 9E6ABADE244F43E2003AE249 /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 9E6ABADF244F43E2003AE249 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 9EE0E1B6244F65B30030DC52 /* Shopist.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shopist.xcconfig; sourceTree = ""; }; 9EE2A9EE244F38DE00A2C252 /* Shopist.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Shopist.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9EE2AA0F244F38E000A2C252 /* ShopistUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ShopistUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9EE2AA13244F38E000A2C252 /* ShopistUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopistUITests.swift; sourceTree = ""; }; @@ -112,13 +121,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 9E6ABACD244F43BD003AE249 /* Utils */ = { + 611E133D2461D75D007B7552 /* xcconfigs */ = { isa = PBXGroup; children = ( - 9E6ABACE244F43BD003AE249 /* ConsoleOutputInterceptor.swift */, - 9E6ABACF244F43BD003AE249 /* UIViewController+KeyboardControlling.swift */, + 611E133E2461D75D007B7552 /* Datadog.xcconfig */, + 611E133F2461D75D007B7552 /* Datadog.local.xcconfig */, ); - path = Utils; + name = xcconfigs; + path = ../xcconfigs; sourceTree = ""; }; 9E6ABAE0244F440B003AE249 /* Resources */ = { @@ -135,6 +145,7 @@ isa = PBXGroup; children = ( 9EE2AA21244F3A3300A2C252 /* Datadog.xcodeproj */, + 611E133D2461D75D007B7552 /* xcconfigs */, 9EE2A9F0244F38DE00A2C252 /* Shopist */, 9EE2AA12244F38E000A2C252 /* ShopistUITests */, 9EE2A9EF244F38DE00A2C252 /* Products */, @@ -154,14 +165,11 @@ 9EE2A9F0244F38DE00A2C252 /* Shopist */ = { isa = PBXGroup; children = ( - 9EE0E1B6244F65B30030DC52 /* Shopist.xcconfig */, 9E6ABAD9244F43D4003AE249 /* Info.plist */, 9E6ABAD1244F43BD003AE249 /* AppDelegate.swift */, 9E6ABACC244F43BC003AE249 /* ExampleAppConfig.swift */, 9E6ABAD2244F43BD003AE249 /* SceneDelegate.swift */, - 9E6ABAD0244F43BD003AE249 /* SendMessageViewController.swift */, 9E6ABAE0244F440B003AE249 /* Resources */, - 9E6ABACD244F43BD003AE249 /* Utils */, ); path = Shopist; sourceTree = ""; @@ -181,7 +189,9 @@ 9EE2AA29244F3A3300A2C252 /* Datadog.framework */, 9EE2AA2B244F3A3300A2C252 /* DatadogObjc.framework */, 9EE2AA2D244F3A3300A2C252 /* DatadogTests.xctest */, + 61441C8C2461A531003D8BB8 /* DatadogBenchmarkTests.xctest */, 9EE2AA2F244F3A3300A2C252 /* DatadogIntegrationTests.xctest */, + 61441C8E2461A531003D8BB8 /* Example.app */, ); name = Products; sourceTree = ""; @@ -189,6 +199,7 @@ 9EE2AA30244F3A4500A2C252 /* Frameworks */ = { isa = PBXGroup; children = ( + 617CEB3C245719E200AD4669 /* OpenTracing.framework */, ); name = Frameworks; sourceTree = ""; @@ -278,6 +289,20 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ + 61441C8C2461A531003D8BB8 /* DatadogBenchmarkTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = DatadogBenchmarkTests.xctest; + remoteRef = 61441C8B2461A531003D8BB8 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 61441C8E2461A531003D8BB8 /* Example.app */ = { + isa = PBXReferenceProxy; + fileType = wrapper.application; + path = Example.app; + remoteRef = 61441C8D2461A531003D8BB8 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 9EE2AA29244F3A3300A2C252 /* Datadog.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; @@ -314,7 +339,6 @@ buildActionMask = 2147483647; files = ( 9E6ABAE1244F4450003AE249 /* Assets.xcassets in Resources */, - 9EE0E1B7244F65B30030DC52 /* Shopist.xcconfig in Resources */, 9EE0E1B0244F65900030DC52 /* LaunchScreen.storyboard in Resources */, 9EE0E1B1244F65900030DC52 /* Main.storyboard in Resources */, ); @@ -334,9 +358,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9E6ABAD6244F43BD003AE249 /* SendMessageViewController.swift in Sources */, - 9E6ABAD5244F43BD003AE249 /* UIViewController+KeyboardControlling.swift in Sources */, - 9E6ABAD4244F43BD003AE249 /* ConsoleOutputInterceptor.swift in Sources */, 9E6ABAD7244F43BD003AE249 /* AppDelegate.swift in Sources */, 9E6ABAD3244F43BD003AE249 /* ExampleAppConfig.swift in Sources */, 9E6ABAD8244F43BD003AE249 /* SceneDelegate.swift in Sources */, @@ -364,7 +385,7 @@ /* Begin XCBuildConfiguration section */ 9EE2AA16244F38E000A2C252 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9EE0E1B6244F65B30030DC52 /* Shopist.xcconfig */; + baseConfigurationReference = 611E133E2461D75D007B7552 /* Datadog.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -425,7 +446,7 @@ }; 9EE2AA17244F38E000A2C252 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9EE0E1B6244F65B30030DC52 /* Shopist.xcconfig */; + baseConfigurationReference = 611E133E2461D75D007B7552 /* Datadog.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; diff --git a/Shopist/Shopist/AppDelegate.swift b/Shopist/Shopist/AppDelegate.swift index 9fd341e6c1..46ce34cddf 100644 --- a/Shopist/Shopist/AppDelegate.swift +++ b/Shopist/Shopist/AppDelegate.swift @@ -9,10 +9,10 @@ import Datadog fileprivate(set) var logger: Logger! +let appConfig = ExampleAppConfig(serviceName: "ios-sdk-example-app") + @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - private lazy var config = ExampleAppConfig() - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize Datadog SDK @@ -20,7 +20,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { appContext: .init(), configuration: Datadog.Configuration .builderUsing( - clientToken: config.clientToken, // use your own client token obtained on Datadog website + clientToken: appConfig.clientToken, // use your own client token obtained on Datadog website environment: "tests" ) .build() @@ -31,10 +31,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Create logger instance logger = Logger.builder - .set(serviceName: "ios-sdk-example-app") + .set(serviceName: appConfig.serviceName) .printLogsToConsole(true, usingFormat: .shortWith(prefix: "[iOS App] ")) .build() + // Register global tracer + Global.sharedTracer = Tracer.initialize(configuration: .init(serviceName: appConfig.serviceName)) + // Set highest verbosity level to see internal actions made in SDK Datadog.verbosityLevel = .debug @@ -55,7 +58,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { - installConsoleOutputInterceptor() return true } diff --git a/Shopist/Shopist/ExampleAppConfig.swift b/Shopist/Shopist/ExampleAppConfig.swift index 54a2200589..ae6bbfaa43 100644 --- a/Shopist/Shopist/ExampleAppConfig.swift +++ b/Shopist/Shopist/ExampleAppConfig.swift @@ -6,23 +6,23 @@ import Foundation -/// Basic configuration to read your Datadog client token from `examples-secret.xcconfig`. struct ExampleAppConfig { + /// Client token read from `Datadog.xcconfig`. let clientToken: String + /// Service name used for logs and traces. + let serviceName: String - init() { + init(serviceName: String) { guard let clientToken = Bundle.main.infoDictionary?["DatadogClientToken"] as? String, !clientToken.isEmpty else { - // If you see this error when running example app it means your `examples-secret.xcconfig` file is - // missing or missconfigured. Please refer to `README.md` file in SDK's repository root folder - // to create it. fatalError(""" โœ‹โ›”๏ธ Cannot read `DATADOG_CLIENT_TOKEN` from `Info.plist` dictionary. - Please create `shopist-secrets.local.xcconfig` in the same folder with `Shopist.xcconfig` - and declare your `DATADOG_CLIENT_TOKEN="your-client-token"` + Please update `Datadog.xcconfig` in the repository root with your own + client token obtained on datadoghq.com. You might need to run `Product > Clean Build Folder` before retrying. """) } self.clientToken = clientToken + self.serviceName = serviceName } } diff --git a/Shopist/Shopist/Resources/Main.storyboard b/Shopist/Shopist/Resources/Main.storyboard index 8309b885ee..7f6944395f 100644 --- a/Shopist/Shopist/Resources/Main.storyboard +++ b/Shopist/Shopist/Resources/Main.storyboard @@ -1,245 +1,23 @@ - + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + - - - - + - + diff --git a/Shopist/Shopist/SendMessageViewController.swift b/Shopist/Shopist/SendMessageViewController.swift index 59279c47d8..18b3fefa24 100644 --- a/Shopist/Shopist/SendMessageViewController.swift +++ b/Shopist/Shopist/SendMessageViewController.swift @@ -6,8 +6,7 @@ import UIKit -class SendMessageViewController: UIViewController { - +class DebugLoggingViewController: UIViewController { @IBOutlet weak var logLevelSegmentedControl: UISegmentedControl! @IBOutlet weak var logMessageTextField: UITextField! @IBOutlet weak var logServiceNameTextField: UITextField! @@ -21,12 +20,18 @@ class SendMessageViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + logServiceNameTextField.text = appConfig.serviceName hideKeyboardWhenTapOutside() startDisplayingDebugInfo(in: consoleTextView) } + private var message: String { + logMessageTextField.text!.isEmpty ? "message" : logMessageTextField.text! + } + @IBAction func didTapSendSingleLog(_ sender: Any) { - let message = logMessageTextField.text ?? "" + sendOnceButton.disableFor(seconds: 0.5) + switch LogLevelSegment(rawValue: logLevelSegmentedControl.selectedSegmentIndex) { case .debug: logger.debug(message) case .info: logger.info(message) @@ -39,7 +44,8 @@ class SendMessageViewController: UIViewController { } @IBAction func didTapSend10Logs(_ sender: Any) { - let message = logMessageTextField.text ?? "" + send10xButton.disableFor(seconds: 0.5) + switch LogLevelSegment(rawValue: logLevelSegmentedControl.selectedSegmentIndex) { case .debug: repeat10x { logger.debug(message) } case .info: repeat10x { logger.info(message) } diff --git a/Shopist/Shopist/Shopist.xcconfig b/Shopist/Shopist/Shopist.xcconfig deleted file mode 100644 index db65f5e8ab..0000000000 --- a/Shopist/Shopist/Shopist.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include? "shopist-secrets.local.xcconfig" diff --git a/Sources/Datadog/Logs/Attributes/UserInfo.swift b/Sources/Datadog/Core/Attributes/UserInfo.swift similarity index 94% rename from Sources/Datadog/Logs/Attributes/UserInfo.swift rename to Sources/Datadog/Core/Attributes/UserInfo.swift index bd64c0d956..02a84e0c57 100644 --- a/Sources/Datadog/Logs/Attributes/UserInfo.swift +++ b/Sources/Datadog/Core/Attributes/UserInfo.swift @@ -22,7 +22,7 @@ internal class UserInfoProvider { /// Information about the user. internal struct UserInfo { - let id: String? // swiftlint:disable:this identifier_name + let id: String? let name: String? let email: String? } diff --git a/Sources/Datadog/Core/AutoInstrumentation/MethodSwizzler.swift b/Sources/Datadog/Core/AutoInstrumentation/MethodSwizzler.swift new file mode 100644 index 0000000000..4760a031b6 --- /dev/null +++ b/Sources/Datadog/Core/AutoInstrumentation/MethodSwizzler.swift @@ -0,0 +1,116 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal class MethodSwizzler { + struct FoundMethod: Hashable { + let method: Method + let klass: AnyClass + + fileprivate init(method: Method, klass: AnyClass) { + self.method = method + self.klass = klass + } + + static func == (lhs: FoundMethod, rhs: FoundMethod) -> Bool { + let methodParity = (lhs.method == rhs.method) + let classParity = (NSStringFromClass(lhs.klass) == NSStringFromClass(rhs.klass)) + return methodParity && classParity + } + + func hash(into hasher: inout Hasher) { + let methodName = NSStringFromSelector(method_getName(method)) + let klassName = NSStringFromClass(klass) + let identifier = "\(methodName)|||\(klassName)" + hasher.combine(identifier) + } + } + + private var implementationCache: [FoundMethod: IMP] + var swizzledMethods: [FoundMethod] { + return Array(implementationCache.keys) + } + + init() { + self.implementationCache = [:] + } + + static func findMethod(with selector: Selector, in klass: AnyClass) throws -> FoundMethod { + /// NOTE: RUMM-452 as we never add/remove methods/classes at runtime, + /// search operation doesn't have to wrapped in sync {...} although it's visible in the interface + var headKlass: AnyClass? = klass + while let someKlass = headKlass { + if let foundMethod = findMethod(with: selector, in: someKlass) { + return FoundMethod(method: foundMethod, klass: someKlass) + } + headKlass = class_getSuperclass(headKlass) + } + throw InternalError(description: "\(NSStringFromSelector(selector)) is not found in \(NSStringFromClass(klass))") + } + + func originalImplementation(of found: FoundMethod) -> TypedIMP { + return sync { + let originalImp: IMP = implementationCache[found] ?? method_getImplementation(found.method) + return unsafeBitCast(originalImp, to: TypedIMP.self) + } + } + + @discardableResult + func swizzle( + _ foundMethod: FoundMethod, + impProvider: (TypedIMP) -> TypedBlockIMP, + onlyIfNonSwizzled: Bool = false + ) -> Bool { + sync { + if onlyIfNonSwizzled && + implementationCache[foundMethod] != nil { + return false + } + let currentIMP = method_getImplementation(foundMethod.method) + let current_typedIMP = unsafeBitCast(currentIMP, to: TypedIMP.self) + let newImpBlock: TypedBlockIMP = impProvider(current_typedIMP) + let newImp: IMP = imp_implementationWithBlock(newImpBlock) + + set(newIMP: newImp, for: foundMethod) + return true + } + } + + // MARK: - Private methods + + @discardableResult + private func sync(block: () -> T) -> T { + objc_sync_enter(self) + defer { objc_sync_exit(self) } + return block() + } + + private static func findMethod(with selector: Selector, in klass: AnyClass) -> Method? { + var methodsCount: UInt32 = 0 + let methodsCountPtr = withUnsafeMutablePointer(to: &methodsCount) { $0 } + guard let methods: UnsafeMutablePointer = class_copyMethodList(klass, methodsCountPtr) else { + return nil + } + defer { + free(methods) + } + for index in 0.. InterceptionResult? +internal struct InterceptionResult { + let modifiedRequest: URLRequest + let taskObserver: TaskObserver +} + +/// Block to be executed at task starting and completion by URLSessionSwizzler +/// starting event is passed at task.resume() +/// completed event is passed when task's completion handler is being executed +internal typealias TaskObserver = (TaskObservationEvent) -> Void +internal enum TaskObservationEvent { + case starting(URLRequest?) + case completed(URLResponse?, Error?) +} + +/// URLSessionSwizzler +/// Responsibility: Invoking Interceptor and TaskObserver at right time and places +/// Interceptor must be invoked at task creation: dataTaskWithURL/Request +/// TaskObserver must be invoked at task resume and completion: task.resume and completionHandler +internal class URLSessionSwizzler { + let dataTaskWithURL: DataTaskWithURL + let dataTaskwithRequest: DataTaskWithRequest + static var resume = Resume() + + init() throws { + self.dataTaskWithURL = try DataTaskWithURL(resume: Self.resume) + self.dataTaskwithRequest = try DataTaskWithRequest(resume: Self.resume) + } + + func swizzle(using interceptor: @escaping RequestInterceptor) { + dataTaskWithURL.swizzle(using: interceptor) + dataTaskwithRequest.swizzle(using: interceptor) + } + + // MARK: - Private + + typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void + + class DataTaskWithURL: MethodSwizzler < + @convention(c) (URLSession, Selector, URL?, CompletionHandler?) -> URLSessionDataTask, + @convention(block) (URLSession, URL?, @escaping CompletionHandler) -> URLSessionDataTask + > { + private static let selector = #selector(URLSession.dataTask(with:completionHandler:) as (URLSession) -> (URL, @escaping CompletionHandler) -> URLSessionDataTask) + + private let method: FoundMethod + private let resume: Resume + init(resume: Resume) throws { + self.method = try Self.findMethod(with: Self.selector, in: URLSession.self) + self.resume = resume + super.init() + } + + func swizzle(using interceptor: @escaping RequestInterceptor) { + typealias BlockIMP = @convention(block) (URLSession, URL?, CompletionHandler?) -> URLSessionDataTask + let resumeSwizzler = resume + swizzle(method) { currentTypedImp -> BlockIMP in + return { impSelf, impURL, impCompletion -> URLSessionDataTask in + var taskObserver: TaskObserver? = nil + let modifiedCompletion: CompletionHandler = { origData, origResponse, origError in + impCompletion?(origData, origResponse, origError) + taskObserver?(.completed(origResponse, origError)) + } + /// NOTE: RUMM-489 in iOS 11/12 dataTaskWithURL: calls dataTaskWithRequest: internally + /// we need to check if the originalRequest already has interceptor headers + /// if so, we don't intercept this request + let task = currentTypedImp(impSelf, Self.selector, impURL, modifiedCompletion) + if let taskRequest = task.originalRequest, + let someObserver = interceptor(taskRequest)?.taskObserver { + /// interception needed + try? resumeSwizzler.swizzleIfNeeded(in: task) + taskObserver = someObserver + task.addPayload(someObserver) + } + return task + } + } + } + } + + class DataTaskWithRequest: MethodSwizzler < + @convention(c) (URLSession, Selector, URLRequest?, CompletionHandler?) -> URLSessionDataTask, + @convention(block) (URLSession, URLRequest, @escaping CompletionHandler) -> URLSessionDataTask + > { + private static let selector = #selector(URLSession.dataTask(with:completionHandler:) as (URLSession) -> (URLRequest, @escaping CompletionHandler) -> URLSessionDataTask) + + private let method: FoundMethod + private let resume: Resume + init(resume: Resume) throws { + self.method = try Self.findMethod(with: Self.selector, in: URLSession.self) + self.resume = resume + super.init() + } + + func swizzle(using interceptor: @escaping RequestInterceptor) { + typealias BlockIMP = @convention(block) (URLSession, URLRequest?, CompletionHandler?) -> URLSessionDataTask + let resumeSwizzler = resume + self.swizzle(self.method) { typedCurrentImp -> BlockIMP in + return { impSelf, impURLRequest, impCompletion -> URLSessionDataTask in + guard let someRequest = impURLRequest, + let interception = interceptor(someRequest) else { + return typedCurrentImp(impSelf, Self.selector, impURLRequest, impCompletion) + } + let modifiedCompletion: CompletionHandler = { origData, origResponse, origError in + impCompletion?(origData, origResponse, origError) + interception.taskObserver(.completed(origResponse, origError)) + } + let task = typedCurrentImp(impSelf, Self.selector, interception.modifiedRequest, modifiedCompletion) + try? resumeSwizzler.swizzleIfNeeded(in: task) + task.addPayload(interception.taskObserver) + return task + } + } + } + } + + class Resume: MethodSwizzler < + @convention(c) (URLSessionTask, Selector) -> Void, + @convention(block) (URLSessionTask) -> Void + > { + private static let selector = #selector(URLSessionTask.resume) + + /// NOTE: RUMM-452 + /// URLSessionTask.resume is not called by its subclasses! + /// Therefore, we swizzle this method in the subclass. + /// This is unlike swizzling dataTaskURL/Request: in URLSession base class + func swizzleIfNeeded(in task: URLSessionTask) throws { + guard let taskClass = object_getClass(task) else { + userLogger.error("Unable to swizzle `URLSessionTask`: \(task) - the trace will not be created.") + return + } + let foundMethod = try Self.findMethod(with: Self.selector, in: taskClass) + // NOTE: RUMM-452 We will probably not need to swizzle tasks every time + // "onlyIfNonSwizzled: true" lets us perform swizzling in given class if not done before + typealias BlockIMP = @convention(block) (URLSessionTask) -> Void + swizzle( + foundMethod, + impProvider: { currentTypedImp -> BlockIMP in + return { impSelf in + impSelf.consumePayloads { $0(.starting(impSelf.currentRequest)) } + return currentTypedImp(impSelf, Self.selector) + } + }, + onlyIfNonSwizzled: true + ) + } + } +} + +/// payloads is an array TaskObservers, each one is executed in task.resume() and completion +private extension URLSessionTask { + /// NOTE: RUMM-452 KVO on task.state could be utilized instead of manually swizzling every task object + /// if we switch to KVO from swizzle(task), this shouldn't require refactoring and contained within this file only + /// therefore we keep payload as an implementation detail of URLSessionSwizzler. + /// unfortunately, KVO in Swift was broken until iOS 13 and task.state didn't seem reliable according to online crash reports + private static var payloadAssociationKey: UInt8 = 0 + private var payloads: [TaskObserver]? { + get { objc_getAssociatedObject(self, &Self.payloadAssociationKey) as? [TaskObserver] } + set { objc_setAssociatedObject(self, &Self.payloadAssociationKey, newValue, .OBJC_ASSOCIATION_RETAIN) } + } + + func addPayload(_ payload: @escaping TaskObserver) { + var current = payloads ?? [TaskObserver]() + current.append(payload) + payloads = current + } + + func consumePayloads(_ block: (TaskObserver) -> Void) { + if let somePayloads = self.payloads { + somePayloads.forEach { block($0) } + } + payloads = nil + } +} diff --git a/Sources/Datadog/Core/PerformancePreset.swift b/Sources/Datadog/Core/PerformancePreset.swift index f4dc71d019..dd81dde717 100644 --- a/Sources/Datadog/Core/PerformancePreset.swift +++ b/Sources/Datadog/Core/PerformancePreset.swift @@ -6,52 +6,70 @@ import Foundation -internal struct PerformancePreset: Equatable { - // MARK: - Data persistence +internal protocol StoragePerformancePreset { + /// Maximum size of a single file (in bytes). + /// Each feature (logging, tracing, ...) serializes its objects data to that file for later upload. + /// If last written file is too big to append next data, new file is created. + var maxFileSize: UInt64 { get } + /// Maximum size of data directory (in bytes). + /// Each feature uses separate directory. + /// If this size is exceeded, the oldest files are deleted until this limit is met again. + var maxDirectorySize: UInt64 { get } + /// Maximum age qualifying given file for reuse (in seconds). + /// If recently used file is younger than this, it is reused - otherwise: new file is created. + var maxFileAgeForWrite: TimeInterval { get } + /// Minimum age qualifying given file for upload (in seconds). + /// If the file is older than this, it is uploaded (and then deleted if upload succeeded). + /// It has an arbitrary offset (~0.5s) over `maxFileAgeForWrite` to ensure that no upload can start for the file being currently written. + var minFileAgeForRead: TimeInterval { get } + /// Maximum age qualifying given file for upload (in seconds). + /// Files older than this are considered obsolete and get deleted without uploading. + var maxFileAgeForRead: TimeInterval { get } + /// Maximum number of serialized objects written to a single file. + /// If number of objects in recently used file reaches this limit, new file is created for new data. + var maxObjectsInFile: Int { get } + /// Maximum size of serialized object data (in bytes). + /// If serialized object data exceeds this limit, it is skipped (not written to file and not uploaded). + var maxObjectSize: UInt64 { get } +} + +internal protocol UploadPerformancePreset { + /// First upload delay (in seconds). + /// It is used as a base value until no more files eligible for upload are found - then `defaultUploadDelay` is used as a new base. + var initialUploadDelay: TimeInterval { get } + /// Default uploads interval (in seconds). + /// At runtime, the upload interval ranges from `minUploadDelay` to `maxUploadDelay` depending + /// on delivery success or failure. + var defaultUploadDelay: TimeInterval { get } + /// Mininum interval of data upload (in seconds). + var minUploadDelay: TimeInterval { get } + /// Maximum interval of data upload (in seconds). + var maxUploadDelay: TimeInterval { get } + /// If upload succeeds, current interval is multiplied by this factor. + /// Should be less or equal `1.0`. + var uploadDelayDecreaseFactor: Double { get } +} + +internal struct PerformancePreset: Equatable, StoragePerformancePreset, UploadPerformancePreset { + // MARK: - StoragePerformancePreset - /// Maximum size of batched logs in single file (in bytes). - /// If last written file is too big to append next log data, new file is created. - let maxBatchSize: UInt64 - /// Maximum size of the log files directory. - /// If this size is exceeded, log files are being deleted (starting from the oldest one) until this limit is met again. - let maxSizeOfLogsDirectory: UInt64 - /// Maximum age of logs file for file reuse (in seconds). - /// If last written file is older than this, new file is created to store next log data. + let maxFileSize: UInt64 + let maxDirectorySize: UInt64 let maxFileAgeForWrite: TimeInterval - /// Minimum age of logs file to be picked for upload (in seconds). - /// It has the arbitrary offset (0.5s) over `maxFileAgeForWrite` to ensure that no upload is started for the file being written. let minFileAgeForRead: TimeInterval - /// Maximum age of logs file to be picked for uload (in seconds). - /// Files older than this age are considered outdated and get deleted with no upload. let maxFileAgeForRead: TimeInterval - /// Maximum number of logs written to single file. - /// If number of logs in last written file reaches this limit, new file is created to store next log data. - let maxLogsPerBatch: Int - /// Maximum size of serialized log data (in bytes). - /// If JSON encoded `Log` exceeds this size, it is dropped (not written to file). - let maxLogSize: UInt64 + let maxObjectsInFile: Int + let maxObjectSize: UInt64 - // MARK: - Data upload + // MARK: - UploadPerformancePreset - /// Initial delay of the first batch upload (in seconds). - /// It is used as a base value until SDK finds no more log batches - then `defaultLogsUploadDelay` is used as a new base. - let initialLogsUploadDelay: TimeInterval - /// Default time interval for logs upload (in seconds). - /// At runtime, the upload interval ranges from `minLogsUploadDelay` to `maxLogsUploadDelay` depending - /// on logs delivery success / failure. - let defaultLogsUploadDelay: TimeInterval - /// Mininum time interval for logs upload (in seconds). - /// By default logs are uploaded with `defaultLogsUploadDelay` which might change depending - /// on logs delivery success / failure. - let minLogsUploadDelay: TimeInterval - /// Maximum time interval for logs upload (in seconds). - /// By default logs are uploaded with `defaultLogsUploadDelay` which might change depending - /// on logs delivery success / failure. - let maxLogsUploadDelay: TimeInterval - /// Change factor of logs upload interval due to upload success. - let logsUploadDelayDecreaseFactor: Double + let initialUploadDelay: TimeInterval + let defaultUploadDelay: TimeInterval + let minUploadDelay: TimeInterval + let maxUploadDelay: TimeInterval + let uploadDelayDecreaseFactor: Double - // MARK: - Performance presets + // MARK: - Predefined presets /// Default performance preset. static let `default` = lowRuntimeImpact @@ -60,40 +78,40 @@ internal struct PerformancePreset: Equatable { /// Minimalizes number of data requests send to the server. static let lowRuntimeImpact = PerformancePreset( // persistence - maxBatchSize: 4 * 1_024 * 1_024, // 4MB - maxSizeOfLogsDirectory: 512 * 1_024 * 1_024, // 512 MB + maxFileSize: 4 * 1_024 * 1_024, // 4MB + maxDirectorySize: 512 * 1_024 * 1_024, // 512 MB maxFileAgeForWrite: 4.75, minFileAgeForRead: 4.75 + 0.5, // `maxFileAgeForWrite` + 0.5s margin maxFileAgeForRead: 18 * 60 * 60, // 18h - maxLogsPerBatch: 500, - maxLogSize: 256 * 1_024, // 256KB + maxObjectsInFile: 500, + maxObjectSize: 256 * 1_024, // 256KB // upload - initialLogsUploadDelay: 5, // postpone to not impact app launch time - defaultLogsUploadDelay: 5, - minLogsUploadDelay: 1, - maxLogsUploadDelay: 20, - logsUploadDelayDecreaseFactor: 0.9 + initialUploadDelay: 5, // postpone to not impact app launch time + defaultUploadDelay: 5, + minUploadDelay: 1, + maxUploadDelay: 20, + uploadDelayDecreaseFactor: 0.9 ) /// Performance preset optimized for instant data delivery. /// Minimalizes the time between receiving data form the user and delivering it to the server. static let instantDataDelivery = PerformancePreset( // persistence - maxBatchSize: `default`.maxBatchSize, - maxSizeOfLogsDirectory: `default`.maxSizeOfLogsDirectory, + maxFileSize: `default`.maxFileSize, + maxDirectorySize: `default`.maxDirectorySize, maxFileAgeForWrite: 2.75, minFileAgeForRead: 2.75 + 0.5, // `maxFileAgeForWrite` + 0.5s margin maxFileAgeForRead: `default`.maxFileAgeForRead, - maxLogsPerBatch: `default`.maxLogsPerBatch, - maxLogSize: `default`.maxLogSize, + maxObjectsInFile: `default`.maxObjectsInFile, + maxObjectSize: `default`.maxObjectSize, // upload - initialLogsUploadDelay: 0.5, // send quick to have a chance for upload in short-lived extensions - defaultLogsUploadDelay: 3, - minLogsUploadDelay: 1, - maxLogsUploadDelay: 5, - logsUploadDelayDecreaseFactor: 0.5 // reduce significantly for more uploads in short-lived extensions + initialUploadDelay: 0.5, // send quick to have a chance for upload in short-lived app extensions + defaultUploadDelay: 3, + minUploadDelay: 1, + maxUploadDelay: 5, + uploadDelayDecreaseFactor: 0.5 // reduce significantly for more uploads in short-lived app extensions ) static func best(for bundleType: BundleType) -> PerformancePreset { diff --git a/Sources/Datadog/Core/Persistence/DataFormat.swift b/Sources/Datadog/Core/Persistence/DataFormat.swift new file mode 100644 index 0000000000..88dfec9f9b --- /dev/null +++ b/Sources/Datadog/Core/Persistence/DataFormat.swift @@ -0,0 +1,29 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// Describes the format of writing and reading data from files. +internal struct DataFormat { + /// Prefixes the batch payload read from file. + let prefixData: Data + /// Suffixes the batch payload read from file. + let suffixData: Data + /// Separates entities written to file. + let separatorData: Data + + // MARK: - Initialization + + init( + prefix: String, + suffix: String, + separator: String + ) { + self.prefixData = prefix.data(using: .utf8)! // swiftlint:disable:this force_unwrapping + self.suffixData = suffix.data(using: .utf8)! // swiftlint:disable:this force_unwrapping + self.separatorData = separator.data(using: .utf8)! // swiftlint:disable:this force_unwrapping + } +} diff --git a/Sources/Datadog/Core/Persistence/FileReader.swift b/Sources/Datadog/Core/Persistence/FileReader.swift index 7b37e676f8..741676d8cd 100644 --- a/Sources/Datadog/Core/Persistence/FileReader.swift +++ b/Sources/Datadog/Core/Persistence/FileReader.swift @@ -14,10 +14,8 @@ internal struct Batch { } internal final class FileReader { - /// Opening bracked used to prefix data in `Batch`. - private let openingBracketData: Data = "[".data(using: .utf8)! // swiftlint:disable:this force_unwrapping - /// Opening bracked used to suffix data in `Batch`. - private let closingBracketData: Data = "]".data(using: .utf8)! // swiftlint:disable:this force_unwrapping + /// Data reading format. + private let dataFormat: DataFormat /// Orchestrator producing reference to readable file. private let orchestrator: FilesOrchestrator /// Queue used to synchronize files access (read / write). @@ -26,7 +24,8 @@ internal final class FileReader { /// Files marked as read. private var filesRead: [ReadableFile] = [] - init(orchestrator: FilesOrchestrator, queue: DispatchQueue) { + init(dataFormat: DataFormat, orchestrator: FilesOrchestrator, queue: DispatchQueue) { + self.dataFormat = dataFormat self.orchestrator = orchestrator self.queue = queue } @@ -43,7 +42,7 @@ internal final class FileReader { if let file = orchestrator.getReadableFile(excludingFilesNamed: Set(filesRead.map { $0.name })) { do { let fileData = try file.read() - let batchData = openingBracketData + fileData + closingBracketData + let batchData = dataFormat.prefixData + fileData + dataFormat.suffixData return Batch(data: batchData, file: file) } catch { developerLogger?.error("๐Ÿ”ฅ Failed to read file: \(error)") diff --git a/Sources/Datadog/Core/Persistence/FileWriter.swift b/Sources/Datadog/Core/Persistence/FileWriter.swift index d4b882a618..2aa240ae74 100644 --- a/Sources/Datadog/Core/Persistence/FileWriter.swift +++ b/Sources/Datadog/Core/Persistence/FileWriter.swift @@ -7,8 +7,8 @@ import Foundation internal final class FileWriter { - /// Comma separator used to separate data values written to file. - private let commaSeparatorData: Data = ",".data(using: .utf8)! // swiftlint:disable:this force_unwrapping + /// Data writting format. + private let dataFormat: DataFormat /// Orchestrator producing reference to writable file. private let orchestrator: FilesOrchestrator /// JSON encoder used to encode data. @@ -16,7 +16,8 @@ internal final class FileWriter { /// Queue used to synchronize files access (read / write) and perform decoding on background thread. private let queue: DispatchQueue - init(orchestrator: FilesOrchestrator, queue: DispatchQueue) { + init(dataFormat: DataFormat, orchestrator: FilesOrchestrator, queue: DispatchQueue) { + self.dataFormat = dataFormat self.orchestrator = orchestrator self.queue = queue self.jsonEncoder = JSONEncoder.default() @@ -38,14 +39,10 @@ internal final class FileWriter { let file = try orchestrator.getWritableFile(writeSize: UInt64(data.count)) if try file.size() == 0 { - try file.append { (write: (Data) throws -> Void) in - try write(data) - } + try file.append(data: data) } else { - let atomicData = commaSeparatorData + data - try file.append { write in - try write(atomicData) - } + let atomicData = dataFormat.separatorData + data + try file.append(data: atomicData) } } catch { userLogger.error("๐Ÿ”ฅ Failed to write log: \(error)") diff --git a/Sources/Datadog/Core/Persistence/Files/File.swift b/Sources/Datadog/Core/Persistence/Files/File.swift index 70e05b9551..1f9f26a5d5 100644 --- a/Sources/Datadog/Core/Persistence/Files/File.swift +++ b/Sources/Datadog/Core/Persistence/Files/File.swift @@ -16,7 +16,7 @@ internal protocol WritableFile { func size() throws -> UInt64 /// Synchronously appends given data at the end of this file. - func append(transaction: ((Data) throws -> Void) throws -> Void) throws + func append(data: Data) throws } /// Provides convenient interface for reading contents and metadata of the file. @@ -43,30 +43,63 @@ internal struct File: WritableFile, ReadableFile { } /// Appends given data at the end of this file. - func append(transaction: ((Data) throws -> Void) throws -> Void) throws { + func append(data: Data) throws { let fileHandle = try FileHandle(forWritingTo: url) - defer { fileHandle.closeFile() } - try objcExceptionHandler.rethrowToSwift { - fileHandle.seekToEndOfFile() - } + if #available(iOS 13.4, *) { + /** + Even though the `fileHandle.seekToEnd()` should be available since iOS 13.0: + ``` + @available(OSX 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public func seekToEnd() throws -> UInt64 + ``` + it crashes on iOS Simulators prior to iOS 13.4: + ``` + Symbol not found: _$sSo12NSFileHandleC10FoundationE9seekToEnds6UInt64VyKF + ``` + */ + defer { try? fileHandle.close() } + try fileHandle.seekToEnd() + try fileHandle.write(contentsOf: data) + } else { + defer { + try? objcExceptionHandler.rethrowToSwift { + fileHandle.closeFile() + } + } - // Writes given data at the end of the file. - func appendData(_ data: Data) throws { try objcExceptionHandler.rethrowToSwift { + fileHandle.seekToEndOfFile() fileHandle.write(data) } } - - try transaction { chunkOfData in - try appendData(chunkOfData) - } } func read() throws -> Data { let fileHandle = try FileHandle(forReadingFrom: url) - defer { fileHandle.closeFile() } - return fileHandle.readDataToEndOfFile() + + if #available(iOS 13.4, *) { + /** + Even though the `fileHandle.seekToEnd()` should be available since iOS 13.0: + ``` + @available(OSX 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + public func readToEnd() throws -> Data? + ``` + it crashes on iOS Simulators prior to iOS 13.4: + ``` + Symbol not found: _$sSo12NSFileHandleC10FoundationE9readToEndAC4DataVSgyKF + ``` + */ + defer { try? fileHandle.close() } + return try fileHandle.readToEnd() ?? Data() + } else { + defer { + try? objcExceptionHandler.rethrowToSwift { + fileHandle.closeFile() + } + } + return fileHandle.readDataToEndOfFile() + } } func size() throws -> UInt64 { diff --git a/Sources/Datadog/Core/Persistence/FilesOrchestrator.swift b/Sources/Datadog/Core/Persistence/FilesOrchestrator.swift index e80dedcbd6..bbe8c55736 100644 --- a/Sources/Datadog/Core/Persistence/FilesOrchestrator.swift +++ b/Sources/Datadog/Core/Persistence/FilesOrchestrator.swift @@ -6,63 +6,34 @@ import Foundation -internal struct WritableFileConditions { - let maxDirectorySize: UInt64 - let maxFileSize: UInt64 - let maxFileAgeForWrite: TimeInterval - let maxNumberOfUsesOfFile: Int - let maxWriteSize: UInt64 - - init(performance: PerformancePreset) { - self.maxDirectorySize = performance.maxSizeOfLogsDirectory - self.maxFileSize = performance.maxBatchSize - self.maxFileAgeForWrite = performance.maxFileAgeForWrite - self.maxNumberOfUsesOfFile = performance.maxLogsPerBatch - self.maxWriteSize = performance.maxLogSize - } -} - -internal struct ReadableFileConditions { - let minFileAgeForRead: TimeInterval - let maxFileAgeForRead: TimeInterval - - init(performance: PerformancePreset) { - minFileAgeForRead = performance.minFileAgeForRead - maxFileAgeForRead = performance.maxFileAgeForRead - } -} - internal class FilesOrchestrator { /// Directory where files are stored. private let directory: Directory /// Date provider. private let dateProvider: DateProvider - /// Conditions for picking up writable file. - private let writeConditions: WritableFileConditions - /// Conditions for picking up readable file. - private let readConditions: ReadableFileConditions + /// Performance rules for writing and reading files. + private let performance: StoragePerformancePreset /// Name of the last file returned by `getWritableFile()`. private var lastWritableFileName: String? = nil /// Tracks number of times the file at `lastWritableFileURL` was returned from `getWritableFile()`. + /// This should correspond with number of objects stored in file, assuming that majority of writes succeed (the difference is negligible). private var lastWritableFileUsesCount: Int = 0 init( directory: Directory, - writeConditions: WritableFileConditions, - readConditions: ReadableFileConditions, + performance: StoragePerformancePreset, dateProvider: DateProvider ) { self.directory = directory - self.writeConditions = writeConditions - self.readConditions = readConditions + self.performance = performance self.dateProvider = dateProvider } // MARK: - `WritableFile` orchestration func getWritableFile(writeSize: UInt64) throws -> WritableFile { - if writeSize > writeConditions.maxWriteSize { - throw InternalError(description: "data exceeds the maximum size of \(writeConditions.maxWriteSize) bytes.") + if writeSize > performance.maxObjectSize { + throw InternalError(description: "data exceeds the maximum size of \(performance.maxObjectSize) bytes.") } let lastWritableFileOrNil = reuseLastWritableFileIfPossible(writeSize: writeSize) @@ -92,9 +63,9 @@ internal class FilesOrchestrator { let lastFileCreationDate = fileCreationDateFrom(fileName: lastFile.name) let lastFileAge = dateProvider.currentDate().timeIntervalSince(lastFileCreationDate) - let fileIsRecentEnough = lastFileAge <= writeConditions.maxFileAgeForWrite - let fileHasRoomForMore = (try lastFile.size() + writeSize) <= writeConditions.maxFileSize - let fileCanBeUsedMoreTimes = (lastWritableFileUsesCount + 1) <= writeConditions.maxNumberOfUsesOfFile + let fileIsRecentEnough = lastFileAge <= performance.maxFileAgeForWrite + let fileHasRoomForMore = (try lastFile.size() + writeSize) <= performance.maxFileSize + let fileCanBeUsedMoreTimes = (lastWritableFileUsesCount + 1) <= performance.maxObjectsInFile if fileIsRecentEnough && fileHasRoomForMore && fileCanBeUsedMoreTimes { return lastFile @@ -124,7 +95,7 @@ internal class FilesOrchestrator { } let oldestFileAge = dateProvider.currentDate().timeIntervalSince(creationDate) - let fileIsOldEnough = oldestFileAge >= readConditions.minFileAgeForRead + let fileIsOldEnough = oldestFileAge >= performance.minFileAgeForRead return fileIsOldEnough ? oldestFile : nil } catch { @@ -154,8 +125,8 @@ internal class FilesOrchestrator { let accumulatedFilesSize = filesWithSizeSortedByCreationDate.map { $0.size }.reduce(0, +) - if accumulatedFilesSize > writeConditions.maxDirectorySize { - let sizeToFree = accumulatedFilesSize - writeConditions.maxDirectorySize + if accumulatedFilesSize > performance.maxDirectorySize { + let sizeToFree = accumulatedFilesSize - performance.maxDirectorySize var sizeFreed: UInt64 = 0 while sizeFreed < sizeToFree && !filesWithSizeSortedByCreationDate.isEmpty { @@ -169,7 +140,7 @@ internal class FilesOrchestrator { private func deleteFileIfItsObsolete(file: File, fileCreationDate: Date) throws -> (file: File, creationDate: Date)? { let fileAge = dateProvider.currentDate().timeIntervalSince(fileCreationDate) - if fileAge > readConditions.maxFileAgeForRead { + if fileAge > performance.maxFileAgeForRead { try file.delete() return nil } else { diff --git a/Sources/Datadog/Core/System/NetworkConnectionInfoProvider.swift b/Sources/Datadog/Core/System/NetworkConnectionInfoProvider.swift index c6ec3fd04e..17126b8b23 100644 --- a/Sources/Datadog/Core/System/NetworkConnectionInfoProvider.swift +++ b/Sources/Datadog/Core/System/NetworkConnectionInfoProvider.swift @@ -15,7 +15,7 @@ internal struct NetworkConnectionInfo { /// The network might be reachable after trying. case maybe /// The network is not reachable. - case no // swiftlint:disable:this identifier_name + case no } /// Network connection interfaces. diff --git a/Sources/Datadog/Core/Upload/DataUploadDelay.swift b/Sources/Datadog/Core/Upload/DataUploadDelay.swift index 71135c5457..dfa2dd420a 100644 --- a/Sources/Datadog/Core/Upload/DataUploadDelay.swift +++ b/Sources/Datadog/Core/Upload/DataUploadDelay.swift @@ -15,12 +15,12 @@ internal struct DataUploadDelay { private var delay: TimeInterval - init(performance: PerformancePreset) { - self.defaultDelay = performance.defaultLogsUploadDelay - self.minDelay = performance.minLogsUploadDelay - self.maxDelay = performance.maxLogsUploadDelay - self.decreaseFactor = performance.logsUploadDelayDecreaseFactor - self.delay = performance.initialLogsUploadDelay + init(performance: UploadPerformancePreset) { + self.defaultDelay = performance.defaultUploadDelay + self.minDelay = performance.minUploadDelay + self.maxDelay = performance.maxUploadDelay + self.decreaseFactor = performance.uploadDelayDecreaseFactor + self.delay = performance.initialUploadDelay } mutating func nextUploadDelay() -> TimeInterval { diff --git a/Sources/Datadog/Core/Upload/DataUploadWorker.swift b/Sources/Datadog/Core/Upload/DataUploadWorker.swift index fb8370c296..3f19abdc7d 100644 --- a/Sources/Datadog/Core/Upload/DataUploadWorker.swift +++ b/Sources/Datadog/Core/Upload/DataUploadWorker.swift @@ -20,6 +20,8 @@ internal class DataUploadWorker { private let acceptableUploadStatuses: Set = [ .success, .redirection, .clientError, .unknown ] + /// Name of the feature this worker is performing uploads for. + private let featureName: String /// Delay used to schedule consecutive uploads. private var delay: DataUploadDelay @@ -29,13 +31,15 @@ internal class DataUploadWorker { fileReader: FileReader, dataUploader: DataUploader, uploadConditions: DataUploadConditions, - delay: DataUploadDelay + delay: DataUploadDelay, + featureName: String ) { self.queue = queue self.fileReader = fileReader self.uploadConditions = uploadConditions self.dataUploader = dataUploader self.delay = delay + self.featureName = featureName scheduleNextUpload(after: self.delay.nextUploadDelay()) } @@ -46,33 +50,33 @@ internal class DataUploadWorker { return } - developerLogger?.info("โณ Checking for next batch...") + developerLogger?.info("โณ (\(self.featureName)) Checking for next batch...") let isSystemReady = self.uploadConditions.canPerformUpload() let nextBatch = isSystemReady ? self.fileReader.readNextBatch() : nil if let batch = nextBatch { - developerLogger?.info("โณ Uploading batch...") - userLogger.debug("โณ Uploading batch...") + developerLogger?.info("โณ (\(self.featureName)) Uploading batch...") + userLogger.debug("โณ (\(self.featureName)) Uploading batch...") let uploadStatus = self.dataUploader.upload(data: batch.data) let shouldBeAccepted = self.acceptableUploadStatuses.contains(uploadStatus) if shouldBeAccepted { self.fileReader.markBatchAsRead(batch) - developerLogger?.info(" โ†’ accepted, won't be retransmitted: \(uploadStatus)") - userLogger.debug(" โ†’ accepted, won't be retransmitted: \(uploadStatus)") + developerLogger?.info(" โ†’ (\(self.featureName)) accepted, won't be retransmitted: \(uploadStatus)") + userLogger.debug(" โ†’ (\(self.featureName)) accepted, won't be retransmitted: \(uploadStatus)") } else { - developerLogger?.info(" โ†’ not delivered, will be retransmitted: \(uploadStatus)") - userLogger.debug(" โ†’ not delivered, will be retransmitted: \(uploadStatus)") + developerLogger?.info(" โ†’ (\(self.featureName)) not delivered, will be retransmitted: \(uploadStatus)") + userLogger.debug(" โ†’ (\(self.featureName)) not delivered, will be retransmitted: \(uploadStatus)") } self.delay.decrease() } else { let batchLabel = nextBatch != nil ? "YES" : (isSystemReady ? "NO" : "NOT CHECKED") let systemLabel = isSystemReady ? "โœ…" : "โŒ" - developerLogger?.info("๐Ÿ’ก No upload. Batch to upload: \(batchLabel), System conditions: \(systemLabel)") - userLogger.debug("๐Ÿ’ก No upload. Batch to upload: \(batchLabel), System conditions: \(systemLabel)") + developerLogger?.info("๐Ÿ’ก (\(self.featureName)) No upload. Batch to upload: \(batchLabel), System conditions: \(systemLabel)") + userLogger.debug("๐Ÿ’ก (\(self.featureName)) No upload. Batch to upload: \(batchLabel), System conditions: \(systemLabel)") self.delay.increaseOnce() } diff --git a/Sources/Datadog/Core/Upload/DataUploader.swift b/Sources/Datadog/Core/Upload/DataUploader.swift index 28c6cd6757..4565af0431 100644 --- a/Sources/Datadog/Core/Upload/DataUploader.swift +++ b/Sources/Datadog/Core/Upload/DataUploader.swift @@ -9,33 +9,45 @@ import Foundation /// Creates URL and adds query items before providing them internal class UploadURLProvider { private let urlWithClientToken: URL - private let dateProvider: DateProvider + private let queryItemProviders: [QueryItemProvider] - private var queryItems: [URLQueryItem] { - // batch_time - let currentTimeMillis = dateProvider.currentDate().currentTimeMillis - let batchTimeQueryItem = URLQueryItem(name: "batch_time", value: "\(currentTimeMillis)") - // ddsource - let ddSourceQueryItem = URLQueryItem(name: "ddsource", value: "ios") + class QueryItemProvider { + let value: () -> URLQueryItem - return [ddSourceQueryItem, batchTimeQueryItem] + /// Creates `batch_time=...` query item adding current timestamp (in milliseconds) to the URL. + static func batchTime(using dateProvider: DateProvider) -> QueryItemProvider { + return QueryItemProvider { + let timestamp = dateProvider.currentDate().timeIntervalSince1970.toMilliseconds + return URLQueryItem(name: "batch_time", value: "\(timestamp)") + } + } + + /// Creates `ddsource=ios` query item. + static func ddsource() -> QueryItemProvider { + let queryItem = URLQueryItem(name: "ddsource", value: Datadog.Constants.ddsource) + return QueryItemProvider { queryItem } + } + + private init(value: @escaping () -> URLQueryItem) { + self.value = value + } } var url: URL { var urlComponents = URLComponents(url: urlWithClientToken, resolvingAgainstBaseURL: false) - urlComponents?.percentEncodedQueryItems = queryItems + urlComponents?.percentEncodedQueryItems = queryItemProviders.map { $0.value() } guard let url = urlComponents?.url else { - userLogger.error("๐Ÿ”ฅ Failed to create URL from \(urlWithClientToken) with \(queryItems)") - developerLogger?.error("๐Ÿ”ฅ Failed to create URL from \(urlWithClientToken) with \(queryItems)") + userLogger.error("๐Ÿ”ฅ Failed to create URL from \(urlWithClientToken) with \(queryItemProviders)") + developerLogger?.error("๐Ÿ”ฅ Failed to create URL from \(urlWithClientToken) with \(queryItemProviders)") return urlWithClientToken } return url } - init(urlWithClientToken: URL, dateProvider: DateProvider) { + init(urlWithClientToken: URL, queryItemProviders: [QueryItemProvider]) { self.urlWithClientToken = urlWithClientToken - self.dateProvider = dateProvider + self.queryItemProviders = queryItemProviders } } diff --git a/Sources/Datadog/Core/Upload/HTTPHeaders.swift b/Sources/Datadog/Core/Upload/HTTPHeaders.swift index 901f7ecbcd..b2c08f9a8f 100644 --- a/Sources/Datadog/Core/Upload/HTTPHeaders.swift +++ b/Sources/Datadog/Core/Upload/HTTPHeaders.swift @@ -8,18 +8,43 @@ import Foundation /// HTTP headers associated with requests send by SDK. internal struct HTTPHeaders { - private struct Constants { - static let contentTypeField = "Content-Type" - static let contentTypeValue = "application/json" - static let userAgentField = "User-Agent" + enum ContentType: String { + case applicationJSON = "application/json" + case textPlainUTF8 = "text/plain;charset=UTF-8" + } + + struct HTTPHeader { + let field: String + let value: String + + // MARK: - Supported headers + + static func contentTypeHeader(contentType: ContentType) -> HTTPHeader { + return HTTPHeader(field: "Content-Type", value: contentType.rawValue) + } + + static func userAgentHeader(appName: String, appVersion: String, device: MobileDevice) -> HTTPHeader { + return HTTPHeader( + field: "User-Agent", + value: "\(appName)/\(appVersion) CFNetwork (\(device.model); \(device.osName)/\(device.osVersion))" + ) + } + + // MARK: - Initialization + + private init(field: String, value: String) { + self.field = field + self.value = value + } } let all: [String: String] - init(appName: String, appVersion: String, device: MobileDevice) { - self.all = [ - Constants.contentTypeField: Constants.contentTypeValue, - Constants.userAgentField: "\(appName)/\(appVersion) CFNetwork (\(device.model); \(device.osName)/\(device.osVersion))" - ] + init(headers: [HTTPHeader]) { + self.all = headers.reduce([:]) { acc, next in + var dictionary = acc + dictionary[next.field] = next.value + return dictionary + } } } diff --git a/Sources/Datadog/Core/Utils/DateFormatting.swift b/Sources/Datadog/Core/Utils/DateFormatting.swift new file mode 100644 index 0000000000..76bb362685 --- /dev/null +++ b/Sources/Datadog/Core/Utils/DateFormatting.swift @@ -0,0 +1,42 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal protocol DateFormatterType { + func string(from date: Date) -> String +} + +extension ISO8601DateFormatter: DateFormatterType {} +extension DateFormatter: DateFormatterType {} + +/// Date formatter producing `ISO8601` string representation of a given date. +/// Should be used to encode dates in messages send to the server. +internal let iso8601DateFormatter: DateFormatterType = { + // As there is a known crash in iOS 11.0 and 11.1 when using `.withFractionalSeconds` option in `ISO8601DateFormatter`, + // we use different `DateFormatterType` implementation depending on the OS version. The problem was fixed by Apple in iOS 11.2. + if #available(iOS 11.2, *) { + let formatter = ISO8601DateFormatter() + formatter.formatOptions.insert(.withFractionalSeconds) + return formatter + } else { + let iso8601Formatter = DateFormatter() + iso8601Formatter.locale = Locale(identifier: "en_US_POSIX") + iso8601Formatter.timeZone = TimeZone(abbreviation: "UTC")! // swiftlint:disable:this force_unwrapping + iso8601Formatter.calendar = Calendar(identifier: .gregorian) + iso8601Formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" // ISO8601 format + return iso8601Formatter + } +}() + +/// Date formatter producing string representation of a given date for user-facing features (like console output). +internal func presentationDateFormatter(withTimeZone timeZone: TimeZone = .current) -> DateFormatterType { + let formatter = DateFormatter() + formatter.timeZone = timeZone + formatter.calendar = Calendar(identifier: .gregorian) + formatter.dateFormat = "HH:mm:ss.SSS" + return formatter +} diff --git a/Sources/Datadog/Core/Utils/EncodableValue.swift b/Sources/Datadog/Core/Utils/EncodableValue.swift index 619e80ab8c..3cc959f33e 100644 --- a/Sources/Datadog/Core/Utils/EncodableValue.swift +++ b/Sources/Datadog/Core/Utils/EncodableValue.swift @@ -15,6 +15,82 @@ internal struct EncodableValue: Encodable { } func encode(to encoder: Encoder) throws { - try value.encode(to: encoder) + if let urlValue = value as? URL { + /** + "URL itself prefers a keyed container which allows it to encode its base and relative string separately (...)" + Discussion: https:forums.swift.org/t/how-to-encode-objects-of-unknown-type/12253/11 + + It means that following code: + ``` + try EncodableValue(URL(string: "https:example.com")!).encode(to: encoder) + ``` + encodes the KVO representation of the URL: `{"relative":"https:example.com"}`. + As we very much prefer `"https:example.com"`, here we switch to encode `.absoluteString` directly. + */ + try urlValue.absoluteString.encode(to: encoder) + } else { + try value.encode(to: encoder) + } + } +} + +/// Value type converting any `Encodable` to its lossless JSON string representation. +/// +/// For example: +/// * it encodes `"abc"` string as `"abc"` JSON string value +/// * it encodes `1` integer as `"1"` JSON string value +/// * it encodes `true` boolean as `"true"` JSON string value +/// * it encodes `Person(name: "foo")` encodable struct as `"{\"name\": \"foo\"}"` JSON string value +/// +/// This encoding doesn't happen instantly. Instead, it is deferred to the actual `encoder.encode(jsonStringEncodableValue)` call. +internal struct JSONStringEncodableValue: Encodable { + /// Encoder used to encode `encodable` as JSON String value. + /// It is invoked lazily at `encoder.encode(jsonStringEncodableValue)` so its encoding errors can be propagated in master-type encoding. + private let jsonEncoder: JSONEncoder + private let encodable: EncodableValue + + init(_ value: Encodable, encodedUsing jsonEncoder: JSONEncoder) { + self.jsonEncoder = jsonEncoder + self.encodable = EncodableValue(value) + } + + func encode(to encoder: Encoder) throws { + if let stringValue = encodable.value as? String { + try stringValue.encode(to: encoder) + } else if let urlValue = encodable.value as? URL { + // Switch to encode `url.absoluteString` directly - see the comment in `EncodableValue` + try urlValue.absoluteString.encode(to: encoder) + } else { + let jsonData: Data + + if #available(iOS 13.0, *) { + jsonData = try jsonEncoder.encode(encodable) + } else { + // Prior to `iOS13.0` the `JSONEncoder` is unable to encode primitive values - it expects them to be + // wrapped inside top-level JSON object (array or dictionary). Reference: https://bugs.swift.org/browse/SR-6163 + // + // As a workaround, we serialize the `encodable` as a JSON array and then remove `[` and `]` bytes from serialized data. + let temporaryJsonArrayData = try jsonEncoder.encode([encodable]) + + let subdataStartIndex = temporaryJsonArrayData.startIndex.advanced(by: 1) + let subdataEndIndex = temporaryJsonArrayData.endIndex.advanced(by: -1) + + guard subdataStartIndex < subdataEndIndex else { + // This error should never be thrown, as the `temporaryJsonArrayData` will always contain at + // least two bytes standing for `[` and `]`. This check is just for sanity. + let encodingContext = EncodingError.Context( + codingPath: encoder.codingPath, + debugDescription: "Cannot safely encode value within a temporary array container." + ) + throw EncodingError.invalidValue(encodable.value, encodingContext) + } + + jsonData = temporaryJsonArrayData.subdata(in: subdataStartIndex.. ISO8601DateFormatter { - let formatter = ISO8601DateFormatter() - if #available(iOS 11.2, *) { - formatter.formatOptions.insert(.withFractionalSeconds) - } - return formatter - } - - static var encodingStrategy: JSONEncoder.DateEncodingStrategy { - let formatter = Self.default() - return .custom { date, encoder in - var container = encoder.singleValueContainer() - let formatted = formatter.string(from: date) - try container.encode(formatted) - } - } -} diff --git a/Sources/Datadog/Core/Utils/JSONEncoder.swift b/Sources/Datadog/Core/Utils/JSONEncoder.swift index 108c3f1bb7..f6a2ae407b 100644 --- a/Sources/Datadog/Core/Utils/JSONEncoder.swift +++ b/Sources/Datadog/Core/Utils/JSONEncoder.swift @@ -9,7 +9,11 @@ import Foundation extension JSONEncoder { static func `default`() -> JSONEncoder { let encoder = JSONEncoder() - encoder.dateEncodingStrategy = ISO8601DateFormatter.encodingStrategy + encoder.dateEncodingStrategy = .custom { date, encoder in + var container = encoder.singleValueContainer() + let formatted = iso8601DateFormatter.string(from: date) + try container.encode(formatted) + } if #available(iOS 13.0, OSX 10.15, *) { encoder.outputFormatting = [.withoutEscapingSlashes] } diff --git a/Sources/Datadog/Datadog.swift b/Sources/Datadog/Datadog.swift index 08ed951eea..97b8b3148e 100644 --- a/Sources/Datadog/Datadog.swift +++ b/Sources/Datadog/Datadog.swift @@ -6,10 +6,6 @@ import Foundation -/// SDK version associated with logs. -/// Should be synced with SDK releases. -internal let sdkVersion = "1.2.4" - /// Datadog SDK configuration object. public class Datadog { /// Provides information about the app. @@ -54,7 +50,7 @@ public class Datadog { do { try initializeOrThrow(appContext: appContext, configuration: configuration) } catch { - consolePrint("๐Ÿ”ฅ \(error)") + consolePrint("\(error)") } } @@ -64,7 +60,7 @@ public class Datadog { public static var verbosityLevel: LogLevel? = nil public static func setUserInfo( - id: String? = nil, // swiftlint:disable:this identifier_name + id: String? = nil, name: String? = nil, email: String? = nil ) { @@ -86,36 +82,84 @@ public class Datadog { appContext: appContext ) - let logsUploadURLProvider = UploadURLProvider( - urlWithClientToken: validConfiguration.logsUploadURLWithClientToken, - dateProvider: SystemDateProvider() - ) - let performance = PerformancePreset.best(for: appContext.bundleType) let dateProvider = SystemDateProvider() let userInfoProvider = UserInfoProvider() let networkConnectionInfoProvider = NetworkConnectionInfoProvider() let carrierInfoProvider = CarrierInfoProvider() - self.instance = Datadog( - userInfoProvider: userInfoProvider + // First, initialize internal loggers: + + let internalLoggerConfiguration = InternalLoggerConfiguration( + applicationVersion: validConfiguration.applicationVersion, + environment: validConfiguration.environment, + userInfoProvider: userInfoProvider, + networkConnectionInfoProvider: networkConnectionInfoProvider, + carrierInfoProvider: carrierInfoProvider ) - // Initialize features: + userLogger = createSDKUserLogger(configuration: internalLoggerConfiguration) + developerLogger = createSDKDeveloperLogger(configuration: internalLoggerConfiguration) + + // Then, initialize features: let httpClient = HTTPClient() + let mobileDevice = MobileDevice.current + + var logging: LoggingFeature? + var tracing: TracingFeature? + + if configuration.loggingEnabled { + logging = LoggingFeature( + directory: try obtainLoggingFeatureDirectory(), + configuration: validConfiguration, + performance: performance, + mobileDevice: mobileDevice, + httpClient: httpClient, + logsUploadURLProvider: UploadURLProvider( + urlWithClientToken: validConfiguration.logsUploadURLWithClientToken, + queryItemProviders: [ + .ddsource(), + .batchTime(using: dateProvider) + ] + ), + dateProvider: dateProvider, + userInfoProvider: userInfoProvider, + networkConnectionInfoProvider: networkConnectionInfoProvider, + carrierInfoProvider: carrierInfoProvider + ) + } - LoggingFeature.instance = LoggingFeature( - directory: try obtainLoggingFeatureDirectory(), - configuration: validConfiguration, - performance: performance, - mobileDevice: MobileDevice.current, - httpClient: httpClient, - logsUploadURLProvider: logsUploadURLProvider, - dateProvider: dateProvider, - userInfoProvider: userInfoProvider, - networkConnectionInfoProvider: networkConnectionInfoProvider, - carrierInfoProvider: carrierInfoProvider + if configuration.tracingEnabled { + tracing = TracingFeature( + directory: try obtainTracingFeatureDirectory(), + configuration: validConfiguration, + performance: performance, + loggingFeatureAdapter: logging.flatMap { LoggingForTracingAdapter(loggingFeature: $0) }, + mobileDevice: mobileDevice, + httpClient: httpClient, + tracesUploadURLProvider: UploadURLProvider( + urlWithClientToken: validConfiguration.tracesUploadURLWithClientToken, + queryItemProviders: [ + .batchTime(using: dateProvider) + ] + ), + dateProvider: dateProvider, + tracingUUIDGenerator: DefaultTracingUUIDGenerator(), + userInfoProvider: userInfoProvider, + networkConnectionInfoProvider: networkConnectionInfoProvider, + carrierInfoProvider: carrierInfoProvider + ) + } + + LoggingFeature.instance = logging + TracingFeature.instance = tracing + TracingAutoInstrumentation.instance = TracingAutoInstrumentation(with: configuration) + TracingAutoInstrumentation.instance?.apply() + + // Only after all features were initialized with no error thrown: + self.instance = Datadog( + userInfoProvider: userInfoProvider ) } @@ -129,9 +173,16 @@ public class Datadog { throw ProgrammerError(description: "Attempted to stop SDK before it was initialized.") } - // Deinitialize features: + // First, reset internal loggers: + userLogger = createNoOpSDKUserLogger() + developerLogger = nil + // Then, deinitialize features: LoggingFeature.instance = nil + TracingFeature.instance = nil + TracingAutoInstrumentation.instance = nil + + // Deinitialize `Datadog`: Datadog.instance = nil } } @@ -140,17 +191,17 @@ public class Datadog { internal typealias AppContext = Datadog.AppContext /// An exception thrown due to programmer error when calling SDK public API. -/// It make the SDK non-functional and print the error to developer in debugger console.. +/// It makes the SDK non-functional and print the error to developer in debugger console.. /// When thrown, check if configuration passed to `Datadog.initialize(...)` is correct -/// and if you not call any other SDK methods before it returns. +/// and if you do not call any other SDK methods before it returns. internal struct ProgrammerError: Error, CustomStringConvertible { - init(description: String) { self.description = "Datadog SDK usage error: \(description)" } + init(description: String) { self.description = "๐Ÿ”ฅ Datadog SDK usage error: \(description)" } let description: String } /// An exception thrown internally by SDK. -/// It is always handled by SDK and never passed to the user until `Datadog.verbosity` is set (then it might be printed in debugger console). -/// `InternalError` might be thrown due to SDK internal inconsistency or external issues (e.g. I/O errors). The SDK +/// It is always handled by SDK (keeps it functional) and never passed to the user until `Datadog.verbosity` is set (then it might be printed in debugger console). +/// `InternalError` might be thrown due to programmer error (API misuse) or SDK internal inconsistency or external issues (e.g. I/O errors). The SDK /// should always recover from that failures. internal struct InternalError: Error, CustomStringConvertible { let description: String diff --git a/Sources/Datadog/DatadogConfiguration.swift b/Sources/Datadog/DatadogConfiguration.swift index 9e1255bb9e..79be1b5b6b 100644 --- a/Sources/Datadog/DatadogConfiguration.swift +++ b/Sources/Datadog/DatadogConfiguration.swift @@ -7,16 +7,21 @@ import Foundation extension Datadog { + internal struct Constants { + /// Value for `ddsource` send by different features. + static let ddsource = "ios" + } + /// Datadog SDK configuration. public struct Configuration { /// Determines server to which logs are sent. public enum LogsEndpoint { /// US based servers. /// Sends logs to [app.datadoghq.com](https://app.datadoghq.com/). - case us // swiftlint:disable:this identifier_name + case us /// Europe based servers. /// Sends logs to [app.datadoghq.eu](https://app.datadoghq.eu/). - case eu // swiftlint:disable:this identifier_name + case eu /// User-defined server. case custom(url: String) @@ -29,10 +34,34 @@ extension Datadog { } } + /// Determines server to which traces are sent. + public enum TracesEndpoint { + /// US based servers. + /// Sends traces to [app.datadoghq.com](https://app.datadoghq.com/). + case us + /// Europe based servers. + /// Sends traces to [app.datadoghq.eu](https://app.datadoghq.eu/). + case eu + /// User-defined server. + case custom(url: String) + + internal var url: String { + switch self { + case .us: return "https://public-trace-http-intake.logs.datadoghq.com/v1/input/" + case .eu: return "https://public-trace-http-intake.logs.datadoghq.eu/v1/input/" + case let .custom(url: url): return url + } + } + } + internal let clientToken: String + internal let environment: String + internal var loggingEnabled: Bool + internal var tracingEnabled: Bool internal let logsEndpoint: LogsEndpoint + internal var tracesEndpoint: TracesEndpoint internal let serviceName: String? - internal let environment: String + internal var tracedHosts = Set() /// Creates configuration builder and sets client token. /// - Parameter clientToken: client token obtained on Datadog website. @@ -53,14 +82,74 @@ extension Datadog { public class Builder { internal let clientToken: String internal let environment: String - internal var serviceName: String? = nil + internal var loggingEnabled = true + internal var tracingEnabled = true internal var logsEndpoint: LogsEndpoint = .us + internal var tracesEndpoint: TracesEndpoint = .us + internal var serviceName: String? = nil + internal var tracedHosts = Set() internal init(clientToken: String, environment: String) { self.clientToken = clientToken self.environment = environment } + // MARK: - Features Configuration + + /// Enables or disables the logging feature. + /// + /// This option is meant to opt-out from using Datadog Logging entirely, no matter of your environment or build configuration. If you need to + /// disable logging only for certain scenarios (e.g. in `DEBUG` build configuration), use `sendLogsToDatadog(false)` available + /// on `Logger.Builder`. + /// + /// If `enableLogging(false)` is set, the SDK won't instantiate underlying resources required for + /// running the logging feature. This will give you additional performance optimization if you only use tracing, but not logging. + /// + /// **NOTE**: If you use logging for tracing (`span.log(fields:)`) keep the logging feature enabled. Otherwise the logs + /// you send for `span` objects won't be delivered to Datadog. + /// + /// - Parameter enabled: `true` by default + public func enableLogging(_ enabled: Bool) -> Builder { + self.loggingEnabled = enabled + return self + } + + /// Enables or disables the tracing feature. + /// + /// This option is meant to opt-out from using Datadog Tracing entirely, no matter of your environment or build configuration. If you need to + /// disable tracing only for certain scenarios (e.g. in `DEBUG` build configuration), do not set `Global.sharedTracer` to `Tracer`, + /// and your app will be using the no-op tracer instance. + /// + /// If `enableTracing(false)` is set, the SDK won't instantiate underlying resources required for + /// running the tracing feature. This will give you additional performance optimization if you only use logging, but not tracing. + /// + /// - Parameter enabled: `true` by default + public func enableTracing(_ enabled: Bool) -> Builder { + self.tracingEnabled = enabled + return self + } + + /// Sets the hosts to be automatically traced. + /// + /// Every request made to a traced host and its subdomains will create its Span with related information; _such as url, method, status code, error (if any)_. + /// Example, if `tracedHosts` is ["example.com"], then every network request such as the ones below will be automatically traced and generate a span. + /// "https://example.com/any/path" + /// "https://api.example.com/any/path" + /// + /// If your backend is also being traced with Datadog agents, you can see the full trace (eg: client>server>database) in your dashboard with our distributed tracing feature. + /// A few HTTP headers are injected to auto-traced network requests so that you can see your spans in your backend as well. + /// + /// If `tracedHosts` is empty, automatic tracing is disabled. + /// **IMPORTANT:** Non-empty `tracedHost`s will lead to modifying implementation of some `URLSession` methods, in case your app relies on `URLSession` internals please refer to `URLSessionSwizzler.swift` file for details + /// + /// - Parameter tracedHosts: empty by default + public func set(tracedHosts: Set) -> Builder { + self.tracedHosts = tracedHosts + return self + } + + // MARK: - Endpoints Configuration + /// Sets the server endpoint to which logs are sent. /// - Parameter logsEndpoint: server endpoint (default value is `LogsEndpoint.us`) public func set(logsEndpoint: LogsEndpoint) -> Builder { @@ -68,6 +157,15 @@ extension Datadog { return self } + /// Sets the server endpoint to which traces are sent. + /// - Parameter tracesEndpoint: server endpoint (default value is `TracesEndpoint.us` ) + public func set(tracesEndpoint: TracesEndpoint) -> Builder { + self.tracesEndpoint = tracesEndpoint + return self + } + + // MARK: - Other Settings + /// Sets the default service name associated with data send to Datadog. /// NOTE: The `serviceName` can be also overwriten by each `Logger` instance. /// - Parameter serviceName: the service name (default value is set to application bundle identifier) @@ -80,9 +178,13 @@ extension Datadog { public func build() -> Configuration { return Configuration( clientToken: clientToken, + environment: environment, + loggingEnabled: loggingEnabled, + tracingEnabled: tracingEnabled, logsEndpoint: logsEndpoint, + tracesEndpoint: tracesEndpoint, serviceName: serviceName, - environment: environment + tracedHosts: tracedHosts ) } } @@ -100,6 +202,7 @@ extension Datadog { internal let environment: String internal let logsUploadURLWithClientToken: URL + internal let tracesUploadURLWithClientToken: URL } } @@ -114,6 +217,10 @@ extension Datadog.ValidConfiguration { logsUploadURLWithClientToken: try ifValid( endpointURLString: configuration.logsEndpoint.url, clientToken: configuration.clientToken + ), + tracesUploadURLWithClientToken: try ifValid( + endpointURLString: configuration.tracesEndpoint.url, + clientToken: configuration.clientToken ) ) } diff --git a/Sources/Datadog/FeaturesIntegration/LoggingForTracingAdapter.swift b/Sources/Datadog/FeaturesIntegration/LoggingForTracingAdapter.swift new file mode 100644 index 0000000000..8589ee3a53 --- /dev/null +++ b/Sources/Datadog/FeaturesIntegration/LoggingForTracingAdapter.swift @@ -0,0 +1,83 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// Adapts the Logging feature for Tracing. This stands for a thin integration layer between features. +internal struct LoggingForTracingAdapter { + private let loggingFeature: LoggingFeature + + init(loggingFeature: LoggingFeature) { + self.loggingFeature = loggingFeature + } + + // MARK: - LogOutput + + func resolveLogOutput(usingTracingFeature tracingFeature: TracingFeature, tracerConfiguration: Tracer.Configuration) -> AdaptedLogOutput { + return AdaptedLogOutput( + loggingOutput: LogFileOutput( + logBuilder: LogBuilder( + applicationVersion: tracingFeature.configuration.applicationVersion, + environment: tracingFeature.configuration.environment, + serviceName: tracerConfiguration.serviceName ?? tracingFeature.configuration.serviceName, + loggerName: "trace", + userInfoProvider: tracingFeature.userInfoProvider, + networkConnectionInfoProvider: tracerConfiguration.sendNetworkInfo ? tracingFeature.networkConnectionInfoProvider : nil, + carrierInfoProvider: tracerConfiguration.sendNetworkInfo ? tracingFeature.carrierInfoProvider : nil + ), + fileWriter: loggingFeature.storage.writer + ) + ) + } + + internal struct TracingAttributes { + static let traceID = "dd.trace_id" + static let spanID = "dd.span_id" + } + + /// Bridges logs created by Tracing feature to Logging feature's output. + internal struct AdaptedLogOutput { + private struct Constants { + static let defaultLogMessage = "Span event" + } + + /// Actual `LogOutput` bridged to `LoggingFeature`. + let loggingOutput: LogOutput + + func writeLog(withSpanContext spanContext: DDSpanContext, fields: [String: Encodable], date: Date) { + var userAttributes = fields + + // get the log message and optional error kind + let message = (userAttributes.removeValue(forKey: OTLogFields.message) as? String) ?? Constants.defaultLogMessage + let errorKind = userAttributes.removeValue(forKey: OTLogFields.errorKind) as? String + + // infer the log level + let isErrorEvent = fields[OTLogFields.event] as? String == "error" + let hasErrorKind = errorKind != nil + let level: LogLevel = (isErrorEvent || hasErrorKind) ? .error : .info + + // set tracing attributes + var internalAttributes = [ + TracingAttributes.traceID: "\(spanContext.traceID.rawValue)", + TracingAttributes.spanID: "\(spanContext.spanID.rawValue)" + ] + if let errorKind = errorKind { + internalAttributes[OTLogFields.errorKind] = errorKind + } + + loggingOutput.writeLogWith( + level: level, + message: message, + date: date, + attributes: LogAttributes( + userAttributes: userAttributes, + internalAttributes: internalAttributes + ), + tags: [] + ) + } + } +} diff --git a/Sources/Datadog/Logger.swift b/Sources/Datadog/Logger.swift index 0c4adc823b..c536a35964 100644 --- a/Sources/Datadog/Logger.swift +++ b/Sources/Datadog/Logger.swift @@ -73,9 +73,26 @@ public typealias AttributeKey = String /// public typealias AttributeValue = Encodable +/// Because `Logger` is a common name widely used across different projects, the `Datadog.Logger` may conflict when +/// using `Logger.builder`. In such case, following `DDLogger` typealias can be used to avoid compiler ambiguity. +/// +/// Usage: +/// +/// import Datadog +/// +/// // logger reference +/// var logger: DDLogger! +/// +/// // instantiate Datadog logger +/// logger = DDLogger.builder.build() +/// +public typealias DDLogger = Logger + public class Logger { /// Writes `Log` objects to output. let logOutput: LogOutput + /// Provides date for log creation. + private let dateProvider: DateProvider /// Attributes associated with every log. private var loggerAttributes: [String: Encodable] = [:] /// Taggs associated with every log. @@ -83,9 +100,13 @@ public class Logger { /// Queue ensuring thread-safety of the `Logger`. It synchronizes tags and attributes mutation. private let queue: DispatchQueue - init(logOutput: LogOutput, identifier: String) { + init(logOutput: LogOutput, dateProvider: DateProvider, identifier: String) { self.logOutput = logOutput - self.queue = DispatchQueue(label: "com.datadoghq.logger-\(identifier)", qos: .userInteractive) + self.dateProvider = dateProvider + self.queue = DispatchQueue( + label: "com.datadoghq.logger-\(identifier)", + target: .global(qos: .userInteractive) + ) } // MARK: - Logging @@ -232,6 +253,7 @@ public class Logger { // MARK: - Private private func log(level: LogLevel, message: String, messageAttributes: [String: Encodable]?) { + let date = dateProvider.currentDate() let combinedAttributes = queue.sync { return self.loggerAttributes.merging(messageAttributes ?? [:]) { _, messageAttributeValue in return messageAttributeValue // use message attribute when the same key appears also in logger attributes @@ -241,7 +263,13 @@ public class Logger { return self.loggerTags } - logOutput.writeLogWith(level: level, message: message, attributes: combinedAttributes, tags: tags) + logOutput.writeLogWith( + level: level, + message: message, + date: date, + attributes: LogAttributes(userAttributes: combinedAttributes, internalAttributes: nil), + tags: tags + ) } // MARK: - Logger.Builder @@ -326,9 +354,10 @@ public class Logger { do { return try buildOrThrow() } catch { - consolePrint("๐Ÿ”ฅ \(error)") + consolePrint("\(error)") return Logger( logOutput: NoOpLogOutput(), + dateProvider: SystemDateProvider(), identifier: "no-op" ) } @@ -336,11 +365,16 @@ public class Logger { private func buildOrThrow() throws -> Logger { guard let loggingFeature = LoggingFeature.instance else { - throw ProgrammerError(description: "`Datadog.initialize()` must be called prior to `Logger.builder.build()`.") + throw ProgrammerError( + description: Datadog.instance == nil + ? "`Datadog.initialize()` must be called prior to `Logger.builder.build()`." + : "`Logger.builder.build()` produces a non-functional logger, as the logging feature is disabled." + ) } return Logger( logOutput: resolveLogsOutput(for: loggingFeature), + dateProvider: loggingFeature.dateProvider, identifier: resolveLoggerName(for: loggingFeature) ) } @@ -351,7 +385,6 @@ public class Logger { environment: loggingFeature.configuration.environment, serviceName: serviceName ?? loggingFeature.configuration.serviceName, loggerName: resolveLoggerName(for: loggingFeature), - dateProvider: loggingFeature.dateProvider, userInfoProvider: loggingFeature.userInfoProvider, networkConnectionInfoProvider: sendNetworkInfo ? loggingFeature.networkConnectionInfoProvider : nil, carrierInfoProvider: sendNetworkInfo ? loggingFeature.carrierInfoProvider : nil @@ -367,7 +400,8 @@ public class Logger { ), LogConsoleOutput( logBuilder: logBuilder, - format: format + format: format, + timeZone: .current ) ] ) @@ -379,7 +413,8 @@ public class Logger { case (false, let format?): return LogConsoleOutput( logBuilder: logBuilder, - format: format + format: format, + timeZone: .current ) case (false, nil): return NoOpLogOutput() diff --git a/Sources/Datadog/Logs/Log/LogBuilder.swift b/Sources/Datadog/Logging/Log/LogBuilder.swift similarity index 79% rename from Sources/Datadog/Logs/Log/LogBuilder.swift rename to Sources/Datadog/Logging/Log/LogBuilder.swift index b7928b8515..9b63c7e1be 100644 --- a/Sources/Datadog/Logs/Log/LogBuilder.swift +++ b/Sources/Datadog/Logging/Log/LogBuilder.swift @@ -6,7 +6,7 @@ import Foundation -/// Builds `Log` representation as it was received from the user (without sanitization). +/// Builds `Log` representation (for later serialization) from data received from user. internal struct LogBuilder { /// Application version to write in log. let applicationVersion: String @@ -16,8 +16,6 @@ internal struct LogBuilder { let serviceName: String /// Logger name to write in log. let loggerName: String - /// Current date to write in log. - let dateProvider: DateProvider /// Shared user info provider. let userInfoProvider: UserInfoProvider /// Shared network connection info provider (or `nil` if disabled for given logger). @@ -25,13 +23,9 @@ internal struct LogBuilder { /// Shared mobile carrier info provider (or `nil` if disabled for given logger). let carrierInfoProvider: CarrierInfoProviderType? - func createLogWith(level: LogLevel, message: String, attributes: [String: Encodable], tags: Set) -> Log { - let encodableAttributes = Dictionary( - uniqueKeysWithValues: attributes.map { name, value in (name, EncodableValue(value)) } - ) - + func createLogWith(level: LogLevel, message: String, date: Date, attributes: LogAttributes, tags: Set) -> Log { return Log( - date: dateProvider.currentDate(), + date: date, status: logStatus(for: level), message: message, serviceName: serviceName, @@ -43,7 +37,7 @@ internal struct LogBuilder { userInfo: userInfoProvider.value, networkConnectionInfo: networkConnectionInfoProvider?.current, mobileCarrierInfo: carrierInfoProvider?.current, - attributes: !encodableAttributes.isEmpty ? encodableAttributes : nil, + attributes: attributes, tags: !tags.isEmpty ? Array(tags) : nil ) } diff --git a/Sources/Datadog/Logs/Log/LogEncoder.swift b/Sources/Datadog/Logging/Log/LogEncoder.swift similarity index 84% rename from Sources/Datadog/Logs/Log/LogEncoder.swift rename to Sources/Datadog/Logging/Log/LogEncoder.swift index c5126960b5..a9bff89d51 100644 --- a/Sources/Datadog/Logs/Log/LogEncoder.swift +++ b/Sources/Datadog/Logging/Log/LogEncoder.swift @@ -9,12 +9,12 @@ import Foundation /// `Encodable` representation of log. It gets sanitized before encoding. internal struct Log: Encodable { enum Status: String, Encodable { - case debug = "DEBUG" - case info = "INFO" - case notice = "NOTICE" - case warn = "WARN" - case error = "ERROR" - case critical = "CRITICAL" + case debug + case info + case notice + case warn + case error + case critical } let date: Date @@ -29,7 +29,7 @@ internal struct Log: Encodable { let userInfo: UserInfo let networkConnectionInfo: NetworkConnectionInfo? let mobileCarrierInfo: CarrierInfo? - let attributes: [String: EncodableValue]? + let attributes: LogAttributes let tags: [String]? func encode(to encoder: Encoder) throws { @@ -134,10 +134,21 @@ internal struct LogEncoder { try container.encode(carrierInfo.carrierAllowsVOIP, forKey: .mobileNetworkCarrierAllowsVoIP) } - // Encode custom user attributes - if let attributes = log.attributes { - var attributesContainer = encoder.container(keyedBy: DynamicCodingKey.self) - try attributes.forEach { try attributesContainer.encode($0.value, forKey: DynamicCodingKey($0.key)) } + // Encode attributes... + var attributesContainer = encoder.container(keyedBy: DynamicCodingKey.self) + + // ... first, user attributes ... + let encodableUserAttributes = Dictionary( + uniqueKeysWithValues: log.attributes.userAttributes.map { name, value in (name, EncodableValue(value)) } + ) + try encodableUserAttributes.forEach { try attributesContainer.encode($0.value, forKey: DynamicCodingKey($0.key)) } + + // ... then, internal attributes: + if let internalAttributes = log.attributes.internalAttributes { + let encodableInternalAttributes = Dictionary( + uniqueKeysWithValues: internalAttributes.map { name, value in (name, EncodableValue(value)) } + ) + try encodableInternalAttributes.forEach { try attributesContainer.encode($0.value, forKey: DynamicCodingKey($0.key)) } } // Encode tags diff --git a/Sources/Datadog/Logs/Log/LogSanitizer.swift b/Sources/Datadog/Logging/Log/LogSanitizer.swift similarity index 85% rename from Sources/Datadog/Logs/Log/LogSanitizer.swift rename to Sources/Datadog/Logging/Log/LogSanitizer.swift index faefa37a2f..29e60a8f99 100644 --- a/Sources/Datadog/Logs/Log/LogSanitizer.swift +++ b/Sources/Datadog/Logging/Log/LogSanitizer.swift @@ -10,9 +10,12 @@ import Foundation internal struct LogSanitizer { struct Constraints { /// Attribute names reserved for Datadog. - /// If any of those is used by user, the attribute will be ignored. + /// If any of those is used by the user, the attribute will be ignored. static let reservedAttributeNames: Set = [ - "host", "message", "status", "service", "source", "error.kind", "error.message", "error.stack", "ddtags" + "host", "message", "status", "service", "source", "error.message", "error.stack", "ddtags", + OTLogFields.errorKind, + LoggingForTracingAdapter.TracingAttributes.traceID, + LoggingForTracingAdapter.TracingAttributes.spanID, ] /// Maximum number of nested levels in attribute name. E.g. `person.address.street` has 3 levels. /// If attribute name exceeds this number, extra levels are escaped by using `_` character (`one.two.(...).nine.ten_eleven_twelve`). @@ -57,19 +60,22 @@ internal struct LogSanitizer { // MARK: - Attributes sanitization - private func sanitize(attributes rawAttributes: [String: EncodableValue]?) -> [String: EncodableValue]? { - if let rawAttributes = rawAttributes { - var attributes = removeInvalidAttributes(rawAttributes) - attributes = removeReservedAttributes(attributes) - attributes = sanitizeAttributeNames(attributes) - attributes = limitToMaxNumberOfAttributes(attributes) - return attributes - } else { - return nil - } + private func sanitize(attributes rawAttributes: LogAttributes) -> LogAttributes { + // Sanitizes only `userAttributes`, `internalAttributes` remain untouched + var userAttributes = rawAttributes.userAttributes + userAttributes = removeInvalidAttributes(userAttributes) + userAttributes = removeReservedAttributes(userAttributes) + userAttributes = sanitizeAttributeNames(userAttributes) + let userAttributesLimit = Constraints.maxNumberOfAttributes - (rawAttributes.internalAttributes?.count ?? 0) + userAttributes = limitToMaxNumberOfAttributes(userAttributes, limit: userAttributesLimit) + + return LogAttributes( + userAttributes: userAttributes, + internalAttributes: rawAttributes.internalAttributes + ) } - private func removeInvalidAttributes(_ attributes: [String: EncodableValue]) -> [String: EncodableValue] { + private func removeInvalidAttributes(_ attributes: [String: Encodable]) -> [String: Encodable] { // Attribute name cannot be empty return attributes.filter { attribute in if attribute.key.isEmpty { @@ -80,7 +86,7 @@ internal struct LogSanitizer { } } - private func removeReservedAttributes(_ attributes: [String: EncodableValue]) -> [String: EncodableValue] { + private func removeReservedAttributes(_ attributes: [String: Encodable]) -> [String: Encodable] { return attributes.filter { attribute in if Constraints.reservedAttributeNames.contains(attribute.key) { userLogger.error("'\(attribute.key)' is a reserved attribute name. This attribute will be ignored.") @@ -90,8 +96,8 @@ internal struct LogSanitizer { } } - private func sanitizeAttributeNames(_ attributes: [String: EncodableValue]) -> [String: EncodableValue] { - let sanitizedAttributes: [(String, EncodableValue)] = attributes.map { name, value in + private func sanitizeAttributeNames(_ attributes: [String: Encodable]) -> [String: Encodable] { + let sanitizedAttributes: [(String, Encodable)] = attributes.map { name, value in let sanitizedName = sanitize(attributeName: name) if sanitizedName != name { userLogger.error("Attribute '\(name)' was modified to '\(sanitizedName)' to match Datadog constraints.") @@ -118,9 +124,9 @@ internal struct LogSanitizer { return sanitized } - private func limitToMaxNumberOfAttributes(_ attributes: [String: EncodableValue]) -> [String: EncodableValue] { - // Only `Constants.maxNumberOfAttributes` of attributes are allowed. - if attributes.count > Constraints.maxNumberOfAttributes { + private func limitToMaxNumberOfAttributes(_ attributes: [String: Encodable], limit: Int) -> [String: Encodable] { + // Only `limit` number of attributes are allowed. + if attributes.count > limit { let extraAttributesCount = attributes.count - Constraints.maxNumberOfAttributes userLogger.error("Number of attributes exceeds the limit of \(Constraints.maxNumberOfAttributes). \(extraAttributesCount) attribute(s) will be ignored.") return Dictionary(uniqueKeysWithValues: attributes.dropLast(extraAttributesCount)) diff --git a/Sources/Datadog/Logs/LogOutputs/LogConsoleOutput.swift b/Sources/Datadog/Logging/LogOutputs/LogConsoleOutput.swift similarity index 65% rename from Sources/Datadog/Logs/LogOutputs/LogConsoleOutput.swift rename to Sources/Datadog/Logging/LogOutputs/LogConsoleOutput.swift index 7d68ee9580..176db93150 100644 --- a/Sources/Datadog/Logs/LogOutputs/LogConsoleOutput.swift +++ b/Sources/Datadog/Logging/LogOutputs/LogConsoleOutput.swift @@ -13,17 +13,6 @@ internal protocol ConsoleLogFormatter { /// `LogOutput` which prints logs to console. internal struct LogConsoleOutput: LogOutput { - /// Time formatter used for `.short` output format. - static func shortTimeFormatter(calendar: Calendar = .current, timeZone: TimeZone = .current) -> Formatter { - let formatter = ISO8601DateFormatter.default() - if #available(iOS 11.2, *) { - formatter.formatOptions = [.withFractionalSeconds, .withFullTime] - } else { - formatter.formatOptions = [.withFullTime] - } - return formatter - } - private let logBuilder: LogBuilder private let formatter: ConsoleLogFormatter private let printingFunction: (String) -> Void @@ -31,14 +20,14 @@ internal struct LogConsoleOutput: LogOutput { init( logBuilder: LogBuilder, format: Logger.Builder.ConsoleLogFormat, - printingFunction: @escaping (String) -> Void = { consolePrint($0) }, - timeFormatter: Formatter = LogConsoleOutput.shortTimeFormatter() + timeZone: TimeZone, + printingFunction: @escaping (String) -> Void = { consolePrint($0) } ) { switch format { case .short: - self.formatter = ShortLogFormatter(timeFormatter: timeFormatter) + self.formatter = ShortLogFormatter(timeZone: timeZone) case .shortWith(let prefix): - self.formatter = ShortLogFormatter(timeFormatter: timeFormatter, prefix: prefix) + self.formatter = ShortLogFormatter(timeZone: timeZone, prefix: prefix) case .json: self.formatter = JSONLogFormatter() case .jsonWith(let prefix): @@ -48,8 +37,8 @@ internal struct LogConsoleOutput: LogOutput { self.printingFunction = printingFunction } - func writeLogWith(level: LogLevel, message: String, attributes: [String: Encodable], tags: Set) { - let log = logBuilder.createLogWith(level: level, message: message, attributes: attributes, tags: tags) + func writeLogWith(level: LogLevel, message: String, date: Date, attributes: LogAttributes, tags: Set) { + let log = logBuilder.createLogWith(level: level, message: message, date: date, attributes: attributes, tags: tags) printingFunction(formatter.format(log: log)) } } @@ -80,17 +69,17 @@ private struct JSONLogFormatter: ConsoleLogFormatter { /// Formats log as custom short string. private struct ShortLogFormatter: ConsoleLogFormatter { - private let timeFormatter: Formatter + private let timeFormatter: DateFormatterType private let prefix: String - init(timeFormatter: Formatter, prefix: String = "") { - self.timeFormatter = timeFormatter + init(timeZone: TimeZone, prefix: String = "") { + self.timeFormatter = presentationDateFormatter(withTimeZone: timeZone) self.prefix = prefix } func format(log: Log) -> String { - let time = timeFormatter.string(for: log.date) + let time = timeFormatter.string(from: log.date) let status = log.status.rawValue.uppercased() - return "\(prefix)\(time ?? "null") [\(status)] \(log.message)" + return "\(prefix)\(time) [\(status)] \(log.message)" } } diff --git a/Sources/Datadog/Logs/LogOutputs/LogFileOutput.swift b/Sources/Datadog/Logging/LogOutputs/LogFileOutput.swift similarity index 74% rename from Sources/Datadog/Logs/LogOutputs/LogFileOutput.swift rename to Sources/Datadog/Logging/LogOutputs/LogFileOutput.swift index 523f3d981f..67b51ce941 100644 --- a/Sources/Datadog/Logs/LogOutputs/LogFileOutput.swift +++ b/Sources/Datadog/Logging/LogOutputs/LogFileOutput.swift @@ -11,8 +11,8 @@ internal struct LogFileOutput: LogOutput { let logBuilder: LogBuilder let fileWriter: FileWriter - func writeLogWith(level: LogLevel, message: String, attributes: [String: Encodable], tags: Set) { - let log = logBuilder.createLogWith(level: level, message: message, attributes: attributes, tags: tags) + func writeLogWith(level: LogLevel, message: String, date: Date, attributes: LogAttributes, tags: Set) { + let log = logBuilder.createLogWith(level: level, message: message, date: date, attributes: attributes, tags: tags) fileWriter.write(value: log) } } diff --git a/Sources/Datadog/Logging/LogOutputs/LogOutput.swift b/Sources/Datadog/Logging/LogOutputs/LogOutput.swift new file mode 100644 index 0000000000..107cbb20b5 --- /dev/null +++ b/Sources/Datadog/Logging/LogOutputs/LogOutput.swift @@ -0,0 +1,19 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal struct LogAttributes { + /// Log attributes received from the user. They are subject for sanitization. + let userAttributes: [String: Encodable] + /// Log attributes added internally by the SDK. They are not a subject for sanitization. + let internalAttributes: [String: Encodable]? +} + +/// Type writting logs to some destination. +internal protocol LogOutput { + func writeLogWith(level: LogLevel, message: String, date: Date, attributes: LogAttributes, tags: Set) +} diff --git a/Sources/Datadog/Logs/LogOutputs/LogUtilityOutputs.swift b/Sources/Datadog/Logging/LogOutputs/LogUtilityOutputs.swift similarity index 64% rename from Sources/Datadog/Logs/LogOutputs/LogUtilityOutputs.swift rename to Sources/Datadog/Logging/LogOutputs/LogUtilityOutputs.swift index a02dca2e44..36e4d2ee73 100644 --- a/Sources/Datadog/Logs/LogOutputs/LogUtilityOutputs.swift +++ b/Sources/Datadog/Logging/LogOutputs/LogUtilityOutputs.swift @@ -8,7 +8,7 @@ import Foundation /// `LogOutput` which does nothing. internal struct NoOpLogOutput: LogOutput { - func writeLogWith(level: LogLevel, message: String, attributes: [String: Encodable], tags: Set) {} + func writeLogWith(level: LogLevel, message: String, date: Date, attributes: LogAttributes, tags: Set) {} } /// Combines one or more `LogOutputs` into one. @@ -19,8 +19,8 @@ internal struct CombinedLogOutput: LogOutput { self.combinedOutputs = outputs } - func writeLogWith(level: LogLevel, message: String, attributes: [String: Encodable], tags: Set) { - combinedOutputs.forEach { $0.writeLogWith(level: level, message: message, attributes: attributes, tags: tags) } + func writeLogWith(level: LogLevel, message: String, date: Date, attributes: LogAttributes, tags: Set) { + combinedOutputs.forEach { $0.writeLogWith(level: level, message: message, date: date, attributes: attributes, tags: tags) } } } @@ -28,9 +28,9 @@ internal struct ConditionalLogOutput: LogOutput { let conditionedOutput: LogOutput let condition: (LogLevel) -> Bool - func writeLogWith(level: LogLevel, message: String, attributes: [String: Encodable], tags: Set) { + func writeLogWith(level: LogLevel, message: String, date: Date, attributes: LogAttributes, tags: Set) { if condition(level) { - conditionedOutput.writeLogWith(level: level, message: message, attributes: attributes, tags: tags) + conditionedOutput.writeLogWith(level: level, message: message, date: date, attributes: attributes, tags: tags) } } } diff --git a/Sources/Datadog/Logs/LoggingFeature.swift b/Sources/Datadog/Logging/LoggingFeature.swift similarity index 73% rename from Sources/Datadog/Logs/LoggingFeature.swift rename to Sources/Datadog/Logging/LoggingFeature.swift index d04109c60b..27e4749096 100644 --- a/Sources/Datadog/Logs/LoggingFeature.swift +++ b/Sources/Datadog/Logging/LoggingFeature.swift @@ -42,30 +42,29 @@ internal final class LoggingFeature { /// Reads logs from files. let reader: FileReader + /// NOTE: any change to logs data format requires updating the logs directory url to be unique + static let dataFormat = DataFormat(prefix: "[", suffix: "]", separator: ",") + init( directory: Directory, performance: PerformancePreset, - dateProvider: DateProvider + dateProvider: DateProvider, + readWriteQueue: DispatchQueue ) { - let readWriteQueue = DispatchQueue( - label: "com.datadoghq.ios-sdk-logs-read-write", - target: .global(qos: .utility) - ) let orchestrator = FilesOrchestrator( directory: directory, - writeConditions: WritableFileConditions(performance: performance), - readConditions: ReadableFileConditions(performance: performance), + performance: performance, dateProvider: dateProvider ) - self.writer = FileWriter(orchestrator: orchestrator, queue: readWriteQueue) - self.reader = FileReader(orchestrator: orchestrator, queue: readWriteQueue) + self.writer = FileWriter(dataFormat: Storage.dataFormat, orchestrator: orchestrator, queue: readWriteQueue) + self.reader = FileReader(dataFormat: Storage.dataFormat, orchestrator: orchestrator, queue: readWriteQueue) } } /// Encapsulates upload stack setup for `LoggingFeature`. class Upload { - /// Uploads logs server. + /// Uploads logs to server. let uploader: DataUploadWorker init( @@ -75,34 +74,37 @@ internal final class LoggingFeature { mobileDevice: MobileDevice, httpClient: HTTPClient, logsUploadURLProvider: UploadURLProvider, - networkConnectionInfoProvider: NetworkConnectionInfoProviderType + networkConnectionInfoProvider: NetworkConnectionInfoProviderType, + uploadQueue: DispatchQueue ) { - let dataUploader = DataUploader( - urlProvider: logsUploadURLProvider, - httpClient: httpClient, - httpHeaders: HTTPHeaders( - appName: configuration.applicationName, - appVersion: configuration.applicationVersion, - device: mobileDevice - ) - ) - - let uploadQueue = DispatchQueue( - label: "com.datadoghq.ios-sdk-logs-upload", - target: .global(qos: .utility) + let httpHeaders = HTTPHeaders( + headers: [ + .contentTypeHeader(contentType: .applicationJSON), + .userAgentHeader( + appName: configuration.applicationName, + appVersion: configuration.applicationVersion, + device: mobileDevice + ) + ] ) - let uploadConditions = DataUploadConditions( batteryStatus: BatteryStatusProvider(mobileDevice: mobileDevice), networkConnectionInfo: networkConnectionInfoProvider ) + let dataUploader = DataUploader( + urlProvider: logsUploadURLProvider, + httpClient: httpClient, + httpHeaders: httpHeaders + ) + self.uploader = DataUploadWorker( queue: uploadQueue, fileReader: storage.reader, dataUploader: dataUploader, uploadConditions: uploadConditions, - delay: DataUploadDelay(performance: performance) + delay: DataUploadDelay(performance: performance), + featureName: "logging" ) } } @@ -131,10 +133,20 @@ internal final class LoggingFeature { self.carrierInfoProvider = carrierInfoProvider // Initialize components + let readWriteQueue = DispatchQueue( + label: "com.datadoghq.ios-sdk-logs-read-write", + target: .global(qos: .utility) + ) self.storage = Storage( directory: directory, performance: performance, - dateProvider: dateProvider + dateProvider: dateProvider, + readWriteQueue: readWriteQueue + ) + + let uploadQueue = DispatchQueue( + label: "com.datadoghq.ios-sdk-logs-upload", + target: .global(qos: .utility) ) self.upload = Upload( storage: self.storage, @@ -143,7 +155,8 @@ internal final class LoggingFeature { mobileDevice: mobileDevice, httpClient: httpClient, logsUploadURLProvider: logsUploadURLProvider, - networkConnectionInfoProvider: networkConnectionInfoProvider + networkConnectionInfoProvider: networkConnectionInfoProvider, + uploadQueue: uploadQueue ) } } diff --git a/Sources/Datadog/OpenTracing/OTConstants.swift b/Sources/Datadog/OpenTracing/OTConstants.swift new file mode 100644 index 0000000000..7aaed0dd4a --- /dev/null +++ b/Sources/Datadog/OpenTracing/OTConstants.swift @@ -0,0 +1,87 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-2020 Datadog, Inc. +*/ + +import Foundation + +/// A collection of standard `Span` tag keys defined by Open Tracing. +/// Use them as the `key` in `span.setTag(key:value:)`. Use the expected type for the `value`. +/// +/// See more: [Span tags table](https://github.com/opentracing/specification/blob/master/semantic_conventions.md#span-tags-table) +/// +public struct OTTags { + /// Expected value: `String`. + public static let component = "component" + + /// Expected value: `String` + public static let dbInstance = "db.instance" + + /// Expected value: `String` + public static let dbStatement = "db.statement" + + /// Expected value: `String` + public static let dbType = "db.type" + + /// Expected value: `String` + public static let dbUser = "db.user" + + /// Expected value: `Bool` + public static let error = "error" + + /// Expected value: `String` + public static let httpMethod = "http.method" + + /// Expected value: `Int` + public static let httpStatusCode = "http.status_code" + + /// Expected value: `String` + public static let httpUrl = "http.url" + + /// Expected value: `String` + public static let messageBusDestination = "message_bus.destination" + + /// Expected value: `String` + public static let peerAddress = "peer.address" + + /// Expected value: `String` + public static let peerHostname = "peer.hostname" + + /// Expected value: `String` + public static let peerIPv4 = "peer.ipv4" + + /// Expected value: `String` + public static let peerIPv6 = "peer.ipv6" + + /// Expected value: `Int` + public static let peerPort = "peer.port" + + /// Expected value: `String` + public static let peerService = "peer.service" + + /// Expected value: `Int` + public static let samplingPriority = "sampling.priority" + + /// Expected value: `String` + public static let spanKind = "span.kind" +} + +/// A collection of standard `Span` log fields defined by Open Tracing. +/// Use them as the `key` for `fields` dictionary in `span.log(fields:)`. Use the expected type for the value. +/// +/// See more: [Log fields table](https://github.com/opentracing/specification/blob/master/semantic_conventions.md#log-fields-table) +/// +public struct OTLogFields { + /// Expected value: `String` + public static let errorKind = "error.kind" + + /// Expected value: `String` + public static let event = "event" + + /// Expected value: `String` + public static let message = "message" + + /// Expected value: `String` + public static let stack = "stack" +} diff --git a/Sources/Datadog/OpenTracing/OTFormat.swift b/Sources/Datadog/OpenTracing/OTFormat.swift new file mode 100644 index 0000000000..91fb7def8e --- /dev/null +++ b/Sources/Datadog/OpenTracing/OTFormat.swift @@ -0,0 +1,50 @@ +import Foundation + +/// "Format", "Carrier", "Extract", "Inject", and "Text Map" are opentracing-specific concepts. See: +/// https://github.com/opentracing/specification/blob/master/specification.md#inject-a-spancontext-into-a-carrier +/// https://github.com/opentracing/specification/blob/master/specification.md#extract-a-spancontext-from-a-carrier +/// https://github.com/opentracing/specification/blob/master/specification.md#note-required-formats-for-injection-and-extraction + +/// Format and carrier for extract(). +/// A FormatReader is used to extract a SpanContext from a carrier. +/// The type of the child protocol is the format descriptor. +/// The carrier is specified by the protocol's return type for the getter, usually `getAll()`. +/// +/// Marker protocol. +public protocol OTFormatReader: OTCustomFormatReader {} + +/// Format and carrier for inject(). +/// A FormatWriter is used to inject a SpanContext into a carrier. +/// The type of the child protocol is the format descriptor. +/// The carrier is specified by the protocol's parameter type for the setter, usually `setAll()` +/// +/// Marker protocol. +public protocol OTFormatWriter: OTCustomFormatWriter {} + +/// Read interface for a textmap +public protocol OTTextMapReader: OTFormatReader {} + +/// Write interface for a textmap +public protocol OTTextMapWriter: OTFormatWriter {} + +/// Read interface for HTTP headers +public protocol OTHTTPHeadersReader: OTTextMapReader {} + +/// Write interface for HTTP headers +public protocol OTHTTPHeadersWriter: OTTextMapWriter {} + +/// Read interface for a custom carrier +public protocol OTCustomFormatReader { + /// Extract a span context from the custom carrier + /// + /// - returns: extracted span context from the custom carrier, or nil on failure + func extract() -> OTSpanContext? +} + +/// Write interface for a custom carrier +public protocol OTCustomFormatWriter { + /// Inject a span context into the custom carrier + /// + /// - parameter spanContext: context to inject into the custom carrier + func inject(spanContext: OTSpanContext) +} diff --git a/Sources/Datadog/OpenTracing/OTGlobal.swift b/Sources/Datadog/OpenTracing/OTGlobal.swift new file mode 100644 index 0000000000..d6e9ba2a08 --- /dev/null +++ b/Sources/Datadog/OpenTracing/OTGlobal.swift @@ -0,0 +1,7 @@ +/// Struct that stores the shared tracer +public struct Global { + private init() {} + + /// Shared tracer instance used throughout the app + public static var sharedTracer: OTTracer = DDNoopGlobals.tracer +} diff --git a/Sources/Datadog/OpenTracing/OTReference.swift b/Sources/Datadog/OpenTracing/OTReference.swift new file mode 100644 index 0000000000..3bc1f3487f --- /dev/null +++ b/Sources/Datadog/OpenTracing/OTReference.swift @@ -0,0 +1,25 @@ +/// OpenTracing span reference +public struct OTReference { + /// Type of reference + public let type: OTReferenceType + + /// Span context that the reference points to + public let context: OTSpanContext + + public static func child(of parent: OTSpanContext) -> OTReference { + return OTReference(type: .childOf, context: parent) + } + + public static func follows(from precedingContext: OTSpanContext) -> OTReference { + return OTReference(type: .followsFrom, context: precedingContext) + } +} + +/// Enum representing the type of reference +public enum OTReferenceType: String { + /// The CHILD_OF reference type, used to denote direct causal relationships + case childOf = "CHILD_OF" + + /// The FOLLOWS_FROM reference type, currently used to denote all other non-causal relationships + case followsFrom = "FOLLOWS_FROM" +} diff --git a/Sources/Datadog/OpenTracing/OTSpan.swift b/Sources/Datadog/OpenTracing/OTSpan.swift new file mode 100644 index 0000000000..ebece2c3b0 --- /dev/null +++ b/Sources/Datadog/OpenTracing/OTSpan.swift @@ -0,0 +1,58 @@ +import Foundation + +/// Represents information related to an event with a timespan +public protocol OTSpan { + /// The span context that refers to this span + var context: OTSpanContext { get } + + /// The tracer that produced this span + func tracer() -> OTTracer + + /// Set the name of the operation this span represents + /// + /// - parameter operationName: The name of the operation this span represents + func setOperationName(_ operationName: String) + + /// Add a new tag or replace an existing tag key with this value + /// + /// - parameter key: Key of the tag to set + /// - parameter value: Value of the tag to set + func setTag(key: String, value: Encodable) + + /// Add a new log with the supplied fields and timestamp + /// + /// - parameter fields: Fields to set on the span log + /// - parameter timestamp: Timestamp to use for the span log + func log(fields: [String: Encodable], timestamp: Date) + + /// Add a new baggage item or replace an existing baggage item value for the given key + /// + /// - parameter key: Key of the baggage item to set + /// - parameter value: Value of the baggage item to set + func setBaggageItem(key: String, value: String) + + /// Get the baggage item corresponding to the given key; nil if the baggage item does not exist + /// + /// - parameter key: Key of the baggage item to get + func baggageItem(withKey key: String) -> String? + + /// Finish the span at the specified time, or at some default time if nil + /// + /// - parameter time: If non-nil, time at which to finish the span; default time is used if nil + func finish(at time: Date) +} + +/// Convenience extension +public extension OTSpan { + /// Add a new log with the supplied fields and the current timestamp + /// + /// - parameter fields: Fields to set on the span log + func log(fields: [String: Encodable]) { + self.log(fields: fields, timestamp: Date()) + } + + /// Finish the span at the current time + func finish() { + self.finish(at: Date()) + } +} diff --git a/Sources/Datadog/OpenTracing/OTSpanContext.swift b/Sources/Datadog/OpenTracing/OTSpanContext.swift new file mode 100644 index 0000000000..47c3b15e0b --- /dev/null +++ b/Sources/Datadog/OpenTracing/OTSpanContext.swift @@ -0,0 +1,9 @@ +/// Span context captures any implementation-dependent state such as trace ID and span ID, as well as the +/// baggage items +public protocol OTSpanContext { + /// Iterate through the baggage items + /// + /// - parameter callback: Lambda invoked with each baggage item key-value pair as the parameters. + /// If the lambda returns true, iteration will stop. + func forEachBaggageItem(callback: (_ key: String, _ value: String) -> Bool) +} diff --git a/Sources/Datadog/OpenTracing/OTTracer.swift b/Sources/Datadog/OpenTracing/OTTracer.swift new file mode 100644 index 0000000000..31279bbc79 --- /dev/null +++ b/Sources/Datadog/OpenTracing/OTTracer.swift @@ -0,0 +1,84 @@ +import Foundation + +/// Tracer is the starting point for all OpenTracing instrumentation. Use it +/// to create OTSpans, inject/extract them between processes, and so on. +/// +/// Tracer should be thread-safe. +public protocol OTTracer { + /// Start a new span with the given operation name. + /// + /// - parameter operationName: the operation name for the newly-started span + /// - parameter references: an optional list of Reference instances to record causal relationships + /// - parameter tags: a set of tag keys and values per OTSpan#setTag:value:, or nil to start with + /// an empty tag map + /// - parameter startTime: an explicitly specified start timestamp for the OTSpan, or nil to use the + /// current walltime + /// - returns: a valid Span instance; it is the caller's responsibility to call finish() + func startSpan( + operationName: String, + references: [OTReference]?, + tags: [String: Encodable]?, + startTime: Date? + ) -> OTSpan + + /// Transfer the span information into the carrier of the given format. + /// + /// For example: + /// + /// let httpHeaders: [String: String] = ... + /// OpenTracing.Tracer.shared().inject(spanContext: span, format: OpenTracing.Format.TextMap, + /// carrier: httpHeaders) + /// + /// - SeeAlso: [propagation](http://opentracing.io/propagation/) + /// + /// - parameter spanContext: the OTSpanContext instance to inject + /// - parameter writer: the desired inject carrier format and corresponding carrier. Format is + /// specified via the type, and the carrier is the backing store being written + /// to. + func inject(spanContext: OTSpanContext, writer: OTFormatWriter) + + /// Extract a SpanContext previously (and remotely) injected into the carrier of the given format. + /// + /// For example: + /// let headerMap: [String: String] = req.headers // or similar + /// OpenTracing.SpanContext ctx = + /// OpenTracing.Tracer.shared().extract(format: OpenTracing.Format.TextMap, carrier: headerMap) + /// OpenTracing.Span span = + /// OpenTracing.Tracer.shared().startSpan(operationName: "methodName", childOf: ctx) + /// + /// - SeeAlso: [propagation](http://opentracing.io/propagation/) + /// + /// - parameter reader: the desired extract carrier format and corresponding carrier. Format is + /// specified via the type, and the carrier is the backing store being read from. + /// @returns a newly-created OTSpanContext that belongs to the trace previously + /// injected into the carrier (presumably in a remote process) + /// + func extract(reader: OTFormatReader) -> OTSpanContext? +} + +/// Extension for a convenience startSpan() with a single parent rather than a list of references +public extension OTTracer { + /// Start a new span with the given operation name. + /// + /// - parameter operationName: the operation name for the newly-started span + /// - parameter parent: span context that will be a parent reference; nil creates a root span + /// - parameter tags: a set of tag keys and values per OTSpan#setTag:value:, or nil to start with + /// an empty tag map + /// - parameter startTime: an explicitly specified start timestamp for the OTSpan, or nil to use the + /// current walltime + /// - returns: a valid Span instance; it is the caller's responsibility to call finish() + func startSpan( + operationName: String, + childOf parent: OTSpanContext? = nil, + tags: [String: Encodable]? = nil, + startTime: Date? = nil + ) -> OTSpan { + let references = parent.map { [OTReference.child(of: $0)] } + return self.startSpan( + operationName: operationName, + references: references, + tags: tags, + startTime: startTime + ) + } +} diff --git a/Sources/Datadog/Tracer.swift b/Sources/Datadog/Tracer.swift new file mode 100644 index 0000000000..ea814a8456 --- /dev/null +++ b/Sources/Datadog/Tracer.swift @@ -0,0 +1,179 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// Datadog - specific span `tags` to be used with `tracer.startSpan(operationName:references:tags:startTime:)` +/// and `span.setTag(key:value:)`. +public struct DDTags { + /// A Datadog-specific span tag, which sets the value appearing in the "RESOURCE" column + /// in traces explorer on [app.datadoghq.com](https://app.datadoghq.com/) + /// Can be used to customize the resource names grouped under the same operation name. + /// + /// Expects `String` value set for a tag. + public static let resource = "resource.name" + + /// Those keys used to encode information received from the user through `OpenTracingLogFields`, `OpenTracingTagKeys` or custom fields. + /// Supported by Datadog platform. + internal static let errorType = "error.type" + internal static let errorMessage = "error.msg" + internal static let errorStack = "error.stack" +} + +/// Because `Tracer` is a common name widely used across different projects, the `Datadog.Tracer` may conflict when +/// doing `import Datadog`. In such case, following `DDTracer` typealias can be used to avoid compiler ambiguity. +/// +/// Usage: +/// +/// import Datadog +/// +/// // tracer reference +/// var tracer: DDTracer! +/// +/// // instantiate Datadog tracer +/// tracer = DDTracer.initialize(...) +/// +public typealias DDTracer = Tracer + +public class Tracer: OTTracer { + /// Writes `Span` objects to output. + internal let spanOutput: SpanOutput + /// Writes span logs to output. + /// Equals `nil` if Logging feature is disabled. + internal let logOutput: LoggingForTracingAdapter.AdaptedLogOutput? + /// Queue ensuring thread-safety of the `Tracer` and `DDSpan` operations. + internal let queue: DispatchQueue + + private let dateProvider: DateProvider + private let tracingUUIDGenerator: TracingUUIDGenerator + + /// Tags to be set on all spans. They are set at initialization from Tracer.Configuration + private let globalTags: [String: Encodable]? + + // MARK: - Initialization + + /// Initializes the Datadog Tracer. + /// - Parameters: + /// - configuration: the tracer configuration obtained using `Tracer.Configuration()`. + public static func initialize(configuration: Configuration) -> OTTracer { + do { + guard let tracingFeature = TracingFeature.instance else { + throw ProgrammerError( + description: Datadog.instance == nil + ? "`Datadog.initialize()` must be called prior to `Tracer.initialize()`." + : "`Tracer.initialize(configuration:)` produces a non-functional tracer, as the tracing feature is disabled." + ) + } + return DDTracer( + tracingFeature: tracingFeature, + tracerConfiguration: configuration + ) + } catch { + consolePrint("\(error)") + return DDNoopTracer() + } + } + + internal convenience init(tracingFeature: TracingFeature, tracerConfiguration: Configuration) { + self.init( + spanOutput: SpanFileOutput( + spanBuilder: SpanBuilder( + applicationVersion: tracingFeature.configuration.applicationVersion, + environment: tracingFeature.configuration.environment, + serviceName: tracerConfiguration.serviceName ?? tracingFeature.configuration.serviceName, + userInfoProvider: tracingFeature.userInfoProvider, + networkConnectionInfoProvider: tracerConfiguration.sendNetworkInfo ? tracingFeature.networkConnectionInfoProvider : nil, + carrierInfoProvider: tracerConfiguration.sendNetworkInfo ? tracingFeature.carrierInfoProvider : nil + ), + fileWriter: tracingFeature.storage.writer + ), + logOutput: tracingFeature + .loggingFeatureAdapter? + .resolveLogOutput(usingTracingFeature: tracingFeature, tracerConfiguration: tracerConfiguration), + dateProvider: tracingFeature.dateProvider, + tracingUUIDGenerator: tracingFeature.tracingUUIDGenerator, + globalTags: tracerConfiguration.globalTags + ) + } + + internal init( + spanOutput: SpanOutput, + logOutput: LoggingForTracingAdapter.AdaptedLogOutput?, + dateProvider: DateProvider, + tracingUUIDGenerator: TracingUUIDGenerator, + globalTags: [String: Encodable]? + ) { + self.spanOutput = spanOutput + self.logOutput = logOutput + self.queue = DispatchQueue( + label: "com.datadoghq.tracer", + target: .global(qos: .userInteractive) + ) + self.dateProvider = dateProvider + self.tracingUUIDGenerator = tracingUUIDGenerator + self.globalTags = globalTags + } + + // MARK: - Open Tracing interface + + public func startSpan(operationName: String, references: [OTReference]? = nil, tags: [String: Encodable]? = nil, startTime: Date? = nil) -> OTSpan { + let parentSpanContext = references?.compactMap { $0.context.dd }.last + let spanContext = createSpanContext(parentSpanContext: parentSpanContext) + return startSpan( + spanContext: spanContext, + operationName: operationName, + tags: tags, + startTime: startTime + ) + } + + public func inject(spanContext: OTSpanContext, writer: OTFormatWriter) { + writer.inject(spanContext: spanContext) + } + + public func extract(reader: OTFormatReader) -> OTSpanContext? { + // TODO: RUMM-385 - we don't need to support it now + return nil + } + + // MARK: - Internal + + internal func createSpanContext(parentSpanContext: DDSpanContext? = nil) -> DDSpanContext { + return DDSpanContext( + traceID: parentSpanContext?.traceID ?? tracingUUIDGenerator.generateUnique(), + spanID: tracingUUIDGenerator.generateUnique(), + parentSpanID: parentSpanContext?.spanID, + baggageItems: BaggageItems(targetQueue: queue, parentSpanItems: parentSpanContext?.baggageItems) + ) + } + + internal func startSpan(spanContext: DDSpanContext, operationName: String, tags: [String: Encodable]? = nil, startTime: Date? = nil) -> OTSpan { + var combinedTags = globalTags ?? [:] + if let tags = tags { + combinedTags.merge(tags) { _, last in last } + } + + return DDSpan( + tracer: self, + context: spanContext, + operationName: operationName, + startTime: startTime ?? dateProvider.currentDate(), + tags: combinedTags + ) + } + + internal func write(span: DDSpan, finishTime: Date) { + spanOutput.write(ddspan: span, finishTime: finishTime) + } + + internal func writeLog(for span: DDSpan, fields: [String: Encodable], date: Date) { + guard let logOutput = logOutput else { + userLogger.warn("The log for span \"\(span.operationName)\" will not be send, because the Logging feature is disabled.") + return + } + logOutput.writeLog(withSpanContext: span.ddContext, fields: fields, date: date) + } +} diff --git a/Sources/Datadog/TracerConfiguration.swift b/Sources/Datadog/TracerConfiguration.swift new file mode 100644 index 0000000000..2a377bfb57 --- /dev/null +++ b/Sources/Datadog/TracerConfiguration.swift @@ -0,0 +1,37 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +extension Tracer { + /// Datadog Tracer configuration. + public struct Configuration { + /// The service name that will appear in traces (if not provided or `nil`, the SDK default `serviceName` will be used). + public var serviceName: String? + + /// Enriches traces with network connection info. + /// This means: reachability status, connection type, mobile carrier name and many more will be added to every span and span logs. + /// For full list of network info attributes see `NetworkConnectionInfo` and `CarrierInfo`. + /// - Parameter enabled: `false` by default + public var sendNetworkInfo: Bool + + /// Tags that will be added to all new spans created by the tracer. + public var globalTags: [String: Encodable]? + + /// Initializes the Datadog Tracer configuration. + /// - Parameter serviceName: the service name that will appear in traces (if not provided or `nil`, the SDK default `serviceName` will be used). + /// - Parameter sendNetworkInfo: adds network connection info to every span and span logs (`false` by default). + public init( + serviceName: String? = nil, + sendNetworkInfo: Bool = false, + globalTags: [String: Encodable]? = nil + ) { + self.serviceName = serviceName + self.sendNetworkInfo = sendNetworkInfo + self.globalTags = globalTags + } + } +} diff --git a/Sources/Datadog/Tracing/DDNoOps.swift b/Sources/Datadog/Tracing/DDNoOps.swift new file mode 100644 index 0000000000..24c3995d57 --- /dev/null +++ b/Sources/Datadog/Tracing/DDNoOps.swift @@ -0,0 +1,34 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal struct DDNoopGlobals { + static let tracer = DDNoopTracer() + static let span = DDNoopSpan() + static let context = DDNoopSpanContext() +} + +internal struct DDNoopTracer: OTTracer { + func extract(reader: OTFormatReader) -> OTSpanContext? { DDNoopGlobals.context } + func inject(spanContext: OTSpanContext, writer: OTFormatWriter) {} + func startSpan(operationName: String, references: [OTReference]?, tags: [String: Encodable]?, startTime: Date?) -> OTSpan { DDNoopGlobals.span } +} + +internal struct DDNoopSpan: OTSpan { + var context: OTSpanContext { DDNoopGlobals.context } + func tracer() -> OTTracer { DDNoopGlobals.tracer } + func setOperationName(_ operationName: String) {} + func finish(at time: Date) {} + func log(fields: [String: Encodable], timestamp: Date) {} + func baggageItem(withKey key: String) -> String? { nil } + func setBaggageItem(key: String, value: String) {} + func setTag(key: String, value: Encodable) {} +} + +internal struct DDNoopSpanContext: OTSpanContext { + func forEachBaggageItem(callback: (String, String) -> Bool) {} +} diff --git a/Sources/Datadog/Tracing/DDSpan.swift b/Sources/Datadog/Tracing/DDSpan.swift new file mode 100644 index 0000000000..7deac7014f --- /dev/null +++ b/Sources/Datadog/Tracing/DDSpan.swift @@ -0,0 +1,123 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal class DDSpan: OTSpan { + /// The `Tracer` which created this span. + private let ddTracer: Tracer + /// Span context. + internal let ddContext: DDSpanContext + /// Span creation date + internal let startTime: Date + + /// Unsynchronized span operation name. Use `self.operationName` setter & getter. + private var unsafeOperationName: String + private(set) var operationName: String { + get { ddTracer.queue.sync { unsafeOperationName } } + set { ddTracer.queue.async { self.unsafeOperationName = newValue } } + } + + /// Unsynchronized span tags. Use `self.tags` setter & getter. + private var unsafeTags: [String: Encodable] + private(set) var tags: [String: Encodable] { + get { ddTracer.queue.sync { unsafeTags } } + set { ddTracer.queue.async { self.unsafeTags = newValue } } + } + + /// Unsychronized span completion. Use `self.isFinished` setter & getter. + private var unsafeIsFinished: Bool + private(set) var isFinished: Bool { + get { ddTracer.queue.sync { unsafeIsFinished } } + set { ddTracer.queue.async { self.unsafeIsFinished = newValue } } + } + + /// Unsychronized span log fields. Use `self.logFields` setter & getter. + private var unsafeLogFields: [[String: Encodable]] = [] + /// A collection of all log fields send for this span. + private(set) var logFields: [[String: Encodable]] { + get { ddTracer.queue.sync { unsafeLogFields } } + set { ddTracer.queue.async { self.unsafeLogFields = newValue } } + } + + init( + tracer: Tracer, + context: DDSpanContext, + operationName: String, + startTime: Date, + tags: [String: Encodable] + ) { + self.ddTracer = tracer + self.ddContext = context + self.startTime = startTime + self.unsafeOperationName = operationName + self.unsafeTags = tags + self.unsafeIsFinished = false + } + + // MARK: - Open Tracing interface + + var context: OTSpanContext { + return ddContext + } + + func tracer() -> OTTracer { + return ddTracer + } + + func setOperationName(_ operationName: String) { + if warnIfFinished("setOperationName(_:)") { + return + } + self.operationName = operationName + } + + func setTag(key: String, value: Encodable) { + if warnIfFinished("setTag(key:value:)") { + return + } + self.tags[key] = value + } + + func setBaggageItem(key: String, value: String) { + if warnIfFinished("setBaggageItem(key:value:)") { + return + } + ddContext.baggageItems.set(key: key, value: value) + } + + func baggageItem(withKey key: String) -> String? { + if warnIfFinished("baggageItem(withKey:)") { + return nil + } + return ddContext.baggageItems.get(key: key) + } + + func finish(at time: Date) { + if warnIfFinished("finish(at:)") { + return + } + isFinished = true + ddTracer.write(span: self, finishTime: time) + } + + func log(fields: [String: Encodable], timestamp: Date) { + if warnIfFinished("log(fields:timestamp:)") { + return + } + logFields.append(fields) + ddTracer.writeLog(for: self, fields: fields, date: timestamp) + } + + // MARK: - Private + + private func warnIfFinished(_ methodName: String) -> Bool { + return warn( + if: isFinished, + message: "๐Ÿ”ฅ Calling `\(methodName)` on a finished span (\"\(operationName)\") is not allowed." + ) + } +} diff --git a/Sources/Datadog/Tracing/DDSpanContext.swift b/Sources/Datadog/Tracing/DDSpanContext.swift new file mode 100644 index 0000000000..ffef2aa172 --- /dev/null +++ b/Sources/Datadog/Tracing/DDSpanContext.swift @@ -0,0 +1,71 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal struct DDSpanContext: OTSpanContext { + /// This span's trace ID. + let traceID: TracingUUID + /// This span ID. + let spanID: TracingUUID + /// The ID of the parent span or `nil` if this span is the root span. + let parentSpanID: TracingUUID? + /// The baggage items of this span. + let baggageItems: BaggageItems + + // MARK: - Open Tracing interface + + func forEachBaggageItem(callback: (String, String) -> Bool) { + for (itemKey, itemValue) in baggageItems.all { + if callback(itemKey, itemValue) { + break + } + } + } +} + +/// Baggage items are used to propagate span information from parent to child. This propagation is +/// unidirectional and recursive, so the grandchild of a span `A` will contain the `A's` baggage items, +/// but `A` won't contain items of its descendants. +internal class BaggageItems { + /// Queue used to synchronize `unsafeItems` access. + private let queue: DispatchQueue + /// Baggage items of the parent `DDSpan` or`nil` for items of the root span. + private let parent: BaggageItems? + + /// Unsynchronized baggage items dictionary. Use `queue` to synchronize the access. + private var unsafeItems: [String: String] = [:] + + init(targetQueue: DispatchQueue, parentSpanItems: BaggageItems?) { + self.queue = DispatchQueue(label: "com.datadoghq.BaggageItem", target: targetQueue) + self.parent = parentSpanItems + } + + func set(key: String, value: String) { + queue.async { self.unsafeItems[key] = value } + } + + func get(key: String) -> String? { + queue.sync { self.unsafeItems[key] } + } + + var all: [String: String] { + queue.sync { self.unsafeAll } + } + + /// Returns all baggage items for the span, including its parent items. + /// This property is unsafe and should be accessed using `queue`. + private var unsafeAll: [String: String] { + let parentItems = parent?.unsafeAll ?? [:] + let selfItems = unsafeItems + + let allItems = parentItems.merging(selfItems) { _, selfItem -> String in + return selfItem + } + + return allItems + } +} diff --git a/Sources/Datadog/Tracing/Propagation/HTTPHeadersWriter.swift b/Sources/Datadog/Tracing/Propagation/HTTPHeadersWriter.swift new file mode 100644 index 0000000000..0f69b4fa7b --- /dev/null +++ b/Sources/Datadog/Tracing/Propagation/HTTPHeadersWriter.swift @@ -0,0 +1,43 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +public class HTTPHeadersWriter: OTHTTPHeadersWriter { + private enum Constants: String, CaseIterable { + case traceIDField = "x-datadog-trace-id" + case parentSpanIDField = "x-datadog-parent-id" + // TODO: RUMM-338 support `x-datadog-sampling-priority`. `dd-trace-ot` reference: + // https://github.com/DataDog/dd-trace-java/blob/4ba0ca0f9da748d4018310d026b1a72b607947f1/dd-trace-ot/src/main/java/datadog/opentracing/propagation/DatadogHttpCodec.java#L23 + } + + public init() {} + + /// The `tracePropagationHTTPHeaders` will be used by customers to add additional headers to the + /// `URLRequest` in order to propagate the trace to Datadog-OT-instrumented backend. + /// + /// TODO: revisit in RUMM-386 + public private(set) var tracePropagationHTTPHeaders: [String: String] = [:] + + public func inject(spanContext: OTSpanContext) { + guard let spanContext = spanContext.dd else { + return + } + + tracePropagationHTTPHeaders = [ + Constants.traceIDField.rawValue: String(spanContext.traceID.rawValue), + Constants.parentSpanIDField.rawValue: String(spanContext.spanID.rawValue) + ] + } + + internal static func canInject(to request: URLRequest) -> Bool { + let containsHeaders: Bool + containsHeaders = Constants.allCases.contains { headerKey -> Bool in + return request.value(forHTTPHeaderField: headerKey.rawValue) != nil + } + return !containsHeaders + } +} diff --git a/Sources/Datadog/Tracing/Span/SpanBuilder.swift b/Sources/Datadog/Tracing/Span/SpanBuilder.swift new file mode 100644 index 0000000000..8e93780927 --- /dev/null +++ b/Sources/Datadog/Tracing/Span/SpanBuilder.swift @@ -0,0 +1,60 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// Builds `Span` representation (for later serialization) from `DDSpan`. +internal struct SpanBuilder { + /// Application version to encode in span. + let applicationVersion: String + /// Environment to encode in span. + let environment: String + /// Service name to encode in span. + let serviceName: String + /// Shared user info provider. + let userInfoProvider: UserInfoProvider + /// Shared network connection info provider (or `nil` if disabled for given tracer). + let networkConnectionInfoProvider: NetworkConnectionInfoProviderType? + /// Shared mobile carrier info provider (or `nil` if disabled for given tracer). + let carrierInfoProvider: CarrierInfoProviderType? + + /// Encodes tag `Span` tag values as JSON string + private let tagsJSONEncoder: JSONEncoder = .default() + + func createSpan(from ddspan: DDSpan, finishTime: Date) throws -> Span { + let tagsReducer = SpanTagsReducer(spanTags: ddspan.tags, logFields: ddspan.logFields) + + var jsonStringEncodedTags: [String: JSONStringEncodableValue] = [:] + + // First, add baggage items as tags... + for (itemKey, itemValue) in ddspan.ddContext.baggageItems.all { + jsonStringEncodedTags[itemKey] = JSONStringEncodableValue(itemValue, encodedUsing: tagsJSONEncoder) + } + + // ... then, add regular tags + for (tagName, tagValue) in tagsReducer.reducedSpanTags { + jsonStringEncodedTags[tagName] = JSONStringEncodableValue(tagValue, encodedUsing: tagsJSONEncoder) + } + + return Span( + traceID: ddspan.ddContext.traceID, + spanID: ddspan.ddContext.spanID, + parentID: ddspan.ddContext.parentSpanID, + operationName: ddspan.operationName, + serviceName: serviceName, + resource: tagsReducer.extractedResourceName ?? ddspan.operationName, + startTime: ddspan.startTime, + duration: finishTime.timeIntervalSince(ddspan.startTime), + isError: tagsReducer.extractedIsError ?? false, + tracerVersion: sdkVersion, + applicationVersion: applicationVersion, + networkConnectionInfo: networkConnectionInfoProvider?.current, + mobileCarrierInfo: carrierInfoProvider?.current, + userInfo: userInfoProvider.value, + tags: jsonStringEncodedTags + ) + } +} diff --git a/Sources/Datadog/Tracing/Span/SpanEncoder.swift b/Sources/Datadog/Tracing/Span/SpanEncoder.swift new file mode 100644 index 0000000000..892f97290e --- /dev/null +++ b/Sources/Datadog/Tracing/Span/SpanEncoder.swift @@ -0,0 +1,202 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// `SpanEnvelope` allows encoding multiple spans sharing the same `traceID` to a single payload. +internal struct SpanEnvelope: Encodable { + enum CodingKeys: String, CodingKey { + case spans = "spans" + case environment = "env" + } + + let spans: [Span] + let environment: String + + /// The initializer to encode single `Span` within an envelope. + init(span: Span, environment: String) { + self.init(spans: [span], environment: environment) + } + + /// This initializer is `private` now, as we don't yet + /// support batching multiple spans sharing the same `traceID` within a single payload. + private init(spans: [Span], environment: String) { + self.spans = spans + self.environment = environment + } +} + +/// `Encodable` representation of span. +internal struct Span: Encodable { + let traceID: TracingUUID + let spanID: TracingUUID + let parentID: TracingUUID? + let operationName: String + let serviceName: String + let resource: String + let startTime: Date + let duration: TimeInterval + let isError: Bool + + // MARK: - Meta + + let tracerVersion: String + let applicationVersion: String + let networkConnectionInfo: NetworkConnectionInfo? + let mobileCarrierInfo: CarrierInfo? + let userInfo: UserInfo + + /// Custom tags, received from user + let tags: [String: JSONStringEncodableValue] + + func encode(to encoder: Encoder) throws { + try SpanEncoder().encode(self, to: encoder) + } +} + +/// Encodes `Span` to given encoder. +internal struct SpanEncoder { + /// Coding keys for permanent `Span` attributes. + enum StaticCodingKeys: String, CodingKey { + // MARK: - Attributes + + case traceID = "trace_id" + case spanID = "span_id" + case parentID = "parent_id" + case operationName = "name" + case serviceName = "service" + case resource + case type + case startTime = "start" + case duration + case isError = "error" + + // MARK: - Metrics + + case isRootSpan = "metrics._top_level" + case samplingPriority = "metrics._sampling_priority_v1" + + // MARK: - Meta + + case source = "meta._dd.source" + case applicationVersion = "meta.version" + case tracerVersion = "meta.tracer.version" + + case userId = "meta.usr.id" + case userName = "meta.usr.name" + case userEmail = "meta.usr.email" + + case networkReachability = "meta.network.client.reachability" + case networkAvailableInterfaces = "meta.network.client.available_interfaces" + case networkConnectionSupportsIPv4 = "meta.network.client.supports_ipv4" + case networkConnectionSupportsIPv6 = "meta.network.client.supports_ipv6" + case networkConnectionIsExpensive = "meta.network.client.is_expensive" + case networkConnectionIsConstrained = "meta.network.client.is_constrained" + + case mobileNetworkCarrierName = "meta.network.client.sim_carrier.name" + case mobileNetworkCarrierISOCountryCode = "meta.network.client.sim_carrier.iso_country" + case mobileNetworkCarrierRadioTechnology = "meta.network.client.sim_carrier.technology" + case mobileNetworkCarrierAllowsVoIP = "meta.network.client.sim_carrier.allows_voip" + } + + /// Coding keys for dynamic `Span` attributes specified by user. + private struct DynamicCodingKey: CodingKey { + var stringValue: String + var intValue: Int? + init?(stringValue: String) { self.stringValue = stringValue } + init?(intValue: Int) { return nil } + init(_ string: String) { self.stringValue = string } + } + + func encode(_ span: Span, to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StaticCodingKeys.self) + try container.encode(span.traceID.toHexadecimalString, forKey: .traceID) + try container.encode(span.spanID.toHexadecimalString, forKey: .spanID) + + let parentSpanID = span.parentID ?? TracingUUID(rawValue: 0) // 0 is a reserved ID for a root span (ref: DDTracer.java#L600) + try container.encode(parentSpanID.toHexadecimalString, forKey: .parentID) + + try container.encode(span.operationName, forKey: .operationName) + try container.encode(span.serviceName, forKey: .serviceName) + try container.encode(span.resource, forKey: .resource) + try container.encode("custom", forKey: .type) + + try container.encode(span.startTime.timeIntervalSince1970.toNanoseconds, forKey: .startTime) + try container.encode(span.duration.toNanoseconds, forKey: .duration) + + let isError = span.isError ? 1 : 0 + try container.encode(isError, forKey: .isError) + + try encodeDefaultMetrics(span, to: &container) + try encodeDefaultMeta(span, to: &container) + + var customAttributesContainer = encoder.container(keyedBy: DynamicCodingKey.self) + try encodeCustomMeta(span, to: &customAttributesContainer) + } + + /// Encodes default `metrics.*` attributes + private func encodeDefaultMetrics(_ span: Span, to container: inout KeyedEncodingContainer) throws { + // NOTE: RUMM-299 only numeric values are supported for `metrics.*` attributes + if span.parentID == nil { + try container.encode(1, forKey: .isRootSpan) + } + try container.encode(1, forKey: .samplingPriority) + } + + /// Encodes default `meta.*` attributes + private func encodeDefaultMeta(_ span: Span, to container: inout KeyedEncodingContainer) throws { + // NOTE: RUMM-299 only string values are supported for `meta.*` attributes + try container.encode(Datadog.Constants.ddsource, forKey: .source) + try container.encode(span.tracerVersion, forKey: .tracerVersion) + try container.encode(span.applicationVersion, forKey: .applicationVersion) + + try span.userInfo.id.ifNotNil { try container.encode($0, forKey: .userId) } + try span.userInfo.name.ifNotNil { try container.encode($0, forKey: .userName) } + try span.userInfo.email.ifNotNil { try container.encode($0, forKey: .userEmail) } + + if let networkConnectionInfo = span.networkConnectionInfo { + try container.encode(networkConnectionInfo.reachability, forKey: .networkReachability) + if let availableInterfaces = networkConnectionInfo.availableInterfaces, availableInterfaces.count > 0 { + // Because only string values are supported for `meta.*` attributes, available network interfaces + // are represented as names concatenated using `+` symbol, i.e.: "wifi+cellular", "cellular" + let availableInterfacesString = availableInterfaces.map { $0.rawValue }.joined(separator: "+") + try container.encode(availableInterfacesString, forKey: .networkAvailableInterfaces) + } + if let supportsIPv4 = networkConnectionInfo.supportsIPv4 { + try container.encode(supportsIPv4 ? "1" : "0", forKey: .networkConnectionSupportsIPv4) + } + if let supportsIPv6 = networkConnectionInfo.supportsIPv6 { + try container.encode(supportsIPv6 ? "1" : "0", forKey: .networkConnectionSupportsIPv6) + } + if let isExpensive = networkConnectionInfo.isExpensive { + try container.encode(isExpensive ? "1" : "0", forKey: .networkConnectionIsExpensive) + } + if let isConstrained = networkConnectionInfo.isConstrained { + try container.encode(isConstrained ? "1" : "0", forKey: .networkConnectionIsConstrained) + } + } + + if let carrierInfo = span.mobileCarrierInfo { + if let carrierName = carrierInfo.carrierName { + try container.encode(carrierName, forKey: .mobileNetworkCarrierName) + } + if let carrierISOCountryCode = carrierInfo.carrierISOCountryCode { + try container.encode(carrierISOCountryCode, forKey: .mobileNetworkCarrierISOCountryCode) + } + try container.encode(carrierInfo.radioAccessTechnology, forKey: .mobileNetworkCarrierRadioTechnology) + try container.encode(carrierInfo.carrierAllowsVOIP ? "1" : "0", forKey: .mobileNetworkCarrierAllowsVoIP) + } + } + + /// Encodes `meta.*` attributes coming from user + private func encodeCustomMeta(_ span: Span, to container: inout KeyedEncodingContainer) throws { + // NOTE: RUMM-299 only string values are supported for `meta.*` attributes + try span.tags.forEach { + let metaKey = "meta.\($0.key)" + try container.encode($0.value, forKey: DynamicCodingKey(metaKey)) + } + } +} diff --git a/Sources/Datadog/Tracing/Span/SpanTagsReducer.swift b/Sources/Datadog/Tracing/Span/SpanTagsReducer.swift new file mode 100644 index 0000000000..68fc18cc8e --- /dev/null +++ b/Sources/Datadog/Tracing/Span/SpanTagsReducer.swift @@ -0,0 +1,66 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// Reduces `DDSpan` tags and log attributes by extracting values that require separate handling. +/// +/// The responsibility of `SpanTagsReducer` is to capture Open Tracing [tags](https://github.com/opentracing/specification/blob/master/semantic_conventions.md#span-tags-table) +/// and [log fields](https://github.com/opentracing/specification/blob/master/semantic_conventions.md#log-fields-table) given by the user and +/// transform them into the format used by Datadog. This happens in two ways, by: +/// - extracting information from `spanTags` and `logFields` to `extracted*` variables, +/// - reducing the initial `spanTags` collection to `reducedSpanTags` by removing extracted information. +/// +/// In result, the `reducedSpanTags` will contain only the tags that do NOT require special handling by Datadog and can be send as generic `span.meta.*` JSON values. +/// Values extracted from `spanTags` and `logFields` will be passed to the `Span` encoding process in a type-safe manner. +internal struct SpanTagsReducer { + /// Tags for generic `span.meta.*` encoding in `Span` JSON. + let reducedSpanTags: [String: Encodable] + + // MARK: - Extracted Info + + /// Error information requiring a special encoding in `Span` JSON. + let extractedIsError: Bool? + /// Resource name requiring a special encoding in `Span` JSON. + let extractedResourceName: String? + + // MARK: - Initialization + + init(spanTags: [String: Encodable], logFields: [[String: Encodable]]) { + var mutableSpanTags = spanTags + + var extractedIsError: Bool? = nil + var extractedResourceName: String? = nil + + // extract error from `logFields` + for fields in logFields { + let isErrorEvent = fields[OTLogFields.event] as? String == "error" + let errorKind = fields[OTLogFields.errorKind] as? String + + if isErrorEvent || errorKind != nil { + extractedIsError = true + mutableSpanTags[DDTags.errorMessage] = fields[OTLogFields.message] as? String + mutableSpanTags[DDTags.errorType] = errorKind + mutableSpanTags[DDTags.errorStack] = fields[OTLogFields.stack] as? String + break // ignore next logs + } + } + + // extract error from `mutableSpanTags` + if mutableSpanTags[OTTags.error] as? Bool == true { + extractedIsError = true + } + + // extract resource name from `mutableSpanTags` + if let resourceName = mutableSpanTags.removeValue(forKey: DDTags.resource) as? String { + extractedResourceName = resourceName + } + + self.reducedSpanTags = mutableSpanTags + self.extractedIsError = extractedIsError + self.extractedResourceName = extractedResourceName + } +} diff --git a/Sources/Datadog/Tracing/SpanOutputs/SpanFileOutput.swift b/Sources/Datadog/Tracing/SpanOutputs/SpanFileOutput.swift new file mode 100644 index 0000000000..c0672d5328 --- /dev/null +++ b/Sources/Datadog/Tracing/SpanOutputs/SpanFileOutput.swift @@ -0,0 +1,23 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// `SpanOutput` which saves spans to file. +internal struct SpanFileOutput: SpanOutput { + let spanBuilder: SpanBuilder + let fileWriter: FileWriter + + func write(ddspan: DDSpan, finishTime: Date) { + do { + let span = try spanBuilder.createSpan(from: ddspan, finishTime: finishTime) + let envelope = SpanEnvelope(span: span, environment: spanBuilder.environment) + fileWriter.write(value: envelope) + } catch { + userLogger.error("๐Ÿ”ฅ Failed to build span: \(error)") + } + } +} diff --git a/Sources/Datadog/Logs/LogOutputs/LogOutput.swift b/Sources/Datadog/Tracing/SpanOutputs/SpanOutput.swift similarity index 59% rename from Sources/Datadog/Logs/LogOutputs/LogOutput.swift rename to Sources/Datadog/Tracing/SpanOutputs/SpanOutput.swift index 4883370a7e..aff3023c6b 100644 --- a/Sources/Datadog/Logs/LogOutputs/LogOutput.swift +++ b/Sources/Datadog/Tracing/SpanOutputs/SpanOutput.swift @@ -6,7 +6,7 @@ import Foundation -/// Type writting logs to some destination. -internal protocol LogOutput { - func writeLogWith(level: LogLevel, message: String, attributes: [String: Encodable], tags: Set) +/// Type writting spans to some destination. +internal protocol SpanOutput { + func write(ddspan: DDSpan, finishTime: Date) } diff --git a/Sources/Datadog/Tracing/TracingAutoInstrumentation.swift b/Sources/Datadog/Tracing/TracingAutoInstrumentation.swift new file mode 100644 index 0000000000..4a8c891533 --- /dev/null +++ b/Sources/Datadog/Tracing/TracingAutoInstrumentation.swift @@ -0,0 +1,124 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal class TracingAutoInstrumentation { + static var instance: TracingAutoInstrumentation? + + let swizzler: URLSessionSwizzler + let urlFilter: URLFiltering + var interceptor: RequestInterceptor { + TracingRequestInterceptor.build(with: urlFilter) + } + + convenience init?(with configuration: Datadog.Configuration) { + if !configuration.tracingEnabled || configuration.tracedHosts.isEmpty { + return nil + } + let urlFilter = URLFilter( + includedHosts: configuration.tracedHosts, + excludedURLs: [ + configuration.logsEndpoint.url, + configuration.tracesEndpoint.url + ] + ) + self.init(urlFilter: urlFilter) + } + + init?(urlFilter: URLFiltering) { + do { + self.swizzler = try URLSessionSwizzler() + self.urlFilter = urlFilter + } catch { + userLogger.warn("๐Ÿ”ฅ Network requests won't be traced automatically: \(error)") + developerLogger?.warn("๐Ÿ”ฅ Network requests won't be traced automatically: \(error)") + return nil + } + } + + func apply() { + swizzler.swizzle(using: interceptor) + } +} + +private enum TracingRequestInterceptor { + static func build(with filter: URLFiltering) -> RequestInterceptor { + let interceptor: RequestInterceptor = { urlRequest in + guard let tracer = Global.sharedTracer as? DDTracer, + filter.allows(urlRequest.url), + HTTPHeadersWriter.canInject(to: urlRequest) else { + return nil + } + let spanContext = tracer.createSpanContext() + let headersWriter = HTTPHeadersWriter() + headersWriter.inject(spanContext: spanContext) + let tracingHeaders = headersWriter.tracePropagationHTTPHeaders + var modifiedRequest = urlRequest + tracingHeaders.forEach { modifiedRequest.setValue($1, forHTTPHeaderField: $0) } + + let observer: TaskObserver = tracingTaskObserver(tracer: tracer, spanContext: spanContext) + return InterceptionResult(modifiedRequest: modifiedRequest, taskObserver: observer) + } + return interceptor + } + + private static func tracingTaskObserver( + tracer: DDTracer, + spanContext: DDSpanContext + ) -> TaskObserver { + var startedSpan: OTSpan? = nil + let observer: TaskObserver = { observedEvent in + switch observedEvent { + case .starting(let request): + if let ongoingSpan = startedSpan { + userLogger.warn("\(String(describing: request)) is starting a new trace but it's already started a trace before: \(ongoingSpan)") + developerLogger?.warn("\(String(describing: request)) is starting a new trace but it's already started a trace before: \(ongoingSpan)") + } + let span = tracer.startSpan( + spanContext: spanContext, + operationName: "urlsession.request" + ) + let url = request?.url?.absoluteString ?? "unknown_url" + let method = request?.httpMethod ?? "unknown_method" + span.setTag(key: DDTags.resource, value: url) + span.setTag(key: OTTags.httpUrl, value: url) + span.setTag(key: OTTags.httpMethod, value: method) + startedSpan = span + case .completed(let response, let error): + guard let completedSpan = startedSpan else { + break + } + if let someError = error { + completedSpan.handleError(someError) + } + if let statusCode = (response as? HTTPURLResponse)?.statusCode { + completedSpan.setTag(key: OTTags.httpStatusCode, value: statusCode) + if (400..<500).contains(statusCode) { + completedSpan.setTag(key: OTTags.error, value: true) + } + if statusCode == 404 { + completedSpan.setTag(key: DDTags.resource, value: "404") + } + } + completedSpan.finish() + } + } + return observer + } +} + +private extension OTSpan { + func handleError(_ error: Error) { + setTag(key: OTTags.error, value: true) + setTag(key: DDTags.errorStack, value: String(describing: error)) + let nsError = error as NSError + let errorKind = "\(nsError.domain) - \(nsError.code)" + setTag(key: DDTags.errorType, value: errorKind) + let errorMessage = nsError.localizedDescription + setTag(key: DDTags.errorMessage, value: errorMessage) + } +} diff --git a/Sources/Datadog/Tracing/TracingFeature.swift b/Sources/Datadog/Tracing/TracingFeature.swift new file mode 100644 index 0000000000..81e7446c51 --- /dev/null +++ b/Sources/Datadog/Tracing/TracingFeature.swift @@ -0,0 +1,175 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// Obtains a subdirectory in `/Library/Caches` where span files are stored. +internal func obtainTracingFeatureDirectory() throws -> Directory { + return try Directory(withSubdirectoryPath: "com.datadoghq.traces/v1") +} + +/// Creates and owns componetns enabling tracing feature. +/// Bundles dependencies for other tracing-related components created later at runtime (i.e. `Tracer`). +internal final class TracingFeature { + /// Single, shared instance of `TracingFeatureFeature`. + internal static var instance: TracingFeature? + + // MARK: - Configuration + + let configuration: Datadog.ValidConfiguration + + // MARK: - Integration With Other Features + + /// Integration with Logging feature, which enables the `span.log()` functionality. + /// Equals `nil` if Logging feature is disabled. + let loggingFeatureAdapter: LoggingForTracingAdapter? + + // MARK: - Dependencies + + let dateProvider: DateProvider + let tracingUUIDGenerator: TracingUUIDGenerator + let userInfoProvider: UserInfoProvider + let networkConnectionInfoProvider: NetworkConnectionInfoProviderType + let carrierInfoProvider: CarrierInfoProviderType + + // MARK: - Components + + /// Span files storage. + let storage: Storage + /// Spans upload worker. + let upload: Upload + + /// Encapsulates storage stack setup for `TracingFeature`. + class Storage { + /// Writes spans to files. + let writer: FileWriter + /// Reads spans from files. + let reader: FileReader + + /// NOTE: any change to tracing data format requires updating the tracing directory url to be unique + static let dataFormat = DataFormat(prefix: "", suffix: "", separator: "\n") + + init( + directory: Directory, + performance: PerformancePreset, + dateProvider: DateProvider, + readWriteQueue: DispatchQueue + ) { + let orchestrator = FilesOrchestrator( + directory: directory, + performance: performance, + dateProvider: dateProvider + ) + + self.writer = FileWriter(dataFormat: Storage.dataFormat, orchestrator: orchestrator, queue: readWriteQueue) + self.reader = FileReader(dataFormat: Storage.dataFormat, orchestrator: orchestrator, queue: readWriteQueue) + } + } + + /// Encapsulates upload stack setup for `TracingFeature`. + class Upload { + /// Uploads spans to server. + let uploader: DataUploadWorker + + init( + storage: Storage, + configuration: Datadog.ValidConfiguration, + performance: PerformancePreset, + mobileDevice: MobileDevice, + httpClient: HTTPClient, + tracesUploadURLProvider: UploadURLProvider, + networkConnectionInfoProvider: NetworkConnectionInfoProviderType, + uploadQueue: DispatchQueue + ) { + let httpHeaders = HTTPHeaders( + headers: [ + .contentTypeHeader(contentType: .textPlainUTF8), + .userAgentHeader( + appName: configuration.applicationName, + appVersion: configuration.applicationVersion, + device: mobileDevice + ) + ] + ) + let uploadConditions = DataUploadConditions( + batteryStatus: BatteryStatusProvider(mobileDevice: mobileDevice), + networkConnectionInfo: networkConnectionInfoProvider + ) + + let dataUploader = DataUploader( + urlProvider: tracesUploadURLProvider, + httpClient: httpClient, + httpHeaders: httpHeaders + ) + + self.uploader = DataUploadWorker( + queue: uploadQueue, + fileReader: storage.reader, + dataUploader: dataUploader, + uploadConditions: uploadConditions, + delay: DataUploadDelay(performance: performance), + featureName: "tracing" + ) + } + } + + // MARK: - Initialization + + init( + directory: Directory, + configuration: Datadog.ValidConfiguration, + performance: PerformancePreset, + loggingFeatureAdapter: LoggingForTracingAdapter?, + mobileDevice: MobileDevice, + httpClient: HTTPClient, + tracesUploadURLProvider: UploadURLProvider, + dateProvider: DateProvider, + tracingUUIDGenerator: TracingUUIDGenerator, + userInfoProvider: UserInfoProvider, + networkConnectionInfoProvider: NetworkConnectionInfoProviderType, + carrierInfoProvider: CarrierInfoProviderType + ) { + // Configuration + self.configuration = configuration + + // Integration with other features + self.loggingFeatureAdapter = loggingFeatureAdapter + + // Bundle dependencies + self.dateProvider = dateProvider + self.tracingUUIDGenerator = tracingUUIDGenerator + self.userInfoProvider = userInfoProvider + self.networkConnectionInfoProvider = networkConnectionInfoProvider + self.carrierInfoProvider = carrierInfoProvider + + // Initialize components + let readWriteQueue = DispatchQueue( + label: "com.datadoghq.ios-sdk-spans-read-write", + target: .global(qos: .utility) + ) + self.storage = Storage( + directory: directory, + performance: performance, + dateProvider: dateProvider, + readWriteQueue: readWriteQueue + ) + + let uploadQueue = DispatchQueue( + label: "com.datadoghq.ios-sdk-spans-upload", + target: .global(qos: .utility) + ) + self.upload = Upload( + storage: self.storage, + configuration: configuration, + performance: performance, + mobileDevice: mobileDevice, + httpClient: httpClient, + tracesUploadURLProvider: tracesUploadURLProvider, + networkConnectionInfoProvider: networkConnectionInfoProvider, + uploadQueue: uploadQueue + ) + } +} diff --git a/Sources/Datadog/Tracing/UUIDs/TracingUUID.swift b/Sources/Datadog/Tracing/UUIDs/TracingUUID.swift new file mode 100644 index 0000000000..7941d7c7d3 --- /dev/null +++ b/Sources/Datadog/Tracing/UUIDs/TracingUUID.swift @@ -0,0 +1,20 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal struct TracingUUID: Equatable { + /// The unique integer (64-bit unsigned) ID of the trace containing this span. + /// - See also: [Datadog API Reference - Send Traces](https://docs.datadoghq.com/api/?lang=bash#send-traces) + let rawValue: UInt64 + + var toHexadecimalString: String { + return String(rawValue, radix: 16, uppercase: true) + } +} + +internal typealias TraceID = TracingUUID +internal typealias SpanID = TracingUUID diff --git a/Sources/Datadog/Tracing/UUIDs/TracingUUIDGenerator.swift b/Sources/Datadog/Tracing/UUIDs/TracingUUIDGenerator.swift new file mode 100644 index 0000000000..902b4373c5 --- /dev/null +++ b/Sources/Datadog/Tracing/UUIDs/TracingUUIDGenerator.swift @@ -0,0 +1,28 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal protocol TracingUUIDGenerator { + func generateUnique() -> TracingUUID +} + +internal struct DefaultTracingUUIDGenerator: TracingUUIDGenerator { + /// Describes the lower and upper boundary of tracing ID generation. + /// * Lower: starts with `1` as `0` is reserved for historical reason: 0 == "unset", ref: dd-trace-java:DDId.java. + /// * Upper: equals to `2 ^ 63 - 1` as some tracers can't handle the `2 ^ 64 -1` range, ref: dd-trace-java:DDId.java. + internal static let defaultGenerationRange = (1...UInt64.max >> 1) + + internal let range: ClosedRange + + init(range: ClosedRange = Self.defaultGenerationRange) { + self.range = range + } + + func generateUnique() -> TracingUUID { + return TracingUUID(rawValue: .random(in: range)) + } +} diff --git a/Sources/Datadog/Tracing/Utils/Casting.swift b/Sources/Datadog/Tracing/Utils/Casting.swift new file mode 100644 index 0000000000..d9796c0722 --- /dev/null +++ b/Sources/Datadog/Tracing/Utils/Casting.swift @@ -0,0 +1,11 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +// swiftlint:disable identifier_name +internal extension OTSpanContext { + var dd: DDSpanContext? { warnIfCannotCast(value: self) } +} +// swiftlint:enable identifier_name diff --git a/Sources/Datadog/Tracing/Utils/URLFilter.swift b/Sources/Datadog/Tracing/Utils/URLFilter.swift new file mode 100644 index 0000000000..58ab3ea392 --- /dev/null +++ b/Sources/Datadog/Tracing/Utils/URLFilter.swift @@ -0,0 +1,49 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +internal protocol URLFiltering { + func allows(_ url: URL?) -> Bool +} + +internal struct URLFilter: URLFiltering, Equatable { + private let excludedURLs: Set + private let inclusionRegex: String + + init(includedHosts: Set, excludedURLs: Set) { + self.inclusionRegex = Self.buildRegexString(from: includedHosts) + self.excludedURLs = excludedURLs + } + + func allows(_ url: URL?) -> Bool { + guard !excludes(url), + let host = url?.host else { + return false + } + let isIncluded = host.range(of: inclusionRegex, options: .regularExpression) != nil + return isIncluded + } + + private func excludes(_ url: URL?) -> Bool { + if let absoluteString = url?.absoluteString { + return excludedURLs.contains { + absoluteString.starts(with: $0) + } + } + return true + } + + /// matches hosts and their subdomains: example.com -> example.com, api.example.com, sub.example.com, etc. + private static func buildRegexString(from hosts: Set) -> String { + return hosts.map { + let escaped = NSRegularExpression.escapedPattern(for: $0) + /// pattern = "^(.*\\.)*tracedHost1|^(.*\\.)*tracedHost2|..." + return "^(.*\\.)*\(escaped)$" + } + .joined(separator: "|") + } +} diff --git a/Sources/Datadog/Tracing/Utils/Warnings.swift b/Sources/Datadog/Tracing/Utils/Warnings.swift new file mode 100644 index 0000000000..40f94c3016 --- /dev/null +++ b/Sources/Datadog/Tracing/Utils/Warnings.swift @@ -0,0 +1,26 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// Returns `true` if the warning was raised. `false` otherwise. +internal func warn(if condition: @autoclosure () -> Bool, message: String) -> Bool { + if condition() { + userLogger.warn(message) + return true + } else { + return false + } +} + +/// Returns `nil` if the warning was raised. `T` otherwise. +internal func warnIfCannotCast(value: Any) -> T? { + guard let castedValue = value as? T else { + userLogger.warn("๐Ÿ”ฅ Using \(type(of: value as Any)) while \(T.self) was expected.") + return nil + } + return castedValue +} diff --git a/Sources/Datadog/Utils/InternalLoggers.swift b/Sources/Datadog/Utils/InternalLoggers.swift index 936d89bafe..6a1fd4c70d 100644 --- a/Sources/Datadog/Utils/InternalLoggers.swift +++ b/Sources/Datadog/Utils/InternalLoggers.swift @@ -6,81 +6,99 @@ import Foundation +/// Necessary configuration to instantiate `developerLogger` and `userLogger`. +internal struct InternalLoggerConfiguration { + let applicationVersion: String + let environment: String + let userInfoProvider: UserInfoProvider + let networkConnectionInfoProvider: NetworkConnectionInfoProviderType + let carrierInfoProvider: CarrierInfoProviderType +} + /// Global SDK `Logger` using console output. /// This logger is meant for debugging purposes during SDK development, hence **it should print useful information to SDK developer**. /// It is only instantiated when `DD_SDK_DEVELOPMENT` compilation condition is set for `Datadog` target. /// Some information posted with `developerLogger` may be also passed to `userLogger` with `.debug()` level to help SDK users /// understand why the SDK is not operating. -internal var developerLogger = createSDKDeveloperLogger() +/// +/// This `Logger` may be instantited on above conditions as soon as the SDK is initialized. +internal var developerLogger: Logger? = nil /// Global SDK `Logger` using console output. /// This logger is meant for debugging purposes when using SDK, hence **it should print useful information to SDK user**. /// It is only used when `Datadog.verbosityLevel` value is set. /// Every information posted to user should be properly classified (most commonly `.debug()` or `.error()`) according to /// its context: does the message pop up due to user error or user's app environment error? or is it SDK error? -internal var userLogger = createSDKUserLogger() +/// +/// This no-op `Logger` gets replaced with working instance as soon as the SDK is initialized. +internal var userLogger = createNoOpSDKUserLogger() internal func createSDKDeveloperLogger( + configuration: InternalLoggerConfiguration, consolePrintFunction: @escaping (String) -> Void = { consolePrint($0) }, dateProvider: DateProvider = SystemDateProvider(), - timeFormatter: Formatter = LogConsoleOutput.shortTimeFormatter() + timeZone: TimeZone = .current ) -> Logger? { if CompilationConditions.isSDKCompiledForDevelopment == false { return nil } - guard let loggingFeature = LoggingFeature.instance else { - return nil - } - let consoleOutput = LogConsoleOutput( logBuilder: LogBuilder( - applicationVersion: loggingFeature.configuration.applicationVersion, - environment: loggingFeature.configuration.environment, + applicationVersion: configuration.applicationVersion, + environment: configuration.environment, serviceName: "sdk-developer", loggerName: "sdk-developer", - dateProvider: dateProvider, - userInfoProvider: loggingFeature.userInfoProvider, - networkConnectionInfoProvider: loggingFeature.networkConnectionInfoProvider, - carrierInfoProvider: loggingFeature.carrierInfoProvider + userInfoProvider: configuration.userInfoProvider, + networkConnectionInfoProvider: configuration.networkConnectionInfoProvider, + carrierInfoProvider: configuration.carrierInfoProvider ), format: .shortWith(prefix: "๐Ÿถ โ†’ "), - printingFunction: consolePrintFunction, - timeFormatter: timeFormatter + timeZone: timeZone, + printingFunction: consolePrintFunction ) - return Logger(logOutput: consoleOutput, identifier: "sdk-developer") + return Logger( + logOutput: consoleOutput, + dateProvider: dateProvider, + identifier: "sdk-developer" + ) +} + +internal func createNoOpSDKUserLogger() -> Logger { + return Logger( + logOutput: NoOpLogOutput(), + dateProvider: SystemDateProvider(), + identifier: "no-op" + ) } internal func createSDKUserLogger( + configuration: InternalLoggerConfiguration, consolePrintFunction: @escaping (String) -> Void = { consolePrint($0) }, dateProvider: DateProvider = SystemDateProvider(), - timeFormatter: Formatter = LogConsoleOutput.shortTimeFormatter() + timeZone: TimeZone = .current ) -> Logger { - guard let loggingFeature = LoggingFeature.instance else { - return Logger(logOutput: NoOpLogOutput(), identifier: "no-op") - } - let consoleOutput = LogConsoleOutput( logBuilder: LogBuilder( - applicationVersion: loggingFeature.configuration.applicationVersion, - environment: loggingFeature.configuration.environment, + applicationVersion: configuration.applicationVersion, + environment: configuration.environment, serviceName: "sdk-user", loggerName: "sdk-user", - dateProvider: dateProvider, - userInfoProvider: loggingFeature.userInfoProvider, - networkConnectionInfoProvider: loggingFeature.networkConnectionInfoProvider, - carrierInfoProvider: loggingFeature.carrierInfoProvider + userInfoProvider: configuration.userInfoProvider, + networkConnectionInfoProvider: configuration.networkConnectionInfoProvider, + carrierInfoProvider: configuration.carrierInfoProvider ), format: .shortWith(prefix: "[DATADOG SDK] ๐Ÿถ โ†’ "), - printingFunction: consolePrintFunction, - timeFormatter: timeFormatter + timeZone: timeZone, + printingFunction: consolePrintFunction ) return Logger( logOutput: ConditionalLogOutput(conditionedOutput: consoleOutput) { logLevel in logLevel.rawValue >= (Datadog.verbosityLevel?.rawValue ?? .max) }, + dateProvider: dateProvider, identifier: "sdk-user" ) } diff --git a/Sources/Datadog/Utils/SwiftExtensions.swift b/Sources/Datadog/Utils/SwiftExtensions.swift index 67147b37a6..5811a4080b 100644 --- a/Sources/Datadog/Utils/SwiftExtensions.swift +++ b/Sources/Datadog/Utils/SwiftExtensions.swift @@ -16,18 +16,31 @@ extension Optional { } } -// MARK: - Date +// MARK: - TimeInterval -extension Date { +extension TimeInterval { // NOTE: RUMM-182 counterpart of currentTimeMillis in Java // https://docs.oracle.com/javase/7/docs/api/java/lang/System.html#currentTimeMillis() - var currentTimeMillis: UInt64 { + var toMilliseconds: UInt64 { do { - let miliseconds = self.timeIntervalSince1970 * 1_000 + let miliseconds = self * 1_000 return try UInt64(withReportingOverflow: miliseconds) } catch { - userLogger.error("๐Ÿ”ฅ Failed to convert timestamp: \(error)") - developerLogger?.error("๐Ÿ”ฅ Failed to convert timestamp: \(error)") + userLogger.error("๐Ÿ”ฅ Failed to convert `\(self)` time interval in milliseconds: \(error)") + developerLogger?.error("๐Ÿ”ฅ Failed to convert `\(self)` time interval in milliseconds: \(error)") + return UInt64.max + } + } + + /// Returns `TimeInterval` represented in nanoseconds. + /// Note: as `TimeInterval` yields sub-millisecond precision the nanoseconds precission will be lost. + var toNanoseconds: UInt64 { + do { + let nanoseconds = self * 1_000_000_000 + return try UInt64(withReportingOverflow: nanoseconds) + } catch { + userLogger.error("๐Ÿ”ฅ Failed to convert `\(self)` time interval in nanoseconds: \(error)") + developerLogger?.error("๐Ÿ”ฅ Failed to convert `\(self)` time interval in nanoseconds: \(error)") return UInt64.max } } diff --git a/Sources/Datadog/Versioning.swift b/Sources/Datadog/Versioning.swift new file mode 100644 index 0000000000..52f78f582e --- /dev/null +++ b/Sources/Datadog/Versioning.swift @@ -0,0 +1,3 @@ +// GENERATED FILE: Do not edit directly + +internal let sdkVersion = "1.3.0-beta3" diff --git a/Sources/DatadogObjc/DatadogConfiguration+objc.swift b/Sources/DatadogObjc/DatadogConfiguration+objc.swift index bac1c78534..352a4b5d3e 100644 --- a/Sources/DatadogObjc/DatadogConfiguration+objc.swift +++ b/Sources/DatadogObjc/DatadogConfiguration+objc.swift @@ -22,6 +22,21 @@ public class DDLogsEndpoint: NSObject { public static func custom(url: String) -> DDLogsEndpoint { .init(sdkEndpoint: .custom(url: url)) } } +@objcMembers +public class DDTracesEndpoint: NSObject { + internal let sdkEndpoint: Datadog.Configuration.TracesEndpoint + + internal init(sdkEndpoint: Datadog.Configuration.TracesEndpoint) { + self.sdkEndpoint = sdkEndpoint + } + + // MARK: - Public + + public static func eu() -> DDTracesEndpoint { .init(sdkEndpoint: .eu) } + public static func us() -> DDTracesEndpoint { .init(sdkEndpoint: .us) } + public static func custom(url: String) -> DDTracesEndpoint { .init(sdkEndpoint: .custom(url: url)) } +} + @objcMembers public class DDConfiguration: NSObject { internal let sdkConfiguration: Datadog.Configuration @@ -49,8 +64,29 @@ public class DDConfigurationBuilder: NSObject { // MARK: - Public + @available(*, deprecated, renamed: "set(logsEndpoint:)") public func set(endpoint: DDLogsEndpoint) { - _ = sdkBuilder.set(logsEndpoint: endpoint.sdkEndpoint) + set(logsEndpoint: endpoint) + } + + public func enableLogging(_ enabled: Bool) { + _ = sdkBuilder.enableLogging(enabled) + } + + public func enableTracing(_ enabled: Bool) { + _ = sdkBuilder.enableTracing(enabled) + } + + public func set(logsEndpoint: DDLogsEndpoint) { + _ = sdkBuilder.set(logsEndpoint: logsEndpoint.sdkEndpoint) + } + + public func set(tracesEndpoint: DDTracesEndpoint) { + _ = sdkBuilder.set(tracesEndpoint: tracesEndpoint.sdkEndpoint) + } + + public func set(tracedHosts: Set) { + _ = sdkBuilder.set(tracedHosts: tracedHosts) } public func set(serviceName: String) { diff --git a/Sources/DatadogObjc/OpenTracing/OTGlobal+objc.swift b/Sources/DatadogObjc/OpenTracing/OTGlobal+objc.swift new file mode 100644 index 0000000000..d18b211946 --- /dev/null +++ b/Sources/DatadogObjc/OpenTracing/OTGlobal+objc.swift @@ -0,0 +1,24 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Datadog + +@objcMembers +@objc(OTGlobal) +public class OTGlobal: NSObject { + public static func initSharedTracer(_ tracer: OTTracer) { + guard let ddtracer = tracer.dd else { + return + } + + sharedTracer = ddtracer + + // We must also set the Swift `sharedTracer` as it's used internally by auto-instrumentation feature. + Global.sharedTracer = ddtracer.swiftTracer + } + + public internal(set) static var sharedTracer: OTTracer = noopTracer +} diff --git a/Sources/DatadogObjc/OpenTracing/OTNoop.swift b/Sources/DatadogObjc/OpenTracing/OTNoop.swift new file mode 100644 index 0000000000..29f0188f07 --- /dev/null +++ b/Sources/DatadogObjc/OpenTracing/OTNoop.swift @@ -0,0 +1,38 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +internal let noopTracer: OTTracer = DDNoopTracer() +internal let noopSpan: OTSpan = DDNoopSpan() +internal let noopSpanContext: OTSpanContext = DDNoopSpanContext() + +private class DDNoopTracer: OTTracer { + func startSpan(_ operationName: String) -> OTSpan { noopSpan } + func startSpan(_ operationName: String, tags: NSDictionary?) -> OTSpan { noopSpan } + func startSpan(_ operationName: String, childOf parent: OTSpanContext?) -> OTSpan { noopSpan } + func startSpan(_ operationName: String, childOf parent: OTSpanContext?, tags: NSDictionary?) -> OTSpan { noopSpan } + func startSpan(_ operationName: String, childOf parent: OTSpanContext?, tags: NSDictionary?, startTime: Date?) -> OTSpan { noopSpan } + func inject(_ spanContext: OTSpanContext, format: String, carrier: Any) throws {} + func extractWithFormat(_ format: String, carrier: Any) throws {} +} + +private class DDNoopSpan: OTSpan { + var context: OTSpanContext { noopSpanContext } + var tracer: OTTracer { noopTracer } + func setOperationName(_ operationName: String) {} + func setTag(_ key: String, value: NSString) {} + func setTag(_ key: String, numberValue: NSNumber) {} + func setTag(_ key: String, boolValue: Bool) {} + func log(_ fields: [String: NSObject]) {} + func log(_ fields: [String: NSObject], timestamp: Date?) {} + func setBaggageItem(_ key: String, value: String) -> OTSpan { self } + func getBaggageItem(_ key: String) -> String? { nil } + func finish() {} + func finishWithTime(_ finishTime: Date?) {} +} + +private class DDNoopSpanContext: OTSpanContext { + func forEachBaggageItem(_ callback: (String, String) -> Bool) {} +} diff --git a/Sources/DatadogObjc/OpenTracing/OTSpan+objc.swift b/Sources/DatadogObjc/OpenTracing/OTSpan+objc.swift new file mode 100644 index 0000000000..8f109f85a4 --- /dev/null +++ b/Sources/DatadogObjc/OpenTracing/OTSpan+objc.swift @@ -0,0 +1,27 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +@objc +/// Corresponds to: https://github.com/opentracing/opentracing-objc/blob/master/Pod/Classes/OTSpan.h +public protocol OTSpan { + var context: OTSpanContext { get } + var tracer: OTTracer { get } + + func setOperationName(_ operationName: String) + + func setTag(_ key: String, value: NSString) + func setTag(_ key: String, numberValue: NSNumber) + func setTag(_ key: String, boolValue: Bool) + + func log(_ fields: [String: NSObject]) + func log(_ fields: [String: NSObject], timestamp: Date?) + + func setBaggageItem(_ key: String, value: String) -> OTSpan + func getBaggageItem(_ key: String) -> String? + + func finish() + func finishWithTime(_ finishTime: Date?) +} diff --git a/Sources/DatadogObjc/OpenTracing/OTSpanContext+objc.swift b/Sources/DatadogObjc/OpenTracing/OTSpanContext+objc.swift new file mode 100644 index 0000000000..3a28e32efb --- /dev/null +++ b/Sources/DatadogObjc/OpenTracing/OTSpanContext+objc.swift @@ -0,0 +1,11 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +@objc +/// Corresponds to: https://github.com/opentracing/opentracing-objc/blob/master/Pod/Classes/OTSpanContext.h +public protocol OTSpanContext { + func forEachBaggageItem(_ callback: (_ key: String, _ value: String) -> Bool) +} diff --git a/Sources/DatadogObjc/OpenTracing/OTTracer+objc.swift b/Sources/DatadogObjc/OpenTracing/OTTracer+objc.swift new file mode 100644 index 0000000000..bcd9a8e897 --- /dev/null +++ b/Sources/DatadogObjc/OpenTracing/OTTracer+objc.swift @@ -0,0 +1,19 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +public let OTFormatHTTPHeaders = "OTFormatHTTPHeaders" + +@objc +/// Corresponds to: https://github.com/opentracing/opentracing-objc/blob/master/Pod/Classes/OTTracer.h +public protocol OTTracer { + func startSpan(_ operationName: String) -> OTSpan + func startSpan(_ operationName: String, tags: NSDictionary?) -> OTSpan + func startSpan(_ operationName: String, childOf parent: OTSpanContext?) -> OTSpan + func startSpan(_ operationName: String, childOf parent: OTSpanContext?, tags: NSDictionary?) -> OTSpan + func startSpan(_ operationName: String, childOf parent: OTSpanContext?, tags: NSDictionary?, startTime: Date?) -> OTSpan + func inject(_ spanContext: OTSpanContext, format: String, carrier: Any) throws + func extractWithFormat(_ format: String, carrier: Any) throws +} diff --git a/Sources/DatadogObjc/Tracer+objc.swift b/Sources/DatadogObjc/Tracer+objc.swift new file mode 100644 index 0000000000..cafe8ce2a3 --- /dev/null +++ b/Sources/DatadogObjc/Tracer+objc.swift @@ -0,0 +1,142 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import class Datadog.Tracer +import protocol Datadog.OTTracer +import struct Datadog.OTReference +import class Datadog.HTTPHeadersWriter + +@objcMembers +public class DDTracer: DatadogObjc.OTTracer { + public static func initialize(configuration: DDTracerConfiguration) -> DatadogObjc.OTTracer { + return DDTracer(configuration: configuration) + } + + // MARK: - Internal + + internal let swiftTracer: Datadog.OTTracer + + internal convenience init(configuration: DDTracerConfiguration) { + self.init( + swiftTracer: Datadog.Tracer.initialize( + configuration: configuration.swiftConfiguration + ) + ) + } + + internal init(swiftTracer: Datadog.OTTracer) { + self.swiftTracer = swiftTracer + } + + // MARK: - OTTracer + + public func startSpan(_ operationName: String) -> OTSpan { + return DDSpanObjc( + objcTracer: self, + swiftSpan: swiftTracer.startSpan(operationName: operationName) + ) + } + + public func startSpan(_ operationName: String, tags: NSDictionary?) -> OTSpan { + return DDSpanObjc( + objcTracer: self, + swiftSpan: swiftTracer.startSpan( + operationName: operationName, + tags: tags.flatMap { castTagsToSwift($0) } + ) + ) + } + + public func startSpan(_ operationName: String, childOf parent: OTSpanContext?) -> OTSpan { + let ddspanContext = parent?.dd + return DDSpanObjc( + objcTracer: self, + swiftSpan: swiftTracer.startSpan( + operationName: operationName, + childOf: ddspanContext?.swiftSpanContext + ) + ) + } + + public func startSpan( + _ operationName: String, + childOf parent: OTSpanContext?, + tags: NSDictionary? + ) -> OTSpan { + let ddspanContext = parent?.dd + return DDSpanObjc( + objcTracer: self, + swiftSpan: swiftTracer.startSpan( + operationName: operationName, + childOf: ddspanContext?.swiftSpanContext, + tags: tags.flatMap { castTagsToSwift($0) } + ) + ) + } + + public func startSpan( + _ operationName: String, + childOf parent: OTSpanContext?, + tags: NSDictionary?, + startTime: Date? + ) -> OTSpan { + let ddspanContext = parent?.dd + return DDSpanObjc( + objcTracer: self, + swiftSpan: swiftTracer.startSpan( + operationName: operationName, + childOf: ddspanContext?.swiftSpanContext, + tags: tags.flatMap { castTagsToSwift($0) }, + startTime: startTime + ) + ) + } + + public func inject(_ spanContext: OTSpanContext, format: String, carrier: Any) throws { + guard format == OTFormatHTTPHeaders, let objcWriter = carrier as? DDHTTPHeadersWriter else { + let error = NSError( + domain: "DDTracer", + code: 0, + userInfo: [ + NSLocalizedDescriptionKey: "Trying to inject `OTSpanContext` using wrong format or carrier.", + NSLocalizedRecoverySuggestionErrorKey: "Use `DDHTTPHeadersWriter` carrier with `OTFormatHTTPHeaders` format." + ] + ) + throw error + } + guard let ddspanContext = spanContext.dd else { + return + } + swiftTracer.inject( + spanContext: ddspanContext.swiftSpanContext, + writer: objcWriter.swiftHTTPHeadersWriter + ) + } + + public func extractWithFormat(_ format: String, carrier: Any) throws { + // TODO: RUMM-385 - we don't need to support it now + } + + // MARK: - Private + + private func castTagsToSwift(_ tags: NSDictionary) -> [String: Encodable] { + guard let dictionary = tags as? [String: Any] else { + return [:] + } + + return dictionary.mapValues { objcTagValue in + // As underlying `Datadog.JSONStringEncodableValue` provides special handling for `String` and `URL` + // when converting those `Encodables` to lossless JSON string representation, we have to cast them directly: + if let stringValue = objcTagValue as? String { + return stringValue + } else if let urlValue = objcTagValue as? URL { + return urlValue + } else { + return AnyEncodable(objcTagValue) + } + } + } +} diff --git a/Sources/DatadogObjc/TracerConfiguration+objc.swift b/Sources/DatadogObjc/TracerConfiguration+objc.swift new file mode 100644 index 0000000000..cafc0fc7d9 --- /dev/null +++ b/Sources/DatadogObjc/TracerConfiguration+objc.swift @@ -0,0 +1,24 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Datadog + +@objcMembers +public class DDTracerConfiguration: NSObject { + internal var swiftConfiguration = Tracer.Configuration() + + override public init() {} + + // MARK: - Public + + public func set(serviceName: String) { + swiftConfiguration.serviceName = serviceName + } + + public func sendNetworkInfo(_ enabled: Bool) { + swiftConfiguration.sendNetworkInfo = enabled + } +} diff --git a/Sources/DatadogObjc/Tracing/DDSpan+objc.swift b/Sources/DatadogObjc/Tracing/DDSpan+objc.swift new file mode 100644 index 0000000000..15a1ab1bf2 --- /dev/null +++ b/Sources/DatadogObjc/Tracing/DDSpan+objc.swift @@ -0,0 +1,77 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import protocol Datadog.OTSpan + +internal class DDSpanObjc: NSObject, DatadogObjc.OTSpan { + let swiftSpan: Datadog.OTSpan + + init(objcTracer: DatadogObjc.OTTracer, swiftSpan: Datadog.OTSpan) { + self.tracer = objcTracer + self.context = DDSpanContextObjc(swiftSpanContext: swiftSpan.context) + self.swiftSpan = swiftSpan + } + + // MARK: - Open Tracing Objective-C Interface + + let tracer: OTTracer + + let context: OTSpanContext + + func setOperationName(_ operationName: String) { + swiftSpan.setOperationName(operationName) + } + + func setTag(_ key: String, value: NSString) { + swiftSpan.setTag(key: key, value: value as String) + } + + func setTag(_ key: String, numberValue: NSNumber) { + swiftSpan.setTag(key: key, value: AnyEncodable(numberValue)) + } + + func setTag(_ key: String, boolValue: Bool) { + swiftSpan.setTag(key: key, value: boolValue) + } + + func log(_ fields: [String: NSObject]) { + self.log(fields, timestamp: Date()) + } + + func log(_ fields: [String: NSObject], timestamp: Date?) { + if let timestamp = timestamp { + swiftSpan.log( + fields: fields.mapValues { AnyEncodable($0) }, + timestamp: timestamp + ) + } else { + swiftSpan.log( + fields: fields.mapValues { AnyEncodable($0) } + ) + } + } + + func setBaggageItem(_ key: String, value: String) -> OTSpan { + swiftSpan.setBaggageItem(key: key, value: value) + return self + } + + func getBaggageItem(_ key: String) -> String? { + return swiftSpan.baggageItem(withKey: key) + } + + func finish() { + swiftSpan.finish() + } + + func finishWithTime(_ finishTime: Date?) { + if let finishTime = finishTime { + swiftSpan.finish(at: finishTime) + } else { + swiftSpan.finish() + } + } +} diff --git a/Sources/DatadogObjc/Tracing/DDSpanContext+objc.swift b/Sources/DatadogObjc/Tracing/DDSpanContext+objc.swift new file mode 100644 index 0000000000..16478c40e7 --- /dev/null +++ b/Sources/DatadogObjc/Tracing/DDSpanContext+objc.swift @@ -0,0 +1,23 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import protocol Datadog.OTSpanContext + +internal class DDSpanContextObjc: NSObject, OTSpanContext { + let swiftSpanContext: Datadog.OTSpanContext + + internal init(swiftSpanContext: Datadog.OTSpanContext) { + self.swiftSpanContext = swiftSpanContext + } + + // MARK: - Open Tracing Objective-C Interface + + func forEachBaggageItem(_ callback: (_ key: String, _ value: String) -> Bool) { + // Corresponds to: + // - (void)forEachBaggageItem:(BOOL (^) (NSString* key, NSString* value))callback; + swiftSpanContext.forEachBaggageItem(callback: callback) + } +} diff --git a/Sources/DatadogObjc/Tracing/Propagation/HTTPHeadersWriter+objc.swift b/Sources/DatadogObjc/Tracing/Propagation/HTTPHeadersWriter+objc.swift new file mode 100644 index 0000000000..f75f8b8d0b --- /dev/null +++ b/Sources/DatadogObjc/Tracing/Propagation/HTTPHeadersWriter+objc.swift @@ -0,0 +1,14 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import class Datadog.HTTPHeadersWriter + +@objcMembers +public class DDHTTPHeadersWriter: NSObject { + let swiftHTTPHeadersWriter = HTTPHeadersWriter() + + override public init() {} +} diff --git a/Sources/DatadogObjc/Tracing/Utils/Casting.swift b/Sources/DatadogObjc/Tracing/Utils/Casting.swift new file mode 100644 index 0000000000..51905539de --- /dev/null +++ b/Sources/DatadogObjc/Tracing/Utils/Casting.swift @@ -0,0 +1,28 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Datadog + +// swiftlint:disable identifier_name +internal extension DatadogObjc.OTTracer { + var dd: DDTracer? { warnIfCannotCast(value: self) } +} +internal extension DatadogObjc.OTSpan { + var dd: DDSpanObjc? { warnIfCannotCast(value: self) } +} +internal extension DatadogObjc.OTSpanContext { + var dd: DDSpanContextObjc? { warnIfCannotCast(value: self) } +} +// swiftlint:enable identifier_name + +/// Returns `nil` if the warning was raised. `T` otherwise. +private func warnIfCannotCast(value: Any) -> T? { + guard let castedValue = value as? T else { + print("๐Ÿ”ฅ Using \(type(of: value as Any)) while \(T.self) was expected.") + return nil + } + return castedValue +} diff --git a/Tests/DatadogBenchmarkTests/DataUploaderBenchmarkTests.swift b/Tests/DatadogBenchmarkTests/DataUploaderBenchmarkTests.swift new file mode 100644 index 0000000000..ccde9eeaea --- /dev/null +++ b/Tests/DatadogBenchmarkTests/DataUploaderBenchmarkTests.swift @@ -0,0 +1,89 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +import HTTPServerMock +@testable import Datadog + +struct ServerConnectionError: Error { + let description: String +} + +@available(iOS 13.0, *) +class DataUploaderBenchmarkTests: XCTestCase { + private(set) var server: ServerMock! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUpWithError() throws { + try super.setUpWithError() + temporaryDirectory.create() + server = try connectToServer() + } + + override func tearDownWithError() throws { + server = nil + temporaryDirectory.delete() + try super.tearDownWithError() + } + + /// NOTE: In RUMM-610 we noticed that due to internal `NSCache` used by the `URLSession` + /// requests memory was leaked after upload. This benchmark ensures that uploading data with + /// `DataUploader` leaves no memory footprint (the memory peak after upload is less or equal `0kB`). + func testUploadingDataToServer_leavesNoMemoryFootprint() throws { + let dataUploader = DataUploader( + urlProvider: mockUniqueUploadURLProvider(), + httpClient: HTTPClient(), + httpHeaders: HTTPHeaders(headers: []) + ) + + // `measure` runs 5 iterations + measure(metrics: [XCTMemoryMetric()]) { + // in each, 10 requests are done: + (0..<10).forEach { _ in + let data = Data(repeating: 0x41, count: 10 * 1_024 * 1_024) + _ = dataUploader.upload(data: data) + } + // After all, the baseline asserts `0kB` or less grow in Physical Memory. + // This makes sure that no request data is leaked (e.g. due to internal caching). + } + } + + /// Creates the `UploadURLProvider` giving an unique URL for each upload. + /// URLs are differentiated by the value of `batch` query item. + private func mockUniqueUploadURLProvider() -> UploadURLProvider { + struct BatchTimeProvider: DateProvider { + func currentDate() -> Date { + Date(timeIntervalSince1970: .random(in: 0..<1_000_000)) + } + } + return UploadURLProvider( + urlWithClientToken: server.obtainUniqueRecordingSession().recordingURL, + queryItemProviders: [.batchTime(using: BatchTimeProvider())] + ) + } + + // MARK: - `HTTPServerMock` connection + + func connectToServer() throws -> ServerMock { + let testsBundle = Bundle(for: DataUploaderBenchmarkTests.self) + guard let serverAddress = testsBundle.object(forInfoDictionaryKey: "MockServerAddress") as? String else { + throw ServerConnectionError(description: "Cannot obtain `MockServerAddress` from `Info.plist`") + } + + guard let serverURL = URL(string: "http://\(serverAddress)") else { + throw ServerConnectionError(description: "`MockServerAddress` obtained from `Info.plist` is invalid.") + } + + let serverProcessRunner = ServerProcessRunner(serverURL: serverURL) + guard let serverProcess = serverProcessRunner.waitUntilServerIsReachable() else { + throw ServerConnectionError(description: "Cannot connect to server. Is server running properly on \(serverURL.absoluteString)?") + } + + print("๐ŸŒ Connected to mock server on \(serverURL.absoluteString)") + + let connectedServer = ServerMock(serverProcess: serverProcess) + return connectedServer + } +} diff --git a/Tests/DatadogIntegrationTests/Benchmark/LoggingBenchmarkTests.swift b/Tests/DatadogBenchmarkTests/LoggingBenchmarkTests.swift similarity index 95% rename from Tests/DatadogIntegrationTests/Benchmark/LoggingBenchmarkTests.swift rename to Tests/DatadogBenchmarkTests/LoggingBenchmarkTests.swift index 50dfcad92c..64a341c4d6 100644 --- a/Tests/DatadogIntegrationTests/Benchmark/LoggingBenchmarkTests.swift +++ b/Tests/DatadogBenchmarkTests/LoggingBenchmarkTests.swift @@ -7,7 +7,7 @@ import Datadog import XCTest -class LoggingBenchmarkTests: BenchmarkTests { +class LoggingBenchmarkTests: XCTestCase { private let message = "foobar-message" func testCreatingOneLog() { diff --git a/Tests/DatadogIntegrationTests/Benchmark/LoggingIOBenchmarkTests.swift b/Tests/DatadogBenchmarkTests/LoggingStorageBenchmarkTests.swift similarity index 80% rename from Tests/DatadogIntegrationTests/Benchmark/LoggingIOBenchmarkTests.swift rename to Tests/DatadogBenchmarkTests/LoggingStorageBenchmarkTests.swift index cee5baaffb..e3d36e85ae 100644 --- a/Tests/DatadogIntegrationTests/Benchmark/LoggingIOBenchmarkTests.swift +++ b/Tests/DatadogBenchmarkTests/LoggingStorageBenchmarkTests.swift @@ -7,7 +7,7 @@ import XCTest @testable import Datadog -class LoggingIOBenchmarkTests: XCTestCase { +class LoggingStorageBenchmarkTests: XCTestCase { // swiftlint:disable implicitly_unwrapped_optional private var queue: DispatchQueue! private var directory: Directory! @@ -15,22 +15,22 @@ class LoggingIOBenchmarkTests: XCTestCase { private var reader: FileReader! // swiftlint:enable implicitly_unwrapped_optional - override func setUp() { - super.setUp() + override func setUpWithError() throws { + try super.setUpWithError() self.queue = DispatchQueue(label: "com.datadoghq.benchmark-logs-io", target: .global(qos: .utility)) - self.directory = try! Directory(withSubdirectoryPath: "logging-benchmark") + self.directory = try Directory(withSubdirectoryPath: "logging-benchmark") - let orchestrator = FilesOrchestrator( + let storage = LoggingFeature.Storage( directory: directory, - writeConditions: WritableFileConditions(performance: .default), - readConditions: ReadableFileConditions(performance: .default), - dateProvider: SystemDateProvider() + performance: .default, + dateProvider: SystemDateProvider(), + readWriteQueue: queue ) - self.writer = FileWriter(orchestrator: orchestrator, queue: queue) - self.reader = FileReader(orchestrator: orchestrator, queue: queue) + self.writer = storage.writer + self.reader = storage.reader - XCTAssertTrue(try! directory.files().count == 0) + XCTAssertTrue(try directory.files().count == 0) } override func tearDown() { @@ -96,7 +96,10 @@ class LoggingIOBenchmarkTests: XCTestCase { isConstrained: false ), mobileCarrierInfo: nil, - attributes: ["attribute": EncodableValue("value")], + attributes: LogAttributes( + userAttributes: ["user.attribute": "value"], + internalAttributes: ["internal.attribute": "value"] + ), tags: ["tag:value"] ) } diff --git a/Tests/DatadogBenchmarkTests/TracingBenchmarkTests.swift b/Tests/DatadogBenchmarkTests/TracingBenchmarkTests.swift new file mode 100644 index 0000000000..19637adcff --- /dev/null +++ b/Tests/DatadogBenchmarkTests/TracingBenchmarkTests.swift @@ -0,0 +1,40 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Datadog +import XCTest + +class TracingBenchmarkTests: XCTestCase { + private let operationName = "foobar-span" + let tracer = Global.sharedTracer + + func testCreatingAndEndingOneSpan() { + measure { + let testSpan = tracer.startSpan(operationName: operationName) + testSpan.finish() + } + } + + func testCreatingOneSpanWithBaggageItems() { + measure { + let testSpan = tracer.startSpan(operationName: operationName) + (0..<16).forEach { index in + testSpan.setBaggageItem(key: "a\(index)", value: "v\(index)") + } + testSpan.finish() + } + } + + func testCreatingOneSpanWithTags() { + measure { + let testSpan = tracer.startSpan(operationName: operationName) + (0..<8).forEach { index in + testSpan.setTag(key: "t\(index)", value: "v\(index)") + } + testSpan.finish() + } + } +} diff --git a/Tests/DatadogBenchmarkTests/TracingStorageBenchmarkTests.swift b/Tests/DatadogBenchmarkTests/TracingStorageBenchmarkTests.swift new file mode 100644 index 0000000000..9c182854f2 --- /dev/null +++ b/Tests/DatadogBenchmarkTests/TracingStorageBenchmarkTests.swift @@ -0,0 +1,107 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class TracingStorageBenchmarkTests: XCTestCase { + // swiftlint:disable implicitly_unwrapped_optional + private var queue: DispatchQueue! + private var directory: Directory! + private var writer: FileWriter! + private var reader: FileReader! + // swiftlint:enable implicitly_unwrapped_optional + + override func setUpWithError() throws { + try super.setUpWithError() + self.queue = DispatchQueue(label: "com.datadoghq.benchmark-traces-io", target: .global(qos: .utility)) + self.directory = try Directory(withSubdirectoryPath: "tracing-benchmark") + + let storage = TracingFeature.Storage( + directory: directory, + performance: .default, + dateProvider: SystemDateProvider(), + readWriteQueue: queue + ) + + self.writer = storage.writer + self.reader = storage.reader + + XCTAssertTrue(try directory.files().count == 0) + } + + override func tearDown() { + self.directory.delete() + queue = nil + directory = nil + writer = nil + reader = nil + super.tearDown() + } + + func testWrittingSpansOnDisc() throws { + let log = createRandomizedSpan() + + measure { + writer.write(value: log) + queue.sync {} // wait to complete async write + } + } + + func testReadingSpansFromDisc() throws { + while try directory.files().count < 10 { // `measureMetrics {}` is fired 10 times so 10 batch files are required + writer.write(value: createRandomizedSpan()) + queue.sync {} // wait to complete async write + } + + // Wait enough time for `reader` to accept the youngest batch file + Thread.sleep(forTimeInterval: PerformancePreset.default.minFileAgeForRead + 0.1) + + measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) { + self.startMeasuring() + let batch = reader.readNextBatch() + self.stopMeasuring() + + XCTAssertNotNil(batch, "Not enough batch files were created for this benchmark.") + + if let batch = batch { + reader.markBatchAsRead(batch) + } + } + } + + // MARK: - Helpers + + private func createRandomizedSpan() -> Span { + let tracingUUIDGenerator = DefaultTracingUUIDGenerator() + return Span( + traceID: tracingUUIDGenerator.generateUnique(), + spanID: tracingUUIDGenerator.generateUnique(), + parentID: nil, + operationName: "span \(Int.random(in: 0..<100))", + serviceName: "service-name", + resource: "benchmarks", + startTime: Date(), + duration: Double.random(in: 0.0..<1.0), + isError: false, + tracerVersion: "0.0.0", + applicationVersion: "0.0.0", + networkConnectionInfo: NetworkConnectionInfo( + reachability: .yes, + availableInterfaces: [.cellular], + supportsIPv4: true, + supportsIPv6: true, + isExpensive: false, + isConstrained: false + ), + mobileCarrierInfo: nil, + userInfo: .init(id: "abc-123", name: "foo", email: "foo@bar.com"), + tags: [ + "tag": JSONStringEncodableValue("value", encodedUsing: JSONEncoder()) + ] + ) + } +} diff --git a/Tests/DatadogIntegrationTests/Benchmark/BenchmarkTests.swift b/Tests/DatadogIntegrationTests/Benchmark/BenchmarkTests.swift deleted file mode 100644 index 319ffdd859..0000000000 --- a/Tests/DatadogIntegrationTests/Benchmark/BenchmarkTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -/* -* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. -* This product includes software developed at Datadog (https://www.datadoghq.com/). -* Copyright 2019-2020 Datadog, Inc. -*/ - -import XCTest -import HTTPServerMock -@testable import Datadog - -/// Shared server instance for all test cases. -private(set) var server: ServerMock! // swiftlint:disable:this implicitly_unwrapped_optional -/// Shared server session for all test cases. -private(set) var serverSession: ServerSession! // swiftlint:disable:this implicitly_unwrapped_optional - -/// Base class providing mock server instrumentation. -class BenchmarkTests: XCTestCase { - override class func setUp() { - super.setUp() - if server == nil { server = try! setUpMockServerConnection() } - if serverSession == nil { serverSession = server.obtainUniqueRecordingSession() } - } - - override func setUp() { - super.setUp() - Datadog.initialize( - appContext: Datadog.AppContext(mainBundle: Bundle.main), - configuration: Datadog.Configuration - .builderUsing(clientToken: "client-token", environment: "benchmarks") - .set(logsEndpoint: .custom(url: serverSession.recordingURL.absoluteString)) - .build() - ) - } - - override func tearDown() { - try! Datadog.deinitializeOrThrow() - super.tearDown() - } - - // MARK: - `HTTPServerMock` connection - - private static func setUpMockServerConnection() throws -> ServerMock { - let testsBundle = Bundle(for: BenchmarkTests.self) - guard let serverAddress = testsBundle.object(forInfoDictionaryKey: "MockServerAddress") as? String else { - throw ServerConnectionError(description: "Cannot obtain `MockServerAddress` from `Info.plist`") - } - - guard let serverURL = URL(string: "http://\(serverAddress)") else { - throw ServerConnectionError(description: "`MockServerAddress` obtained from `Info.plist` is invalid.") - } - - let serverProcessRunner = ServerProcessRunner(serverURL: serverURL) - guard let serverProcess = serverProcessRunner.waitUntilServerIsReachable() else { - throw ServerConnectionError(description: "Cannot connect to server. Is server running properly on \(serverURL.absoluteString)?") - } - - print("๐ŸŒ Connected to mock server on \(serverURL.absoluteString)") - - let connectedServer = ServerMock(serverProcess: serverProcess) - return connectedServer - } -} diff --git a/Tests/DatadogIntegrationTests/Integration/IntegrationTests.swift b/Tests/DatadogIntegrationTests/IntegrationTests.swift similarity index 89% rename from Tests/DatadogIntegrationTests/Integration/IntegrationTests.swift rename to Tests/DatadogIntegrationTests/IntegrationTests.swift index 868abb75af..4beefcfcf7 100644 --- a/Tests/DatadogIntegrationTests/Integration/IntegrationTests.swift +++ b/Tests/DatadogIntegrationTests/IntegrationTests.swift @@ -15,15 +15,15 @@ struct ServerConnectionError: Error { class IntegrationTests: XCTestCase { private(set) var server: ServerMock! // swiftlint:disable:this implicitly_unwrapped_optional - override func setUp() { - super.setUp() - server = try! connectToServer() - logsDirectory.delete() + override func setUpWithError() throws { + try super.setUp() + server = try connectToServer() } - override func tearDown() { + override func tearDownWithError() throws { server = nil - super.tearDown() + + try super.tearDownWithError() } // MARK: - `HTTPServerMock` connection diff --git a/Tests/DatadogIntegrationTests/Integration/LoggingIntegrationTests.swift b/Tests/DatadogIntegrationTests/LoggingIntegrationTests.swift similarity index 51% rename from Tests/DatadogIntegrationTests/Integration/LoggingIntegrationTests.swift rename to Tests/DatadogIntegrationTests/LoggingIntegrationTests.swift index 4382aec676..1c2acdd245 100644 --- a/Tests/DatadogIntegrationTests/Integration/LoggingIntegrationTests.swift +++ b/Tests/DatadogIntegrationTests/LoggingIntegrationTests.swift @@ -4,82 +4,60 @@ * Copyright 2019-2020 Datadog, Inc. */ -import Datadog import HTTPServerMock import XCTest +// swiftlint:disable trailing_closure class LoggingIntegrationTests: IntegrationTests { private struct Constants { /// Time needed for logs to be uploaded to mock server. static let logsDeliveryTime: TimeInterval = 30 } - // swiftlint:disable trailing_closure - func testLogsAreUploadedToServer() throws { + func testLaunchTheAppAndSendLogs() throws { let serverSession = server.obtainUniqueRecordingSession() - // Initialize SDK - Datadog.initialize( - appContext: .init(mainBundle: Bundle.init(for: type(of: self))), - configuration: Datadog.Configuration.builderUsing(clientToken: "client-token", environment: "integration") - .set(logsEndpoint: .custom(url: serverSession.recordingURL.absoluteString)) - .build() - ) - - // Create logger - let logger = Logger.builder - .set(serviceName: "service-name") - .set(loggerName: "logger-name") - .sendNetworkInfo(true) - .build() - - // Send logs - logger.addTag(withKey: "tag1", value: "tag-value") - logger.add(tag: "tag2") - - logger.addAttribute(forKey: "logger-attribute1", value: "string value") - logger.addAttribute(forKey: "logger-attribute2", value: 1_000) - - logger.debug("debug message", attributes: ["attribute": "value"]) - logger.info("info message", attributes: ["attribute": "value"]) - logger.notice("notice message", attributes: ["attribute": "value"]) - logger.warn("warn message", attributes: ["attribute": "value"]) - logger.error("error message", attributes: ["attribute": "value"]) - logger.critical("critical message", attributes: ["attribute": "value"]) - - // Wait for delivery - Thread.sleep(forTimeInterval: Constants.logsDeliveryTime) - - // Assert - let recordedRequests = try serverSession.getRecordedPOSTRequests() + let app = ExampleApplication() + app.launchWith(mockServerURL: serverSession.recordingURL) + app.tapSendLogsForUITests() + + // Return desired count or timeout + let recordedRequests = try serverSession.pullRecordedPOSTRequests(count: 1, timeout: Constants.logsDeliveryTime) + recordedRequests.forEach { request in - XCTAssertTrue(request.path.contains("/client-token?ddsource=ios")) + // Example path here: `/36882784-420B-494F-910D-CBAC5897A309/ui-tests-client-token?ddsource=ios&batch_time=1589969230153` + let pathRegexp = #"^(.*)(/ui-tests-client-token\?ddsource=ios&batch_time=)([0-9]+)$"# + XCTAssertNotNil(request.path.range(of: pathRegexp, options: .regularExpression, range: nil, locale: nil)) + XCTAssertTrue(request.httpHeaders.contains("Content-Type: application/json")) } + // Assert logs let logMatchers = try recordedRequests .flatMap { request in try LogMatcher.fromArrayOfJSONObjectsData(request.httpBody) } - logMatchers[0].assertStatus(equals: "DEBUG") + XCTAssertEqual(logMatchers.count, 6) + + logMatchers[0].assertStatus(equals: "debug") logMatchers[0].assertMessage(equals: "debug message") - logMatchers[1].assertStatus(equals: "INFO") + logMatchers[1].assertStatus(equals: "info") logMatchers[1].assertMessage(equals: "info message") - logMatchers[2].assertStatus(equals: "NOTICE") + logMatchers[2].assertStatus(equals: "notice") logMatchers[2].assertMessage(equals: "notice message") - logMatchers[3].assertStatus(equals: "WARN") + logMatchers[3].assertStatus(equals: "warn") logMatchers[3].assertMessage(equals: "warn message") - logMatchers[4].assertStatus(equals: "ERROR") + logMatchers[4].assertStatus(equals: "error") logMatchers[4].assertMessage(equals: "error message") - logMatchers[5].assertStatus(equals: "CRITICAL") + logMatchers[5].assertStatus(equals: "critical") logMatchers[5].assertMessage(equals: "critical message") logMatchers.forEach { matcher in matcher.assertDate(matches: { Date().timeIntervalSince($0) < Constants.logsDeliveryTime * 2 }) - matcher.assertServiceName(equals: "service-name") + matcher.assertServiceName(equals: "ui-tests-service-name") matcher.assertLoggerName(equals: "logger-name") matcher.assertLoggerVersion(matches: { version in version.split(separator: ".").count == 3 }) matcher.assertApplicationVersion(equals: "1.0") @@ -89,27 +67,38 @@ class LoggingIntegrationTests: IntegrationTests { "logger-attribute1": "string value", "logger-attribute2": 1_000, "attribute": "value", + "some-url": "https://example.com/image.png" ] ) - matcher.assertTags(equal: ["env:integration", "tag1:tag-value", "tag2"]) + + #if DEBUG + matcher.assertTags(equal: ["env:integration", "build_configuration:debug", "tag1:tag-value", "tag2"]) + #else + matcher.assertTags(equal: ["env:integration", "build_configuration:release", "tag1:tag-value", "tag2"]) + #endif matcher.assertValue( forKeyPath: LogMatcher.JSONKey.networkReachability, matches: { LogMatcher.allowedNetworkReachabilityValues.contains($0) } ) - matcher.assertValue( - forKeyPath: LogMatcher.JSONKey.networkAvailableInterfaces, - matches: { (values: [String]) -> Bool in - LogMatcher.allowedNetworkAvailableInterfacesValues.isSuperset(of: Set(values)) - } - ) - matcher.assertValue(forKeyPath: LogMatcher.JSONKey.networkConnectionSupportsIPv4, isTypeOf: Bool.self) - matcher.assertValue(forKeyPath: LogMatcher.JSONKey.networkConnectionSupportsIPv6, isTypeOf: Bool.self) - matcher.assertValue(forKeyPath: LogMatcher.JSONKey.networkConnectionIsExpensive, isTypeOf: Bool.self) - matcher.assertValue( - forKeyPath: LogMatcher.JSONKey.networkConnectionIsConstrained, - isTypeOf: Optional.self - ) + + if #available(iOS 12.0, *) { + matcher.assertValue( + forKeyPath: LogMatcher.JSONKey.networkAvailableInterfaces, + matches: { (values: [String]) -> Bool in + LogMatcher.allowedNetworkAvailableInterfacesValues.isSuperset(of: Set(values)) + } + ) + + matcher.assertValue(forKeyPath: LogMatcher.JSONKey.networkConnectionSupportsIPv4, isTypeOf: Bool.self) + matcher.assertValue(forKeyPath: LogMatcher.JSONKey.networkConnectionSupportsIPv6, isTypeOf: Bool.self) + matcher.assertValue(forKeyPath: LogMatcher.JSONKey.networkConnectionIsExpensive, isTypeOf: Bool.self) + + matcher.assertValue( + forKeyPath: LogMatcher.JSONKey.networkConnectionIsConstrained, + isTypeOf: Optional.self + ) + } #if targetEnvironment(simulator) // When running on iOS Simulator @@ -126,5 +115,5 @@ class LoggingIntegrationTests: IntegrationTests { #endif } } - // swiftlint:enable trailing_closure } +// swiftlint:enable trailing_closure diff --git a/Tests/DatadogIntegrationTests/TracingIntegrationTests.swift b/Tests/DatadogIntegrationTests/TracingIntegrationTests.swift new file mode 100644 index 0000000000..66a72fc4fa --- /dev/null +++ b/Tests/DatadogIntegrationTests/TracingIntegrationTests.swift @@ -0,0 +1,192 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import HTTPServerMock +import XCTest + +class TracingIntegrationTests: IntegrationTests { + private struct Constants { + /// Time needed for data to be uploaded to mock server. + static let dataDeliveryTime: TimeInterval = 30 + } + + func testLaunchTheAppAndSendTraces() throws { + let testBeginTimeInNanoseconds = UInt64(Date().timeIntervalSince1970 * 1_000_000_000) + + // Server session recording mock data requests send to `HTTPServerMock`. + // Used to inspect trace HTTP headers. + let dataSourceServerSession = server.obtainUniqueRecordingSession() + // Server session recording spans send to `HTTPServerMock`. + let tracingServerSession = server.obtainUniqueRecordingSession() + // Server session recording logs send to `HTTPServerMock`. + let loggingServerSession = server.obtainUniqueRecordingSession() + + let app = ExampleApplication() + app.launchWith( + mockLogsEndpointURL: loggingServerSession.recordingURL, + mockTracesEndpointURL: tracingServerSession.recordingURL, + mockSourceEndpointURL: dataSourceServerSession.recordingURL + ) + app.tapSendTracesForUITests() + + // Return desired count or timeout + let recordedTracingRequests = try tracingServerSession.pullRecordedPOSTRequests(count: 1, timeout: Constants.dataDeliveryTime) + + recordedTracingRequests.forEach { request in + // Example path here: `/36882784-420B-494F-910D-CBAC5897A309/ui-tests-client-token?batch_time=1589969230153` + let pathRegexp = #"^(.*)(/ui-tests-client-token\?batch_time=)([0-9]+)$"# + XCTAssertNotNil(request.path.range(of: pathRegexp, options: .regularExpression, range: nil, locale: nil)) + XCTAssertTrue(request.httpHeaders.contains("Content-Type: text/plain;charset=UTF-8")) + } + + let testEndTimeInNanoseconds = UInt64(Date().timeIntervalSince1970 * 1_000_000_000) + + // Assert spans + let spanMatchers = try recordedTracingRequests + .flatMap { request in try SpanMatcher.fromNewlineSeparatedJSONObjectsData(request.httpBody) } + + let autoTracedWithURL = spanMatchers[3] + let autoTracedWithRequest = spanMatchers[4] + let autoTracedWithError = spanMatchers[5] + + let recordedNetworkRequests = try dataSourceServerSession.pullRecordedPOSTRequests(count: 1, timeout: Constants.dataDeliveryTime) + XCTAssert(recordedNetworkRequests.count == 1) + let traceID = try autoTracedWithRequest.traceID().hexadecimalNumberToDecimal + XCTAssert(recordedNetworkRequests.first!.httpHeaders.contains("x-datadog-trace-id: \(traceID)"), "Trace: \(traceID) Actual: \(recordedNetworkRequests.first!.httpHeaders)") + let spanID = try autoTracedWithRequest.spanID().hexadecimalNumberToDecimal + XCTAssert(recordedNetworkRequests.first!.httpHeaders.contains("x-datadog-parent-id: \(spanID)"), "Span: \(spanID) Actual: \(recordedNetworkRequests.first!.httpHeaders)") + XCTAssert(recordedNetworkRequests.first!.httpHeaders.contains("creation-method: dataTaskWithRequest")) + + XCTAssertEqual(spanMatchers.count, 6) + XCTAssertEqual(try spanMatchers[0].operationName(), "data downloading") + XCTAssertEqual(try spanMatchers[1].operationName(), "data presentation") + XCTAssertEqual(try spanMatchers[2].operationName(), "view appearing") + + XCTAssertEqual(try autoTracedWithURL.operationName(), "urlsession.request") + XCTAssertEqual(try autoTracedWithRequest.operationName(), "urlsession.request") + XCTAssertEqual(try autoTracedWithError.operationName(), "urlsession.request") + + // All spans share the same `trace_id` + XCTAssertEqual(try spanMatchers[0].traceID(), try spanMatchers[1].traceID()) + XCTAssertEqual(try spanMatchers[0].traceID(), try spanMatchers[2].traceID()) + + // "data downloading" and "data presentation" are childs of "view appearing" + XCTAssertEqual(try spanMatchers[0].parentSpanID(), try spanMatchers[2].spanID()) + XCTAssertEqual(try spanMatchers[1].parentSpanID(), try spanMatchers[2].spanID()) + + // auto-instrumentation generates unique trace ids + XCTAssertNotEqual(try autoTracedWithURL.traceID(), try spanMatchers[0].traceID()) + XCTAssertNotEqual(try autoTracedWithRequest.traceID(), try spanMatchers[0].traceID()) + XCTAssertNotEqual(try autoTracedWithError.traceID(), try spanMatchers[0].traceID()) + + XCTAssertNil(try? spanMatchers[0].metrics.isRootSpan()) + XCTAssertNil(try? spanMatchers[1].metrics.isRootSpan()) + XCTAssertEqual(try spanMatchers[2].metrics.isRootSpan(), 1) + XCTAssertEqual(try autoTracedWithURL.metrics.isRootSpan(), 1) + XCTAssertEqual(try autoTracedWithRequest.metrics.isRootSpan(), 1) + XCTAssertEqual(try autoTracedWithError.metrics.isRootSpan(), 1) + + // "data downloading" span's tags + XCTAssertEqual(try spanMatchers[0].meta.custom(keyPath: "meta.data.kind"), "image") + XCTAssertEqual(try spanMatchers[0].meta.custom(keyPath: "meta.data.url"), "https://example.com/image.png") + + // "data presentation" span contains error + XCTAssertEqual(try spanMatchers[0].isError(), 0) + XCTAssertEqual(try spanMatchers[1].isError(), 1) + XCTAssertEqual(try spanMatchers[2].isError(), 0) + XCTAssertEqual(try autoTracedWithURL.isError(), 0) + XCTAssertEqual(try autoTracedWithRequest.isError(), 0) + XCTAssertEqual(try autoTracedWithError.isError(), 1) + + // "data downloading" span has custom resource name + XCTAssertEqual(try spanMatchers[0].resource(), "GET /image.png") + XCTAssertEqual(try spanMatchers[1].resource(), try spanMatchers[1].operationName()) + XCTAssertEqual(try spanMatchers[2].resource(), try spanMatchers[2].operationName()) + + let targetURL = dataSourceServerSession.recordingURL + XCTAssert(try autoTracedWithURL.resource().contains(targetURL.host!)) + XCTAssertEqual(try autoTracedWithRequest.resource(), targetURL.absoluteString) + + // assert baggage item: + XCTAssertEqual(try spanMatchers[0].meta.custom(keyPath: "meta.class"), "SendTracesFixtureViewController") + XCTAssertEqual(try spanMatchers[1].meta.custom(keyPath: "meta.class"), "SendTracesFixtureViewController") + XCTAssertEqual(try spanMatchers[2].meta.custom(keyPath: "meta.class"), "SendTracesFixtureViewController") + + try spanMatchers.forEach { matcher in + XCTAssertGreaterThan(try matcher.startTime(), testBeginTimeInNanoseconds) + XCTAssertLessThan(try matcher.startTime(), testEndTimeInNanoseconds) + + XCTAssertEqual(try matcher.serviceName(), "ui-tests-service-name") + XCTAssertEqual(try matcher.type(), "custom") + XCTAssertEqual(try matcher.environment(), "integration") + + XCTAssertEqual(try matcher.meta.source(), "ios") + XCTAssertEqual(try matcher.meta.tracerVersion().split(separator: ".").count, 3) + XCTAssertEqual(try matcher.meta.applicationVersion(), "1.0") + + XCTAssertTrue( + SpanMatcher.allowedNetworkReachabilityValues.contains( + try matcher.meta.networkReachability() + ) + ) + + if #available(iOS 12.0, *) { // The `iOS11NetworkConnectionInfoProvider` doesn't provide those info + try matcher.meta.networkAvailableInterfaces().split(separator: "+").forEach { interface in + XCTAssertTrue( + SpanMatcher.allowedNetworkAvailableInterfacesValues.contains(String(interface)) + ) + } + + XCTAssertTrue(["0", "1"].contains(try matcher.meta.networkConnectionSupportsIPv4())) + XCTAssertTrue(["0", "1"].contains(try matcher.meta.networkConnectionSupportsIPv6())) + XCTAssertTrue(["0", "1"].contains(try matcher.meta.networkConnectionIsExpensive())) + } + + if #available(iOS 13.0, *) { + XCTAssertTrue(["0", "1"].contains(try matcher.meta.networkConnectionIsConstrained())) + } + + #if targetEnvironment(simulator) + // When running on iOS Simulator + XCTAssertNil(try? matcher.meta.mobileNetworkCarrierName()) + XCTAssertNil(try? matcher.meta.mobileNetworkCarrierISOCountryCode()) + XCTAssertNil(try? matcher.meta.mobileNetworkCarrierRadioTechnology()) + XCTAssertNil(try? matcher.meta.mobileNetworkCarrierAllowsVoIP()) + #else + // When running on physical device with SIM card registered + XCTAssertNotNil(try? matcher.meta.mobileNetworkCarrierName()) + XCTAssertNotNil(try? matcher.meta.mobileNetworkCarrierISOCountryCode()) + XCTAssertNotNil(try? matcher.meta.mobileNetworkCarrierRadioTechnology()) + XCTAssertNotNil(try? matcher.meta.mobileNetworkCarrierAllowsVoIP()) + #endif + } + + // Assert logs requests + let recordedLoggingRequests = try loggingServerSession.pullRecordedPOSTRequests(count: 1, timeout: Constants.dataDeliveryTime) + + // Assert logs + let logMatchers = try recordedLoggingRequests + .flatMap { request in try LogMatcher.fromArrayOfJSONObjectsData(request.httpBody) } + + XCTAssertEqual(logMatchers.count, 1) + + logMatchers[0].assertStatus(equals: "info") + logMatchers[0].assertMessage(equals: "download progress") + logMatchers[0].assertValue(forKey: "progress", equals: 0.99) + + // Assert logs are linked to "data downloading" span + logMatchers[0].assertValue(forKey: "dd.trace_id", equals: try spanMatchers[0].traceID().hexadecimalNumberToDecimal) + logMatchers[0].assertValue(forKey: "dd.span_id", equals: try spanMatchers[0].spanID().hexadecimalNumberToDecimal) + } +} + +private extension String { + /// Tracing feature uses hexadecimal representation of trace and span IDs, while Logging uses decimals. + /// This helper converts hexadecimal string to decimal string for comparison. + var hexadecimalNumberToDecimal: String { + return "\(UInt64(self, radix: 16)!)" + } +} diff --git a/Tests/DatadogIntegrationTests/UITestsHelpers.swift b/Tests/DatadogIntegrationTests/UITestsHelpers.swift new file mode 100644 index 0000000000..bf68fc78c1 --- /dev/null +++ b/Tests/DatadogIntegrationTests/UITestsHelpers.swift @@ -0,0 +1,34 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest + +/// Convenient interface to navigate through Example app's main screen. +class ExampleApplication: XCUIApplication { + /// Launches the app by mocking all feature endpoints with `mockServerURL`. + func launchWith(mockServerURL: URL) { + self.launchWith(mockLogsEndpointURL: mockServerURL, mockTracesEndpointURL: mockServerURL, mockSourceEndpointURL: mockServerURL) + } + + /// Launches the app by mocking feature endpoints separately. + func launchWith(mockLogsEndpointURL: URL, mockTracesEndpointURL: URL, mockSourceEndpointURL: URL) { + launchArguments = ["IS_RUNNING_UI_TESTS"] + launchEnvironment = [ + "DD_MOCK_LOGS_ENDPOINT_URL": mockLogsEndpointURL.absoluteString, + "DD_MOCK_TRACES_ENDPOINT_URL": mockTracesEndpointURL.absoluteString, + "DD_MOCK_SOURCE_ENDPOINT_URL": mockSourceEndpointURL.absoluteString + ] + super.launch() + } + + func tapSendLogsForUITests() { + tables.staticTexts["Send logs for UI Tests"].tap() + } + + func tapSendTracesForUITests() { + tables.staticTexts["Send traces for UI Tests"].tap() + } +} diff --git a/Tests/DatadogTests/Datadog/Core/AutoInstrumentation/MethodSwizzlerTests.swift b/Tests/DatadogTests/Datadog/Core/AutoInstrumentation/MethodSwizzlerTests.swift new file mode 100644 index 0000000000..8475c04842 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Core/AutoInstrumentation/MethodSwizzlerTests.swift @@ -0,0 +1,198 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +extension MethodSwizzler { + func unswizzle() { + for foundMethod in swizzledMethods { + let originalTypedIMP = originalImplementation(of: foundMethod) + let originalIMP: IMP = unsafeBitCast(originalTypedIMP, to: IMP.self) + method_setImplementation(foundMethod.method, originalIMP) + } + } +} + +@objcMembers +private class EmptySubclass: BaseClass { } + +@objcMembers +private class BaseClass: NSObject { + static let returnValue = "this is base class" + func methodToSwizzle() -> String { + return Self.returnValue + } +} + +class MethodSwizzlerTests: XCTestCase { + private typealias TypedIMPReturnString = @convention(c) (AnyObject, Selector) -> String + private typealias TypedBlockIMPReturnString = @convention(block) (AnyObject) -> String + + private let selToSwizzle = #selector(BaseClass.methodToSwizzle) + private let newIMPReturnString: TypedBlockIMPReturnString = { _ in String.mockAny() } + + private typealias Swizzler = MethodSwizzler + private let swizzler = Swizzler() + + override func tearDown() { + super.tearDown() + swizzler.unswizzle() + } + + func test_simpleSwizzle() throws { + let obj = BaseClass() + + // before + XCTAssertNotEqual(obj.perform(selToSwizzle)?.takeUnretainedValue() as? String, String.mockAny()) + // swizzle + let foundMethod = try Swizzler.findMethod(with: selToSwizzle, in: BaseClass.self) + swizzler.swizzle(foundMethod) { currentImp -> TypedBlockIMPReturnString in + return { impSelf in + return currentImp(impSelf, self.selToSwizzle).appending(String.mockAny()) + } + } + // after + XCTAssertEqual(obj.perform(selToSwizzle)?.takeUnretainedValue() as? String, BaseClass.returnValue + String.mockAny()) + } + + func test_searchWrongSelector() { + let wrongSelToSwizzle = Selector(("selector_who_never_existed")) + + let expectedErrorDescription = "\(NSStringFromSelector(wrongSelToSwizzle)) is not found in \(NSStringFromClass(BaseClass.self))" + XCTAssertThrowsError(try Swizzler.findMethod(with: wrongSelToSwizzle, in: BaseClass.self), "Wrong selector should throw") { error in + let internalError = error as? InternalError + XCTAssertEqual(internalError?.description, expectedErrorDescription) + } + } + + func test_swizzle_alreadySwizzledSelector() throws { + let foundMethod = try Swizzler.findMethod(with: selToSwizzle, in: BaseClass.self) + + let beforeOrigTypedIMP = swizzler.originalImplementation(of: foundMethod) + // first swizzling + let firstAppendedReturnValue = "first" + swizzler.swizzle(foundMethod) { currentImp -> TypedBlockIMPReturnString in + return { impSelf in + return currentImp(impSelf, self.selToSwizzle).appending(firstAppendedReturnValue) + } + } + + let secondAppendedReturnValue = "second" + swizzler.swizzle(foundMethod) { currentImp -> TypedBlockIMPReturnString in + return { impSelf in + return currentImp(impSelf, self.selToSwizzle).appending(secondAppendedReturnValue) + } + } + + let afterOrigTypedIMP = swizzler.originalImplementation(of: foundMethod) + + let obj = BaseClass() + let expectedReturnValue = BaseClass.returnValue + firstAppendedReturnValue + secondAppendedReturnValue + XCTAssertEqual(obj.perform(selToSwizzle)?.takeUnretainedValue() as? String, expectedReturnValue) + XCTAssertEqual( + unsafeBitCast(beforeOrigTypedIMP, to: IMP.self), + unsafeBitCast(afterOrigTypedIMP, to: IMP.self) + ) + } + + func test_swizzleIfNonSwizzled_alreadySwizzledSelector() throws { + let foundMethod = try Swizzler.findMethod(with: selToSwizzle, in: BaseClass.self) + // first swizzling + swizzler.swizzle(foundMethod) { _ in newIMPReturnString } + + let secondSwizzlingReturnValue = "Second swizzling" + let newImp: TypedBlockIMPReturnString = { _ in secondSwizzlingReturnValue } + XCTAssertFalse( + swizzler.swizzle(foundMethod, impProvider: { _ in newImp }, onlyIfNonSwizzled: true), + "Already swizzled method should not be swizzled again" + ) + + let obj = BaseClass() + XCTAssertNotEqual(obj.perform(selToSwizzle)?.takeUnretainedValue() as? String, secondSwizzlingReturnValue) + } + + func test_findSubclassMethod() throws { + let subclassMethod = try Swizzler.findMethod(with: selToSwizzle, in: EmptySubclass.self) + + XCTAssertNotNil(subclassMethod) + XCTAssertEqual(NSStringFromClass(subclassMethod.klass), NSStringFromClass(BaseClass.self)) + } + + func test_lazyEvaluationOfNewIMP() throws { + let foundMethod = try Swizzler.findMethod(with: selToSwizzle, in: BaseClass.self) + // first swizzling + swizzler.swizzle(foundMethod) { _ in newIMPReturnString } + + XCTAssertFalse( + swizzler.swizzle( + foundMethod, + impProvider: { _ -> TypedBlockIMPReturnString in + XCTFail("New IMP should not be created after error") + return newIMPReturnString + }, + onlyIfNonSwizzled: true + ), + "Already swizzled method should not be swizzled again" + ) + } + + func test_originalIMP_immutability() throws { + let foundMethod = try Swizzler.findMethod(with: selToSwizzle, in: BaseClass.self) + + // first swizzling + swizzler.swizzle(foundMethod) { _ -> TypedBlockIMPReturnString in + return { _ in + "first" + } + } + // second swizzling + swizzler.swizzle(foundMethod) { _ -> TypedBlockIMPReturnString in + return { _ in + "second" + } + } + + // revert to original imp + let originalTypedImp = swizzler.originalImplementation(of: foundMethod) + let originalImp = unsafeBitCast(originalTypedImp, to: IMP.self) + method_setImplementation(foundMethod.method, originalImp) + + let obj = BaseClass() + let expectedReturnValue = BaseClass.returnValue + XCTAssertEqual(obj.perform(selToSwizzle)?.takeUnretainedValue() as? String, expectedReturnValue) + } + + func test_swizzleFromMultipleThreads() throws { + let selector = selToSwizzle + let foundMethod = try Swizzler.findMethod(with: selector, in: BaseClass.self) + + let appendString = "swizzled" + let iterations = 10 + let expectation = self.expectation(description: "concurrent expectation") + expectation.expectedFulfillmentCount = iterations + + DispatchQueue.concurrentPerform(iterations: iterations) { _ in + swizzler.swizzle( + foundMethod, + impProvider: { originalImp -> TypedBlockIMPReturnString in + return { impSelf -> String in + return originalImp(impSelf, selector).appending(appendString) + } + }, + onlyIfNonSwizzled: true + ) + expectation.fulfill() + } + + waitForExpectations(timeout: 0.1) { error in + XCTAssertNil(error) + + let returnValue = BaseClass().perform(selector)?.takeUnretainedValue() as? String + XCTAssertEqual(returnValue, "\(BaseClass.returnValue)\(appendString)") + } + } +} diff --git a/Tests/DatadogTests/Datadog/Core/AutoInstrumentation/NSURLSessionBridge.h b/Tests/DatadogTests/Datadog/Core/AutoInstrumentation/NSURLSessionBridge.h new file mode 100644 index 0000000000..2dc0d4e1a0 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Core/AutoInstrumentation/NSURLSessionBridge.h @@ -0,0 +1,17 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-2020 Datadog, Inc. +*/ + +#import + +@interface NSURLSessionBridge : NSObject + ++ (NSURLSessionDataTask *)session:(NSURLSession *)session dataTaskWithURL:(NSURL *)url completionHandler:(void(^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; ++ (NSURLSessionDataTask *)session:(NSURLSession *)session dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void(^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; + ++ (id)new NS_UNAVAILABLE; +- (id)init NS_UNAVAILABLE; + +@end diff --git a/Tests/DatadogTests/Datadog/Core/AutoInstrumentation/NSURLSessionBridge.m b/Tests/DatadogTests/Datadog/Core/AutoInstrumentation/NSURLSessionBridge.m new file mode 100644 index 0000000000..ae4d57a824 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Core/AutoInstrumentation/NSURLSessionBridge.m @@ -0,0 +1,19 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-2020 Datadog, Inc. +*/ + +#import "NSURLSessionBridge.h" + +@implementation NSURLSessionBridge + ++ (NSURLSessionDataTask *)session:(NSURLSession *)session dataTaskWithURL:(NSURL *)url completionHandler:(void(^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler { + return [session dataTaskWithURL:url completionHandler:completionHandler]; +} + ++ (NSURLSessionDataTask *)session:(NSURLSession *)session dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void(^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler { + return [session dataTaskWithRequest:request completionHandler:completionHandler]; +} + +@end diff --git a/Tests/DatadogTests/Datadog/Core/AutoInstrumentation/URLSessionSwizzlerTests.swift b/Tests/DatadogTests/Datadog/Core/AutoInstrumentation/URLSessionSwizzlerTests.swift new file mode 100644 index 0000000000..fe4996b211 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Core/AutoInstrumentation/URLSessionSwizzlerTests.swift @@ -0,0 +1,284 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class URLSessionSwizzlerTests: XCTestCase { + /// URL.mockAny() is a valid URL so that it loads actual resource and exceeds expectation timeouts + /// We use unsupported URLs so that the task goes through its URLProtocol chain and **immediately** returns with "unsupported URL" error + let mockURL = URL(string: "foo://example.com")! + let mockURLRequest = URLRequest(url: URL(string: "foo://example.com")!) + let modifiedURLRequest = URLRequest(url: URL(string: "bar://example.com")!) + let secondModifiedURLRequest = URLRequest(url: URL(string: "bar://foo.example.com")!) + + let timeout = 1.0 + + var session: URLSession { URLSession.shared } + // swiftlint:disable implicitly_unwrapped_optional + var firstSwizzler: URLSessionSwizzler! + var secondSwizzler: URLSessionSwizzler! + var thirdSwizzler: URLSessionSwizzler! + // swiftlint:enable implicitly_unwrapped_optional + + override func setUpWithError() throws { + try super.setUpWithError() + firstSwizzler = try URLSessionSwizzler() + secondSwizzler = try URLSessionSwizzler() + thirdSwizzler = try URLSessionSwizzler() + } + + override func tearDown() { + super.tearDown() + firstSwizzler.unswizzle() + } + + func test_dataTask_urlCompletion() { + let completionExpectation = XCTestExpectation(description: "completionExpectation") + // intercepts and injects modifiedHTTPHeaders + let mock1 = MockInterceptor(id: 1, modifiedRequest: modifiedURLRequest) + // does NOT intercept + let mock2 = MockInterceptor(id: 2, modifiedRequest: nil) + // intercepts and injects secondModifiedHTTPHeaders + let mock3 = MockInterceptor(id: 3, modifiedRequest: secondModifiedURLRequest) + + firstSwizzler.swizzle(using: mock1.block) + secondSwizzler.swizzle(using: mock2.block) + thirdSwizzler.swizzle(using: mock3.block) + + let task = session.dataTask(with: mockURL) { _, _, _ in + completionExpectation.fulfill() + } + + let taskRequest = task.originalRequest! + if #available(iOS 13.0, *) { + XCTAssertEqual(taskRequest.url, mockURL) + } else { + XCTAssertEqual(taskRequest.url, modifiedURLRequest.url) + } + + wait( + for: [ + mock1.interceptionExpectation, + mock2.interceptionExpectation, + mock3.interceptionExpectation + ], + timeout: timeout + ) + + task.resume() + + /// we expect secondInterceptor not to observe as it does not intercept in the first place + let resumeExpectations: [XCTestExpectation] = [ + mock1.observationStartingExpectation, + mock3.observationStartingExpectation, + completionExpectation, + mock3.observationCompletedExpectation, + mock1.observationCompletedExpectation + ] + wait( + for: resumeExpectations, + timeout: timeout, + enforceOrder: true + ) + } + + func test_dataTask_requestCompletion() { + let completionExpectation = XCTestExpectation(description: "completionExpectation") + let mock1 = MockInterceptor(id: 1, modifiedRequest: modifiedURLRequest) + let mock2 = MockInterceptor(id: 2, modifiedRequest: nil) + let mock3 = MockInterceptor(id: 3, modifiedRequest: secondModifiedURLRequest) + + firstSwizzler.swizzle(using: mock1.block) + secondSwizzler.swizzle(using: mock2.block) + thirdSwizzler.swizzle(using: mock3.block) + + let task = session.dataTask(with: mockURLRequest) { _, _, _ in + completionExpectation.fulfill() + } + + let taskRequest = task.originalRequest! + XCTAssertEqual(taskRequest, modifiedURLRequest) + + wait( + for: [ + mock1.interceptionExpectation, + mock2.interceptionExpectation, + mock3.interceptionExpectation + ], + timeout: timeout + ) + + task.resume() + + let resumeExpectations: [XCTestExpectation] = [ + mock1.observationStartingExpectation, + mock3.observationStartingExpectation, + completionExpectation, + mock3.observationCompletedExpectation, + mock1.observationCompletedExpectation + ] + wait( + for: resumeExpectations, + timeout: timeout, + enforceOrder: true + ) + } + + // edgeCases test that our swizzling doesn't break anything in ObjC + // MARK: - Edge cases + + private var taskObservation: NSKeyValueObservation? = nil + + func test_dataTask_urlCompletion_edgeCase_nilURLNilCompletion() throws { + let completionExpectation = XCTestExpectation(description: "completionExpectation") + let interceptor = MockInterceptor(id: 1, modifiedRequest: modifiedURLRequest) + firstSwizzler.swizzle(using: interceptor.block) + + let nilURL: URL? = nil + let task = NSURLSessionBridge.session(self.session, dataTaskWith: nilURL, completionHandler: nil) + + let someTask = try XCTUnwrap(task) + + wait(for: [interceptor.interceptionExpectation], timeout: timeout) + + taskObservation = someTask.observe(\.state) { observedTask, _ in + if case URLSessionTask.State.completed = observedTask.state { + completionExpectation.fulfill() + } + } + + someTask.resume() + + let resumeExpectations: [XCTestExpectation] = [ + interceptor.observationStartingExpectation, + completionExpectation, + interceptor.observationCompletedExpectation + ] + wait(for: resumeExpectations, timeout: timeout) + } + + func test_dataTask_requestCompletion_edgeCase_nilCompletion() throws { + let completionExpectation = XCTestExpectation(description: "completionExpectation") + let interceptor = MockInterceptor(id: 1, modifiedRequest: modifiedURLRequest) + firstSwizzler.swizzle(using: interceptor.block) + + let task = NSURLSessionBridge.session(self.session, dataTaskWith: mockURLRequest, completionHandler: nil) + + let someTask = try XCTUnwrap(task) + XCTAssertEqual(someTask.originalRequest, modifiedURLRequest) + + wait(for: [interceptor.interceptionExpectation], timeout: timeout) + + taskObservation = someTask.observe(\.state) { observedTask, _ in + if case URLSessionTask.State.completed = observedTask.state { + completionExpectation.fulfill() + } + } + + someTask.resume() + + let resumeExpectations: [XCTestExpectation] = [ + interceptor.observationStartingExpectation, + completionExpectation, + interceptor.observationCompletedExpectation + ] + wait(for: resumeExpectations, timeout: timeout) + } + + func test_dataTask_requestCompletion_edgeCase_nilRequestNilCompletion() throws { + /// nil URLRequest throws an exception, tested in iOS 13.5 + /// we test that we do NOT change the thrown exception after swizzling + let nilRequest: URLRequest? = nil + + var originalException: NSError? = nil + var originalTask: URLSessionTask? = nil + + XCTAssertThrowsError( + try objcExceptionHandler.rethrowToSwift { + originalTask = NSURLSessionBridge.session(self.session, dataTaskWith: nilRequest, completionHandler: nil) + } + ) { error in + originalException = error as NSError + } + + // swizzling happens + let interceptor = MockInterceptor(id: 1, modifiedRequest: modifiedURLRequest) + firstSwizzler.swizzle(using: interceptor.block) + + var swizzledException: NSError? = nil + var swizzledTask: URLSessionTask? = nil + + XCTAssertThrowsError( + try objcExceptionHandler.rethrowToSwift { + swizzledTask = NSURLSessionBridge.session(self.session, dataTaskWith: nilRequest, completionHandler: nil) + } + ) { error in + swizzledException = error as NSError + } + + XCTAssertEqual(swizzledException, originalException) + XCTAssertEqual(swizzledTask != nil, originalTask != nil) + } +} + +class URLSessionSwizzlerTests_DefaultConfig: URLSessionSwizzlerTests { + private let _session = URLSession(configuration: .default) + override var session: URLSession { _session } +} + +class URLSessionSwizzlerTests_CustomDelegate: URLSessionSwizzlerTests { + private class SessionDelegate: NSObject, URLSessionDelegate { } + private let _session = URLSession(configuration: .default, delegate: SessionDelegate(), delegateQueue: OperationQueue()) + override var session: URLSession { _session } +} + +extension URLSessionSwizzler { + func unswizzle() { + dataTaskWithURL.unswizzle() + dataTaskwithRequest.unswizzle() + Self.resume.unswizzle() + Self.resume = URLSessionSwizzler.Resume() + } +} + +private class MockInterceptor { + let block: RequestInterceptor + let interceptionExpectation: XCTestExpectation + let observationStartingExpectation: XCTestExpectation + let observationCompletedExpectation: XCTestExpectation + + init(id: Int, modifiedRequest: URLRequest?) { + let interceptionExpectation = XCTestExpectation(description: "\(id): interceptionExpectation") + let observationStartingExpectation = XCTestExpectation(description: "\(id): observationExpectation.start") + let observationCompletedExpectation = XCTestExpectation(description: "\(id): observationExpectation.completed") + + if modifiedRequest == nil { + observationStartingExpectation.isInverted = true + observationCompletedExpectation.isInverted = true + } + let observer: TaskObserver = { event in + switch event { + case .starting: + observationStartingExpectation.fulfill() + case .completed: + observationCompletedExpectation.fulfill() + } + } + let interceptor: RequestInterceptor = { originalRequest in + interceptionExpectation.fulfill() + if let someRequest = modifiedRequest { + return InterceptionResult(modifiedRequest: someRequest, taskObserver: observer) + } else { + return nil + } + } + self.interceptionExpectation = interceptionExpectation + self.observationStartingExpectation = observationStartingExpectation + self.observationCompletedExpectation = observationCompletedExpectation + self.block = interceptor + } +} diff --git a/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift b/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift index 46b374981d..1cca361102 100644 --- a/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift +++ b/Tests/DatadogTests/Datadog/Core/PerformancePresetTests.swift @@ -22,8 +22,8 @@ class PerformancePresetTests: XCTestCase { presets.forEach { preset in XCTAssertLessThan( - preset.maxBatchSize, - preset.maxSizeOfLogsDirectory, + preset.maxFileSize, + preset.maxDirectorySize, "Size of individual file must not exceed the directory size limit." ) XCTAssertLessThan( @@ -37,22 +37,22 @@ class PerformancePresetTests: XCTestCase { "File read boundaries must be consistent." ) XCTAssertGreaterThan( - preset.maxLogsUploadDelay, - preset.minLogsUploadDelay, + preset.maxUploadDelay, + preset.minUploadDelay, "Upload delay boundaries must be consistent." ) XCTAssertGreaterThan( - preset.maxLogsUploadDelay, - preset.minLogsUploadDelay, + preset.maxUploadDelay, + preset.minUploadDelay, "Upload delay boundaries must be consistent." ) XCTAssertLessThanOrEqual( - preset.logsUploadDelayDecreaseFactor, + preset.uploadDelayDecreaseFactor, 1, "Upload delay should not be increased towards infinity." ) XCTAssertGreaterThan( - preset.logsUploadDelayDecreaseFactor, + preset.uploadDelayDecreaseFactor, 0, "Upload delay must never result with 0." ) diff --git a/Tests/DatadogTests/Datadog/Core/Persistence/FileReaderTests.swift b/Tests/DatadogTests/Datadog/Core/Persistence/FileReaderTests.swift index 30b0e862fb..c9d1a6b5a0 100644 --- a/Tests/DatadogTests/Datadog/Core/Persistence/FileReaderTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Persistence/FileReaderTests.swift @@ -22,12 +22,17 @@ class FileReaderTests: XCTestCase { func testItReadsSingleBatch() throws { let reader = FileReader( - orchestrator: .mockReadAllFiles(in: temporaryDirectory), + dataFormat: .mockWith(prefix: "[", suffix: "]"), + orchestrator: FilesOrchestrator( + directory: temporaryDirectory, + performance: StoragePerformanceMock.readAllFiles, + dateProvider: SystemDateProvider() + ), queue: queue ) _ = try temporaryDirectory - .createFile(named: .mockAnyFileName()) - .append { write in try write("ABCD".utf8Data) } + .createFile(named: Date.mockAny().toFileName) + .append(data: "ABCD".utf8Data) XCTAssertEqual(try temporaryDirectory.files().count, 1) let batch = reader.readNextBatch() @@ -38,22 +43,22 @@ class FileReaderTests: XCTestCase { func testItMarksBatchesAsRead() throws { let dateProvider = RelativeDateProvider(advancingBySeconds: 60) let reader = FileReader( + dataFormat: .mockWith(prefix: "[", suffix: "]"), orchestrator: FilesOrchestrator( directory: temporaryDirectory, - writeConditions: WritableFileConditions(performance: .mockUnitTestsPerformancePreset()), - readConditions: ReadableFileConditions(performance: .mockUnitTestsPerformancePreset()), + performance: StoragePerformanceMock.readAllFiles, dateProvider: dateProvider ), queue: queue ) let file1 = try temporaryDirectory.createFile(named: dateProvider.currentDate().toFileName) - try file1.append { write in try write("1".utf8Data) } + try file1.append(data: "1".utf8Data) let file2 = try temporaryDirectory.createFile(named: dateProvider.currentDate().toFileName) - try file2.append { write in try write("2".utf8Data) } + try file2.append(data: "2".utf8Data) let file3 = try temporaryDirectory.createFile(named: dateProvider.currentDate().toFileName) - try file3.append { write in try write("3".utf8Data) } + try file3.append(data: "3".utf8Data) var batch: Batch batch = try reader.readNextBatch().unwrapOrThrow() diff --git a/Tests/DatadogTests/Datadog/Core/Persistence/FileWriterTests.swift b/Tests/DatadogTests/Datadog/Core/Persistence/FileWriterTests.swift index 817db3bc60..1f13ad1026 100644 --- a/Tests/DatadogTests/Datadog/Core/Persistence/FileWriterTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Persistence/FileWriterTests.swift @@ -23,7 +23,12 @@ class FileWriterTests: XCTestCase { func testItWritesDataToSingleFile() throws { let expectation = self.expectation(description: "write completed") let writer = FileWriter( - orchestrator: .mockWriteToSingleFile(in: temporaryDirectory), + dataFormat: DataFormat(prefix: "[", suffix: "]", separator: ","), + orchestrator: FilesOrchestrator( + directory: temporaryDirectory, + performance: PerformancePreset.default, + dateProvider: SystemDateProvider() + ), queue: queue ) @@ -40,74 +45,6 @@ class FileWriterTests: XCTestCase { ) } - /// NOTE: Test added after incident-4797 - func testGivenFileContainingData_whenNextDataFails_itDoesNotMalformTheEndOfTheFile() throws { - let previousObjcExceptionHandler = objcExceptionHandler - defer { objcExceptionHandler = previousObjcExceptionHandler } - - let expectation = self.expectation(description: "write completed") - let writer = FileWriter( - orchestrator: .mockWriteToSingleFile(in: temporaryDirectory), - queue: queue - ) - - objcExceptionHandler = ObjcExceptionHandlerDeferredMock( - throwingError: ErrorMock("I/O exception"), - /* - Following the logic in `FileWriter` and `File`, the 3 comes from: - - succeed on `fileHandle.seekToEndOfFile()` to prepare the file for the first write - - succeed on `fileHandle.write(_:)` for `writer.write(value: ["key1": "value1"])` - - succeed on `fileHandle.seekToEndOfFile()` to prepare the file for the second `writer.write(value:)` - - throw an `I/O exception` for `fileHandle.write(_:)` for the second write - */ - afterSucceedingCallsCounts: 3 - ) - - writer.write(value: ["key1": "value1"]) // first write (2 calls to `ObjcExceptionHandler`) - writer.write(value: ["key2": "value2"]) // second write (2 calls to `ObjcExceptionHandler`, where the latter fails) - - waitForWritesCompletion(on: queue, thenFulfill: expectation) - waitForExpectations(timeout: 1, handler: nil) - XCTAssertEqual(try temporaryDirectory.files().count, 1) - - XCTAssertEqual( - try temporaryDirectory.files()[0].read().utf8String, - #"{"key1":"value1"}"# // second write should be ignored due to `I/O exception` - ) - } - - /// NOTE: Test added after incident-4797 - func testWhenIOExceptionsHappenRandomly_theFileIsNeverMalformed() throws { - let previousObjcExceptionHandler = objcExceptionHandler - defer { objcExceptionHandler = previousObjcExceptionHandler } - - let expectation = self.expectation(description: "write completed") - let writer = FileWriter( - orchestrator: .mockWriteToSingleFile(in: temporaryDirectory), - queue: queue - ) - - objcExceptionHandler = ObjcExceptionHandlerNonDeterministicMock( - throwingError: ErrorMock("I/O exception"), - withProbability: 0.2 - ) - - struct Foo: Codable { - let foo = "bar" - } - - (0...500).forEach { _ in writer.write(value: Foo()) } - - waitForWritesCompletion(on: queue, thenFulfill: expectation) - waitForExpectations(timeout: 5, handler: nil) - XCTAssertEqual(try temporaryDirectory.files().count, 1) - - let fileData = try temporaryDirectory.files()[0].read() - let jsonDecoder = JSONDecoder() - - XCTAssertNoThrow(try jsonDecoder.decode([Foo].self, from: "[".utf8Data + fileData + "]".utf8Data)) - } - func testGivenErrorVerbosity_whenIndividualDataExceedsMaxWriteSize_itDropsDataAndPrintsError() throws { let expectation1 = self.expectation(description: "write completed") let expectation2 = self.expectation(description: "second write completed") @@ -115,21 +52,21 @@ class FileWriterTests: XCTestCase { defer { userLogger = previousUserLogger } let output = LogOutputMock() - userLogger = Logger(logOutput: output, identifier: "sdk-user") + userLogger = Logger(logOutput: output, dateProvider: SystemDateProvider(), identifier: "sdk-user") let writer = FileWriter( + dataFormat: .mockWith(prefix: "[", suffix: "]"), orchestrator: FilesOrchestrator( directory: temporaryDirectory, - writeConditions: WritableFileConditions( - performance: .mockWith( - maxBatchSize: .max, - maxSizeOfLogsDirectory: .max, - maxFileAgeForWrite: .distantFuture, - maxLogsPerBatch: .max, - maxLogSize: 17 // 17 bytes is enough to write {"key1":"value1"} JSON - ) + performance: StoragePerformanceMock( + maxFileSize: .max, + maxDirectorySize: .max, + maxFileAgeForWrite: .distantFuture, + minFileAgeForRead: .mockAny(), + maxFileAgeForRead: .mockAny(), + maxObjectsInFile: .max, + maxObjectSize: 17 // 17 bytes is enough to write {"key1":"value1"} JSON ), - readConditions: .mockReadAllFiles(), dateProvider: SystemDateProvider() ), queue: queue @@ -156,10 +93,15 @@ class FileWriterTests: XCTestCase { defer { userLogger = previousUserLogger } let output = LogOutputMock() - userLogger = Logger(logOutput: output, identifier: "sdk-user") + userLogger = Logger(logOutput: output, dateProvider: SystemDateProvider(), identifier: "sdk-user") let writer = FileWriter( - orchestrator: .mockWriteToSingleFile(in: temporaryDirectory), + dataFormat: .mockAny(), + orchestrator: FilesOrchestrator( + directory: temporaryDirectory, + performance: PerformancePreset.default, + dateProvider: SystemDateProvider() + ), queue: queue ) @@ -176,25 +118,84 @@ class FileWriterTests: XCTestCase { let expectation = self.expectation(description: "write completed") let previousUserLogger = userLogger defer { userLogger = previousUserLogger } - let previousObjcExceptionHandler = objcExceptionHandler - defer { objcExceptionHandler = previousObjcExceptionHandler } let output = LogOutputMock() - userLogger = Logger(logOutput: output, identifier: "sdk-user") - objcExceptionHandler = ObjcExceptionHandlerMock(throwingError: ErrorMock("I/O exception")) + userLogger = Logger(logOutput: output, dateProvider: SystemDateProvider(), identifier: "sdk-user") let writer = FileWriter( - orchestrator: .mockWriteToSingleFile(in: temporaryDirectory), + dataFormat: .mockAny(), + orchestrator: FilesOrchestrator( + directory: temporaryDirectory, + performance: PerformancePreset.default, + dateProvider: SystemDateProvider() + ), queue: queue ) - writer.write(value: ["whatever"]) + writer.write(value: ["ok"]) // will create the file + queue.async { try? temporaryDirectory.files()[0].makeReadonly() } + writer.write(value: ["won't be written"]) + queue.async { try? temporaryDirectory.files()[0].makeReadWrite() } waitForWritesCompletion(on: queue, thenFulfill: expectation) waitForExpectations(timeout: 1, handler: nil) XCTAssertEqual(output.recordedLog?.level, .error) - XCTAssertEqual(output.recordedLog?.message, "๐Ÿ”ฅ Failed to write log: I/O exception") + XCTAssertNotNil(output.recordedLog?.message) + XCTAssertTrue(output.recordedLog!.message.contains("You donโ€™t have permission")) + } + + /// NOTE: Test added after incident-4797 + func testWhenIOExceptionsHappenRandomly_theFileIsNeverMalformed() throws { + let expectation = self.expectation(description: "write completed") + let writer = FileWriter( + dataFormat: DataFormat(prefix: "[", suffix: "]", separator: ","), + orchestrator: FilesOrchestrator( + directory: temporaryDirectory, + performance: StoragePerformanceMock( + maxFileSize: .max, + maxDirectorySize: .max, + maxFileAgeForWrite: .distantFuture, // write to single file + minFileAgeForRead: .distantFuture, + maxFileAgeForRead: .distantFuture, + maxObjectsInFile: .max, // write to single file + maxObjectSize: .max + ), + dateProvider: SystemDateProvider() + ), + queue: queue + ) + + let ioInterruptionQueue = DispatchQueue(label: "com.datadohq.file-writer-random-io") + + func randomlyInterruptIO(for file: File?) { + ioInterruptionQueue.async { try? file?.makeReadonly() } + ioInterruptionQueue.async { try? file?.makeReadWrite() } + } + + struct Foo: Codable { + let foo = "bar" + } + + // Write 500 of `Foo`s and interrupt writes randomly + (0..<500).forEach { _ in + writer.write(value: Foo()) + randomlyInterruptIO(for: try? temporaryDirectory.files().first) + } + + ioInterruptionQueue.sync { } + waitForWritesCompletion(on: queue, thenFulfill: expectation) + waitForExpectations(timeout: 5, handler: nil) + XCTAssertEqual(try temporaryDirectory.files().count, 1) + + let fileData = try temporaryDirectory.files()[0].read() + let jsonDecoder = JSONDecoder() + + // Assert that data written is not malformed + let writtenData = try jsonDecoder.decode([Foo].self, from: "[".utf8Data + fileData + "]".utf8Data) + // Assert that some, but not all `Foo`s were skipped + XCTAssertGreaterThan(writtenData.count, 0) + XCTAssertLessThan(writtenData.count, 500) } private func waitForWritesCompletion(on queue: DispatchQueue, thenFulfill expectation: XCTestExpectation) { diff --git a/Tests/DatadogTests/Datadog/Core/Persistence/Files/DirectoryTests.swift b/Tests/DatadogTests/Datadog/Core/Persistence/Files/DirectoryTests.swift index c46bcd03bb..c44233e343 100644 --- a/Tests/DatadogTests/Datadog/Core/Persistence/Files/DirectoryTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Persistence/Files/DirectoryTests.swift @@ -73,9 +73,7 @@ class DirectoryTests: XCTestCase { let files = try directory.files() XCTAssertEqual(files.count, 3) - XCTAssertTrue(files.contains { file in file.url == directory.url.appendingPathComponent("f1") }) - XCTAssertTrue(files.contains { file in file.url == directory.url.appendingPathComponent("f2") }) - XCTAssertTrue(files.contains { file in file.url == directory.url.appendingPathComponent("f3") }) + files.forEach { file in XCTAssertTrue(file.url.relativePath.contains(directory.url.relativePath)) } files.forEach { file in XCTAssertTrue(fileManager.fileExists(atPath: file.url.path)) } } } diff --git a/Tests/DatadogTests/Datadog/Core/Persistence/Files/FileTests.swift b/Tests/DatadogTests/Datadog/Core/Persistence/Files/FileTests.swift index 9f50b0a258..a2ec46c5dd 100644 --- a/Tests/DatadogTests/Datadog/Core/Persistence/Files/FileTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Persistence/Files/FileTests.swift @@ -23,19 +23,15 @@ class FileTests: XCTestCase { func testItAppendsDataToFile() throws { let file = try temporaryDirectory.createFile(named: "file") - try file.append { write in - try write(Data([0x41, 0x41, 0x41, 0x41, 0x41])) // 5 bytes - } + try file.append(data: Data([0x41, 0x41, 0x41, 0x41, 0x41])) // 5 bytes XCTAssertEqual( try Data(contentsOf: file.url), Data([0x41, 0x41, 0x41, 0x41, 0x41]) ) - try file.append { write in - try write(Data([0x42, 0x42, 0x42, 0x42, 0x42])) // 5 bytes - try write(Data([0x41, 0x41, 0x41, 0x41, 0x41])) // 5 bytes - } + try file.append(data: Data([0x42, 0x42, 0x42, 0x42, 0x42])) // 5 bytes + try file.append(data: Data([0x41, 0x41, 0x41, 0x41, 0x41])) // 5 bytes XCTAssertEqual( try Data(contentsOf: file.url), @@ -51,12 +47,12 @@ class FileTests: XCTestCase { func testItReadsDataFromFile() throws { let file = try temporaryDirectory.createFile(named: "file") - try file.append { write in try write("Hello ๐Ÿ‘‹".utf8Data) } + try file.append(data: "Hello ๐Ÿ‘‹".utf8Data) XCTAssertEqual(try file.read().utf8String, "Hello ๐Ÿ‘‹") } - func tetsItDeletesFile() throws { + func testItDeletesFile() throws { let file = try temporaryDirectory.createFile(named: "file") XCTAssertTrue(fileManager.fileExists(atPath: file.url.path)) @@ -68,10 +64,29 @@ class FileTests: XCTestCase { func testItReturnsFileSize() throws { let file = try temporaryDirectory.createFile(named: "file") - try file.append { write in try write(.mock(ofSize: 5)) } + try file.append(data: .mock(ofSize: 5)) XCTAssertEqual(try file.size(), 5) - try file.append { write in try write(.mock(ofSize: 10)) } + try file.append(data: .mock(ofSize: 10)) XCTAssertEqual(try file.size(), 15) } + + func testWhenIOExceptionHappens_itThrowsWhenWritting() throws { + let file = try temporaryDirectory.createFile(named: "file") + try file.delete() + + XCTAssertThrowsError(try file.append(data: .mock(ofSize: 5))) { error in + XCTAssertEqual((error as NSError).localizedDescription, "The file โ€œfileโ€ doesnโ€™t exist.") + } + } + + func testWhenIOExceptionHappens_itThrowsWhenReading() throws { + let file = try temporaryDirectory.createFile(named: "file") + try file.append(data: .mock(ofSize: 5)) + try file.delete() + + XCTAssertThrowsError(try file.read()) { error in + XCTAssertEqual((error as NSError).localizedDescription, "The file โ€œfileโ€ doesnโ€™t exist.") + } + } } diff --git a/Tests/DatadogTests/Datadog/Core/Persistence/FilesOrchestratorTests.swift b/Tests/DatadogTests/Datadog/Core/Persistence/FilesOrchestratorTests.swift index 9f109ee623..7729436e5f 100644 --- a/Tests/DatadogTests/Datadog/Core/Persistence/FilesOrchestratorTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Persistence/FilesOrchestratorTests.swift @@ -8,8 +8,7 @@ import XCTest @testable import Datadog class FilesOrchestratorTests: XCTestCase { - private let defaultWriteConditions = WritableFileConditions(performance: .default) - private let defaultReadConditions = ReadableFileConditions(performance: .default) + private let performance: PerformancePreset = .default override func setUp() { super.setUp() @@ -25,8 +24,7 @@ class FilesOrchestratorTests: XCTestCase { private func configureOrchestrator(using dateProvider: DateProvider) -> FilesOrchestrator { return FilesOrchestrator( directory: temporaryDirectory, - writeConditions: defaultWriteConditions, - readConditions: defaultReadConditions, + performance: performance, dateProvider: dateProvider ) } @@ -57,7 +55,7 @@ class FilesOrchestratorTests: XCTestCase { var nextFile: WritableFile // use file maximum number of times - for _ in (0 ..< defaultWriteConditions.maxNumberOfUsesOfFile).dropLast() { // skip first use + for _ in (0 ..< performance.maxObjectsInFile).dropLast() { // skip first use nextFile = try orchestrator.getWritableFile(writeSize: 1) XCTAssertEqual(nextFile.name, previousFile.name) // assert it uses same file previousFile = nextFile @@ -71,12 +69,12 @@ class FilesOrchestratorTests: XCTestCase { func testGivenDefaultWriteConditions_whenFileHasNoRoomForMore_itCreatesNewFile() throws { let orchestrator = configureOrchestrator(using: RelativeDateProvider(advancingBySeconds: 1)) let chunkedData: [Data] = .mockChunksOf( - totalSize: defaultWriteConditions.maxFileSize, - maxChunkSize: defaultWriteConditions.maxWriteSize + totalSize: performance.maxFileSize, + maxChunkSize: performance.maxObjectSize ) - let file1 = try orchestrator.getWritableFile(writeSize: UInt64(defaultWriteConditions.maxWriteSize)) - try file1.append { write in try chunkedData.forEach { chunk in try write(chunk) } } + let file1 = try orchestrator.getWritableFile(writeSize: performance.maxObjectSize) + try chunkedData.forEach { chunk in try file1.append(data: chunk) } let file2 = try orchestrator.getWritableFile(writeSize: 1) XCTAssertNotEqual(file1.name, file2.name) @@ -87,7 +85,7 @@ class FilesOrchestratorTests: XCTestCase { let orchestrator = configureOrchestrator(using: dateProvider) let file1 = try orchestrator.getWritableFile(writeSize: 1) - dateProvider.advance(bySeconds: 1 + defaultWriteConditions.maxFileAgeForWrite) + dateProvider.advance(bySeconds: 1 + performance.maxFileAgeForWrite) let file2 = try orchestrator.getWritableFile(writeSize: 1) XCTAssertNotEqual(file1.name, file2.name) @@ -123,30 +121,29 @@ class FilesOrchestratorTests: XCTestCase { let orchestrator = FilesOrchestrator( directory: temporaryDirectory, - writeConditions: WritableFileConditions( - performance: .mockWith( - maxBatchSize: oneMB, // 1MB - maxSizeOfLogsDirectory: 3 * oneMB, // 3MB, - maxFileAgeForWrite: .distantFuture, - maxLogsPerBatch: 1, // create new file each time - maxLogSize: .max - ) + performance: StoragePerformanceMock( + maxFileSize: oneMB, // 1MB + maxDirectorySize: 3 * oneMB, // 3MB, + maxFileAgeForWrite: .distantFuture, + minFileAgeForRead: .mockAny(), + maxFileAgeForRead: .mockAny(), + maxObjectsInFile: 1, // create new file each time + maxObjectSize: .max ), - readConditions: defaultReadConditions, dateProvider: RelativeDateProvider(advancingBySeconds: 1) ) // write 1MB to first file (1MB of directory size in total) let file1 = try orchestrator.getWritableFile(writeSize: oneMB) - try file1.append { write in try write(.mock(ofSize: oneMB)) } + try file1.append(data: .mock(ofSize: oneMB)) // write 1MB to second file (2MB of directory size in total) let file2 = try orchestrator.getWritableFile(writeSize: oneMB) - try file2.append { write in try write(.mock(ofSize: oneMB)) } + try file2.append(data: .mock(ofSize: oneMB)) // write 1MB to third file (3MB of directory size in total) let file3 = try orchestrator.getWritableFile(writeSize: oneMB + 1) // +1 byte to exceed the limit - try file3.append { write in try write(.mock(ofSize: oneMB + 1)) } + try file3.append(data: .mock(ofSize: oneMB + 1)) XCTAssertEqual(try temporaryDirectory.files().count, 3) @@ -155,7 +152,7 @@ class FilesOrchestratorTests: XCTestCase { let file4 = try orchestrator.getWritableFile(writeSize: oneMB) XCTAssertEqual(try temporaryDirectory.files().count, 3) XCTAssertNil(try? temporaryDirectory.file(named: file1.name)) - try file4.append { write in try write(.mock(ofSize: oneMB + 1)) } + try file4.append(data: .mock(ofSize: oneMB + 1)) _ = try orchestrator.getWritableFile(writeSize: oneMB) XCTAssertEqual(try temporaryDirectory.files().count, 3) @@ -167,7 +164,7 @@ class FilesOrchestratorTests: XCTestCase { func testGivenDefaultReadConditions_whenThereAreNoFiles_itReturnsNil() throws { let dateProvider = RelativeDateProvider() let orchestrator = configureOrchestrator(using: dateProvider) - dateProvider.advance(bySeconds: 1 + defaultReadConditions.minFileAgeForRead) + dateProvider.advance(bySeconds: 1 + performance.minFileAgeForRead) XCTAssertNil(orchestrator.getReadableFile()) } @@ -176,7 +173,7 @@ class FilesOrchestratorTests: XCTestCase { let orchestrator = configureOrchestrator(using: dateProvider) let file = try temporaryDirectory.createFile(named: dateProvider.currentDate().toFileName) - dateProvider.advance(bySeconds: 1 + defaultReadConditions.minFileAgeForRead) + dateProvider.advance(bySeconds: 1 + performance.minFileAgeForRead) XCTAssertEqual(orchestrator.getReadableFile()?.name, file.name) } @@ -185,7 +182,7 @@ class FilesOrchestratorTests: XCTestCase { let orchestrator = configureOrchestrator(using: dateProvider) _ = try temporaryDirectory.createFile(named: dateProvider.currentDate().toFileName) - dateProvider.advance(bySeconds: 0.5 * defaultReadConditions.minFileAgeForRead) + dateProvider.advance(bySeconds: 0.5 * performance.minFileAgeForRead) XCTAssertNil(orchestrator.getReadableFile()) } @@ -195,7 +192,7 @@ class FilesOrchestratorTests: XCTestCase { let fileNames = (0..<4).map { _ in dateProvider.currentDate().toFileName } try fileNames.forEach { fileName in _ = try temporaryDirectory.createFile(named: fileName) } - dateProvider.advance(bySeconds: 1 + defaultReadConditions.minFileAgeForRead) + dateProvider.advance(bySeconds: 1 + performance.minFileAgeForRead) XCTAssertEqual(orchestrator.getReadableFile()?.name, fileNames[0]) try temporaryDirectory.file(named: fileNames[0]).delete() XCTAssertEqual(orchestrator.getReadableFile()?.name, fileNames[1]) @@ -213,7 +210,7 @@ class FilesOrchestratorTests: XCTestCase { let fileNames = (0..<4).map { _ in dateProvider.currentDate().toFileName } try fileNames.forEach { fileName in _ = try temporaryDirectory.createFile(named: fileName) } - dateProvider.advance(bySeconds: 1 + defaultReadConditions.minFileAgeForRead) + dateProvider.advance(bySeconds: 1 + performance.minFileAgeForRead) XCTAssertEqual( orchestrator.getReadableFile(excludingFilesNamed: Set(fileNames[0...2]))?.name, @@ -226,7 +223,7 @@ class FilesOrchestratorTests: XCTestCase { let orchestrator = configureOrchestrator(using: dateProvider) _ = try temporaryDirectory.createFile(named: dateProvider.currentDate().toFileName) - dateProvider.advance(bySeconds: 2 * defaultReadConditions.maxFileAgeForRead) + dateProvider.advance(bySeconds: 2 * performance.maxFileAgeForRead) XCTAssertNil(orchestrator.getReadableFile()) XCTAssertEqual(try temporaryDirectory.files().count, 0) @@ -237,7 +234,7 @@ class FilesOrchestratorTests: XCTestCase { let orchestrator = configureOrchestrator(using: dateProvider) _ = try temporaryDirectory.createFile(named: dateProvider.currentDate().toFileName) - dateProvider.advance(bySeconds: 1 + defaultReadConditions.minFileAgeForRead) + dateProvider.advance(bySeconds: 1 + performance.minFileAgeForRead) let readableFile = try orchestrator.getReadableFile().unwrapOrThrow() XCTAssertEqual(try temporaryDirectory.files().count, 1) diff --git a/Tests/DatadogTests/Datadog/Core/System/NetworkConnectionInfoProviderTests.swift b/Tests/DatadogTests/Datadog/Core/System/NetworkConnectionInfoProviderTests.swift index 2b85b87b3b..ec0ad3763c 100644 --- a/Tests/DatadogTests/Datadog/Core/System/NetworkConnectionInfoProviderTests.swift +++ b/Tests/DatadogTests/Datadog/Core/System/NetworkConnectionInfoProviderTests.swift @@ -25,42 +25,45 @@ class NetworkConnectionInfoProviderTests: XCTestCase { // MARK: - iOS 12+ - @available(iOS 12, *) func testNWPathNetworkConnectionInfoProviderGivesValue() { - let provider = NWPathNetworkConnectionInfoProvider() + if #available(iOS 12.0, *) { + let provider = NWPathNetworkConnectionInfoProvider() - pullNetworkConnectionInfo( - from: provider, - on: DispatchQueue(label: "com.datadoghq.pulling-NWPathNetworkConnectionInfoProvider", target: .global(qos: .utility)), - thenFulfill: expectation(description: "Receive `NetworkConnectionInfo` from `NWPathNetworkConnectionInfoProvider`") - ) + pullNetworkConnectionInfo( + from: provider, + on: DispatchQueue(label: "com.datadoghq.pulling-NWPathNetworkConnectionInfoProvider", target: .global(qos: .utility)), + thenFulfill: expectation(description: "Receive `NetworkConnectionInfo` from `NWPathNetworkConnectionInfoProvider`") + ) - waitForExpectations(timeout: 1, handler: nil) + waitForExpectations(timeout: 1, handler: nil) + } } - @available(iOS 12, *) func testNWPathNetworkConnectionInfoProviderCanBeSafelyAccessedFromConcurrentThreads() { - let provider = NWPathNetworkConnectionInfoProvider() + if #available(iOS 12.0, *) { + let provider = NWPathNetworkConnectionInfoProvider() - DispatchQueue.concurrentPerform(iterations: 1_000) { _ in - _ = provider.current + DispatchQueue.concurrentPerform(iterations: 1_000) { _ in + _ = provider.current + } } } - @available(iOS 12, *) func testNWPathMonitorHandling() { - weak var nwPathMonitorWeakReference: NWPathMonitor? + if #available(iOS 12.0, *) { + weak var nwPathMonitorWeakReference: NWPathMonitor? - autoreleasepool { - let nwPathMonitor = NWPathMonitor() - _ = NWPathNetworkConnectionInfoProvider(monitor: nwPathMonitor) - nwPathMonitorWeakReference = nwPathMonitor - XCTAssertNotNil(nwPathMonitor.queue, "`NWPathMonitor` is started with synchronization queue") - } + autoreleasepool { + let nwPathMonitor = NWPathMonitor() + _ = NWPathNetworkConnectionInfoProvider(monitor: nwPathMonitor) + nwPathMonitorWeakReference = nwPathMonitor + XCTAssertNotNil(nwPathMonitor.queue, "`NWPathMonitor` is started with synchronization queue") + } - Thread.sleep(forTimeInterval: 0.5) + Thread.sleep(forTimeInterval: 0.5) - XCTAssertNil(nwPathMonitorWeakReference, "`NWPathMonitor` is deallocated with `NWPathNetworkConnectionInfoProvider`") + XCTAssertNil(nwPathMonitorWeakReference, "`NWPathMonitor` is deallocated with `NWPathNetworkConnectionInfoProvider`") + } } // MARK: - iOS 11 @@ -90,21 +93,23 @@ class NetworkConnectionInfoConversionTests: XCTestCase { typealias Reachability = NetworkConnectionInfo.Reachability typealias Interface = NetworkConnectionInfo.Interface - @available(iOS 12, *) func testNWPathStatus() { - XCTAssertEqual(Reachability(from: .satisfied), .yes) - XCTAssertEqual(Reachability(from: .unsatisfied), .no) - XCTAssertEqual(Reachability(from: .requiresConnection), .maybe) + if #available(iOS 12.0, *) { + XCTAssertEqual(Reachability(from: .satisfied), .yes) + XCTAssertEqual(Reachability(from: .unsatisfied), .no) + XCTAssertEqual(Reachability(from: .requiresConnection), .maybe) + } } - @available(iOS 12, *) func testNWInterface() { - XCTAssertEqual(Array(fromInterfaceTypes: []), []) - XCTAssertEqual(Array(fromInterfaceTypes: [.wifi]), [.wifi]) - XCTAssertEqual(Array(fromInterfaceTypes: [.wiredEthernet]), [.wiredEthernet]) - XCTAssertEqual(Array(fromInterfaceTypes: [.wifi, .wifi]), [.wifi, .wifi]) - XCTAssertEqual(Array(fromInterfaceTypes: [.wifi, .cellular]), [.wifi, .cellular]) - XCTAssertEqual(Array(fromInterfaceTypes: [.loopback, .other]), [.loopback, .other]) + if #available(iOS 12.0, *) { + XCTAssertEqual(Array(fromInterfaceTypes: []), []) + XCTAssertEqual(Array(fromInterfaceTypes: [.wifi]), [.wifi]) + XCTAssertEqual(Array(fromInterfaceTypes: [.wiredEthernet]), [.wiredEthernet]) + XCTAssertEqual(Array(fromInterfaceTypes: [.wifi, .wifi]), [.wifi, .wifi]) + XCTAssertEqual(Array(fromInterfaceTypes: [.wifi, .cellular]), [.wifi, .cellular]) + XCTAssertEqual(Array(fromInterfaceTypes: [.loopback, .other]), [.loopback, .other]) + } } func testSCReachability() { diff --git a/Tests/DatadogTests/Datadog/Core/Upload/DataUploadWorkerTests.swift b/Tests/DatadogTests/Datadog/Core/Upload/DataUploadWorkerTests.swift index 2b4b32bf45..c0d1f98a25 100644 --- a/Tests/DatadogTests/Datadog/Core/Upload/DataUploadWorkerTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Upload/DataUploadWorkerTests.swift @@ -21,16 +21,23 @@ class DataUploadWorkerTests: XCTestCase { super.tearDown() } - func testItUploadsAllLogs() throws { + func testItUploadsAllData() throws { let dateProvider = RelativeDateProvider(advancingBySeconds: 1) let orchestrator = FilesOrchestrator( directory: temporaryDirectory, - writeConditions: .mockWriteToNewFileEachTime(), - readConditions: .mockReadAllFiles(), + performance: StoragePerformanceMock.writeEachObjectToNewFileAndReadAllFiles, dateProvider: dateProvider ) - let writer = FileWriter(orchestrator: orchestrator, queue: fileReadWriteQueue) - let reader = FileReader(orchestrator: orchestrator, queue: fileReadWriteQueue) + let writer = FileWriter( + dataFormat: .mockWith(prefix: "[", suffix: "]"), + orchestrator: orchestrator, + queue: fileReadWriteQueue + ) + let reader = FileReader( + dataFormat: .mockWith(prefix: "[", suffix: "]"), + orchestrator: orchestrator, + queue: fileReadWriteQueue + ) let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) let dataUploader = DataUploader( urlProvider: .mockAny(), @@ -48,12 +55,26 @@ class DataUploadWorkerTests: XCTestCase { queue: uploaderQueue, fileReader: reader, dataUploader: dataUploader, - uploadConditions: .mockAlwaysPerformingUpload(), - delay: .mockConstantDelay(of: 0.1) + uploadConditions: DataUploadConditions( + batteryStatus: BatteryStatusProviderMock.mockWith( + status: BatteryStatus(state: .full, level: 100, isLowPowerModeEnabled: false) // always upload + ), + networkConnectionInfo: NetworkConnectionInfoProviderMock( + networkConnectionInfo: NetworkConnectionInfo( + reachability: .yes, // always upload + availableInterfaces: [.wifi], + supportsIPv4: true, + supportsIPv6: true, + isExpensive: false, + isConstrained: false + ) + ) + ), + delay: DataUploadDelay(performance: UploadPerformanceMock.veryQuick), + featureName: .mockAny() ) - let timeout: TimeInterval = 1 // enough to send 3 logs with 0.1 second interval - let recordedRequests = server.waitAndReturnRequests(count: 3, timeout: timeout) + let recordedRequests = server.waitAndReturnRequests(count: 3) XCTAssertTrue(recordedRequests.contains { $0.httpBody == #"[{"k1":"v1"}]"#.utf8Data }) XCTAssertTrue(recordedRequests.contains { $0.httpBody == #"[{"k2":"v2"}]"#.utf8Data }) XCTAssertTrue(recordedRequests.contains { $0.httpBody == #"[{"k3":"v3"}]"#.utf8Data }) diff --git a/Tests/DatadogTests/Datadog/Core/Upload/DataUploaderTests.swift b/Tests/DatadogTests/Datadog/Core/Upload/DataUploaderTests.swift index 6df277bef9..10542d9430 100644 --- a/Tests/DatadogTests/Datadog/Core/Upload/DataUploaderTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Upload/DataUploaderTests.swift @@ -8,12 +8,40 @@ import XCTest @testable import Datadog class DataUploadURLProviderTests: XCTestCase { - func testItBuildsValidURL() throws { + func testDDSourceQueryItem() { + let item: UploadURLProvider.QueryItemProvider = .ddsource() + + XCTAssertEqual(item.value().name, "ddsource") + XCTAssertEqual(item.value().value, "ios") + } + + func testBatchTimeQueryItem() { let dateProvider = RelativeDateProvider(using: Date.mockDecember15th2019At10AMUTC()) + let item: UploadURLProvider.QueryItemProvider = .batchTime(using: dateProvider) + + XCTAssertEqual(item.value().name, "batch_time") + XCTAssertEqual(item.value().value, "1576404000000") + dateProvider.advance(bySeconds: 9.999) + XCTAssertEqual(item.value().name, "batch_time") + XCTAssertEqual(item.value().value, "1576404009999") + } + + func testItBuildsValidURLUsingNoQueryItems() throws { let urlProvider = UploadURLProvider( urlWithClientToken: URL(string: "https://api.example.com/v1/endpoint/abc")!, - dateProvider: dateProvider + queryItemProviders: [] ) + + XCTAssertEqual(urlProvider.url, URL(string: "https://api.example.com/v1/endpoint/abc?")) + } + + func testItBuildsValidURLUsingAllQueryItems() throws { + let dateProvider = RelativeDateProvider(using: Date.mockDecember15th2019At10AMUTC()) + let urlProvider = UploadURLProvider( + urlWithClientToken: URL(string: "https://api.example.com/v1/endpoint/abc")!, + queryItemProviders: [.ddsource(), .batchTime(using: dateProvider)] + ) + XCTAssertEqual(urlProvider.url, URL(string: "https://api.example.com/v1/endpoint/abc?ddsource=ios&batch_time=1576404000000")) dateProvider.advance(bySeconds: 9.999) XCTAssertEqual(urlProvider.url, URL(string: "https://api.example.com/v1/endpoint/abc?ddsource=ios&batch_time=1576404009999")) @@ -100,25 +128,4 @@ class DataUploaderTests: XCTestCase { XCTAssertEqual(status, .unknown) server.waitFor(requestsCompletion: 1) } - - // MARK: - HTTP Headers - - func testItSendsCustomHTTPHeaders() { - let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) - let uploader = DataUploader( - urlProvider: .mockAny(), - httpClient: HTTPClient(session: server.urlSession), - httpHeaders: HTTPHeaders( - appName: "app-name", - appVersion: "1.0.0", - device: .mockWith(model: "iPhone", osName: "iOS", osVersion: "13.3.1") - ) - ) - - _ = uploader.upload(data: .mockAny()) - - let request = server.waitAndReturnRequests(count: 1)[0] - XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json") - XCTAssertEqual(request.allHTTPHeaderFields?["User-Agent"], "app-name/1.0.0 CFNetwork (iPhone; iOS/13.3.1)") - } } diff --git a/Tests/DatadogTests/Datadog/Core/Upload/HTTPHeadersTests.swift b/Tests/DatadogTests/Datadog/Core/Upload/HTTPHeadersTests.swift index 67f0d77158..17873d3e32 100644 --- a/Tests/DatadogTests/Datadog/Core/Upload/HTTPHeadersTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Upload/HTTPHeadersTests.swift @@ -8,18 +8,43 @@ import XCTest @testable import Datadog class HTTPHeadersTests: XCTestCase { - func testWhenRunningOnMobileDevice_itCreatesExpectedHeaders() { - let headers = HTTPHeaders( - appName: "app-name", - appVersion: "1.0.0", + func testContentTypeHeader() { + let applicationJSON = HTTPHeaders.HTTPHeader.contentTypeHeader(contentType: .applicationJSON) + XCTAssertEqual(applicationJSON.field, "Content-Type") + XCTAssertEqual(applicationJSON.value, "application/json") + + let plainText = HTTPHeaders.HTTPHeader.contentTypeHeader(contentType: .textPlainUTF8) + XCTAssertEqual(plainText.field, "Content-Type") + XCTAssertEqual(plainText.value, "text/plain;charset=UTF-8") + } + + func testUserAgentHeader() { + let userAgent = HTTPHeaders.HTTPHeader.userAgentHeader( + appName: "FoobarApp", + appVersion: "1.2.3", device: .mockWith(model: "iPhone", osName: "iOS", osVersion: "13.3.1") ) + XCTAssertEqual(userAgent.field, "User-Agent") + XCTAssertEqual(userAgent.value, "FoobarApp/1.2.3 CFNetwork (iPhone; iOS/13.3.1)") + } + + func testComposingHeaders() { + let headers = HTTPHeaders( + headers: [ + .contentTypeHeader(contentType: .applicationJSON), + .userAgentHeader( + appName: "FoobarApp", + appVersion: "1.2.3", + device: .mockWith(model: "iPhone", osName: "iOS", osVersion: "13.3.1") + ) + ] + ) XCTAssertEqual( headers.all, [ - "Content-Type": "application/json", - "User-Agent": "app-name/1.0.0 CFNetwork (iPhone; iOS/13.3.1)" + "Content-Type": "application/json", + "User-Agent": "FoobarApp/1.2.3 CFNetwork (iPhone; iOS/13.3.1)" ] ) } diff --git a/Tests/DatadogTests/Datadog/Core/Upload/LogsUploadDelayTests.swift b/Tests/DatadogTests/Datadog/Core/Upload/LogsUploadDelayTests.swift index 0f21486988..f5a52a887f 100644 --- a/Tests/DatadogTests/Datadog/Core/Upload/LogsUploadDelayTests.swift +++ b/Tests/DatadogTests/Datadog/Core/Upload/LogsUploadDelayTests.swift @@ -8,46 +8,46 @@ import XCTest @testable import Datadog class DataUploadDelayTests: XCTestCase { - private let mockPerformancePreset: PerformancePreset = .mockWith( - initialLogsUploadDelay: 3, - defaultLogsUploadDelay: 5, - minLogsUploadDelay: 1, - maxLogsUploadDelay: 20, - logsUploadDelayDecreaseFactor: 0.9 + private let mockPerformance = UploadPerformanceMock( + initialUploadDelay: 3, + defaultUploadDelay: 5, + minUploadDelay: 1, + maxUploadDelay: 20, + uploadDelayDecreaseFactor: 0.9 ) func testWhenNotModified_itReturnsInitialDelay() { - var delay = DataUploadDelay(performance: mockPerformancePreset) - XCTAssertEqual(delay.nextUploadDelay(), mockPerformancePreset.initialLogsUploadDelay) - XCTAssertEqual(delay.nextUploadDelay(), mockPerformancePreset.initialLogsUploadDelay) + var delay = DataUploadDelay(performance: mockPerformance) + XCTAssertEqual(delay.nextUploadDelay(), mockPerformance.initialUploadDelay) + XCTAssertEqual(delay.nextUploadDelay(), mockPerformance.initialUploadDelay) } func testWhenDecreasing_itGoesDownToMinimumDelay() { - var delay = DataUploadDelay(performance: mockPerformancePreset) + var delay = DataUploadDelay(performance: mockPerformance) var previousValue: TimeInterval = delay.nextUploadDelay() - while previousValue != mockPerformancePreset.minLogsUploadDelay { + while previousValue != mockPerformance.minUploadDelay { delay.decrease() let nextValue = delay.nextUploadDelay() XCTAssertEqual( nextValue / previousValue, - mockPerformancePreset.logsUploadDelayDecreaseFactor, + mockPerformance.uploadDelayDecreaseFactor, accuracy: 0.1 ) - XCTAssertLessThanOrEqual(nextValue, max(previousValue, mockPerformancePreset.minLogsUploadDelay)) + XCTAssertLessThanOrEqual(nextValue, max(previousValue, mockPerformance.minUploadDelay)) previousValue = nextValue } } func testWhenIncreasedOnce_itReturnsMaximumDelayOnceThenGoesBackToDefaultDelay() { - var delay = DataUploadDelay(performance: mockPerformancePreset) + var delay = DataUploadDelay(performance: mockPerformance) delay.decrease() delay.increaseOnce() - XCTAssertEqual(delay.nextUploadDelay(), mockPerformancePreset.maxLogsUploadDelay) - XCTAssertEqual(delay.nextUploadDelay(), mockPerformancePreset.defaultLogsUploadDelay) - XCTAssertEqual(delay.nextUploadDelay(), mockPerformancePreset.defaultLogsUploadDelay) + XCTAssertEqual(delay.nextUploadDelay(), mockPerformance.maxUploadDelay) + XCTAssertEqual(delay.nextUploadDelay(), mockPerformance.defaultUploadDelay) + XCTAssertEqual(delay.nextUploadDelay(), mockPerformance.defaultUploadDelay) } } diff --git a/Tests/DatadogTests/Datadog/Core/Utils/DateFormattingTests.swift b/Tests/DatadogTests/Datadog/Core/Utils/DateFormattingTests.swift new file mode 100644 index 0000000000..c238e287eb --- /dev/null +++ b/Tests/DatadogTests/Datadog/Core/Utils/DateFormattingTests.swift @@ -0,0 +1,30 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class DateFormattingTests: XCTestCase { + private let date: Date = .mockDecember15th2019At10AMUTC(addingTimeInterval: 0.001) + + func testISO8601DateFormatter() { + XCTAssertEqual( + iso8601DateFormatter.string(from: date), + "2019-12-15T10:00:00.001Z" + ) + } + + func testPresentationDateFormatter() { + XCTAssertEqual( + presentationDateFormatter(withTimeZone: .UTC).string(from: date), + "10:00:00.001" + ) + XCTAssertEqual( + presentationDateFormatter(withTimeZone: .EET).string(from: date), + "12:00:00.001" + ) + } +} diff --git a/Tests/DatadogTests/Datadog/Core/Utils/EncodableValueTests.swift b/Tests/DatadogTests/Datadog/Core/Utils/EncodableValueTests.swift new file mode 100644 index 0000000000..efd378cad2 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Core/Utils/EncodableValueTests.swift @@ -0,0 +1,93 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class EncodableValueTests: XCTestCase { + func testItEncodesDifferentEncodableValues() throws { + let encoder = JSONEncoder() + + XCTAssertEqual( + try encoder.encode(EncodingContainer(EncodableValue("string"))).utf8String, + #"{"value":"string"}"# + ) + XCTAssertEqual( + try encoder.encode(EncodingContainer(EncodableValue(123))).utf8String, + #"{"value":123}"# + ) + XCTAssertEqual( + try encoder.encode(EncodableValue(["a", "b", "c"])).utf8String, + #"["a","b","c"]"# + ) + XCTAssertEqual( + try encoder.encode( + EncodingContainer(EncodableValue(URL(string: "https://example.com/image.png")!)) + ).utf8String, + #"{"value":"https:\/\/example.com\/image.png"}"# + ) + struct Foo: Encodable { + let bar = "bar_" + let bizz = "bizz_" + } + XCTAssertEqual( + try encoder.encode(EncodableValue(Foo())).utf8String, + #"{"bar":"bar_","bizz":"bizz_"}"# + ) + } +} + +class JSONStringEncodableValueTests: XCTestCase { + func testItEncodesDifferentEncodableValuesAsString() throws { + let encoder = JSONEncoder() + + XCTAssertEqual( + try encoder.encode( + EncodingContainer(JSONStringEncodableValue("string", encodedUsing: JSONEncoder())) + ).utf8String, + #"{"value":"string"}"# + ) + XCTAssertEqual( + try encoder.encode( + EncodingContainer(JSONStringEncodableValue(123, encodedUsing: JSONEncoder())) + ).utf8String, + #"{"value":"123"}"# + ) + XCTAssertEqual( + try encoder.encode( + EncodingContainer(JSONStringEncodableValue(["a", "b", "c"], encodedUsing: JSONEncoder())) + ).utf8String, + #"{"value":"[\"a\",\"b\",\"c\"]"}"# + ) + XCTAssertEqual( + try encoder.encode( + EncodingContainer( + JSONStringEncodableValue(URL(string: "https://example.com/image.png")!, encodedUsing: JSONEncoder()) + ) + ).utf8String, + #"{"value":"https:\/\/example.com\/image.png"}"# + ) + struct Foo: Encodable { + let bar = "bar_" + let bizz = "bizz_" + } + XCTAssertEqual( + try encoder.encode( + EncodingContainer(JSONStringEncodableValue(Foo(), encodedUsing: JSONEncoder())) + ).utf8String, + #"{"value":"{\"bar\":\"bar_\",\"bizz\":\"bizz_\"}"}"# + ) + } + + func testWhenValueCannotBeEncoded_itThrowsErrorDuringEncoderInvocation() { + let encoder = JSONEncoder() + let value = JSONStringEncodableValue(FailingEncodableMock(errorMessage: "ops..."), encodedUsing: JSONEncoder()) + + XCTAssertThrowsError(try encoder.encode(value)) { error in + XCTAssertEqual((error as? ErrorMock)?.description, "ops...") + } + } +} diff --git a/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift b/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift new file mode 100644 index 0000000000..5e4fe43f8f --- /dev/null +++ b/Tests/DatadogTests/Datadog/Core/Utils/JSONEncoderTests.swift @@ -0,0 +1,32 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class JSONEncoderTests: XCTestCase { + private let jsonEncoder = JSONEncoder.default() + + func testDateEncoding() throws { + let encodedDate = try jsonEncoder.encode( + EncodingContainer(Date.mockDecember15th2019At10AMUTC(addingTimeInterval: 0.123)) + ) + + XCTAssertEqual(encodedDate.utf8String, #"{"value":"2019-12-15T10:00:00.123Z"}"#) + } + + func testURLEncoding() throws { + let encodedURL = try jsonEncoder.encode( + EncodingContainer(URL(string: "https://example.com/foo")!) + ) + + if #available(iOS 13.0, OSX 10.15, *) { + XCTAssertEqual(encodedURL.utf8String, #"{"value":"https://example.com/foo"}"#) + } else { + XCTAssertEqual(encodedURL.utf8String, #"{"value":"https:\/\/example.com\/foo"}"#) + } + } +} diff --git a/Tests/DatadogTests/Datadog/DatadogConfigurationTests.swift b/Tests/DatadogTests/Datadog/DatadogConfigurationTests.swift index 490b90120d..fb59e6a5eb 100644 --- a/Tests/DatadogTests/Datadog/DatadogConfigurationTests.swift +++ b/Tests/DatadogTests/Datadog/DatadogConfigurationTests.swift @@ -13,17 +13,24 @@ class DatadogConfigurationTests: XCTestCase { func testDefaultConfiguration() { let defaultConfiguration = Configuration.builderUsing(clientToken: "abcd", environment: "tests").build() XCTAssertEqual(defaultConfiguration.clientToken, "abcd") - XCTAssertEqual(defaultConfiguration.logsEndpoint.url, "https://mobile-http-intake.logs.datadoghq.com/v1/input/") XCTAssertEqual(defaultConfiguration.environment, "tests") + XCTAssertTrue(defaultConfiguration.loggingEnabled) + XCTAssertTrue(defaultConfiguration.tracingEnabled) + XCTAssertEqual(defaultConfiguration.logsEndpoint.url, "https://mobile-http-intake.logs.datadoghq.com/v1/input/") + XCTAssertEqual(defaultConfiguration.tracesEndpoint.url, "https://public-trace-http-intake.logs.datadoghq.com/v1/input/") XCTAssertNil(defaultConfiguration.serviceName) } func testCustomConfiguration() { let configuration = Configuration.builderUsing(clientToken: "abcd", environment: "tests") .set(serviceName: "service-name") + .enableLogging(false) + .enableTracing(false) .build() XCTAssertEqual(configuration.clientToken, "abcd") XCTAssertEqual(configuration.environment, "tests") + XCTAssertFalse(configuration.loggingEnabled) + XCTAssertFalse(configuration.tracingEnabled) XCTAssertEqual(configuration.serviceName, "service-name") } @@ -45,6 +52,23 @@ class DatadogConfigurationTests: XCTestCase { .build() XCTAssertEqual(configuration.logsEndpoint.url, "https://api.example.com/v1/logs/") } + + func testTracingEndpoints() { + var configuration = Configuration.builderUsing(clientToken: .mockAny(), environment: .mockAny()) + .set(tracesEndpoint: .us) + .build() + XCTAssertEqual(configuration.tracesEndpoint.url, "https://public-trace-http-intake.logs.datadoghq.com/v1/input/") + + configuration = Configuration.builderUsing(clientToken: .mockAny(), environment: .mockAny()) + .set(tracesEndpoint: .eu) + .build() + XCTAssertEqual(configuration.tracesEndpoint.url, "https://public-trace-http-intake.logs.datadoghq.eu/v1/input/") + + configuration = Configuration.builderUsing(clientToken: .mockAny(), environment: .mockAny()) + .set(tracesEndpoint: .custom(url: "https://api.example.com/v1/traces/")) + .build() + XCTAssertEqual(configuration.tracesEndpoint.url, "https://api.example.com/v1/traces/") + } } class DatadogValidConfigurationTests: XCTestCase { @@ -145,7 +169,7 @@ class DatadogValidConfigurationTests: XCTestCase { XCTAssertThrowsError(try Configuration(configuration: .mockWith(environment: environment), appContext: .mockAny())) { error in XCTAssertEqual( (error as? ProgrammerError)?.description, - "Datadog SDK usage error: `environment` contains illegal characters (only alphanumerics and `_` are allowed)" + "๐Ÿ”ฅ Datadog SDK usage error: `environment` contains illegal characters (only alphanumerics and `_` are allowed)" ) } } @@ -160,56 +184,60 @@ class DatadogValidConfigurationTests: XCTestCase { } func testLogsUploadURLValidation() throws { - func verify(clientToken: String, logsEndpoint: Datadog.Configuration.LogsEndpoint, expectedLogsUploadURL: URL) throws { - // it equals `Datadog.Configuration.environment` - let configuration = try Configuration( - configuration: .mockWith(clientToken: clientToken, logsEndpoint: logsEndpoint), + func configurationWith( + clientToken: String = "abc", + logsEndpoint: Datadog.Configuration.LogsEndpoint = .us, + tracesEndpoint: Datadog.Configuration.TracesEndpoint = .us + ) throws -> Configuration { + return try Configuration( + configuration: .mockWith(clientToken: clientToken, logsEndpoint: logsEndpoint, tracesEndpoint: tracesEndpoint), appContext: .mockAny() ) - XCTAssertEqual(configuration.logsUploadURLWithClientToken, expectedLogsUploadURL) - } - func verify(clientToken: String, logsEndpoint: Datadog.Configuration.LogsEndpoint, expectedError: String) { - XCTAssertThrowsError( - try Configuration( - configuration: .mockWith(clientToken: clientToken, logsEndpoint: logsEndpoint), - appContext: .mockAny() - ) - ) { error in - XCTAssertEqual((error as? ProgrammerError)?.description, expectedError) - } } - try verify( - clientToken: "abc", - logsEndpoint: .us, - expectedLogsUploadURL: URL(string: "https://mobile-http-intake.logs.datadoghq.com/v1/input/abc")! - ) - try verify( - clientToken: "abc", - logsEndpoint: .eu, - expectedLogsUploadURL: URL(string: "https://mobile-http-intake.logs.datadoghq.eu/v1/input/abc")! - ) - try verify( - clientToken: "abc", - logsEndpoint: .custom(url: "http://example.com/api"), - expectedLogsUploadURL: URL(string: "http://example.com/api/abc")! - ) - verify(clientToken: "", logsEndpoint: .us, expectedError: "Datadog SDK usage error: `clientToken` cannot be empty.") - verify(clientToken: "", logsEndpoint: .eu, expectedError: "Datadog SDK usage error: `clientToken` cannot be empty.") - verify( - clientToken: "", - logsEndpoint: .custom(url: URL.mockAny().absoluteString), - expectedError: "Datadog SDK usage error: `clientToken` cannot be empty." - ) - verify( - clientToken: "abc", - logsEndpoint: .custom(url: ""), - expectedError: "Datadog SDK usage error: The `url` in `.custom(url:)` must be a valid URL string." - ) - verify( - clientToken: "abc", - logsEndpoint: .custom(url: "not a valid url string"), - expectedError: "Datadog SDK usage error: The `url` in `.custom(url:)` must be a valid URL string." + // Valid fixtures: + + XCTAssertEqual( + try configurationWith(clientToken: "abc", logsEndpoint: .us).logsUploadURLWithClientToken, + URL(string: "https://mobile-http-intake.logs.datadoghq.com/v1/input/abc")! + ) + XCTAssertEqual( + try configurationWith(clientToken: "abc", logsEndpoint: .eu).logsUploadURLWithClientToken, + URL(string: "https://mobile-http-intake.logs.datadoghq.eu/v1/input/abc")! + ) + XCTAssertEqual( + try configurationWith(clientToken: "abc", logsEndpoint: .custom(url: "http://example.com/api")).logsUploadURLWithClientToken, + URL(string: "http://example.com/api/abc")! + ) + XCTAssertEqual( + try configurationWith(clientToken: "abc", tracesEndpoint: .us).tracesUploadURLWithClientToken, + URL(string: "https://public-trace-http-intake.logs.datadoghq.com/v1/input/abc")! + ) + XCTAssertEqual( + try configurationWith(clientToken: "abc", tracesEndpoint: .eu).tracesUploadURLWithClientToken, + URL(string: "https://public-trace-http-intake.logs.datadoghq.eu/v1/input/abc")! + ) + XCTAssertEqual( + try configurationWith(clientToken: "abc", tracesEndpoint: .custom(url: "http://example.com/api")).tracesUploadURLWithClientToken, + URL(string: "http://example.com/api/abc")! ) + + // Invalid fixtures: + + XCTAssertThrowsError(try configurationWith(clientToken: "")) { error in + XCTAssertEqual((error as? ProgrammerError)?.description, "๐Ÿ”ฅ Datadog SDK usage error: `clientToken` cannot be empty.") + } + XCTAssertThrowsError(try configurationWith(logsEndpoint: .custom(url: "not a valid url string"))) { error in + XCTAssertEqual( + (error as? ProgrammerError)?.description, + "๐Ÿ”ฅ Datadog SDK usage error: The `url` in `.custom(url:)` must be a valid URL string." + ) + } + XCTAssertThrowsError(try configurationWith(tracesEndpoint: .custom(url: "not a valid url string"))) { error in + XCTAssertEqual( + (error as? ProgrammerError)?.description, + "๐Ÿ”ฅ Datadog SDK usage error: The `url` in `.custom(url:)` must be a valid URL string." + ) + } } } diff --git a/Tests/DatadogTests/Datadog/DatadogTests.swift b/Tests/DatadogTests/Datadog/DatadogTests.swift index 0363e24a67..dfc398360a 100644 --- a/Tests/DatadogTests/Datadog/DatadogTests.swift +++ b/Tests/DatadogTests/Datadog/DatadogTests.swift @@ -48,12 +48,9 @@ class AppContextTests: XCTestCase { class DatadogTests: XCTestCase { private var printFunction: PrintFunctionMock! // swiftlint:disable:this implicitly_unwrapped_optional - private let validConfiguration = Datadog.Configuration( - clientToken: "abc-def", - logsEndpoint: .us, - serviceName: "service-name", - environment: "tests" - ) + private var configurationBuilder: Datadog.Configuration.Builder { + Datadog.Configuration.builderUsing(clientToken: "abc-def", environment: "tests") + } override func setUp() { super.setUp() @@ -72,10 +69,11 @@ class DatadogTests: XCTestCase { // MARK: - Initializing with configuration func testGivenValidConfiguration_itCanBeInitialized() throws { - Datadog.initialize(appContext: .mockAny(), configuration: validConfiguration) + Datadog.initialize(appContext: .mockAny(), configuration: configurationBuilder.build()) XCTAssertNotNil(Datadog.instance) - XCTAssertNoThrow(try Datadog.deinitializeOrThrow()) + + try Datadog.deinitializeOrThrow() } func testGivenInvalidConfiguration_whenInitializing_itPrintsError() throws { @@ -89,9 +87,9 @@ class DatadogTests: XCTestCase { XCTAssertNil(Datadog.instance) } - func testWhenInitializedMoreThanOnce_itPrintsError() throws { - Datadog.initialize(appContext: .mockAny(), configuration: validConfiguration) - Datadog.initialize(appContext: .mockAny(), configuration: validConfiguration) + func testGivenValidConfiguration_whenInitializedMoreThanOnce_itPrintsError() throws { + Datadog.initialize(appContext: .mockAny(), configuration: configurationBuilder.build()) + Datadog.initialize(appContext: .mockAny(), configuration: configurationBuilder.build()) XCTAssertEqual( printFunction.printedMessage, @@ -101,9 +99,87 @@ class DatadogTests: XCTestCase { try Datadog.deinitializeOrThrow() } + // MARK: - Toggling features + + func testEnablingAndDisablingFeatures() throws { + func verify(configuration: Datadog.Configuration, verificationBlock: () -> Void) throws { + Datadog.initialize(appContext: .mockAny(), configuration: configuration) + verificationBlock() + try Datadog.deinitializeOrThrow() + } + + defer { + TracingAutoInstrumentation.instance?.swizzler.unswizzle() + } + + try verify(configuration: configurationBuilder.build()) { + // verify features: + XCTAssertNotNil(LoggingFeature.instance) + XCTAssertNotNil(TracingFeature.instance) + XCTAssertNil(TracingAutoInstrumentation.instance) + // verify integrations: + XCTAssertNotNil(TracingFeature.instance?.loggingFeatureAdapter) + } + try verify(configuration: configurationBuilder.enableLogging(false).build()) { + // verify features: + XCTAssertNil(LoggingFeature.instance) + XCTAssertNotNil(TracingFeature.instance) + XCTAssertNil(TracingAutoInstrumentation.instance) + // verify integrations: + XCTAssertNil(TracingFeature.instance?.loggingFeatureAdapter) + } + try verify(configuration: configurationBuilder.enableTracing(false).build()) { + // verify features: + XCTAssertNotNil(LoggingFeature.instance) + XCTAssertNil(TracingFeature.instance) + XCTAssertNil(TracingAutoInstrumentation.instance) + } + try verify(configuration: configurationBuilder.enableLogging(false).enableTracing(false).build()) { + // verify features: + XCTAssertNil(LoggingFeature.instance) + XCTAssertNil(TracingFeature.instance) + XCTAssertNil(TracingAutoInstrumentation.instance) + } + + let autoTracingConfig = configurationBuilder + .enableTracing(true) + .set(tracedHosts: [String.mockAny()]) + .build() + try verify(configuration: autoTracingConfig) { + XCTAssertNotNil(TracingFeature.instance) + XCTAssertNotNil(TracingAutoInstrumentation.instance) + + let urlFilter = TracingAutoInstrumentation.instance?.urlFilter as? URLFilter + let expectedURLFilter = TracingAutoInstrumentation(with: autoTracingConfig)?.urlFilter as? URLFilter + + XCTAssertNotNil(urlFilter) + XCTAssertEqual(urlFilter, expectedURLFilter) + } + try verify( + configuration: configurationBuilder + .enableTracing(false) + .set(tracedHosts: [String.mockAny()]) + .build() + ) { + XCTAssertNil(TracingFeature.instance) + XCTAssertNil(TracingAutoInstrumentation.instance) + } + } + // MARK: - Defaults func testDefaultVerbosityLevel() { XCTAssertNil(Datadog.verbosityLevel) } + + func testDefaultUserInfo() throws { + Datadog.initialize(appContext: .mockAny(), configuration: configurationBuilder.build()) + + XCTAssertNotNil(Datadog.instance?.userInfoProvider.value) + XCTAssertNil(Datadog.instance?.userInfoProvider.value.id) + XCTAssertNil(Datadog.instance?.userInfoProvider.value.email) + XCTAssertNil(Datadog.instance?.userInfoProvider.value.name) + + try Datadog.deinitializeOrThrow() + } } diff --git a/Tests/DatadogTests/Datadog/FeaturesIntegration/LoggingForTracingAdapterTests.swift b/Tests/DatadogTests/Datadog/FeaturesIntegration/LoggingForTracingAdapterTests.swift new file mode 100644 index 0000000000..221ed5ab5b --- /dev/null +++ b/Tests/DatadogTests/Datadog/FeaturesIntegration/LoggingForTracingAdapterTests.swift @@ -0,0 +1,105 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class LoggingForTracingAdapterTests: XCTestCase { + // MARK: - LoggingForTracingAdapter.AdaptedLogOutput + + func testWritingLogWithOTMessageField() { + let loggingOutput = LogOutputMock() + let tracingOutput = LoggingForTracingAdapter.AdaptedLogOutput(loggingOutput: loggingOutput) + + tracingOutput.writeLog( + withSpanContext: .mockWith(traceID: 1, spanID: 2), + fields: [ + OTLogFields.message: "hello", + "custom field": 123, + ], + date: .mockDecember15th2019At10AMUTC() + ) + + let expectedLog = LogOutputMock.RecordedLog( + level: .info, + message: "hello", + date: .mockDecember15th2019At10AMUTC(), + attributes: LogAttributes( + userAttributes: [ + "custom field": 123, + ], + internalAttributes: [ + "dd.span_id": "2", + "dd.trace_id": "1" + ] + ) + ) + + XCTAssertEqual(loggingOutput.recordedLog, expectedLog) + } + + func testWritingLogWithOTErrorField() { + let loggingOutput = LogOutputMock() + let tracingOutput = LoggingForTracingAdapter.AdaptedLogOutput(loggingOutput: loggingOutput) + + tracingOutput.writeLog( + withSpanContext: .mockAny(), + fields: [OTLogFields.event: "error"], + date: .mockAny() + ) + + let recordedLog1 = loggingOutput.recordedLog + + tracingOutput.writeLog( + withSpanContext: .mockAny(), + fields: [OTLogFields.errorKind: "Swift error"], + date: .mockAny() + ) + + let recordedLog2 = loggingOutput.recordedLog + + tracingOutput.writeLog( + withSpanContext: .mockAny(), + fields: [OTLogFields.event: "error", OTLogFields.errorKind: "Swift error"], + date: .mockAny() + ) + + let recordedLog3 = loggingOutput.recordedLog + + [recordedLog1, recordedLog2, recordedLog3].forEach { log in + XCTAssertEqual(log?.level, .error) + XCTAssertEqual(log?.message, "Span event") + } + } + + func testWritingCustomLogWithoutAnyOTFields() { + let loggingOutput = LogOutputMock() + let tracingOutput = LoggingForTracingAdapter.AdaptedLogOutput(loggingOutput: loggingOutput) + + tracingOutput.writeLog( + withSpanContext: .mockWith(traceID: 1, spanID: 2), + fields: ["custom field": 123], + date: .mockDecember15th2019At10AMUTC() + ) + + let expectedLog = LogOutputMock.RecordedLog( + level: .info, + message: "Span event", // default message is used. + date: .mockDecember15th2019At10AMUTC(), + attributes: LogAttributes( + userAttributes: [ + "custom field": 123, + ], + internalAttributes: [ + "dd.span_id": "2", + "dd.trace_id": "1" + ] + ) + ) + + XCTAssertEqual(loggingOutput.recordedLog, expectedLog) + } +} diff --git a/Tests/DatadogTests/Datadog/LoggerBuilderTests.swift b/Tests/DatadogTests/Datadog/LoggerBuilderTests.swift index fcab3eda99..b70d0c97db 100644 --- a/Tests/DatadogTests/Datadog/LoggerBuilderTests.swift +++ b/Tests/DatadogTests/Datadog/LoggerBuilderTests.swift @@ -48,10 +48,12 @@ class LoggerBuilderTests: XCTestCase { return } + let feature = LoggingFeature.instance! XCTAssertEqual(logBuilder.applicationVersion, "1.2.3") XCTAssertEqual(logBuilder.serviceName, "service-name") XCTAssertEqual(logBuilder.environment, "tests") XCTAssertEqual(logBuilder.loggerName, "com.datadog.unit-tests") + XCTAssertTrue(logBuilder.userInfoProvider === feature.userInfoProvider) XCTAssertNil(logBuilder.networkConnectionInfoProvider) XCTAssertNil(logBuilder.carrierInfoProvider) } @@ -60,7 +62,7 @@ class LoggerBuilderTests: XCTestCase { let logger = Logger.builder .set(serviceName: "custom-service-name") .set(loggerName: "custom-logger-name") - .sendNetworkInfo(false) + .sendNetworkInfo(true) .build() guard let logBuilder = (logger.logOutput as? LogFileOutput)?.logBuilder else { @@ -68,12 +70,14 @@ class LoggerBuilderTests: XCTestCase { return } + let feature = LoggingFeature.instance! XCTAssertEqual(logBuilder.applicationVersion, "1.2.3") XCTAssertEqual(logBuilder.serviceName, "custom-service-name") XCTAssertEqual(logBuilder.environment, "tests") XCTAssertEqual(logBuilder.loggerName, "custom-logger-name") - XCTAssertNil(logBuilder.networkConnectionInfoProvider) - XCTAssertNil(logBuilder.carrierInfoProvider) + XCTAssertTrue(logBuilder.userInfoProvider === feature.userInfoProvider) + XCTAssertTrue(logBuilder.networkConnectionInfoProvider as AnyObject === feature.networkConnectionInfoProvider as AnyObject) + XCTAssertTrue(logBuilder.carrierInfoProvider as AnyObject === feature.carrierInfoProvider as AnyObject) } func testUsingDifferentOutputs() throws { @@ -116,23 +120,6 @@ class LoggerBuilderTests: XCTestCase { } } -class LoggerBuilderErrorTests: XCTestCase { - func testGivenDatadogNotInitialized_whenBuildingLogger_itPrintsError() { - let printFunction = PrintFunctionMock() - consolePrint = printFunction.print - defer { consolePrint = { print($0) } } - - XCTAssertNil(Datadog.instance) - - let logger = Logger.builder.build() - XCTAssertEqual( - printFunction.printedMessage, - "๐Ÿ”ฅ Datadog SDK usage error: `Datadog.initialize()` must be called prior to `Logger.builder.build()`." - ) - assertThat(logger: logger, usesOutput: NoOpLogOutput.self) - } -} - // MARK: - Helpers private func assertThat(logger: Logger, usesOutput outputType: LogOutput.Type, file: StaticString = #file, line: UInt = #line) { diff --git a/Tests/DatadogTests/Datadog/LoggerTests.swift b/Tests/DatadogTests/Datadog/LoggerTests.swift index ff31edf020..fc8c5f9055 100644 --- a/Tests/DatadogTests/Datadog/LoggerTests.swift +++ b/Tests/DatadogTests/Datadog/LoggerTests.swift @@ -23,9 +23,9 @@ class LoggerTests: XCTestCase { super.tearDown() } - // MARK: - Sending logs + // MARK: - Customizing Logger - func testSendingMinimalLogWithDefaultLogger() throws { + func testSendingLogWithDefaultLogger() throws { let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) LoggingFeature.instance = .mockWorkingFeatureWith( server: server, @@ -46,7 +46,7 @@ class LoggerTests: XCTestCase { let logMatcher = try server.waitAndReturnLogMatchers(count: 1)[0] try logMatcher.assertItFullyMatches(jsonString: """ { - "status" : "DEBUG", + "status" : "debug", "message" : "message", "service" : "default-service-name", "logger.name" : "com.datadoghq.ios-sdk", @@ -92,6 +92,30 @@ class LoggerTests: XCTestCase { } } + // MARK: - Sending Customized Logs + + func testSendingLogsWithDifferentDates() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + LoggingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + dateProvider: RelativeDateProvider(startingFrom: .mockDecember15th2019At10AMUTC(), advancingBySeconds: 1) + ) + defer { LoggingFeature.instance = nil } + + let logger = Logger.builder.build() + logger.info("message 1") + logger.info("message 2") + logger.info("message 3") + + let logMatchers = try server.waitAndReturnLogMatchers(count: 3) + // swiftlint:disable trailing_closure + logMatchers[0].assertDate(matches: { $0 == Date.mockDecember15th2019At10AMUTC() }) + logMatchers[1].assertDate(matches: { $0 == Date.mockDecember15th2019At10AMUTC(addingTimeInterval: 1) }) + logMatchers[2].assertDate(matches: { $0 == Date.mockDecember15th2019At10AMUTC(addingTimeInterval: 2) }) + // swiftlint:enable trailing_closure + } + func testSendingLogsWithDifferentLevels() throws { let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) LoggingFeature.instance = .mockWorkingFeatureWith( @@ -109,12 +133,12 @@ class LoggerTests: XCTestCase { logger.critical("message") let logMatchers = try server.waitAndReturnLogMatchers(count: 6) - logMatchers[0].assertStatus(equals: "DEBUG") - logMatchers[1].assertStatus(equals: "INFO") - logMatchers[2].assertStatus(equals: "NOTICE") - logMatchers[3].assertStatus(equals: "WARN") - logMatchers[4].assertStatus(equals: "ERROR") - logMatchers[5].assertStatus(equals: "CRITICAL") + logMatchers[0].assertStatus(equals: "debug") + logMatchers[1].assertStatus(equals: "info") + logMatchers[2].assertStatus(equals: "notice") + logMatchers[3].assertStatus(equals: "warn") + logMatchers[4].assertStatus(equals: "error") + logMatchers[5].assertStatus(equals: "critical") } // MARK: - Sending user info @@ -303,6 +327,9 @@ class LoggerTests: XCTestCase { // nested string literal logger.addAttribute(forKey: "nested.string", value: "hello") + // URL + logger.addAttribute(forKey: "url", value: URL(string: "https://example.com/image.png")!) + logger.info("message") let logMatcher = try server.waitAndReturnLogMatchers(count: 1)[0] @@ -318,6 +345,8 @@ class LoggerTests: XCTestCase { logMatcher.assertValue(forKeyPath: "person.age", equals: 30) logMatcher.assertValue(forKeyPath: "person.nationality", equals: "Polish") logMatcher.assertValue(forKeyPath: "nested.string", equals: "hello") + /// URLs are encoded explicitly as `String` - see the comment in `EncodableValue` + logMatcher.assertValue(forKeyPath: "url", equals: "https://example.com/image.png") } func testSendingMessageAttributes() throws { @@ -461,4 +490,56 @@ class LoggerTests: XCTestCase { server.waitAndAssertNoRequestsSent() } + + // MARK: - Usage + + func testGivenDatadogNotInitialized_whenInitializingLogger_itPrintsError() { + let printFunction = PrintFunctionMock() + consolePrint = printFunction.print + defer { consolePrint = { print($0) } } + + // given + XCTAssertNil(Datadog.instance) + + // when + let logger = Logger.builder.build() + + // then + XCTAssertEqual( + printFunction.printedMessage, + "๐Ÿ”ฅ Datadog SDK usage error: `Datadog.initialize()` must be called prior to `Logger.builder.build()`." + ) + XCTAssertTrue(logger.logOutput is NoOpLogOutput) + } + + func testGivenLoggingFeatureDisabled_whenInitializingLogger_itPrintsError() throws { + let printFunction = PrintFunctionMock() + consolePrint = printFunction.print + defer { consolePrint = { print($0) } } + + // given + Datadog.initialize( + appContext: .mockAny(), + configuration: Datadog.Configuration.builderUsing(clientToken: "abc.def", environment: "tests") + .enableLogging(false) + .build() + ) + + // when + let logger = Logger.builder.build() + + // then + XCTAssertEqual( + printFunction.printedMessage, + "๐Ÿ”ฅ Datadog SDK usage error: `Logger.builder.build()` produces a non-functional logger, as the logging feature is disabled." + ) + XCTAssertTrue(logger.logOutput is NoOpLogOutput) + + try Datadog.deinitializeOrThrow() + } + + func testDDLoggerIsLoggerTypealias() { + XCTAssertTrue(DDLogger.self == Logger.self) + } } +// swiftlint:enable multiline_arguments_brackets diff --git a/Tests/DatadogTests/Datadog/Logs/Log/LogBuilderTests.swift b/Tests/DatadogTests/Datadog/Logging/Log/LogBuilderTests.swift similarity index 72% rename from Tests/DatadogTests/Datadog/Logs/Log/LogBuilderTests.swift rename to Tests/DatadogTests/Datadog/Logging/Log/LogBuilderTests.swift index 2a0bf10830..b51143c47b 100644 --- a/Tests/DatadogTests/Datadog/Logs/Log/LogBuilderTests.swift +++ b/Tests/DatadogTests/Datadog/Logging/Log/LogBuilderTests.swift @@ -10,7 +10,6 @@ import XCTest class LogBuilderTests: XCTestCase { func testItBuildsBasicLog() { let builder: LogBuilder = .mockWith( - date: .mockDecember15th2019At10AMUTC(), applicationVersion: "1.2.3", serviceName: "test-service-name", loggerName: "test-logger-name" @@ -18,7 +17,8 @@ class LogBuilderTests: XCTestCase { let log = builder.createLogWith( level: .debug, message: "debug message", - attributes: ["attribute": "value"], + date: .mockDecember15th2019At10AMUTC(), + attributes: .mockWith(userAttributes: ["attribute": "value"]), tags: ["tag"] ) @@ -29,21 +29,21 @@ class LogBuilderTests: XCTestCase { XCTAssertEqual(log.serviceName, "test-service-name") XCTAssertEqual(log.loggerName, "test-logger-name") XCTAssertEqual(log.tags, ["tag"]) - XCTAssertEqual(log.attributes, ["attribute": EncodableValue("value")]) + XCTAssertEqual(log.attributes.userAttributes as? [String: String], ["attribute": "value"]) XCTAssertEqual( - builder.createLogWith(level: .info, message: "", attributes: [:], tags: []).status, .info + builder.createLogWith(level: .info, message: "", date: .mockAny(), attributes: .mockAny(), tags: []).status, .info ) XCTAssertEqual( - builder.createLogWith(level: .notice, message: "", attributes: [:], tags: []).status, .notice + builder.createLogWith(level: .notice, message: "", date: .mockAny(), attributes: .mockAny(), tags: []).status, .notice ) XCTAssertEqual( - builder.createLogWith(level: .warn, message: "", attributes: [:], tags: []).status, .warn + builder.createLogWith(level: .warn, message: "", date: .mockAny(), attributes: .mockAny(), tags: []).status, .warn ) XCTAssertEqual( - builder.createLogWith(level: .error, message: "", attributes: [:], tags: []).status, .error + builder.createLogWith(level: .error, message: "", date: .mockAny(), attributes: .mockAny(), tags: []).status, .error ) XCTAssertEqual( - builder.createLogWith(level: .critical, message: "", attributes: [:], tags: []).status, .critical + builder.createLogWith(level: .critical, message: "", date: .mockAny(), attributes: .mockAny(), tags: []).status, .critical ) } @@ -53,13 +53,13 @@ class LogBuilderTests: XCTestCase { expectation.expectedFulfillmentCount = 3 DispatchQueue.main.async { - let log = builder.createLogWith(level: .debug, message: "", attributes: [:], tags: []) + let log = builder.createLogWith(level: .debug, message: "", date: .mockAny(), attributes: .mockAny(), tags: []) XCTAssertEqual(log.threadName, "main") expectation.fulfill() } DispatchQueue.global(qos: .default).async { - let log = builder.createLogWith(level: .debug, message: "", attributes: [:], tags: []) + let log = builder.createLogWith(level: .debug, message: "", date: .mockAny(), attributes: .mockAny(), tags: []) XCTAssertEqual(log.threadName, "background") expectation.fulfill() } @@ -69,7 +69,7 @@ class LogBuilderTests: XCTestCase { defer { Thread.current.name = previousName } // reset it as this thread might be picked by `.global(qos: .default)` Thread.current.name = "custom-thread-name" - let log = builder.createLogWith(level: .debug, message: "", attributes: [:], tags: []) + let log = builder.createLogWith(level: .debug, message: "", date: .mockAny(), attributes: .mockAny(), tags: []) XCTAssertEqual(log.threadName, "custom-thread-name") expectation.fulfill() } diff --git a/Tests/DatadogTests/Datadog/Logging/Log/LogSanitizerTests.swift b/Tests/DatadogTests/Datadog/Logging/Log/LogSanitizerTests.swift new file mode 100644 index 0000000000..dfc5d9c391 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Logging/Log/LogSanitizerTests.swift @@ -0,0 +1,208 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class LogSanitizerTests: XCTestCase { + // MARK: - Attributes sanitization + + func testWhenUserAttributeUsesReservedName_itIsIgnored() { + let log = Log.mockWith( + attributes: .mockWith( + userAttributes: [ + // reserved attributes: + "host": mockValue(), + "message": mockValue(), + "status": mockValue(), + "service": mockValue(), + "source": mockValue(), + "error.message": mockValue(), + "error.stack": mockValue(), + "ddtags": mockValue(), + + // valid attributes: + "attribute1": mockValue(), + "attribute2": mockValue(), + "date": mockValue(), + ] + ) + ) + + let sanitized = LogSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.attributes.userAttributes.count, 3) + XCTAssertNotNil(sanitized.attributes.userAttributes["attribute1"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["attribute2"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["date"]) + } + + func testWhenUserAttributeNameExceeds10NestedLevels_itIsEscapedByUnderscore() { + let log = Log.mockWith( + attributes: .mockWith( + userAttributes: [ + "one": mockValue(), + "one.two": mockValue(), + "one.two.three": mockValue(), + "one.two.three.four": mockValue(), + "one.two.three.four.five": mockValue(), + "one.two.three.four.five.six": mockValue(), + "one.two.three.four.five.six.seven": mockValue(), + "one.two.three.four.five.six.seven.eight": mockValue(), + "one.two.three.four.five.six.seven.eight.nine": mockValue(), + "one.two.three.four.five.six.seven.eight.nine.ten": mockValue(), + "one.two.three.four.five.six.seven.eight.nine.ten.eleven": mockValue(), + "one.two.three.four.five.six.seven.eight.nine.ten.eleven.twelve": mockValue(), + ] + ) + ) + + let sanitized = LogSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.attributes.userAttributes.count, 12) + XCTAssertNotNil(sanitized.attributes.userAttributes["one"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five.six"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five.six.seven"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five.six.seven.eight"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five.six.seven.eight.nine.ten"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five.six.seven.eight.nine.ten_eleven"]) + XCTAssertNotNil(sanitized.attributes.userAttributes["one.two.three.four.five.six.seven.eight.nine.ten_eleven_twelve"]) + } + + func testWhenUserAttributeNameIsInvalid_itIsIgnored() { + let log = Log.mockWith( + attributes: .mockWith( + userAttributes: [ + "valid-name": mockValue(), + "": mockValue(), // invalid name + ] + ) + ) + + let sanitized = LogSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.attributes.userAttributes.count, 1) + XCTAssertNotNil(sanitized.attributes.userAttributes["valid-name"]) + } + + func testWhenNumberOfUserAttributesExceedsLimit_itDropsExtraOnes() { + let mockAttributes = (0...1_000).map { index in ("attribute-\(index)", mockValue()) } + let log = Log.mockWith( + attributes: .mockWith( + userAttributes: Dictionary(uniqueKeysWithValues: mockAttributes) + ) + ) + + let sanitized = LogSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.attributes.userAttributes.count, LogSanitizer.Constraints.maxNumberOfAttributes) + } + + func testInternalAttributesAreNotSanitized() { + let log = Log.mockWith( + attributes: .mockWith( + internalAttributes: [ + // reserved attributes: + LoggingForTracingAdapter.TracingAttributes.traceID: mockValue(), + LoggingForTracingAdapter.TracingAttributes.spanID: mockValue(), + + // custom attribute: + "attribute1": mockValue(), + ] + ) + ) + + let sanitized = LogSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.attributes.internalAttributes?.count, 3) + } + + // MARK: - Tags sanitization + + func testWhenTagHasUpperCasedCharacters_itGetsLowerCased() { + let log = Log.mockWith( + tags: ["abcd", "Abcdef:ghi", "ABCDEF:GHIJK", "ABCDEFGHIJK"] + ) + + let sanitized = LogSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.tags, ["abcd", "abcdef:ghi", "abcdef:ghijk", "abcdefghijk"]) + } + + func testWhenTagStartsWithIllegalCharacter_itIsIgnored() { + let log = Log.mockWith( + tags: ["?invalid", "valid", "&invalid", ".abcdefghijk", ":abcd"] + ) + + let sanitized = LogSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.tags, ["valid"]) + } + + func testWhenTagContainsIllegalCharacter_itIsConvertedToUnderscore() { + let log = Log.mockWith( + tags: ["this&needs&underscore", "this*as*well", "this/doesnt", "tag with whitespaces"] + ) + + let sanitized = LogSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.tags, ["this_needs_underscore", "this_as_well", "this/doesnt", "tag_with_whitespaces"]) + } + + func testWhenTagContainsTrailingCommas_itItTruncatesThem() { + let log = Log.mockWith( + tags: ["with-one-comma:", "with-several-commas::::", "with-comma:in-the-middle"] + ) + + let sanitized = LogSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.tags, ["with-one-comma", "with-several-commas", "with-comma:in-the-middle"]) + } + + func testWhenTagExceedsLengthLimit_itIsTruncated() { + let log = Log.mockWith( + tags: [.mockRepeating(character: "a", times: 2 * LogSanitizer.Constraints.maxTagLength)] + ) + + let sanitized = LogSanitizer().sanitize(log: log) + + XCTAssertEqual( + sanitized.tags, + [.mockRepeating(character: "a", times: LogSanitizer.Constraints.maxTagLength)] + ) + } + + func testWhenTagUsesReservedKey_itIsIgnored() { + let log = Log.mockWith( + tags: ["host:abc", "device:abc", "source:abc", "service:abc", "valid"] + ) + + let sanitized = LogSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.tags, ["valid"]) + } + + func testWhenNumberOfTagsExceedsLimit_itDropsExtraOnes() { + let mockTags = (0...1_000).map { index in "tag\(index)" } + let log = Log.mockWith( + tags: mockTags + ) + + let sanitized = LogSanitizer().sanitize(log: log) + + XCTAssertEqual(sanitized.tags?.count, LogSanitizer.Constraints.maxNumberOfTags) + } + + // MARK: - Private + + private func mockValue() -> String { + return .mockAny() + } +} diff --git a/Tests/DatadogTests/Datadog/Logs/LogOutputs/LogConsoleOutputTests.swift b/Tests/DatadogTests/Datadog/Logging/LogOutputs/LogConsoleOutputTests.swift similarity index 56% rename from Tests/DatadogTests/Datadog/Logs/LogOutputs/LogConsoleOutputTests.swift rename to Tests/DatadogTests/Datadog/Logging/LogOutputs/LogConsoleOutputTests.swift index ae882035ac..a62177690e 100644 --- a/Tests/DatadogTests/Datadog/Logs/LogOutputs/LogConsoleOutputTests.swift +++ b/Tests/DatadogTests/Datadog/Logging/LogOutputs/LogConsoleOutputTests.swift @@ -13,22 +13,35 @@ class LogConsoleOutputTests: XCTestCase { var messagePrinted: String = "" let output1 = LogConsoleOutput( - logBuilder: .mockWith(date: .mockDecember15th2019At10AMUTC()), + logBuilder: .mockAny(), format: .short, - printingFunction: { messagePrinted = $0 }, - timeFormatter: LogConsoleOutput.shortTimeFormatter(calendar: .gregorian, timeZone: .UTC) + timeZone: .UTC, + printingFunction: { messagePrinted = $0 } ) - output1.writeLogWith(level: .info, message: "Info message.", attributes: [:], tags: []) - XCTAssertEqual(messagePrinted, "10:00:00.000Z [INFO] Info message.") + output1.writeLogWith(level: .info, message: "Info message.", date: .mockDecember15th2019At10AMUTC(), attributes: .mockAny(), tags: []) + XCTAssertEqual(messagePrinted, "10:00:00.000 [INFO] Info message.") let output2 = LogConsoleOutput( - logBuilder: .mockWith(date: .mockDecember15th2019At10AMUTC()), + logBuilder: .mockAny(), format: .shortWith(prefix: "๐Ÿถ "), - printingFunction: { messagePrinted = $0 }, - timeFormatter: LogConsoleOutput.shortTimeFormatter(calendar: .gregorian, timeZone: .UTC) + timeZone: .UTC, + printingFunction: { messagePrinted = $0 } + ) + output2.writeLogWith(level: .info, message: "Info message.", date: .mockDecember15th2019At10AMUTC(), attributes: .mockAny(), tags: []) + XCTAssertEqual(messagePrinted, "๐Ÿถ 10:00:00.000 [INFO] Info message.") + } + + func testWhenUsingShortFormat_itFormatsTimeInCurrentTimeZone() { + var messagePrinted: String = "" + + let output = LogConsoleOutput( + logBuilder: .mockAny(), + format: .short, + timeZone: .EET, + printingFunction: { messagePrinted = $0 } ) - output2.writeLogWith(level: .info, message: "Info message.", attributes: [:], tags: []) - XCTAssertEqual(messagePrinted, "๐Ÿถ 10:00:00.000Z [INFO] Info message.") + output.writeLogWith(level: .info, message: "Info message.", date: .mockDecember15th2019At10AMUTC(), attributes: .mockAny(), tags: []) + XCTAssertEqual(messagePrinted, "12:00:00.000 [INFO] Info message.") } func testItPrintsLogsUsingJSONFormat() throws { @@ -37,18 +50,20 @@ class LogConsoleOutputTests: XCTestCase { let output1 = LogConsoleOutput( logBuilder: .mockAny(), format: .json, + timeZone: .mockAny(), printingFunction: { messagePrinted = $0 } ) - output1.writeLogWith(level: .info, message: "Info message.", attributes: [:], tags: []) + output1.writeLogWith(level: .info, message: "Info message.", date: .mockDecember15th2019At10AMUTC(), attributes: .mockAny(), tags: []) try LogMatcher.fromJSONObjectData(messagePrinted.utf8Data) .assertMessage(equals: "Info message.") let output2 = LogConsoleOutput( logBuilder: .mockAny(), format: .jsonWith(prefix: "๐Ÿถ โ†’ "), + timeZone: .mockAny(), printingFunction: { messagePrinted = $0 } ) - output2.writeLogWith(level: .info, message: "Info message.", attributes: [:], tags: []) + output2.writeLogWith(level: .info, message: "Info message.", date: .mockDecember15th2019At10AMUTC(), attributes: .mockAny(), tags: []) XCTAssertTrue(messagePrinted.hasPrefix("๐Ÿถ โ†’ ")) try LogMatcher.fromJSONObjectData(messagePrinted.removingPrefix("๐Ÿถ โ†’ ").utf8Data) .assertMessage(equals: "Info message.") diff --git a/Tests/DatadogTests/Datadog/Logging/LogOutputs/LogFileOutputTests.swift b/Tests/DatadogTests/Datadog/Logging/LogOutputs/LogFileOutputTests.swift new file mode 100644 index 0000000000..59aeff08b0 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Logging/LogOutputs/LogFileOutputTests.swift @@ -0,0 +1,58 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class LogFileOutputTests: XCTestCase { + override func setUp() { + super.setUp() + temporaryDirectory.create() + } + + override func tearDown() { + temporaryDirectory.delete() + super.tearDown() + } + + func testItWritesLogToFileAsJSON() throws { + let fileCreationDateProvider = RelativeDateProvider(startingFrom: .mockDecember15th2019At10AMUTC()) + let queue = DispatchQueue(label: "com.datadohq.testItWritesCurrentDateToLogs") + let output = LogFileOutput( + logBuilder: .mockAny(), + fileWriter: FileWriter( + dataFormat: LoggingFeature.Storage.dataFormat, + orchestrator: FilesOrchestrator( + directory: temporaryDirectory, + performance: PerformancePreset.combining( + storagePerformance: .writeEachObjectToNewFileAndReadAllFiles, + uploadPerformance: .noOp + ), + dateProvider: fileCreationDateProvider + ), + queue: queue + ) + ) + + output.writeLogWith(level: .info, message: "log message 1", date: .mockAny(), attributes: .mockAny(), tags: []) + queue.sync {} // wait on writter queue + + fileCreationDateProvider.advance(bySeconds: 1) + + output.writeLogWith(level: .info, message: "log message 2", date: .mockAny(), attributes: .mockAny(), tags: []) + queue.sync {} // wait on writter queue + + let log1FileName = fileNameFrom(fileCreationDate: .mockDecember15th2019At10AMUTC()) + let log1Data = try temporaryDirectory.file(named: log1FileName).read() + let log1Matcher = try LogMatcher.fromJSONObjectData(log1Data) + log1Matcher.assertMessage(equals: "log message 1") + + let log2FileName = fileNameFrom(fileCreationDate: .mockDecember15th2019At10AMUTC(addingTimeInterval: 1)) + let log2Data = try temporaryDirectory.file(named: log2FileName).read() + let log2Matcher = try LogMatcher.fromJSONObjectData(log2Data) + log2Matcher.assertMessage(equals: "log message 2") + } +} diff --git a/Tests/DatadogTests/Datadog/Logs/LogOutputs/LogUtilityOutputsTests.swift b/Tests/DatadogTests/Datadog/Logging/LogOutputs/LogUtilityOutputsTests.swift similarity index 74% rename from Tests/DatadogTests/Datadog/Logs/LogOutputs/LogUtilityOutputsTests.swift rename to Tests/DatadogTests/Datadog/Logging/LogOutputs/LogUtilityOutputsTests.swift index b7e5ad830b..a087ff2d65 100644 --- a/Tests/DatadogTests/Datadog/Logs/LogOutputs/LogUtilityOutputsTests.swift +++ b/Tests/DatadogTests/Datadog/Logging/LogOutputs/LogUtilityOutputsTests.swift @@ -14,24 +14,24 @@ class CombinedLogOutputTests: XCTestCase { let output3 = LogOutputMock() let combinedOutput = CombinedLogOutput(combine: [output1, output2, output3]) - combinedOutput.writeLogWith(level: .info, message: "info message", attributes: [:], tags: []) + combinedOutput.writeLogWith(level: .info, message: "info message", date: .mockDecember15th2019At10AMUTC(), attributes: .mockAny(), tags: []) - XCTAssertEqual(output1.recordedLog, .init(level: .info, message: "info message")) - XCTAssertEqual(output2.recordedLog, .init(level: .info, message: "info message")) - XCTAssertEqual(output3.recordedLog, .init(level: .info, message: "info message")) + XCTAssertEqual(output1.recordedLog, .init(level: .info, message: "info message", date: .mockDecember15th2019At10AMUTC())) + XCTAssertEqual(output2.recordedLog, .init(level: .info, message: "info message", date: .mockDecember15th2019At10AMUTC())) + XCTAssertEqual(output3.recordedLog, .init(level: .info, message: "info message", date: .mockDecember15th2019At10AMUTC())) } func testConditionalLogOutput_writesLogToCombinedOutputOnlyIfConditionIsMet() { let output1 = LogOutputMock() let conditionalOutput1 = ConditionalLogOutput(conditionedOutput: output1) { _ in true } - conditionalOutput1.writeLogWith(level: .info, message: "info message", attributes: [:], tags: []) - XCTAssertEqual(output1.recordedLog, .init(level: .info, message: "info message")) + conditionalOutput1.writeLogWith(level: .info, message: "info message", date: .mockDecember15th2019At10AMUTC(), attributes: .mockAny(), tags: []) + XCTAssertEqual(output1.recordedLog, .init(level: .info, message: "info message", date: .mockDecember15th2019At10AMUTC())) let output2 = LogOutputMock() let conditionalOutput2 = ConditionalLogOutput(conditionedOutput: output2) { _ in false } - conditionalOutput2.writeLogWith(level: .info, message: "info message", attributes: [:], tags: []) + conditionalOutput2.writeLogWith(level: .info, message: "info message", date: .mockAny(), attributes: .mockAny(), tags: []) XCTAssertNil(output2.recordedLog) } } diff --git a/Tests/DatadogTests/Datadog/Logging/LoggingFeatureTests.swift b/Tests/DatadogTests/Datadog/Logging/LoggingFeatureTests.swift new file mode 100644 index 0000000000..ef44694760 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Logging/LoggingFeatureTests.swift @@ -0,0 +1,111 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class LoggingFeatureTests: XCTestCase { + override func setUp() { + super.setUp() + XCTAssertNil(Datadog.instance) + XCTAssertNil(LoggingFeature.instance) + temporaryDirectory.create() + } + + override func tearDown() { + XCTAssertNil(Datadog.instance) + XCTAssertNil(LoggingFeature.instance) + temporaryDirectory.delete() + super.tearDown() + } + + // MARK: - Initialization + + func testInitialization() throws { + let appContext: AppContext = .mockAny() + Datadog.initialize( + appContext: appContext, + configuration: Datadog.Configuration + .builderUsing(clientToken: "abc", environment: "tests") + .build() + ) + + XCTAssertNotNil(LoggingFeature.instance) + + try Datadog.deinitializeOrThrow() + } + + // MARK: - HTTP Headers + + func testItUsesExpectedHTTPHeaders() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + LoggingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + configuration: .mockWith( + applicationName: "FoobarApp", + applicationVersion: "2.1.0" + ), + mobileDevice: .mockWith(model: "iPhone", osName: "iOS", osVersion: "13.3.1") + ) + defer { LoggingFeature.instance = nil } + + let logger = Logger.builder.build() + logger.debug("message") + + let httpHeaders = server.waitAndReturnRequests(count: 1)[0].allHTTPHeaderFields + XCTAssertEqual(httpHeaders?["User-Agent"], "FoobarApp/2.1.0 CFNetwork (iPhone; iOS/13.3.1)") + XCTAssertEqual(httpHeaders?["Content-Type"], "application/json") + } + + // MARK: - Payload Format + + func testItUsesExpectedPayloadFormatForUploads() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + LoggingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + performance: .combining( + storagePerformance: StoragePerformanceMock( + maxFileSize: .max, + maxDirectorySize: .max, + maxFileAgeForWrite: .distantFuture, // write all spans to single file, + minFileAgeForRead: StoragePerformanceMock.readAllFiles.minFileAgeForRead, + maxFileAgeForRead: StoragePerformanceMock.readAllFiles.maxFileAgeForRead, + maxObjectsInFile: 3, // write 3 spans to payload, + maxObjectSize: .max + ), + uploadPerformance: UploadPerformanceMock( + initialUploadDelay: 0.5, // wait enough until spans are written, + defaultUploadDelay: 1, + minUploadDelay: 1, + maxUploadDelay: 1, + uploadDelayDecreaseFactor: 1 + ) + ) + ) + defer { LoggingFeature.instance = nil } + + let logger = Logger.builder.build() + logger.debug("log 1") + logger.debug("log 2") + logger.debug("log 3") + + let payload = server.waitAndReturnRequests(count: 1)[0].httpBody! + + // Expected payload format: + // `[log1JSON,log2JSON,log3JSON]` + + XCTAssertEqual(payload.prefix(1).utf8String, "[", "payload should start with JSON array trait: `[`") + XCTAssertEqual(payload.suffix(1).utf8String, "]", "payload should end with JSON array trait: `]`") + + // Expect payload to be an array of log JSON objects + let logMatchers = try LogMatcher.fromArrayOfJSONObjectsData(payload) + logMatchers[0].assertMessage(equals: "log 1") + logMatchers[1].assertMessage(equals: "log 2") + logMatchers[2].assertMessage(equals: "log 3") + } +} diff --git a/Tests/DatadogTests/Datadog/Logs/Log/LogSanitizerTests.swift b/Tests/DatadogTests/Datadog/Logs/Log/LogSanitizerTests.swift deleted file mode 100644 index a2cc245e91..0000000000 --- a/Tests/DatadogTests/Datadog/Logs/Log/LogSanitizerTests.swift +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. - */ - -import XCTest -@testable import Datadog - -class LogSanitizerTests: XCTestCase { - // MARK: - Attributes sanitization - - func testWhenAttributeUsesReservedName_itIsIgnored() { - let log = Log.mockWith( - attributes: [ - // reserved attributes: - "host": .mockAny(), - "message": .mockAny(), - "status": .mockAny(), - "service": .mockAny(), - "source": .mockAny(), - "error.kind": .mockAny(), - "error.message": .mockAny(), - "error.stack": .mockAny(), - "ddtags": .mockAny(), - - // valid attributes: - "attribute1": .mockAny(), - "attribute2": .mockAny(), - "date": .mockAny(), - ] - ) - - let sanitized = LogSanitizer().sanitize(log: log) - - XCTAssertEqual(sanitized.attributes?.count, 3) - XCTAssertNotNil(sanitized.attributes?["attribute1"]) - XCTAssertNotNil(sanitized.attributes?["attribute2"]) - XCTAssertNotNil(sanitized.attributes?["date"]) - } - - func testWhenAttributeNameExceeds10NestedLevels_itIsEscapedByUnderscore() { - let log = Log.mockWith( - attributes: [ - "one": .mockAny(), - "one.two": .mockAny(), - "one.two.three": .mockAny(), - "one.two.three.four": .mockAny(), - "one.two.three.four.five": .mockAny(), - "one.two.three.four.five.six": .mockAny(), - "one.two.three.four.five.six.seven": .mockAny(), - "one.two.three.four.five.six.seven.eight": .mockAny(), - "one.two.three.four.five.six.seven.eight.nine": .mockAny(), - "one.two.three.four.five.six.seven.eight.nine.ten": .mockAny(), - "one.two.three.four.five.six.seven.eight.nine.ten.eleven": .mockAny(), - "one.two.three.four.five.six.seven.eight.nine.ten.eleven.twelve": .mockAny(), - ] - ) - - let sanitized = LogSanitizer().sanitize(log: log) - - XCTAssertEqual(sanitized.attributes?.count, 12) - XCTAssertNotNil(sanitized.attributes?["one"]) - XCTAssertNotNil(sanitized.attributes?["one.two"]) - XCTAssertNotNil(sanitized.attributes?["one.two.three"]) - XCTAssertNotNil(sanitized.attributes?["one.two.three.four"]) - XCTAssertNotNil(sanitized.attributes?["one.two.three.four.five"]) - XCTAssertNotNil(sanitized.attributes?["one.two.three.four.five.six"]) - XCTAssertNotNil(sanitized.attributes?["one.two.three.four.five.six.seven"]) - XCTAssertNotNil(sanitized.attributes?["one.two.three.four.five.six.seven.eight"]) - XCTAssertNotNil(sanitized.attributes?["one.two.three.four.five.six.seven.eight.nine.ten"]) - XCTAssertNotNil(sanitized.attributes?["one.two.three.four.five.six.seven.eight.nine.ten_eleven"]) - XCTAssertNotNil(sanitized.attributes?["one.two.three.four.five.six.seven.eight.nine.ten_eleven_twelve"]) - } - - func testWhenAttributeNameIsInvalid_itIsIgnored() { - let log = Log.mockWith( - attributes: [ - "valid-name": .mockAny(), - "": .mockAny(), // invalid name - ] - ) - - let sanitized = LogSanitizer().sanitize(log: log) - - XCTAssertEqual(sanitized.attributes?.count, 1) - XCTAssertNotNil(sanitized.attributes?["valid-name"]) - } - - func testWhenNumberOfAttributesExceedsLimit_itDropsExtraOnes() { - let mockAttributes = (0...1_000).map { index in ("attribute-\(index)", EncodableValue.mockAny()) } - let log = Log.mockWith( - attributes: Dictionary(uniqueKeysWithValues: mockAttributes) - ) - - let sanitized = LogSanitizer().sanitize(log: log) - - XCTAssertEqual(sanitized.attributes?.count, LogSanitizer.Constraints.maxNumberOfAttributes) - } - - // MARK: - Tags sanitization - - func testWhenTagHasUpperCasedCharacters_itGetsLowerCased() { - let log = Log.mockWith( - tags: ["abcd", "Abcdef:ghi", "ABCDEF:GHIJK", "ABCDEFGHIJK"] - ) - - let sanitized = LogSanitizer().sanitize(log: log) - - XCTAssertEqual(sanitized.tags, ["abcd", "abcdef:ghi", "abcdef:ghijk", "abcdefghijk"]) - } - - func testWhenTagStartsWithIllegalCharacter_itIsIgnored() { - let log = Log.mockWith( - tags: ["?invalid", "valid", "&invalid", ".abcdefghijk", ":abcd"] - ) - - let sanitized = LogSanitizer().sanitize(log: log) - - XCTAssertEqual(sanitized.tags, ["valid"]) - } - - func testWhenTagContainsIllegalCharacter_itIsConvertedToUnderscore() { - let log = Log.mockWith( - tags: ["this&needs&underscore", "this*as*well", "this/doesnt", "tag with whitespaces"] - ) - - let sanitized = LogSanitizer().sanitize(log: log) - - XCTAssertEqual(sanitized.tags, ["this_needs_underscore", "this_as_well", "this/doesnt", "tag_with_whitespaces"]) - } - - func testWhenTagContainsTrailingCommas_itItTruncatesThem() { - let log = Log.mockWith( - tags: ["with-one-comma:", "with-several-commas::::", "with-comma:in-the-middle"] - ) - - let sanitized = LogSanitizer().sanitize(log: log) - - XCTAssertEqual(sanitized.tags, ["with-one-comma", "with-several-commas", "with-comma:in-the-middle"]) - } - - func testWhenTagExceedsLengthLimit_itIsTruncated() { - let log = Log.mockWith( - tags: [.mockRepeating(character: "a", times: 2 * LogSanitizer.Constraints.maxTagLength)] - ) - - let sanitized = LogSanitizer().sanitize(log: log) - - XCTAssertEqual( - sanitized.tags, - [.mockRepeating(character: "a", times: LogSanitizer.Constraints.maxTagLength)] - ) - } - - func testWhenTagUsesReservedKey_itIsIgnored() { - let log = Log.mockWith( - tags: ["host:abc", "device:abc", "source:abc", "service:abc", "valid"] - ) - - let sanitized = LogSanitizer().sanitize(log: log) - - XCTAssertEqual(sanitized.tags, ["valid"]) - } - - func testWhenNumberOfTagsExceedsLimit_itDropsExtraOnes() { - let mockTags = (0...1_000).map { index in "tag\(index)" } - let log = Log.mockWith( - tags: mockTags - ) - - let sanitized = LogSanitizer().sanitize(log: log) - - XCTAssertEqual(sanitized.tags?.count, LogSanitizer.Constraints.maxNumberOfTags) - } -} diff --git a/Tests/DatadogTests/Datadog/Logs/LogOutputs/LogFileOutputTests.swift b/Tests/DatadogTests/Datadog/Logs/LogOutputs/LogFileOutputTests.swift deleted file mode 100644 index a9d4e41e61..0000000000 --- a/Tests/DatadogTests/Datadog/Logs/LogOutputs/LogFileOutputTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. - */ - -import XCTest -@testable import Datadog - -class LogFileOutputTests: XCTestCase { - override func setUp() { - super.setUp() - temporaryDirectory.create() - } - - override func tearDown() { - temporaryDirectory.delete() - super.tearDown() - } - - func testItWritesLogToFileAsJSON() throws { - let queue = DispatchQueue(label: "any") - let output = LogFileOutput( - logBuilder: .mockWith(date: .mockAny()), - fileWriter: .mockWrittingToSingleFile(in: temporaryDirectory, on: queue) - ) - - output.writeLogWith(level: .info, message: "log message", attributes: [:], tags: []) - - queue.sync {} // wait on writter queue - - let fileData = try temporaryDirectory.files()[0].read() - try LogMatcher.fromJSONObjectData(fileData).assertMessage(equals: "log message") - } -} diff --git a/Tests/DatadogTests/Datadog/Logs/LoggingFeatureTests.swift b/Tests/DatadogTests/Datadog/Logs/LoggingFeatureTests.swift deleted file mode 100644 index c1106d301e..0000000000 --- a/Tests/DatadogTests/Datadog/Logs/LoggingFeatureTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. - */ - -import XCTest -@testable import Datadog - -class LoggingFeatureTests: XCTestCase { - func testInitialization() throws { - let appContext: AppContext = .mockAny() - Datadog.initialize( - appContext: appContext, - configuration: Datadog.Configuration - .builderUsing(clientToken: "abc", environment: "tests") - .build() - ) - - XCTAssertNotNil(LoggingFeature.instance) - - try Datadog.deinitializeOrThrow() - } -} diff --git a/Tests/DatadogTests/Datadog/Mocks/DatadogMocks.swift b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift similarity index 53% rename from Tests/DatadogTests/Datadog/Mocks/DatadogMocks.swift rename to Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift index e2cbd18596..acee0f06b1 100644 --- a/Tests/DatadogTests/Datadog/Mocks/DatadogMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/CoreMocks.swift @@ -4,25 +4,186 @@ * Copyright 2019-2020 Datadog, Inc. */ -import Foundation -import XCTest @testable import Datadog -/* -A collection of SDK object mocks. -It follows the mocking conventions described in `FoundationMocks.swift`. - */ +// MARK: - Configuration Mocks + +extension Datadog.Configuration { + static func mockAny() -> Datadog.Configuration { + return .mockWith() + } + + static func mockWith( + clientToken: String = .mockAny(), + environment: String = .mockAny(), + loggingEnabled: Bool = false, + tracingEnabled: Bool = false, + logsEndpoint: LogsEndpoint = .us, + tracesEndpoint: TracesEndpoint = .us, + serviceName: String? = .mockAny() + ) -> Datadog.Configuration { + return Datadog.Configuration( + clientToken: clientToken, + environment: environment, + loggingEnabled: loggingEnabled, + tracingEnabled: tracingEnabled, + logsEndpoint: logsEndpoint, + tracesEndpoint: tracesEndpoint, + serviceName: serviceName + ) + } +} -// MARK: - Primitive types +extension Datadog.ValidConfiguration { + static func mockAny() -> Datadog.ValidConfiguration { + return mockWith() + } -extension String { - /// Returns string being a valid name of the file managed by `FilesOrchestrator`. - static func mockAnyFileName() -> String { - return Date.mockAny().toFileName + static func mockWith( + applicationName: String = .mockAny(), + applicationVersion: String = .mockAny(), + applicationBundleIdentifier: String = .mockAny(), + serviceName: String = .mockAny(), + environment: String = .mockAny(), + logsUploadURLWithClientToken: URL = .mockAny(), + tracesUploadURLWithClientToken: URL = .mockAny() + ) -> Datadog.ValidConfiguration { + return Datadog.ValidConfiguration( + applicationName: applicationName, + applicationVersion: applicationVersion, + applicationBundleIdentifier: applicationBundleIdentifier, + serviceName: serviceName, + environment: environment, + logsUploadURLWithClientToken: logsUploadURLWithClientToken, + tracesUploadURLWithClientToken: tracesUploadURLWithClientToken + ) } } -// MARK: - Date and time +extension AppContext { + static func mockAny() -> AppContext { + return mockWith() + } + + static func mockWith( + bundleType: BundleType = .iOSApp, + bundleIdentifier: String? = .mockAny(), + bundleVersion: String? = .mockAny(), + bundleName: String? = .mockAny() + ) -> AppContext { + return AppContext( + bundleType: bundleType, + bundleIdentifier: bundleIdentifier, + bundleVersion: bundleVersion, + bundleName: bundleName + ) + } +} + +// MARK: - PerformancePreset Mocks + +struct StoragePerformanceMock: StoragePerformancePreset { + let maxFileSize: UInt64 + let maxDirectorySize: UInt64 + let maxFileAgeForWrite: TimeInterval + let minFileAgeForRead: TimeInterval + let maxFileAgeForRead: TimeInterval + let maxObjectsInFile: Int + let maxObjectSize: UInt64 + + static let noOp = StoragePerformanceMock( + maxFileSize: 0, + maxDirectorySize: 0, + maxFileAgeForWrite: 0, + minFileAgeForRead: 0, + maxFileAgeForRead: 0, + maxObjectsInFile: 0, + maxObjectSize: 0 + ) + + static let readAllFiles = StoragePerformanceMock( + maxFileSize: .max, + maxDirectorySize: .max, + maxFileAgeForWrite: 0, + minFileAgeForRead: -1, // make all files eligible for read + maxFileAgeForRead: .distantFuture, // make all files eligible for read + maxObjectsInFile: .max, + maxObjectSize: .max + ) + + static let writeEachObjectToNewFileAndReadAllFiles = StoragePerformanceMock( + maxFileSize: .max, + maxDirectorySize: .max, + maxFileAgeForWrite: 0, // always return new file for writting + minFileAgeForRead: readAllFiles.minFileAgeForRead, + maxFileAgeForRead: readAllFiles.maxFileAgeForRead, + maxObjectsInFile: 1, // write each data to new file + maxObjectSize: .max + ) +} + +struct UploadPerformanceMock: UploadPerformancePreset { + let initialUploadDelay: TimeInterval + let defaultUploadDelay: TimeInterval + let minUploadDelay: TimeInterval + let maxUploadDelay: TimeInterval + let uploadDelayDecreaseFactor: Double + + static let noOp = UploadPerformanceMock( + initialUploadDelay: .distantFuture, + defaultUploadDelay: .distantFuture, + minUploadDelay: .distantFuture, + maxUploadDelay: .distantFuture, + uploadDelayDecreaseFactor: 1 + ) + + static let veryQuick = UploadPerformanceMock( + initialUploadDelay: 0.05, + defaultUploadDelay: 0.05, + minUploadDelay: 0.05, + maxUploadDelay: 0.05, + uploadDelayDecreaseFactor: 1 + ) +} + +extension PerformancePreset { + static func combining(storagePerformance storage: StoragePerformanceMock, uploadPerformance upload: UploadPerformanceMock) -> PerformancePreset { + PerformancePreset( + maxFileSize: storage.maxFileSize, + maxDirectorySize: storage.maxDirectorySize, + maxFileAgeForWrite: storage.maxFileAgeForWrite, + minFileAgeForRead: storage.minFileAgeForRead, + maxFileAgeForRead: storage.maxFileAgeForRead, + maxObjectsInFile: storage.maxObjectsInFile, + maxObjectSize: storage.maxObjectSize, + initialUploadDelay: upload.initialUploadDelay, + defaultUploadDelay: upload.defaultUploadDelay, + minUploadDelay: upload.minUploadDelay, + maxUploadDelay: upload.maxUploadDelay, + uploadDelayDecreaseFactor: upload.uploadDelayDecreaseFactor + ) + } +} + +// MARK: - Features Common Mocks + +extension DataFormat { + static func mockAny() -> DataFormat { + return mockWith() + } + + static func mockWith( + prefix: String = .mockAny(), + suffix: String = .mockAny(), + separator: String = .mockAny() + ) -> DataFormat { + return DataFormat( + prefix: prefix, + suffix: suffix, + separator: separator + ) + } +} /// `DateProvider` mock returning consecutive dates in custom intervals, starting from given reference date. class RelativeDateProvider: DateProvider { @@ -60,167 +221,49 @@ class RelativeDateProvider: DateProvider { } } -// MARK: - PerformancePreset - -extension PerformancePreset { - /// Mocks performance preset which results with no writes and no uploads. - static func mockNoOp() -> PerformancePreset { - return .mockWith( - maxBatchSize: 0, - maxSizeOfLogsDirectory: 0, - maxFileAgeForWrite: 0, - minFileAgeForRead: 0, - maxFileAgeForRead: 0, - maxLogsPerBatch: 0, - maxLogSize: 0, - initialLogsUploadDelay: .distantFuture, - defaultLogsUploadDelay: .distantFuture, - minLogsUploadDelay: .distantFuture, - maxLogsUploadDelay: .distantFuture, - logsUploadDelayDecreaseFactor: 1 - ) - } - - /// Mocks performance preset which optimizes read / write / upload time for fast unit tests execution. - static func mockUnitTestsPerformancePreset() -> PerformancePreset { - return PerformancePreset( - // persistence - maxBatchSize: .max, // unlimited - maxSizeOfLogsDirectory: .max, // unlimited - maxFileAgeForWrite: 0, // write each data to new file - minFileAgeForRead: -1, // read all files - maxFileAgeForRead: .distantFuture, // read all files - maxLogsPerBatch: 1, // write each data to new file - maxLogSize: .max, // unlimited - - // upload - initialLogsUploadDelay: 0.05, - defaultLogsUploadDelay: 0.05, - minLogsUploadDelay: 0.05, - maxLogsUploadDelay: 0.05, - logsUploadDelayDecreaseFactor: 1 - ) +extension UserInfo { + static func mockAny() -> UserInfo { + return mockEmpty() } - /// Partial mock for performance preset optimized for different write / reads / upload use cases in unit tests. - static func mockWith( - maxBatchSize: UInt64 = .mockAny(), - maxSizeOfLogsDirectory: UInt64 = .mockAny(), - maxFileAgeForWrite: TimeInterval = .mockAny(), - minFileAgeForRead: TimeInterval = .mockAny(), - maxFileAgeForRead: TimeInterval = .mockAny(), - maxLogsPerBatch: Int = .mockAny(), - maxLogSize: UInt64 = .mockAny(), - initialLogsUploadDelay: TimeInterval = .mockAny(), - defaultLogsUploadDelay: TimeInterval = .mockAny(), - minLogsUploadDelay: TimeInterval = .mockAny(), - maxLogsUploadDelay: TimeInterval = .mockAny(), - logsUploadDelayDecreaseFactor: Double = .mockAny() - ) -> PerformancePreset { - return PerformancePreset( - maxBatchSize: maxBatchSize, - maxSizeOfLogsDirectory: maxSizeOfLogsDirectory, - maxFileAgeForWrite: maxFileAgeForWrite, - minFileAgeForRead: minFileAgeForRead, - maxFileAgeForRead: maxFileAgeForRead, - maxLogsPerBatch: maxLogsPerBatch, - maxLogSize: maxLogSize, - initialLogsUploadDelay: initialLogsUploadDelay, - defaultLogsUploadDelay: defaultLogsUploadDelay, - minLogsUploadDelay: minLogsUploadDelay, - maxLogsUploadDelay: maxLogsUploadDelay, - logsUploadDelayDecreaseFactor: logsUploadDelayDecreaseFactor - ) + static func mockEmpty() -> UserInfo { + return UserInfo(id: nil, name: nil, email: nil) } } -// MARK: - Files orchestration - -extension WritableFileConditions { - /// Write conditions causing `FilesOrchestrator` to always pick the same file for writting. - static func mockWriteToSingleFile() -> WritableFileConditions { - return WritableFileConditions( - performance: .mockWith( - maxBatchSize: .max, - maxSizeOfLogsDirectory: .max, - maxFileAgeForWrite: .distantFuture, - maxLogsPerBatch: .max, - maxLogSize: .max - ) - ) - } - - /// Write conditions causing `FilesOrchestrator` to create new file for each write. - static func mockWriteToNewFileEachTime() -> WritableFileConditions { - return WritableFileConditions( - performance: .mockWith( - maxBatchSize: .max, - maxSizeOfLogsDirectory: .max, - maxFileAgeForWrite: .distantFuture, - maxLogsPerBatch: 1, - maxLogSize: .max - ) - ) +extension UserInfoProvider { + static func mockAny() -> UserInfoProvider { + return mockWith() } -} -extension ReadableFileConditions { - /// Read conditions causing `FilesOrchestrator` to pick all files for reading, no matter of their creation time. - static func mockReadAllFiles() -> ReadableFileConditions { - return ReadableFileConditions( - performance: .mockWith( - minFileAgeForRead: -1, - maxFileAgeForRead: .distantFuture - ) - ) + static func mockWith(userInfo: UserInfo = .mockAny()) -> UserInfoProvider { + let provider = UserInfoProvider() + provider.value = userInfo + return provider } } -extension FilesOrchestrator { - /// Mocks `FilesOrchestrator` which always returns the same file for `getWritableFile()`. - static func mockWriteToSingleFile(in directory: Directory) -> FilesOrchestrator { - return FilesOrchestrator( - directory: directory, - writeConditions: .mockWriteToSingleFile(), - readConditions: ReadableFileConditions(performance: .mockUnitTestsPerformancePreset()), - dateProvider: SystemDateProvider() - ) - } - - /// Mocks `FilesOrchestrator` which does not perform age classification for `getReadableFile()`. - static func mockReadAllFiles(in directory: Directory) -> FilesOrchestrator { - return FilesOrchestrator( - directory: directory, - writeConditions: WritableFileConditions(performance: .mockUnitTestsPerformancePreset()), - readConditions: .mockReadAllFiles(), - dateProvider: SystemDateProvider() +extension UploadURLProvider { + static func mockAny() -> UploadURLProvider { + return UploadURLProvider( + urlWithClientToken: URL(string: "https://app.example.com/v2/api?abc-def-ghi")!, + queryItemProviders: [] ) } } -extension FileWriter { - /// Mocks `FileWriter` writting data to single file with given name. - static func mockWrittingToSingleFile( - in directory: Directory, - on queue: DispatchQueue - ) -> FileWriter { - return FileWriter( - orchestrator: .mockWriteToSingleFile(in: directory), - queue: queue - ) +extension HTTPClient { + static func mockAny() -> HTTPClient { + return HTTPClient(session: URLSession()) } } -// MARK: - HTTP - extension HTTPHeaders { static func mockAny() -> HTTPHeaders { - return HTTPHeaders(appName: .mockAny(), appVersion: .mockAny(), device: .mockAny()) + return HTTPHeaders(headers: []) } } -// MARK: - System - extension MobileDevice { static func mockAny() -> MobileDevice { return .mockWith() @@ -395,103 +438,13 @@ class CarrierInfoProviderMock: CarrierInfoProviderType { } } -// MARK: - Persistence and Upload - -extension UploadURLProvider { - static func mockAny() -> UploadURLProvider { - return UploadURLProvider( - urlWithClientToken: URL(string: "https://app.example.com/v2/api?abc-def-ghi")!, - dateProvider: RelativeDateProvider(using: Date.mockDecember15th2019At10AMUTC()) - ) - } -} - -extension DataUploadDelay { - /// Mocks constant delay returning given amount of seconds, no matter of `.decrease()` or `.increaseOnce()` calls. - static func mockConstantDelay(of seconds: TimeInterval) -> DataUploadDelay { - return DataUploadDelay( - performance: .mockWith( - initialLogsUploadDelay: seconds, - defaultLogsUploadDelay: seconds, - minLogsUploadDelay: seconds, - maxLogsUploadDelay: seconds, - logsUploadDelayDecreaseFactor: 1 - ) - ) - } -} - -extension DataUploadConditions { - static func mockAlwaysPerformingUpload() -> DataUploadConditions { - return DataUploadConditions( - batteryStatus: BatteryStatusProviderMock.mockWith( - status: BatteryStatus(state: .full, level: 100, isLowPowerModeEnabled: false) - ), - networkConnectionInfo: NetworkConnectionInfoProviderMock( - networkConnectionInfo: NetworkConnectionInfo( - reachability: .yes, - availableInterfaces: [.wifi], - supportsIPv4: true, - supportsIPv6: true, - isExpensive: false, - isConstrained: false - ) - ) - ) - } -} - -extension HTTPClient { - static func mockAny() -> HTTPClient { - return HTTPClient(session: URLSession()) +extension EncodableValue { + static func mockAny() -> EncodableValue { + return EncodableValue(String.mockAny()) } } -// MARK: - Integration - -extension Datadog.Configuration { - static func mockAny() -> Datadog.Configuration { - return .mockWith() - } - - static func mockWith( - clientToken: String = .mockAny(), - logsEndpoint: LogsEndpoint = .us, - serviceName: String? = .mockAny(), - environment: String = .mockAny() - ) -> Datadog.Configuration { - return Datadog.Configuration( - clientToken: clientToken, - logsEndpoint: logsEndpoint, - serviceName: serviceName, - environment: environment - ) - } -} - -extension Datadog.ValidConfiguration { - static func mockAny() -> Datadog.ValidConfiguration { - return mockWith() - } - - static func mockWith( - applicationName: String = .mockAny(), - applicationVersion: String = .mockAny(), - applicationBundleIdentifier: String = .mockAny(), - serviceName: String = .mockAny(), - environment: String = .mockAny(), - logsUploadURLWithClientToken: URL = .mockAny() - ) -> Datadog.ValidConfiguration { - return Datadog.ValidConfiguration( - applicationName: applicationName, - applicationVersion: applicationVersion, - applicationBundleIdentifier: applicationBundleIdentifier, - serviceName: serviceName, - environment: environment, - logsUploadURLWithClientToken: logsUploadURLWithClientToken - ) - } -} +// MARK: - Global Dependencies Mocks /// Mock which can be used to intercept messages printed by `developerLogger` or /// `userLogger` by overwritting `Datadog.consolePrint` function: @@ -506,59 +459,3 @@ class PrintFunctionMock { printedMessage = message } } - -extension AppContext { - static func mockAny() -> AppContext { - return mockWith() - } - - static func mockWith( - bundleType: BundleType = .iOSApp, - bundleIdentifier: String? = .mockAny(), - bundleVersion: String? = .mockAny(), - bundleName: String? = .mockAny() - ) -> AppContext { - return AppContext( - bundleType: bundleType, - bundleIdentifier: bundleIdentifier, - bundleVersion: bundleVersion, - bundleName: bundleName - ) - } -} - -extension UserInfo { - static func mockAny() -> UserInfo { - return mockEmpty() - } - - static func mockEmpty() -> UserInfo { - return UserInfo(id: nil, name: nil, email: nil) - } -} - -extension UserInfoProvider { - static func mockAny() -> UserInfoProvider { - return mockWith() - } - - static func mockWith(userInfo: UserInfo = .mockAny()) -> UserInfoProvider { - let provider = UserInfoProvider() - provider.value = userInfo - return provider - } -} - -/// `LogOutput` recording received logs. -class LogOutputMock: LogOutput { - struct RecordedLog: Equatable { - let level: LogLevel - let message: String - } - - var recordedLog: RecordedLog? = nil - - func writeLogWith(level: LogLevel, message: String, attributes: [String: Encodable], tags: Set) { - recordedLog = RecordedLog(level: level, message: message) - } -} diff --git a/Tests/DatadogTests/Datadog/Mocks/DatadogPrivateMocks.swift b/Tests/DatadogTests/Datadog/Mocks/DatadogPrivateMocks.swift index cfd25d94b5..70123a77db 100644 --- a/Tests/DatadogTests/Datadog/Mocks/DatadogPrivateMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/DatadogPrivateMocks.swift @@ -6,10 +6,6 @@ import _Datadog_Private -/* - A collection of mocks for `_Datadog_Private` module. - */ - class ObjcExceptionHandlerMock: ObjcExceptionHandler { let error: Error @@ -21,46 +17,3 @@ class ObjcExceptionHandlerMock: ObjcExceptionHandler { throw error } } - -/// An `ObjcExceptionHandler` which results with no error for the first `afterTimes` number of calls. -/// Throws given `throwingError` for all other calls. -class ObjcExceptionHandlerDeferredMock: ObjcExceptionHandler { - private let succeedingCallsCounts: Int - private var currentCallsCount = 0 - - let error: Error - - init(throwingError: Error, afterSucceedingCallsCounts succeedingCallsCounts: Int) { - self.error = throwingError - self.succeedingCallsCounts = succeedingCallsCounts - } - - override func rethrowToSwift(tryBlock: @escaping () -> Void) throws { - if currentCallsCount >= succeedingCallsCounts { - throw error - } else { - tryBlock() - } - currentCallsCount += 1 - } -} - -/// An `ObjcExceptionHandler` which throws given error with given probability. -class ObjcExceptionHandlerNonDeterministicMock: ObjcExceptionHandler { - private let probability: Int - let error: Error - - /// Probability should be described as a number between `0` and `1` - init(throwingError: Error, withProbability probability: Double) { - self.error = throwingError - self.probability = Int(probability * 1_000) - } - - override func rethrowToSwift(tryBlock: @escaping () -> Void) throws { - if Int.random(in: 0...1_000) < probability { - throw error - } else { - tryBlock() - } - } -} diff --git a/Tests/DatadogTests/Datadog/Mocks/LoggingFeatureMocks.swift b/Tests/DatadogTests/Datadog/Mocks/LoggingFeatureMocks.swift index df42eca072..f32711aaa2 100644 --- a/Tests/DatadogTests/Datadog/Mocks/LoggingFeatureMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/LoggingFeatureMocks.swift @@ -6,14 +6,13 @@ @testable import Datadog -/// Collection of mocks for logging feature. extension LoggingFeature { /// Mocks feature instance which performs no writes and no uploads. static func mockNoOp(temporaryDirectory: Directory) -> LoggingFeature { return LoggingFeature( directory: temporaryDirectory, configuration: .mockAny(), - performance: .mockNoOp(), + performance: .combining(storagePerformance: .noOp, uploadPerformance: .noOp), mobileDevice: .mockAny(), httpClient: .mockAny(), logsUploadURLProvider: .mockAny(), @@ -33,7 +32,10 @@ extension LoggingFeature { server: ServerMock, directory: Directory, configuration: Datadog.ValidConfiguration = .mockAny(), - performance: PerformancePreset = .mockUnitTestsPerformancePreset(), + performance: PerformancePreset = .combining( + storagePerformance: .writeEachObjectToNewFileAndReadAllFiles, + uploadPerformance: .veryQuick + ), mobileDevice: MobileDevice = .mockWith( currentBatteryStatus: { // Mock full battery, so it doesn't rely on battery condition for the upload @@ -69,3 +71,119 @@ extension LoggingFeature { ) } } + +// MARK: - Log Mocks + +extension Log { + static func mockWith( + date: Date = .mockAny(), + status: Log.Status = .mockAny(), + message: String = .mockAny(), + serviceName: String = .mockAny(), + environment: String = .mockAny(), + loggerName: String = .mockAny(), + loggerVersion: String = .mockAny(), + threadName: String = .mockAny(), + applicationVersion: String = .mockAny(), + userInfo: UserInfo = .mockAny(), + networkConnectionInfo: NetworkConnectionInfo = .mockAny(), + mobileCarrierInfo: CarrierInfo? = .mockAny(), + attributes: LogAttributes = .mockAny(), + tags: [String]? = nil + ) -> Log { + return Log( + date: date, + status: status, + message: message, + serviceName: serviceName, + environment: environment, + loggerName: loggerName, + loggerVersion: loggerVersion, + threadName: threadName, + applicationVersion: applicationVersion, + userInfo: userInfo, + networkConnectionInfo: networkConnectionInfo, + mobileCarrierInfo: mobileCarrierInfo, + attributes: attributes, + tags: tags + ) + } +} + +extension Log.Status { + static func mockAny() -> Log.Status { + return .info + } +} + +// MARK: - Component Mocks + +extension LogBuilder { + static func mockAny() -> LogBuilder { + return mockWith() + } + + static func mockWith( + applicationVersion: String = .mockAny(), + environment: String = .mockAny(), + serviceName: String = .mockAny(), + loggerName: String = .mockAny(), + userInfoProvider: UserInfoProvider = .mockAny(), + networkConnectionInfoProvider: NetworkConnectionInfoProviderType = NetworkConnectionInfoProviderMock.mockAny(), + carrierInfoProvider: CarrierInfoProviderType = CarrierInfoProviderMock.mockAny() + ) -> LogBuilder { + return LogBuilder( + applicationVersion: applicationVersion, + environment: environment, + serviceName: serviceName, + loggerName: loggerName, + userInfoProvider: userInfoProvider, + networkConnectionInfoProvider: networkConnectionInfoProvider, + carrierInfoProvider: carrierInfoProvider + ) + } +} + +extension LogAttributes: Equatable { + static func mockAny() -> LogAttributes { + return mockWith() + } + + static func mockWith( + userAttributes: [String: Encodable] = [:], + internalAttributes: [String: Encodable]? = nil + ) -> LogAttributes { + return LogAttributes( + userAttributes: userAttributes, + internalAttributes: internalAttributes + ) + } + + public static func == (lhs: LogAttributes, rhs: LogAttributes) -> Bool { + let lhsUserAttributesSorted = lhs.userAttributes.sorted { $0.key < $1.key } + let rhsUserAttributesSorted = rhs.userAttributes.sorted { $0.key < $1.key } + + let lhsInternalAttributesSorted = lhs.internalAttributes?.sorted { $0.key < $1.key } + let rhsInternalAttributesSorted = rhs.internalAttributes?.sorted { $0.key < $1.key } + + return String(describing: lhsUserAttributesSorted) == String(describing: rhsUserAttributesSorted) + && String(describing: lhsInternalAttributesSorted) == String(describing: rhsInternalAttributesSorted) + } +} + +/// `LogOutput` recording received logs. +class LogOutputMock: LogOutput { + struct RecordedLog: Equatable { + var level: LogLevel + var message: String + var date: Date + var attributes = LogAttributes(userAttributes: [:], internalAttributes: nil) + var tags: Set = [] + } + + var recordedLog: RecordedLog? = nil + + func writeLogWith(level: LogLevel, message: String, date: Date, attributes: LogAttributes, tags: Set) { + recordedLog = RecordedLog(level: level, message: message, date: date, attributes: attributes, tags: tags) + } +} diff --git a/Tests/DatadogTests/Datadog/Mocks/LogsMocks.swift b/Tests/DatadogTests/Datadog/Mocks/LogsMocks.swift deleted file mode 100644 index a92e364bf5..0000000000 --- a/Tests/DatadogTests/Datadog/Mocks/LogsMocks.swift +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. - */ - -import Foundation -@testable import Datadog - -/* -A collection of mocks for Logs objects. -It follows the mocking conventions described in `FoundationMocks.swift`. - */ - -extension Log { - static func mockWith( - date: Date = .mockAny(), - status: Log.Status = .mockAny(), - message: String = .mockAny(), - serviceName: String = .mockAny(), - environment: String = .mockAny(), - loggerName: String = .mockAny(), - loggerVersion: String = .mockAny(), - threadName: String = .mockAny(), - applicationVersion: String = .mockAny(), - userInfo: UserInfo = .mockAny(), - networkConnectionInfo: NetworkConnectionInfo = .mockAny(), - mobileCarrierInfo: CarrierInfo? = .mockAny(), - attributes: [String: EncodableValue]? = nil, - tags: [String]? = nil - ) -> Log { - return Log( - date: date, - status: status, - message: message, - serviceName: serviceName, - environment: environment, - loggerName: loggerName, - loggerVersion: loggerVersion, - threadName: threadName, - applicationVersion: applicationVersion, - userInfo: userInfo, - networkConnectionInfo: networkConnectionInfo, - mobileCarrierInfo: mobileCarrierInfo, - attributes: attributes, - tags: tags - ) - } -} - -extension Log.Status { - static func mockAny() -> Log.Status { - return .info - } -} - -extension EncodableValue { - static func mockAny() -> EncodableValue { - return EncodableValue(String.mockAny()) - } -} - -extension LogBuilder { - static func mockAny() -> LogBuilder { - return mockWith() - } - - static func mockWith( - date: Date = .mockAny(), - applicationVersion: String = .mockAny(), - environment: String = .mockAny(), - serviceName: String = .mockAny(), - loggerName: String = .mockAny(), - userInfoProvider: UserInfoProvider = .mockAny(), - networkConnectionInfoProvider: NetworkConnectionInfoProviderType = NetworkConnectionInfoProviderMock.mockAny(), - carrierInfoProvider: CarrierInfoProviderType = CarrierInfoProviderMock.mockAny() - ) -> LogBuilder { - return LogBuilder( - applicationVersion: applicationVersion, - environment: environment, - serviceName: serviceName, - loggerName: loggerName, - dateProvider: RelativeDateProvider(using: date), - userInfoProvider: userInfoProvider, - networkConnectionInfoProvider: networkConnectionInfoProvider, - carrierInfoProvider: carrierInfoProvider - ) - } -} diff --git a/Tests/DatadogTests/Datadog/Mocks/ServerMock.swift b/Tests/DatadogTests/Datadog/Mocks/ServerMock.swift index 9e4609109f..02fdd3fea3 100644 --- a/Tests/DatadogTests/Datadog/Mocks/ServerMock.swift +++ b/Tests/DatadogTests/Datadog/Mocks/ServerMock.swift @@ -188,14 +188,14 @@ class ServerMock { /// Returns recommended timeout for delivering given number of requests if `.mockUnitTestsPerformancePreset()` is used for upload. func recommendedTimeoutFor(numberOfRequestsMade: UInt) -> TimeInterval { - let performancePresetForTests: PerformancePreset = .mockUnitTestsPerformancePreset() + let uploadPerformanceForTests = UploadPerformanceMock.veryQuick // Set the timeout to 40 times more than expected. // In `RUMM-311` we observed 0.66% of flakiness for 150 test runs on CI with arbitrary value of `20`. - return performancePresetForTests.defaultLogsUploadDelay * Double(numberOfRequestsMade) * 40 + return uploadPerformanceForTests.defaultUploadDelay * Double(numberOfRequestsMade) * 40 } } -// MARK: - Logging feature helpers +// MARK: - Feature helpers extension ServerMock { func waitAndReturnLogMatchers(count: UInt, file: StaticString = #file, line: UInt = #line) throws -> [LogMatcher] { @@ -206,6 +206,23 @@ extension ServerMock { line: line ) .map { request in try request.httpBody.unwrapOrThrow() } - .flatMap { requestBody in try LogMatcher.fromArrayOfJSONObjectsData(requestBody) } + .flatMap { requestBody in try LogMatcher.fromArrayOfJSONObjectsData(requestBody, file: file, line: line) } + } +} + +// MARK: - Tracing feature helpers + +extension ServerMock { + func waitAndReturnSpanMatchers(count: UInt, file: StaticString = #file, line: UInt = #line) throws -> [SpanMatcher] { + return try waitAndReturnRequests( + count: count, + timeout: recommendedTimeoutFor(numberOfRequestsMade: count), + file: file, + line: line + ) + .map { request in try request.httpBody.unwrapOrThrow() } + .flatMap { requestBody in + try SpanMatcher.fromNewlineSeparatedJSONObjectsData(requestBody) + } } } diff --git a/Tests/DatadogTests/Datadog/Mocks/CoreTelephonyMocks.swift b/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/CoreTelephonyMocks.swift similarity index 85% rename from Tests/DatadogTests/Datadog/Mocks/CoreTelephonyMocks.swift rename to Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/CoreTelephonyMocks.swift index 2fc63c0bc4..4add09751d 100644 --- a/Tests/DatadogTests/Datadog/Mocks/CoreTelephonyMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/CoreTelephonyMocks.swift @@ -39,6 +39,13 @@ class CTTelephonyNetworkInfoMock: CTTelephonyNetworkInfo { _serviceSubscriberCellularProviders = serviceSubscriberCellularProviders } + // MARK: - iOS 12+ + override var serviceCurrentRadioAccessTechnology: [String: String]? { _serviceCurrentRadioAccessTechnology } override var serviceSubscriberCellularProviders: [String: CTCarrier]? { _serviceSubscriberCellularProviders } + + // MARK: - Prior to iOS 12 + + override var currentRadioAccessTechnology: String? { _serviceCurrentRadioAccessTechnology?.first?.value } + override var subscriberCellularProvider: CTCarrier? { _serviceSubscriberCellularProviders?.first?.value } } diff --git a/Tests/DatadogTests/Datadog/Mocks/FoundationMocks.swift b/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift similarity index 96% rename from Tests/DatadogTests/Datadog/Mocks/FoundationMocks.swift rename to Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift index 5c6c31ffa9..0e471c1571 100644 --- a/Tests/DatadogTests/Datadog/Mocks/FoundationMocks.swift +++ b/Tests/DatadogTests/Datadog/Mocks/SystemFrameworks/FoundationMocks.swift @@ -105,7 +105,13 @@ extension String { } static func mockRandom(length: Int = 10) -> String { - let characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 " + return mockRandom( + among: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ", + length: length + ) + } + + static func mockRandom(among characters: String, length: Int = 10) -> String { return String((0.. TracingFeature { + return TracingFeature( + directory: temporaryDirectory, + configuration: .mockAny(), + performance: .combining(storagePerformance: .noOp, uploadPerformance: .noOp), + loggingFeatureAdapter: nil, + mobileDevice: .mockAny(), + httpClient: .mockAny(), + tracesUploadURLProvider: .mockAny(), + dateProvider: SystemDateProvider(), + tracingUUIDGenerator: DefaultTracingUUIDGenerator(), + userInfoProvider: .mockAny(), + networkConnectionInfoProvider: NetworkConnectionInfoProviderMock.mockWith( + networkConnectionInfo: .mockWith( + reachability: .no // so it doesn't meet the upload condition + ) + ), + carrierInfoProvider: CarrierInfoProviderMock.mockAny() + ) + } + + /// Mocks feature instance which performs uploads to given `ServerMock` with performance optimized for fast delivery in unit tests. + static func mockWorkingFeatureWith( + server: ServerMock, + directory: Directory, + configuration: Datadog.ValidConfiguration = .mockAny(), + performance: PerformancePreset = .combining( + storagePerformance: .writeEachObjectToNewFileAndReadAllFiles, + uploadPerformance: .veryQuick + ), + loggingFeature: LoggingFeature? = nil, + mobileDevice: MobileDevice = .mockWith( + currentBatteryStatus: { + // Mock full battery, so it doesn't rely on battery condition for the upload + return BatteryStatus(state: .full, level: 1, isLowPowerModeEnabled: false) + } + ), + tracesUploadURLProvider: UploadURLProvider = .mockAny(), + dateProvider: DateProvider = SystemDateProvider(), + tracingUUIDGenerator: TracingUUIDGenerator = DefaultTracingUUIDGenerator(), + userInfoProvider: UserInfoProvider = .mockAny(), + networkConnectionInfoProvider: NetworkConnectionInfoProviderType = NetworkConnectionInfoProviderMock.mockWith( + networkConnectionInfo: .mockWith( + reachability: .yes, // so it always meets the upload condition + availableInterfaces: [.wifi], + supportsIPv4: true, + supportsIPv6: true, + isExpensive: true, + isConstrained: false // so it always meets the upload condition + ) + ), + carrierInfoProvider: CarrierInfoProviderType = CarrierInfoProviderMock.mockAny() + ) -> TracingFeature { + return TracingFeature( + directory: directory, + configuration: configuration, + performance: performance, + loggingFeatureAdapter: loggingFeature.flatMap { LoggingForTracingAdapter(loggingFeature: $0) }, + mobileDevice: mobileDevice, + httpClient: HTTPClient(session: server.urlSession), + tracesUploadURLProvider: tracesUploadURLProvider, + dateProvider: dateProvider, + tracingUUIDGenerator: tracingUUIDGenerator, + userInfoProvider: userInfoProvider, + networkConnectionInfoProvider: networkConnectionInfoProvider, + carrierInfoProvider: carrierInfoProvider + ) + } +} + +// MARK: - Span Mocks + +extension DDSpanContext { + static func mockAny() -> DDSpanContext { + return mockWith() + } + + static func mockWith( + traceID: TracingUUID = .mockAny(), + spanID: TracingUUID = .mockAny(), + parentSpanID: TracingUUID? = .mockAny(), + baggageItems: BaggageItems = .mockAny() + ) -> DDSpanContext { + return DDSpanContext( + traceID: traceID, + spanID: spanID, + parentSpanID: parentSpanID, + baggageItems: baggageItems + ) + } +} + +extension BaggageItems { + static func mockAny() -> BaggageItems { + return BaggageItems( + targetQueue: DispatchQueue(label: "com.datadoghq.baggage-items"), + parentSpanItems: nil + ) + } +} + +extension DDSpan { + static func mockAny() -> DDSpan { + return mockWith() + } + + static func mockWith( + tracer: Tracer = .mockAny(), + context: DDSpanContext = .mockAny(), + operationName: String = .mockAny(), + startTime: Date = .mockAny(), + tags: [String: Encodable] = [:] + ) -> DDSpan { + return DDSpan( + tracer: tracer, + context: context, + operationName: operationName, + startTime: startTime, + tags: tags + ) + } +} + +extension TracingUUID { + static func mockAny() -> TracingUUID { + return TracingUUID(rawValue: .mockAny()) + } + + static func mock(_ rawValue: UInt64) -> TracingUUID { + return TracingUUID(rawValue: rawValue) + } +} + +class RelativeTracingUUIDGenerator: TracingUUIDGenerator { + private(set) var uuid: TracingUUID + internal let count: UInt64 + private let queue = DispatchQueue(label: "queue-RelativeTracingUUIDGenerator-\(UUID().uuidString)") + + init(startingFrom uuid: TracingUUID, advancingByCount count: UInt64 = 1) { + self.uuid = uuid + self.count = count + } + + func generateUnique() -> TracingUUID { + return queue.sync { + defer { uuid = TracingUUID(rawValue: uuid.rawValue + count) } + return uuid + } + } +} + +// MARK: - Component Mocks + +extension Tracer { + static func mockAny() -> Tracer { + return mockWith() + } + + static func mockWith( + spanOutput: SpanOutput = SpanOutputMock(), + logOutput: LoggingForTracingAdapter.AdaptedLogOutput = .init(loggingOutput: LogOutputMock()), + dateProvider: DateProvider = SystemDateProvider(), + tracingUUIDGenerator: TracingUUIDGenerator = DefaultTracingUUIDGenerator(), + globalTags: [String: Encodable]? = nil + ) -> Tracer { + return Tracer( + spanOutput: spanOutput, + logOutput: logOutput, + dateProvider: dateProvider, + tracingUUIDGenerator: tracingUUIDGenerator, + globalTags: globalTags + ) + } +} + +extension SpanBuilder { + static func mockAny() -> SpanBuilder { + return mockWith() + } + + static func mockWith( + applicationVersion: String = .mockAny(), + environment: String = .mockAny(), + serviceName: String = .mockAny(), + userInfoProvider: UserInfoProvider = .mockAny(), + networkConnectionInfoProvider: NetworkConnectionInfoProviderType = NetworkConnectionInfoProviderMock.mockAny(), + carrierInfoProvider: CarrierInfoProviderType = CarrierInfoProviderMock.mockAny() + ) -> SpanBuilder { + return SpanBuilder( + applicationVersion: applicationVersion, + environment: environment, + serviceName: serviceName, + userInfoProvider: userInfoProvider, + networkConnectionInfoProvider: networkConnectionInfoProvider, + carrierInfoProvider: carrierInfoProvider + ) + } +} + +/// `SpanOutput` recording received spans. +class SpanOutputMock: SpanOutput { + struct Recorded { + let span: DDSpan + let finishTime: Date + } + + var recorded: Recorded? = nil + + func write(ddspan: DDSpan, finishTime: Date) { + recorded = Recorded(span: ddspan, finishTime: finishTime) + } +} diff --git a/Tests/DatadogTests/Datadog/TracerConfigurationTests.swift b/Tests/DatadogTests/Datadog/TracerConfigurationTests.swift new file mode 100644 index 0000000000..451c0d6c4b --- /dev/null +++ b/Tests/DatadogTests/Datadog/TracerConfigurationTests.swift @@ -0,0 +1,109 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class TracerConfigurationTests: XCTestCase { + private let networkConnectionInfoProvider: NetworkConnectionInfoProviderMock = .mockAny() + private let carrierInfoProvider: CarrierInfoProviderMock = .mockAny() + private var mockServer: ServerMock! // swiftlint:disable:this implicitly_unwrapped_optional + + override func setUp() { + super.setUp() + temporaryDirectory.create() + + mockServer = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + TracingFeature.instance = .mockWorkingFeatureWith( + server: mockServer, + directory: temporaryDirectory, + configuration: .mockWith( + applicationVersion: "1.2.3", + serviceName: "service-name", + environment: "tests" + ), + loggingFeature: .mockNoOp(temporaryDirectory: temporaryDirectory), + networkConnectionInfoProvider: networkConnectionInfoProvider, + carrierInfoProvider: carrierInfoProvider + ) + } + + override func tearDown() { + mockServer.waitAndAssertNoRequestsSent() + TracingFeature.instance = nil + mockServer = nil + + temporaryDirectory.delete() + super.tearDown() + } + + func testDefaultTracer() { + let tracer = Tracer.initialize( + configuration: .init() + ).dd + + guard let spanBuilder = (tracer.spanOutput as? SpanFileOutput)?.spanBuilder else { + XCTFail() + return + } + + let feature = TracingFeature.instance! + XCTAssertEqual(spanBuilder.applicationVersion, "1.2.3") + XCTAssertEqual(spanBuilder.environment, "tests") + XCTAssertEqual(spanBuilder.serviceName, "service-name") + XCTAssertTrue(spanBuilder.userInfoProvider === feature.userInfoProvider) + XCTAssertNil(spanBuilder.networkConnectionInfoProvider) + XCTAssertNil(spanBuilder.carrierInfoProvider) + + guard let tracingLogBuilder = (tracer.logOutput?.loggingOutput as? LogFileOutput)?.logBuilder else { + XCTFail() + return + } + + XCTAssertEqual(tracingLogBuilder.applicationVersion, "1.2.3") + XCTAssertEqual(tracingLogBuilder.environment, "tests") + XCTAssertEqual(tracingLogBuilder.serviceName, "service-name") + XCTAssertEqual(tracingLogBuilder.loggerName, "trace") + XCTAssertTrue(tracingLogBuilder.userInfoProvider === feature.userInfoProvider) + XCTAssertNil(tracingLogBuilder.networkConnectionInfoProvider) + XCTAssertNil(tracingLogBuilder.carrierInfoProvider) + } + + func testCustomizedTracer() { + let tracer = Tracer.initialize( + configuration: .init( + serviceName: "custom-service-name", + sendNetworkInfo: true + ) + ).dd + + guard let spanBuilder = (tracer.spanOutput as? SpanFileOutput)?.spanBuilder else { + XCTFail() + return + } + + let feature = TracingFeature.instance! + XCTAssertEqual(spanBuilder.applicationVersion, "1.2.3") + XCTAssertEqual(spanBuilder.serviceName, "custom-service-name") + XCTAssertEqual(spanBuilder.environment, "tests") + XCTAssertTrue(spanBuilder.userInfoProvider === feature.userInfoProvider) + XCTAssertTrue(spanBuilder.networkConnectionInfoProvider as AnyObject === feature.networkConnectionInfoProvider as AnyObject) + XCTAssertTrue(spanBuilder.carrierInfoProvider as AnyObject === feature.carrierInfoProvider as AnyObject) + + guard let tracingLogBuilder = (tracer.logOutput?.loggingOutput as? LogFileOutput)?.logBuilder else { + XCTFail() + return + } + + XCTAssertEqual(tracingLogBuilder.applicationVersion, "1.2.3") + XCTAssertEqual(tracingLogBuilder.environment, "tests") + XCTAssertEqual(tracingLogBuilder.serviceName, "custom-service-name") + XCTAssertEqual(tracingLogBuilder.loggerName, "trace") + XCTAssertTrue(tracingLogBuilder.userInfoProvider === feature.userInfoProvider) + XCTAssertTrue(tracingLogBuilder.networkConnectionInfoProvider as AnyObject === feature.networkConnectionInfoProvider as AnyObject) + XCTAssertTrue(tracingLogBuilder.carrierInfoProvider as AnyObject === feature.carrierInfoProvider as AnyObject) + } +} diff --git a/Tests/DatadogTests/Datadog/TracerTests.swift b/Tests/DatadogTests/Datadog/TracerTests.swift new file mode 100644 index 0000000000..91361a01e8 --- /dev/null +++ b/Tests/DatadogTests/Datadog/TracerTests.swift @@ -0,0 +1,680 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +// swiftlint:disable multiline_arguments_brackets +class TracerTests: XCTestCase { + override func setUp() { + super.setUp() + XCTAssertNil(Datadog.instance) + XCTAssertNil(LoggingFeature.instance) + temporaryDirectory.create() + } + + override func tearDown() { + XCTAssertNil(Datadog.instance) + XCTAssertNil(LoggingFeature.instance) + temporaryDirectory.delete() + super.tearDown() + } + + // MARK: - Customizing Tracer + + func testSendingSpanWithDefaultTracer() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + configuration: .mockWith( + applicationVersion: "1.0.0", + applicationBundleIdentifier: "com.datadoghq.ios-sdk", + serviceName: "default-service-name", + environment: "custom" + ), + dateProvider: RelativeDateProvider(using: .mockDecember15th2019At10AMUTC()), + tracingUUIDGenerator: RelativeTracingUUIDGenerator(startingFrom: 1) + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize(configuration: .init()) + + let span = tracer.startSpan(operationName: "operation") + span.finish(at: .mockDecember15th2019At10AMUTC(addingTimeInterval: 0.5)) + + let spanMatcher = try server.waitAndReturnSpanMatchers(count: 1)[0] + try spanMatcher.assertItFullyMatches(jsonString: """ + { + "spans": [ + { + "trace_id": "1", + "span_id": "2", + "parent_id": "0", + "name": "operation", + "service": "default-service-name", + "resource": "operation", + "start": 1576404000000000000, + "duration": 500000000, + "error": 0, + "type": "custom", + "meta.tracer.version": "\(sdkVersion)", + "meta.version": "1.0.0", + "meta._dd.source": "ios", + "metrics._top_level": 1, + "metrics._sampling_priority_v1": 1 + } + ], + "env": "custom" + } + """) // TOOD: RUMM-422 Network info is not send by default with spans + } + + func testSendingSpanWithCustomizedTracer() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize( + configuration: .init( + serviceName: "custom-service-name", + sendNetworkInfo: true + ) + ) + + let span = tracer.startSpan(operationName: .mockAny()) + span.finish() + + let spanMatcher = try server.waitAndReturnSpanMatchers(count: 1)[0] + + XCTAssertEqual(try spanMatcher.serviceName(), "custom-service-name") + XCTAssertNoThrow(try spanMatcher.meta.networkAvailableInterfaces()) + XCTAssertNoThrow(try spanMatcher.meta.networkConnectionIsExpensive()) + XCTAssertNoThrow(try spanMatcher.meta.networkReachability()) + XCTAssertNoThrow(try spanMatcher.meta.mobileNetworkCarrierAllowsVoIP()) + XCTAssertNoThrow(try spanMatcher.meta.mobileNetworkCarrierISOCountryCode()) + XCTAssertNoThrow(try spanMatcher.meta.mobileNetworkCarrierName()) + XCTAssertNoThrow(try spanMatcher.meta.mobileNetworkCarrierRadioTechnology()) + XCTAssertNoThrow(try spanMatcher.meta.networkConnectionSupportsIPv4()) + XCTAssertNoThrow(try spanMatcher.meta.networkConnectionSupportsIPv6()) + if #available(iOS 13.0, *) { + XCTAssertNoThrow(try spanMatcher.meta.networkConnectionIsConstrained()) + } + } + + func testSendingSpanWithGlobalTags() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize( + configuration: .init( + serviceName: "custom-service-name", + globalTags: [ + "globaltag1": "globalValue1", + "globaltag2": "globalValue2" + ] + ) + ) + + let span = tracer.startSpan(operationName: .mockAny()) + span.setTag(key: "globaltag2", value: "overwrittenValue" ) + span.finish() + + let spanMatcher = try server.waitAndReturnSpanMatchers(count: 1)[0] + XCTAssertEqual(try spanMatcher.serviceName(), "custom-service-name") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.globaltag1"), "globalValue1") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.globaltag2"), "overwrittenValue") + } + + // MARK: - Sending Customized Spans + + func testSendingCustomizedSpan() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize(configuration: .init()).dd + + let span = tracer.startSpan( + operationName: "operation", + tags: [ + "tag1": "string value", + "error": true, + DDTags.resource: "GET /foo.png" + ], + startTime: .mockDecember15th2019At10AMUTC() + ) + span.setTag(key: "tag2", value: 123) + span.finish(at: .mockDecember15th2019At10AMUTC(addingTimeInterval: 0.5)) + + let spanMatcher = try server.waitAndReturnSpanMatchers(count: 1)[0] + XCTAssertEqual(try spanMatcher.operationName(), "operation") + XCTAssertEqual(try spanMatcher.resource(), "GET /foo.png") + XCTAssertEqual(try spanMatcher.startTime(), 1_576_404_000_000_000_000) + XCTAssertEqual(try spanMatcher.duration(), 500_000_000) + XCTAssertEqual(try spanMatcher.isError(), 1) + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.tag1"), "string value") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.tag2"), "123") + } + + func testSendingSpanWithParentAndBaggageItems() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize(configuration: .init()).dd + + let rootSpan = tracer.startSpan(operationName: "root operation") + let childSpan = tracer.startSpan(operationName: "child operation", childOf: rootSpan.context) + let grandchildSpan = tracer.startSpan(operationName: "grandchild operation", childOf: childSpan.context) + rootSpan.setBaggageItem(key: "root-item", value: "foo") + childSpan.setBaggageItem(key: "child-item", value: "bar") + grandchildSpan.setBaggageItem(key: "grandchild-item", value: "bizz") + + grandchildSpan.setTag(key: "overwritten", value: "b") // This value "b" coming from a tag... + grandchildSpan.setBaggageItem(key: "overwritten", value: "a") // ... should overwrite this "a" coming from the baggage item. + + grandchildSpan.finish() + childSpan.finish() + rootSpan.finish() + + let spanMatchers = try server.waitAndReturnSpanMatchers(count: 3) + let rootMatcher = spanMatchers[2] + let childMatcher = spanMatchers[1] + let grandchildMatcher = spanMatchers[0] + + // Assert child-parent relationship + + XCTAssertEqual(try grandchildMatcher.operationName(), "grandchild operation") + XCTAssertEqual(try grandchildMatcher.traceID(), rootSpan.context.dd.traceID.toHexadecimalString) + XCTAssertEqual(try grandchildMatcher.parentSpanID(), childSpan.context.dd.spanID.toHexadecimalString) + XCTAssertNil(try? grandchildMatcher.metrics.isRootSpan()) + XCTAssertEqual(try grandchildMatcher.meta.custom(keyPath: "meta.root-item"), "foo") + XCTAssertEqual(try grandchildMatcher.meta.custom(keyPath: "meta.child-item"), "bar") + XCTAssertEqual(try grandchildMatcher.meta.custom(keyPath: "meta.grandchild-item"), "bizz") + XCTAssertEqual(try grandchildMatcher.meta.custom(keyPath: "meta.grandchild-item"), "bizz") + XCTAssertEqual(try grandchildMatcher.meta.custom(keyPath: "meta.overwritten"), "b", "Tags should have higher priority than baggage items") + + XCTAssertEqual(try childMatcher.operationName(), "child operation") + XCTAssertEqual(try childMatcher.traceID(), rootSpan.context.dd.traceID.toHexadecimalString) + XCTAssertEqual(try childMatcher.parentSpanID(), rootSpan.context.dd.spanID.toHexadecimalString) + XCTAssertNil(try? childMatcher.metrics.isRootSpan()) + XCTAssertEqual(try childMatcher.meta.custom(keyPath: "meta.root-item"), "foo") + XCTAssertEqual(try childMatcher.meta.custom(keyPath: "meta.child-item"), "bar") + XCTAssertNil(try? childMatcher.meta.custom(keyPath: "meta.grandchild-item")) + + XCTAssertEqual(try rootMatcher.operationName(), "root operation") + XCTAssertEqual(try rootMatcher.parentSpanID(), "0") + XCTAssertEqual(try rootMatcher.metrics.isRootSpan(), 1) + XCTAssertEqual(try rootMatcher.meta.custom(keyPath: "meta.root-item"), "foo") + XCTAssertNil(try? rootMatcher.meta.custom(keyPath: "meta.child-item")) + XCTAssertNil(try? rootMatcher.meta.custom(keyPath: "meta.grandchild-item")) + + // Assert timing constraints + + XCTAssertGreaterThan(try grandchildMatcher.startTime(), try childMatcher.startTime()) + XCTAssertGreaterThan(try childMatcher.startTime(), try rootMatcher.startTime()) + XCTAssertLessThan(try grandchildMatcher.duration(), try childMatcher.duration()) + XCTAssertLessThan(try childMatcher.duration(), try rootMatcher.duration()) + } + + // MARK: - Sending user info + + func testSendingUserInfo() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + Datadog.instance = Datadog( + userInfoProvider: UserInfoProvider() + ) + defer { Datadog.instance = nil } + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + userInfoProvider: Datadog.instance!.userInfoProvider + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize(configuration: .init()).dd + + tracer.startSpan(operationName: "span with no user info").finish() + + Datadog.setUserInfo(id: "abc-123", name: "Foo") + tracer.startSpan(operationName: "span with user `id` and `name`").finish() + + Datadog.setUserInfo(id: "abc-123", name: "Foo", email: "foo@example.com") + tracer.startSpan(operationName: "span with user `id`, `name` and `email`").finish() + + Datadog.setUserInfo(id: nil, name: nil, email: nil) + tracer.startSpan(operationName: "span with no user info").finish() + + let spanMatchers = try server.waitAndReturnSpanMatchers(count: 4) + XCTAssertNil(try? spanMatchers[0].meta.userID()) + XCTAssertNil(try? spanMatchers[0].meta.userName()) + XCTAssertNil(try? spanMatchers[0].meta.userEmail()) + + XCTAssertEqual(try spanMatchers[1].meta.userID(), "abc-123") + XCTAssertEqual(try spanMatchers[1].meta.userName(), "Foo") + XCTAssertNil(try? spanMatchers[1].meta.userEmail()) + + XCTAssertEqual(try spanMatchers[2].meta.userID(), "abc-123") + XCTAssertEqual(try spanMatchers[2].meta.userName(), "Foo") + XCTAssertEqual(try spanMatchers[2].meta.userEmail(), "foo@example.com") + + XCTAssertNil(try? spanMatchers[3].meta.userID()) + XCTAssertNil(try? spanMatchers[3].meta.userName()) + XCTAssertNil(try? spanMatchers[3].meta.userEmail()) + } + + // MARK: - Sending carrier info + + func testSendingCarrierInfoWhenEnteringAndLeavingCellularServiceRange() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + let carrierInfoProvider = CarrierInfoProviderMock(carrierInfo: nil) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + carrierInfoProvider: carrierInfoProvider + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize( + configuration: .init(sendNetworkInfo: true) + ).dd + + // simulate entering cellular service range + carrierInfoProvider.set( + current: .mockWith( + carrierName: "Carrier", + carrierISOCountryCode: "US", + carrierAllowsVOIP: true, + radioAccessTechnology: .LTE + ) + ) + + tracer.startSpan(operationName: "span with carrier info").finish() + + // simulate leaving cellular service range + carrierInfoProvider.set(current: nil) + + tracer.startSpan(operationName: "span with no carrier info").finish() + + let spanMatchers = try server.waitAndReturnSpanMatchers(count: 2) + XCTAssertEqual(try spanMatchers[0].meta.mobileNetworkCarrierName(), "Carrier") + XCTAssertEqual(try spanMatchers[0].meta.mobileNetworkCarrierISOCountryCode(), "US") + XCTAssertEqual(try spanMatchers[0].meta.mobileNetworkCarrierRadioTechnology(), "LTE") + XCTAssertEqual(try spanMatchers[0].meta.mobileNetworkCarrierAllowsVoIP(), "1") + + XCTAssertNil(try? spanMatchers[1].meta.mobileNetworkCarrierName()) + XCTAssertNil(try? spanMatchers[1].meta.mobileNetworkCarrierISOCountryCode()) + XCTAssertNil(try? spanMatchers[1].meta.mobileNetworkCarrierRadioTechnology()) + XCTAssertNil(try? spanMatchers[1].meta.mobileNetworkCarrierAllowsVoIP()) + } + + // MARK: - Sending network info + + func testSendingNetworkConnectionInfoWhenReachabilityChanges() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + let networkConnectionInfoProvider = NetworkConnectionInfoProviderMock.mockAny() + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + networkConnectionInfoProvider: networkConnectionInfoProvider + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize( + configuration: .init(sendNetworkInfo: true) + ).dd + + // simulate reachable network + networkConnectionInfoProvider.set( + current: .mockWith( + reachability: .yes, + availableInterfaces: [.wifi, .cellular], + supportsIPv4: true, + supportsIPv6: true, + isExpensive: true, + isConstrained: true + ) + ) + + tracer.startSpan(operationName: "online span").finish() + + // simulate unreachable network + networkConnectionInfoProvider.set( + current: .mockWith( + reachability: .no, + availableInterfaces: [], + supportsIPv4: false, + supportsIPv6: false, + isExpensive: false, + isConstrained: false + ) + ) + + tracer.startSpan(operationName: "offline span").finish() + + // put the network back online so last span can be send + networkConnectionInfoProvider.set(current: .mockWith(reachability: .yes)) + + let spanMatchers = try server.waitAndReturnSpanMatchers(count: 2) + XCTAssertEqual(try spanMatchers[0].meta.networkReachability(), "yes") + XCTAssertEqual(try spanMatchers[0].meta.networkAvailableInterfaces(), "wifi+cellular") + XCTAssertEqual(try spanMatchers[0].meta.networkConnectionIsConstrained(), "1") + XCTAssertEqual(try spanMatchers[0].meta.networkConnectionIsExpensive(), "1") + XCTAssertEqual(try spanMatchers[0].meta.networkConnectionSupportsIPv4(), "1") + XCTAssertEqual(try spanMatchers[0].meta.networkConnectionSupportsIPv6(), "1") + + XCTAssertEqual(try? spanMatchers[1].meta.networkReachability(), "no") + XCTAssertNil(try? spanMatchers[1].meta.networkAvailableInterfaces()) + XCTAssertEqual(try spanMatchers[1].meta.networkConnectionIsConstrained(), "0") + XCTAssertEqual(try spanMatchers[1].meta.networkConnectionIsExpensive(), "0") + XCTAssertEqual(try spanMatchers[1].meta.networkConnectionSupportsIPv4(), "0") + XCTAssertEqual(try spanMatchers[1].meta.networkConnectionSupportsIPv6(), "0") + } + + // MARK: - Sending logs with different network and battery conditions + + func testGivenBadBatteryConditions_itDoesNotTryToSendTraces() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + mobileDevice: .mockWith( + currentBatteryStatus: { () -> MobileDevice.BatteryStatus in + .mockWith(state: .charging, level: 0.05, isLowPowerModeEnabled: true) + } + ) + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize(configuration: .init()).dd + + tracer.startSpan(operationName: .mockAny()).finish() + + server.waitAndAssertNoRequestsSent() + } + + func testGivenNoNetworkConnection_itDoesNotTryToSendTraces() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + networkConnectionInfoProvider: NetworkConnectionInfoProviderMock.mockWith( + networkConnectionInfo: .mockWith(reachability: .no) + ) + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize(configuration: .init()).dd + + tracer.startSpan(operationName: .mockAny()).finish() + + server.waitAndAssertNoRequestsSent() + } + + // MARK: - Sending tags + + func testSendingSpanTagsOfDifferentEncodableValues() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize(configuration: .init()).dd + + let span = tracer.startSpan(operationName: "operation", tags: [:], startTime: .mockDecember15th2019At10AMUTC()) + + // string literal + span.setTag(key: "string", value: "hello") + + // boolean literal + span.setTag(key: "bool", value: true) + + // integer literal + span.setTag(key: "int", value: 10) + + // Typed 8-bit unsigned Integer + span.setTag(key: "uint-8", value: UInt8(10)) + + // double-precision, floating-point value + span.setTag(key: "double", value: 10.5) + + // array of `Encodable` integer + span.setTag(key: "array-of-int", value: [1, 2, 3]) + + // dictionary of `Encodable` date types + span.setTag(key: "dictionary-with-date", value: [ + "date": Date.mockDecember15th2019At10AMUTC(), + ]) + + struct Person: Codable { + let name: String + let age: Int + let nationality: String + } + + // custom `Encodable` structure + span.setTag(key: "person", value: Person(name: "Adam", age: 30, nationality: "Polish")) + + // nested string literal + span.setTag(key: "nested.string", value: "hello") + + // URL + span.setTag(key: "url", value: URL(string: "https://example.com/image.png")!) + + span.finish(at: .mockDecember15th2019At10AMUTC(addingTimeInterval: 0.5)) + + let spanMatcher = try server.waitAndReturnSpanMatchers(count: 1)[0] + XCTAssertEqual(try spanMatcher.operationName(), "operation") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.string"), "hello") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.bool"), "true") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.int"), "10") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.uint-8"), "10") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.double"), "10.5") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.array-of-int"), "[1,2,3]") + XCTAssertEqual( + try spanMatcher.meta.custom(keyPath: "meta.dictionary-with-date"), + #"{"date":"2019-12-15T10:00:00.000Z"}"# + ) + XCTAssertEqual( + try spanMatcher.meta.custom(keyPath: "meta.person"), + #"{"name":"Adam","age":30,"nationality":"Polish"}"# + ) + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.nested.string"), "hello") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.url"), "https://example.com/image.png") + } + + // MARK: - Sending logs + + func testSendingSpanLogs() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + let loggingFeature = LoggingFeature.mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + performance: .combining(storagePerformance: .readAllFiles, uploadPerformance: .veryQuick) + ) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + performance: .combining(storagePerformance: .noOp, uploadPerformance: .noOp), + loggingFeature: loggingFeature + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize(configuration: .init()) + + let span = tracer.startSpan(operationName: "operation", startTime: .mockDecember15th2019At10AMUTC()) + span.log(fields: [OTLogFields.message: "hello", "custom.field": "value"]) + span.log(fields: [OTLogFields.event: "error", OTLogFields.errorKind: "Swift error", OTLogFields.message: "Ops!"]) + + let logMatchers = try server.waitAndReturnLogMatchers(count: 2) + + let regularLogMatcher = logMatchers[0] + let errorLogMatcher = logMatchers[1] + + regularLogMatcher.assertStatus(equals: "info") + regularLogMatcher.assertMessage(equals: "hello") + regularLogMatcher.assertValue(forKey: "dd.trace_id", equals: "\(span.context.dd.traceID.rawValue)") + regularLogMatcher.assertValue(forKey: "dd.span_id", equals: "\(span.context.dd.spanID.rawValue)") + regularLogMatcher.assertValue(forKey: "custom.field", equals: "value") + + errorLogMatcher.assertStatus(equals: "error") + errorLogMatcher.assertValue(forKey: "event", equals: "error") + errorLogMatcher.assertValue(forKey: "error.kind", equals: "Swift error") + errorLogMatcher.assertMessage(equals: "Ops!") + errorLogMatcher.assertValue(forKey: "dd.trace_id", equals: "\(span.context.dd.traceID.rawValue)") + errorLogMatcher.assertValue(forKey: "dd.span_id", equals: "\(span.context.dd.spanID.rawValue)") + } + + // MARK: - Injecting span context into carrier + + func testItInjectsSpanContextIntoHTTPHeadersWriter() { + let tracer: Tracer = .mockAny() + let spanContext = DDSpanContext(traceID: 1, spanID: 2, parentSpanID: .mockAny(), baggageItems: .mockAny()) + + let httpHeadersWriter = HTTPHeadersWriter() + XCTAssertEqual(httpHeadersWriter.tracePropagationHTTPHeaders, [:]) + + tracer.inject(spanContext: spanContext, writer: httpHeadersWriter) + + let expectedHTTPHeaders = [ + "x-datadog-trace-id": "1", + "x-datadog-parent-id": "2", + ] + XCTAssertEqual(httpHeadersWriter.tracePropagationHTTPHeaders, expectedHTTPHeaders) + } + + // MARK: - Thread safety + + func testRandomlyCallingDifferentAPIsConcurrentlyDoesNotCrash() { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + TracingFeature.instance = .mockNoOp(temporaryDirectory: temporaryDirectory) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize(configuration: .init()) + var spans: [DDSpan] = [] + let queue = DispatchQueue(label: "spans-array-sync") + + // Start 20 spans concurrently + DispatchQueue.concurrentPerform(iterations: 20) { iteration in + let span = tracer.startSpan(operationName: "operation \(iteration)", childOf: nil).dd + queue.async { spans.append(span) } + } + + queue.sync {} // wait for all spans in the array + + /// Calls given closures on each span cuncurrently + func testThreadSafety(closures: [(DDSpan) -> Void]) { + DispatchQueue.concurrentPerform(iterations: 100) { iteration in + closures.forEach { closure in + closure(spans[iteration % spans.count]) + } + } + } + + testThreadSafety( + closures: [ + // swiftlint:disable opening_brace + { span in span.setTag(key: .mockRandom(among: "abcde", length: 1), value: "value") }, + { span in span.setBaggageItem(key: .mockRandom(among: "abcde", length: 1), value: "value") }, + { span in _ = span.baggageItem(withKey: .mockRandom(among: "abcde")) }, + { span in _ = span.context.forEachBaggageItem { _, _ in return false } }, + { span in span.log(fields: [.mockRandom(among: "abcde", length: 1): "value"]) }, + { span in span.finish() } + // swiftlint:enable opening_brace + ] + ) + + server.waitAndAssertNoRequestsSent() + } + + // MARK: - Usage errors + + func testGivenDatadogNotInitialized_whenInitializingTracer_itPrintsError() { + let printFunction = PrintFunctionMock() + consolePrint = printFunction.print + defer { consolePrint = { print($0) } } + + // given + XCTAssertNil(Datadog.instance) + + // when + let tracer = Tracer.initialize(configuration: .init()) + + // then + XCTAssertEqual( + printFunction.printedMessage, + "๐Ÿ”ฅ Datadog SDK usage error: `Datadog.initialize()` must be called prior to `Tracer.initialize()`." + ) + XCTAssertTrue(tracer is DDNoopTracer) + } + + func testGivenTracingFeatureDisabled_whenInitializingTracer_itPrintsError() throws { + let printFunction = PrintFunctionMock() + consolePrint = printFunction.print + defer { consolePrint = { print($0) } } + + // given + Datadog.initialize( + appContext: .mockAny(), + configuration: Datadog.Configuration.builderUsing(clientToken: "abc.def", environment: "tests") + .enableTracing(false) + .build() + ) + + // when + let tracer = Tracer.initialize(configuration: .init()) + + // then + XCTAssertEqual( + printFunction.printedMessage, + "๐Ÿ”ฅ Datadog SDK usage error: `Tracer.initialize(configuration:)` produces a non-functional tracer, as the tracing feature is disabled." + ) + XCTAssertTrue(tracer is DDNoopTracer) + + try Datadog.deinitializeOrThrow() + } + + func testGivenLoggingFeatureDisabled_whenSendingLogFromSpan_itPrintsWarning() throws { + // given + Datadog.initialize( + appContext: .mockAny(), + configuration: Datadog.Configuration.builderUsing(clientToken: "abc.def", environment: "tests") + .enableLogging(false) + .build() + ) + + let output = LogOutputMock() + userLogger = Logger(logOutput: output, dateProvider: SystemDateProvider(), identifier: "sdk-user") + + // when + let tracer = Tracer.initialize(configuration: .init()) + let span = tracer.startSpan(operationName: "foo") + span.log(fields: ["bar": "bizz"]) + + // then + XCTAssertEqual(output.recordedLog?.level, .warn) + XCTAssertEqual(output.recordedLog?.message, "The log for span \"foo\" will not be send, because the Logging feature is disabled.") + + try Datadog.deinitializeOrThrow() + } +} +// swiftlint:enable multiline_arguments_brackets diff --git a/Tests/DatadogTests/Datadog/Tracing/DDSpanContextTests.swift b/Tests/DatadogTests/Datadog/Tracing/DDSpanContextTests.swift new file mode 100644 index 0000000000..b5f40b7a59 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Tracing/DDSpanContextTests.swift @@ -0,0 +1,52 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class DDSpanContextTests: XCTestCase { + private let queue = DispatchQueue(label: "com.datadoghq.\(#file)") + + func testIteratingOverBaggageItems() { + let baggageItems = BaggageItems(targetQueue: queue, parentSpanItems: nil) + baggageItems.set(key: "k1", value: "v1") + baggageItems.set(key: "k2", value: "v2") + baggageItems.set(key: "k3", value: "v3") + baggageItems.set(key: "k4", value: "v4") + + let context: DDSpanContext = .mockWith(baggageItems: baggageItems) + + var allItems: [String: String] = [:] + var someItems: [String: String] = [:] + + context.forEachBaggageItem { itemKey, itemValue -> Bool in + allItems[itemKey] = itemValue + return false // never stop the iteration + } + context.forEachBaggageItem { itemKey, itemValue -> Bool in + someItems[itemKey] = itemValue + return itemKey == "k2" || itemKey == "k3" // stop the iteration at `k2` or `k3`, whichever comes first + } + + let expectedAllItems = ["k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4"] + XCTAssertEqual(allItems, expectedAllItems) + XCTAssertLessThan(someItems.count, expectedAllItems.count) + XCTAssertTrue(Set(someItems.keys).isSubset(of: expectedAllItems.keys)) + } + + func testChildItemsOverwriteTheParentItems() { + let parentBaggageItems = BaggageItems(targetQueue: queue, parentSpanItems: nil) + let childBaggageItems = BaggageItems(targetQueue: queue, parentSpanItems: parentBaggageItems) + + parentBaggageItems.set(key: "foo", value: "a") + XCTAssertEqual(parentBaggageItems.all["foo"], "a") + XCTAssertEqual(childBaggageItems.all["foo"], "a") + + childBaggageItems.set(key: "foo", value: "b") + XCTAssertEqual(parentBaggageItems.all["foo"], "a") + XCTAssertEqual(childBaggageItems.all["foo"], "b") + } +} diff --git a/Tests/DatadogTests/Datadog/Tracing/DDSpanTests.swift b/Tests/DatadogTests/Datadog/Tracing/DDSpanTests.swift new file mode 100644 index 0000000000..895c2c6fff --- /dev/null +++ b/Tests/DatadogTests/Datadog/Tracing/DDSpanTests.swift @@ -0,0 +1,82 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class DDSpanTests: XCTestCase { + func testOverwritingOperationName() { + let span: DDSpan = .mockWith(operationName: "initial") + span.setOperationName("new") + XCTAssertEqual(span.operationName, "new") + } + + // MARK: - Tags + + func testSettingTag() { + let span: DDSpan = .mockWith(operationName: "operation") + XCTAssertEqual(span.tags.count, 0) + + span.setTag(key: "key1", value: "value1") + span.setTag(key: "key2", value: "value2") + + XCTAssertEqual(span.tags.count, 2) + XCTAssertEqual(span.tags["key1"] as? String, "value1") + XCTAssertEqual(span.tags["key2"] as? String, "value2") + } + + // MARK: - Baggage Items + + func testSettingBaggageItems() { + let queue = DispatchQueue(label: "com.datadoghq.\(#function)") + let span: DDSpan = .mockWith( + context: .mockWith(baggageItems: BaggageItems(targetQueue: queue, parentSpanItems: nil)) + ) + + XCTAssertEqual(span.ddContext.baggageItems.all, [:]) + + span.setBaggageItem(key: "foo", value: "bar") + span.setBaggageItem(key: "bizz", value: "buzz") + + XCTAssertEqual(span.baggageItem(withKey: "foo"), "bar") + XCTAssertEqual(span.baggageItem(withKey: "bizz"), "buzz") + XCTAssertEqual(span.ddContext.baggageItems.all, ["foo": "bar", "bizz": "buzz"]) + } + + // MARK: - Usage + + func testGivenFinishedSpan_whenCallingItsAPI_itPrintsErrors() { + let previousUserLogger = userLogger + defer { userLogger = previousUserLogger } + + let output = LogOutputMock() + userLogger = Logger(logOutput: output, dateProvider: SystemDateProvider(), identifier: "sdk-user") + + let span: DDSpan = .mockWith(operationName: "the span") + span.finish() + + let fixtures: [(() -> Void, String)] = [ + ({ _ = span.setOperationName(.mockAny()) }, + "๐Ÿ”ฅ Calling `setOperationName(_:)` on a finished span (\"the span\") is not allowed."), + ({ _ = span.setTag(key: .mockAny(), value: 0) }, + "๐Ÿ”ฅ Calling `setTag(key:value:)` on a finished span (\"the span\") is not allowed."), + ({ _ = span.setBaggageItem(key: .mockAny(), value: .mockAny()) }, + "๐Ÿ”ฅ Calling `setBaggageItem(key:value:)` on a finished span (\"the span\") is not allowed."), + ({ _ = span.baggageItem(withKey: .mockAny()) }, + "๐Ÿ”ฅ Calling `baggageItem(withKey:)` on a finished span (\"the span\") is not allowed."), + ({ _ = span.finish(at: .mockAny()) }, + "๐Ÿ”ฅ Calling `finish(at:)` on a finished span (\"the span\") is not allowed."), + ({ _ = span.log(fields: [:], timestamp: .mockAny()) }, + "๐Ÿ”ฅ Calling `log(fields:timestamp:)` on a finished span (\"the span\") is not allowed."), + ] + + fixtures.forEach { tracerMethod, expectedConsoleWarning in + tracerMethod() + XCTAssertEqual(output.recordedLog?.level, .warn) + XCTAssertEqual(output.recordedLog?.message, expectedConsoleWarning) + } + } +} diff --git a/Tests/DatadogTests/Datadog/Tracing/Span/SpanBuilderTests.swift b/Tests/DatadogTests/Datadog/Tracing/Span/SpanBuilderTests.swift new file mode 100644 index 0000000000..b85ddda559 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Tracing/Span/SpanBuilderTests.swift @@ -0,0 +1,141 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class SpanBuilderTests: XCTestCase { + func testBuildingBasicSpan() throws { + let builder: SpanBuilder = .mockWith(serviceName: "test-service-name") + let ddspan = DDSpan( + tracer: .mockAny(), + context: .mockWith(traceID: 1, spanID: 2, parentSpanID: 1), + operationName: "operation-name", + startTime: .mockDecember15th2019At10AMUTC(), + tags: ["foo": "bar", "bizz": 123] + ) + let span = try builder.createSpan(from: ddspan, finishTime: .mockDecember15th2019At10AMUTC(addingTimeInterval: 0.5)) + + XCTAssertEqual(span.traceID, 1) + XCTAssertEqual(span.spanID, 2) + XCTAssertEqual(span.parentID, 1) + XCTAssertEqual(span.operationName, "operation-name") + XCTAssertEqual(span.serviceName, "test-service-name") + XCTAssertEqual(span.resource, "operation-name") + XCTAssertEqual(span.startTime, .mockDecember15th2019At10AMUTC()) + XCTAssertEqual(span.duration, 0.50, accuracy: 0.01) + XCTAssertFalse(span.isError) + XCTAssertEqual(span.tracerVersion, sdkVersion) + XCTAssertEqual(try span.tags.toEquatable(), ["foo": "bar", "bizz": "123"]) + } + + func testBuildingSpanWithErrorTagSet() throws { + let builder: SpanBuilder = .mockAny() + + // given + var ddspan: DDSpan = .mockWith(tags: [OTTags.error: true]) + var span = try builder.createSpan(from: ddspan, finishTime: .mockAny()) + + // then + XCTAssertTrue(span.isError) + XCTAssertEqual(try span.tags.toEquatable(), ["error": "true"]) + + // given + ddspan = .mockWith(tags: [OTTags.error: false]) + span = try builder.createSpan(from: ddspan, finishTime: .mockAny()) + + // then + XCTAssertFalse(span.isError) + XCTAssertEqual(try span.tags.toEquatable(), ["error": "false"]) + } + + func testBuildingSpanWithErrorLogsSend() throws { + let builder: SpanBuilder = .mockAny() + + // given + var ddspan: DDSpan = .mockWith(tags: [:]) + ddspan.log(fields: [OTLogFields.errorKind: "Swift error"]) + var span = try builder.createSpan(from: ddspan, finishTime: .mockAny()) + + // then + XCTAssertTrue(span.isError) + XCTAssertEqual(try span.tags.toEquatable(), ["error.type": "Swift error"]) // remapped to `error.type` + + // given + ddspan = .mockWith(tags: [:]) + ddspan.log( + fields: [ + OTLogFields.errorKind: "Swift error", + OTLogFields.event: "error", + OTLogFields.message: "Error occured", + OTLogFields.stack: "Foo.swift:42", + ] + ) + span = try builder.createSpan(from: ddspan, finishTime: .mockAny()) + + // then + XCTAssertTrue(span.isError) + XCTAssertEqual( + try span.tags.toEquatable(), + [ + "error.type": "Swift error", // remapped to `error.type` + "error.msg": "Error occured", // remapped to `error.msg` + "error.stack": "Foo.swift:42", // remapped to `error.stack` + ] + ) + + // given + ddspan = .mockWith(tags: [:]) + ddspan.log(fields: ["foo": "bar"]) // ignored + ddspan.log(fields: [OTLogFields.errorKind: "Swift error 1"]) // captured + ddspan.log(fields: [OTLogFields.errorKind: "Swift error 2"]) // ignored + span = try builder.createSpan(from: ddspan, finishTime: .mockAny()) + + // then + XCTAssertTrue(span.isError) + XCTAssertEqual(try span.tags.toEquatable(), ["error.type": "Swift error 1"]) // only first error log is captured + } + + func testBuildingSpanWithErrorTagAndErrorLogsSend() throws { + let builder: SpanBuilder = .mockAny() + + // given + var ddspan: DDSpan = .mockWith(tags: ["error": true]) + ddspan.log(fields: [OTLogFields.event: "error"]) + var span = try builder.createSpan(from: ddspan, finishTime: .mockAny()) + + // then + XCTAssertTrue(span.isError) + + // given + ddspan = .mockWith(tags: ["error": false]) + ddspan.log(fields: [OTLogFields.event: "error"]) + span = try builder.createSpan(from: ddspan, finishTime: .mockAny()) + + // then + XCTAssertTrue(span.isError) + } + + func testBuildingSpanWithResourceNameTagSet() throws { + let builder: SpanBuilder = .mockAny() + + // given + let ddspan: DDSpan = .mockWith(tags: [DDTags.resource: "custom resource name"]) + let span = try builder.createSpan(from: ddspan, finishTime: .mockAny()) + + // then + XCTAssertEqual(span.resource, "custom resource name") + XCTAssertEqual(try span.tags.toEquatable(), [:]) + } +} + +private extension Dictionary where Key == String, Value == JSONStringEncodableValue { + /// Converts `[String: JSONStringEncodableValue]` to `[String: String]` for equitability comparison. + func toEquatable() throws -> [String: String] { + let data = try JSONEncoder().encode(self) + return try data.toJSONObject().mapValues { $0 as! String } + } +} diff --git a/Tests/DatadogTests/Datadog/Tracing/SpanOutputs/SpanFileOutputTests.swift b/Tests/DatadogTests/Datadog/Tracing/SpanOutputs/SpanFileOutputTests.swift new file mode 100644 index 0000000000..87fb8f0ad4 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Tracing/SpanOutputs/SpanFileOutputTests.swift @@ -0,0 +1,54 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class SpanFileOutputTests: XCTestCase { + override func setUp() { + super.setUp() + temporaryDirectory.create() + } + + override func tearDown() { + temporaryDirectory.delete() + super.tearDown() + } + + func testItWritesSpanToFileAsJSON() throws { + let queue = DispatchQueue(label: "any") + let output = SpanFileOutput( + spanBuilder: .mockAny(), + fileWriter: FileWriter( + dataFormat: TracingFeature.Storage.dataFormat, + orchestrator: FilesOrchestrator( + directory: temporaryDirectory, + performance: PerformancePreset.default, + dateProvider: SystemDateProvider() + ), + queue: queue + ) + ) + + let ddspan: DDSpan = .mockWith( + context: .mockWith( + traceID: 29, + spanID: 1, + parentSpanID: nil + ), + operationName: "operation", + startTime: .mockDecember15th2019At10AMUTC() + ) + + output.write(ddspan: ddspan, finishTime: .mockDecember15th2019At10AMUTC(addingTimeInterval: 0.5)) + + queue.sync {} // wait on writter queue + + let fileData = try temporaryDirectory.files()[0].read() + let matcher = try SpanMatcher.fromJSONObjectData(fileData) + XCTAssertEqual(try matcher.operationName(), "operation") + } +} diff --git a/Tests/DatadogTests/Datadog/Tracing/TracingAutoInstrumentationTests.swift b/Tests/DatadogTests/Datadog/Tracing/TracingAutoInstrumentationTests.swift new file mode 100644 index 0000000000..75b2f5be12 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Tracing/TracingAutoInstrumentationTests.swift @@ -0,0 +1,152 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +private struct MockURLFilter: URLFiltering { + let allow: Bool + func allows(_ url: URL?) -> Bool { + return allow + } +} + +class TracingAutoInstrumentationTests: XCTestCase { + func testInitializationWithDatadogConfiguration() throws { + var config = Datadog.Configuration.mockAny() + config.tracingEnabled = true + config.tracedHosts = [String.mockAny()] + let autoInstrumentation = TracingAutoInstrumentation(with: config) + + let urlFilter = try XCTUnwrap(autoInstrumentation?.urlFilter as? URLFilter) + let expectedURLFilter = URLFilter(includedHosts: [String.mockAny()], excludedURLs: [config.logsEndpoint.url, config.tracesEndpoint.url]) + + XCTAssertEqual(urlFilter, expectedURLFilter) + } +} + +class TracingURLSessionHooksTests: XCTestCase { + let spanRecorder = SpanOutputMock() + var previousSharedTracer = Global.sharedTracer + + override func setUp() { + super.setUp() + previousSharedTracer = Global.sharedTracer + Global.sharedTracer = DDTracer.mockWith(spanOutput: spanRecorder) + } + + override func tearDown() { + super.tearDown() + Global.sharedTracer = previousSharedTracer + } + + let tracedHost = "foo.bar" + let tracedRequest = URLRequest(url: URL(string: "http://foo.bar/foo")!) + + func testURLFilter() { + let passingInterceptor = TracingAutoInstrumentation(urlFilter: MockURLFilter(allow: true))!.interceptor + let blockingInterceptor = TracingAutoInstrumentation(urlFilter: MockURLFilter(allow: false))!.interceptor + + XCTAssertNotNil(passingInterceptor(tracedRequest)) + XCTAssertNil(blockingInterceptor(tracedRequest)) + } + + func testTaskObserver() throws { + let interceptor = TracingAutoInstrumentation(urlFilter: MockURLFilter(allow: true))!.interceptor + + let interception = interceptor(tracedRequest) + guard let taskObserver = interception?.taskObserver else { + XCTFail("taskObserver should not be nil") + return + } + + taskObserver(.starting(tracedRequest)) + XCTAssertNil(spanRecorder.recorded) + + taskObserver(.completed(nil, nil)) + XCTAssertNotNil(spanRecorder.recorded) + + let recordedSpanTags = spanRecorder.recorded!.span.tags + XCTAssertEqual(recordedSpanTags[OTTags.httpUrl] as? String, tracedRequest.url!.absoluteString) + XCTAssertEqual(recordedSpanTags[OTTags.httpMethod] as? String, tracedRequest.httpMethod) + } + + func testTaskObserver_response() throws { + let interceptor = TracingAutoInstrumentation(urlFilter: MockURLFilter(allow: true))!.interceptor + + let interception = interceptor(tracedRequest) + guard let taskObserver = interception?.taskObserver else { + XCTFail("taskObserver should not be nil") + return + } + + taskObserver(.starting(tracedRequest)) + XCTAssertNil(spanRecorder.recorded) + + let response = HTTPURLResponse(url: tracedRequest.url!, statusCode: 404, httpVersion: nil, headerFields: nil) + taskObserver(.completed(response, nil)) + XCTAssertNotNil(spanRecorder.recorded) + + let recordedSpanTags = spanRecorder.recorded!.span.tags + XCTAssertEqual(recordedSpanTags[OTTags.httpStatusCode] as? Int, 404) + XCTAssertEqual(recordedSpanTags[DDTags.resource] as? String, "404") + XCTAssertEqual(recordedSpanTags[OTTags.error] as? Bool, true) + } + + func testTaskObserver_NSError() throws { + let interceptor = TracingAutoInstrumentation(urlFilter: MockURLFilter(allow: true))!.interceptor + + let interception = interceptor(tracedRequest) + guard let taskObserver = interception?.taskObserver else { + XCTFail("taskObserver should not be nil") + return + } + + taskObserver(.starting(tracedRequest)) + XCTAssertNil(spanRecorder.recorded) + + let errorDescription = "something happened" + let error = NSError(domain: "unit-test", code: 123, userInfo: [NSLocalizedDescriptionKey: errorDescription]) + taskObserver(.completed(nil, error)) + XCTAssertNotNil(spanRecorder.recorded) + + let recordedSpanTags = spanRecorder.recorded!.span.tags + XCTAssertEqual(recordedSpanTags[OTTags.error] as? Bool, true) + XCTAssertEqual(recordedSpanTags[DDTags.errorType] as? String, "\(error.domain) - \(error.code)") + XCTAssertEqual(recordedSpanTags[DDTags.errorMessage] as? String, errorDescription) + XCTAssertEqual(recordedSpanTags[DDTags.errorStack] as? String, String(describing: error)) + } + + func testTaskObserver_wrongOrder() throws { + let interceptor = TracingAutoInstrumentation(urlFilter: MockURLFilter(allow: true))!.interceptor + + let interception = interceptor(tracedRequest) + let taskObserver: TaskObserver! = interception?.taskObserver //swiftlint:disable:this implicitly_unwrapped_optional + + for _ in 0...3 { + taskObserver(.completed(nil, nil)) + XCTAssertNil(spanRecorder.recorded) + } + } + + func testTaskObserver_duplicateStarts_shouldNotFail() throws { + let interceptor = TracingAutoInstrumentation(urlFilter: MockURLFilter(allow: true))!.interceptor + + let interception = interceptor(tracedRequest) + let taskObserver: TaskObserver! = interception?.taskObserver //swiftlint:disable:this implicitly_unwrapped_optional + + taskObserver(.starting(tracedRequest)) + + let secondRequest = URLRequest(url: URL(string: "2", relativeTo: URL(string: tracedHost))!) + taskObserver(.starting(secondRequest)) + taskObserver(.completed(nil, nil)) + + XCTAssertNotNil(spanRecorder.recorded) + let recordedSpanTags = spanRecorder.recorded!.span.tags + XCTAssertEqual(recordedSpanTags[OTTags.httpUrl] as? String, secondRequest.url!.absoluteString) + XCTAssertNotEqual(recordedSpanTags[OTTags.httpUrl] as? String, tracedRequest.url!.absoluteString) + } +} diff --git a/Tests/DatadogTests/Datadog/Tracing/TracingFeatureTests.swift b/Tests/DatadogTests/Datadog/Tracing/TracingFeatureTests.swift new file mode 100644 index 0000000000..4b04d68eee --- /dev/null +++ b/Tests/DatadogTests/Datadog/Tracing/TracingFeatureTests.swift @@ -0,0 +1,107 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class TracingFeatureTests: XCTestCase { + override func setUp() { + super.setUp() + XCTAssertNil(Datadog.instance) + XCTAssertNil(TracingFeature.instance) + temporaryDirectory.create() + } + + override func tearDown() { + XCTAssertNil(Datadog.instance) + XCTAssertNil(TracingFeature.instance) + temporaryDirectory.delete() + super.tearDown() + } + + // MARK: - Initialization + + func testInitialization() throws { + let appContext: AppContext = .mockAny() + Datadog.initialize( + appContext: appContext, + configuration: Datadog.Configuration + .builderUsing(clientToken: "abc", environment: "tests") + .build() + ) + + XCTAssertNotNil(TracingFeature.instance) + + try Datadog.deinitializeOrThrow() + } + + // MARK: - HTTP Headers + + func testItUsesExpectedHTTPHeaders() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + configuration: .mockWith( + applicationName: "FoobarApp", + applicationVersion: "2.1.0" + ), + mobileDevice: .mockWith(model: "iPhone", osName: "iOS", osVersion: "13.3.1") + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize(configuration: .init()).dd + + let span = tracer.startSpan(operationName: "operation 1") + span.finish() + + let httpHeaders = server.waitAndReturnRequests(count: 1)[0].allHTTPHeaderFields + XCTAssertEqual(httpHeaders?["User-Agent"], "FoobarApp/2.1.0 CFNetwork (iPhone; iOS/13.3.1)") + XCTAssertEqual(httpHeaders?["Content-Type"], "text/plain;charset=UTF-8") + } + + // MARK: - Payload Format + + func testItUsesExpectedPayloadFormatForUploads() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + performance: .combining( + storagePerformance: StoragePerformanceMock( + maxFileSize: .max, + maxDirectorySize: .max, + maxFileAgeForWrite: .distantFuture, // write all spans to single file, + minFileAgeForRead: StoragePerformanceMock.readAllFiles.minFileAgeForRead, + maxFileAgeForRead: StoragePerformanceMock.readAllFiles.maxFileAgeForRead, + maxObjectsInFile: 3, // write 3 spans to payload, + maxObjectSize: .max + ), + uploadPerformance: UploadPerformanceMock( + initialUploadDelay: 0.5, // wait enough until spans are written, + defaultUploadDelay: 1, + minUploadDelay: 1, + maxUploadDelay: 1, + uploadDelayDecreaseFactor: 1 + ) + ) + ) + defer { TracingFeature.instance = nil } + + let tracer = Tracer.initialize(configuration: .init()).dd + + tracer.startSpan(operationName: "operation 1").finish() + tracer.startSpan(operationName: "operation 2").finish() + tracer.startSpan(operationName: "operation 3").finish() + + let payload = server.waitAndReturnRequests(count: 1)[0].httpBody! + + let spanMatchers = try SpanMatcher.fromNewlineSeparatedJSONObjectsData(payload) + XCTAssertEqual(try spanMatchers[0].operationName(), "operation 1") + XCTAssertEqual(try spanMatchers[1].operationName(), "operation 2") + XCTAssertEqual(try spanMatchers[2].operationName(), "operation 3") + } +} diff --git a/Tests/DatadogTests/Datadog/Tracing/UUIDs/TracingUUIDGeneratorTests.swift b/Tests/DatadogTests/Datadog/Tracing/UUIDs/TracingUUIDGeneratorTests.swift new file mode 100644 index 0000000000..ed292b427b --- /dev/null +++ b/Tests/DatadogTests/Datadog/Tracing/UUIDs/TracingUUIDGeneratorTests.swift @@ -0,0 +1,27 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class TracingUUIDGeneratorTests: XCTestCase { + func testDefaultGenerationBoundaries() { + let generator = DefaultTracingUUIDGenerator() + XCTAssertEqual(generator.range.lowerBound, 1) + XCTAssertEqual(generator.range.upperBound, 9_223_372_036_854_775_807) // 2 ^ 63 -1 + } + + func testItGeneratesUUIDsFromGivenBoundaries() { + let generator = DefaultTracingUUIDGenerator(range: 10...15) + var generatedUUIDs: Set = [] + + (0..<1_000).forEach { _ in + generatedUUIDs.insert(generator.generateUnique().rawValue) + } + + XCTAssertEqual(generatedUUIDs, [10, 11, 12, 13, 14, 15]) + } +} diff --git a/Tests/DatadogTests/Datadog/Tracing/UUIDs/TracingUUIDTests.swift b/Tests/DatadogTests/Datadog/Tracing/UUIDs/TracingUUIDTests.swift new file mode 100644 index 0000000000..605aa54773 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Tracing/UUIDs/TracingUUIDTests.swift @@ -0,0 +1,20 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class UUIDTests: XCTestCase { + func testToHexadecimalStringConversion() { + XCTAssertEqual(TracingUUID(rawValue: 0).toHexadecimalString, "0") + XCTAssertEqual(TracingUUID(rawValue: 1).toHexadecimalString, "1") + XCTAssertEqual(TracingUUID(rawValue: 15).toHexadecimalString, "F") + XCTAssertEqual(TracingUUID(rawValue: 16).toHexadecimalString, "10") + XCTAssertEqual(TracingUUID(rawValue: 123).toHexadecimalString, "7B") + XCTAssertEqual(TracingUUID(rawValue: 123_456).toHexadecimalString, "1E240") + XCTAssertEqual(TracingUUID(rawValue: .max).toHexadecimalString, "FFFFFFFFFFFFFFFF") + } +} diff --git a/Tests/DatadogTests/Datadog/Tracing/Utils/Casting.swift b/Tests/DatadogTests/Datadog/Tracing/Utils/Casting.swift new file mode 100644 index 0000000000..64c5ceaf3e --- /dev/null +++ b/Tests/DatadogTests/Datadog/Tracing/Utils/Casting.swift @@ -0,0 +1,29 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +@testable import Datadog + +/* + NOTE: The casting methods defined here do shadow the ones defined in `Datadog.Casting`. + The difference is that here in tests we do force unwrapping (`as!`), whereas in `Datadog` we do `as?` with a warning. + + This is needed for expressiveness in testing, where i.e. `XCTAssertNil(span.context.dd?.parentID)` may give a false positive + without considering if the `parentID` is `nil`. Using `span.context.dd.parentID` mitigates it. + */ + +// swiftlint:disable identifier_name +internal extension OTTracer { + var dd: Tracer { self as! Tracer } +} + +internal extension OTSpan { + var dd: DDSpan { self as! DDSpan } +} + +internal extension OTSpanContext { + var dd: DDSpanContext { self as! DDSpanContext } +} +// swiftlint:enable identifier_name diff --git a/Tests/DatadogTests/Datadog/Tracing/Utils/UUID.swift b/Tests/DatadogTests/Datadog/Tracing/Utils/UUID.swift new file mode 100644 index 0000000000..de2213e2f9 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Tracing/Utils/UUID.swift @@ -0,0 +1,15 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +@testable import Datadog + +extension TracingUUID: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = UInt64 + + public init(integerLiteral value: UInt64) { + self.init(rawValue: value) + } +} diff --git a/Tests/DatadogTests/Datadog/Tracing/Utils/WarningsTests.swift b/Tests/DatadogTests/Datadog/Tracing/Utils/WarningsTests.swift new file mode 100644 index 0000000000..af8e38de49 --- /dev/null +++ b/Tests/DatadogTests/Datadog/Tracing/Utils/WarningsTests.swift @@ -0,0 +1,45 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class WarningsTests: XCTestCase { + func testPrintingWarningsOnDifferentConditions() { + let previousUserLogger = userLogger + defer { userLogger = previousUserLogger } + + let output = LogOutputMock() + userLogger = Logger( + logOutput: output, + dateProvider: RelativeDateProvider(using: .mockDecember15th2019At10AMUTC()), + identifier: "sdk-user" + ) + + XCTAssertTrue(warn(if: true, message: "message")) + XCTAssertEqual(output.recordedLog, .init(level: .warn, message: "message", date: .mockDecember15th2019At10AMUTC())) + + output.recordedLog = nil + + XCTAssertFalse(warn(if: false, message: "message")) + XCTAssertNil(output.recordedLog) + + output.recordedLog = nil + + let failingCast: () -> DDSpan? = { warnIfCannotCast(value: DDNoopSpan()) } + XCTAssertNil(failingCast()) + XCTAssertEqual( + output.recordedLog, + .init(level: .warn, message: "๐Ÿ”ฅ Using DDNoopSpan while DDSpan was expected.", date: .mockDecember15th2019At10AMUTC()) + ) + + output.recordedLog = nil + + let succeedingCast: () -> DDSpan? = { warnIfCannotCast(value: DDSpan.mockAny()) } + XCTAssertNotNil(succeedingCast()) + XCTAssertNil(output.recordedLog) + } +} diff --git a/Tests/DatadogTests/Datadog/Utils/EncodingTests.swift b/Tests/DatadogTests/Datadog/Utils/EncodingTests.swift deleted file mode 100644 index cb9f2b4575..0000000000 --- a/Tests/DatadogTests/Datadog/Utils/EncodingTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2019-2020 Datadog, Inc. - */ - -import XCTest -@testable import Datadog - -class DateFormatterTests: XCTestCase { - func testISO8601FormatWithSubSecondPrecision() throws { - let dateFormatter = ISO8601DateFormatter.default() - - let knownDate: Date = .mockDecember15th2019At10AMUTC(addingTimeInterval: 0.123) - let formattedDate = dateFormatter.string(from: knownDate) - - XCTAssertEqual(formattedDate, "2019-12-15T10:00:00.123Z") - } -} - -class EncodingTests: XCTestCase { - func testEncodingDateWithSubSecondPrecision() throws { - let jsonEncoder = JSONEncoder.default() - - let knownDate: Date = .mockDecember15th2019At10AMUTC(addingTimeInterval: 0.123) - let encodedKnownDate = try jsonEncoder.encode(knownDate) - - let jsonDecoder = JSONDecoder() - let knownDateDecodedString = try jsonDecoder.decode(String.self, from: encodedKnownDate) - - XCTAssertEqual(knownDateDecodedString, "2019-12-15T10:00:00.123Z") - } -} diff --git a/Tests/DatadogTests/Datadog/Utils/InternalLoggersTests.swift b/Tests/DatadogTests/Datadog/Utils/InternalLoggersTests.swift index b124a6b477..8093b04aff 100644 --- a/Tests/DatadogTests/Datadog/Utils/InternalLoggersTests.swift +++ b/Tests/DatadogTests/Datadog/Utils/InternalLoggersTests.swift @@ -8,118 +8,153 @@ import XCTest @testable import Datadog class InternalLoggersTests: XCTestCase { - private var printedMessages: [String]! // swiftlint:disable:this implicitly_unwrapped_optional - private var userLogger: Logger! // swiftlint:disable:this implicitly_unwrapped_optional + private let internalLoggerConfigurationMock = InternalLoggerConfiguration( + applicationVersion: .mockAny(), + environment: .mockAny(), + userInfoProvider: UserInfoProvider.mockAny(), + networkConnectionInfoProvider: NetworkConnectionInfoProviderMock.mockAny(), + carrierInfoProvider: CarrierInfoProviderMock.mockAny() + ) + + // MARK: - User Logger + + func testWhenSDKIsNotInitialized_itUsesNoOpUserLogger() { + XCTAssertTrue(userLogger.logOutput is NoOpLogOutput) + } - override func setUp() { - super.setUp() - temporaryDirectory.create() - LoggingFeature.instance = .mockNoOp(temporaryDirectory: temporaryDirectory) - printedMessages = [] - userLogger = createSDKUserLogger( - consolePrintFunction: { [weak self] in self?.printedMessages.append($0) }, - dateProvider: RelativeDateProvider(startingFrom: .mockDecember15th2019At10AMUTC()), - timeFormatter: LogConsoleOutput.shortTimeFormatter(calendar: .gregorian, timeZone: .UTC) - ) + func testGivenDefaultSDKConfiguration_whenInitialized_itUsesWorkingUserLogger() throws { + let defaultSDKConfiguration = Datadog.Configuration.builderUsing(clientToken: "abc", environment: "test").build() + Datadog.initialize(appContext: .mockAny(), configuration: defaultSDKConfiguration) + XCTAssertTrue((userLogger.logOutput as? ConditionalLogOutput)?.conditionedOutput is LogConsoleOutput) + try Datadog.deinitializeOrThrow() } - override func tearDown() { - printedMessages = nil - userLogger = nil - Datadog.verbosityLevel = nil - LoggingFeature.instance = nil - temporaryDirectory.delete() - super.tearDown() + func testGivenLoggingFeatureDisabled_whenSDKisInitialized_itUsesWorkingUserLogger() throws { + Datadog.initialize(appContext: .mockAny(), configuration: .mockWith(loggingEnabled: false)) + XCTAssertTrue((userLogger.logOutput as? ConditionalLogOutput)?.conditionedOutput is LogConsoleOutput) + try Datadog.deinitializeOrThrow() } - // MARK: - `userLogger` + func testUserLoggerPrintsMessagesAboveGivenVerbosityLevel() { + var printedMessages: [String] = [] - private func logMessageUsingAllLevels(_ message: String) { - userLogger.debug(message) - userLogger.info(message) - userLogger.notice(message) - userLogger.warn(message) - userLogger.error(message) - userLogger.critical(message) - } + let userLogger = createSDKUserLogger( + configuration: internalLoggerConfigurationMock, + consolePrintFunction: { printedMessages.append($0) }, + dateProvider: RelativeDateProvider(using: .mockDecember15th2019At10AMUTC()), + timeZone: .EET + ) - private let expectedMessages = [ - "[DATADOG SDK] ๐Ÿถ โ†’ 10:00:00.000Z [DEBUG] message", - "[DATADOG SDK] ๐Ÿถ โ†’ 10:00:00.000Z [INFO] message", - "[DATADOG SDK] ๐Ÿถ โ†’ 10:00:00.000Z [NOTICE] message", - "[DATADOG SDK] ๐Ÿถ โ†’ 10:00:00.000Z [WARN] message", - "[DATADOG SDK] ๐Ÿถ โ†’ 10:00:00.000Z [ERROR] message", - "[DATADOG SDK] ๐Ÿถ โ†’ 10:00:00.000Z [CRITICAL] message" - ] + let expectedMessages = [ + "[DATADOG SDK] ๐Ÿถ โ†’ 12:00:00.000 [DEBUG] message", + "[DATADOG SDK] ๐Ÿถ โ†’ 12:00:00.000 [INFO] message", + "[DATADOG SDK] ๐Ÿถ โ†’ 12:00:00.000 [NOTICE] message", + "[DATADOG SDK] ๐Ÿถ โ†’ 12:00:00.000 [WARN] message", + "[DATADOG SDK] ๐Ÿถ โ†’ 12:00:00.000 [ERROR] message", + "[DATADOG SDK] ๐Ÿถ โ†’ 12:00:00.000 [CRITICAL] message" + ] - func testUserLoggerDoesNothingWithDefaultVerbosityLevel() { XCTAssertNil(Datadog.verbosityLevel) - logMessageUsingAllLevels("message") + logMessageUsingAllLevels("message", with: userLogger) XCTAssertEqual(printedMessages, []) - } - func testUserLoggerPrintsWithVerbosityLevel_debug() { + printedMessages = [] Datadog.verbosityLevel = .debug - logMessageUsingAllLevels("message") + logMessageUsingAllLevels("message", with: userLogger) XCTAssertEqual(printedMessages, Array(expectedMessages[0.. = ["foo.bar", "example", "my.app.org"] + let excluded: Set = ["exclude.me", "and.me.too"] + let filter = URLFilter(includedHosts: included, excludedURLs: excluded) + + for host in included { + let subdomainURL = URL(string: "http://www.\(host)/foo")! + XCTAssertTrue(filter.allows(subdomainURL)) + + let complexURL = URL(string: "http://johnny:p4ssw0rd@\(host):999/script.ext;param=value?query=value#ref")! + XCTAssertTrue(filter.allows(complexURL)) + + let differentScheme = URL(string: "https://\(host)/foo")! + XCTAssertTrue(filter.allows(differentScheme)) + } + + let nonIncludedHost = URL(string: "https://non.traced.host")! + XCTAssertFalse(filter.allows(nonIncludedHost)) + + let nonEscapedDotURL = URL(string: "https://foo-bar.com")! + XCTAssertFalse(filter.allows(nonEscapedDotURL)) + + let extendedIncludedHost = URL(string: "https://foo.bar.asd")! + XCTAssertFalse(filter.allows(extendedIncludedHost)) + + let fileURL = URL(string: "file://some-file")! + XCTAssertTrue(fileURL.isFileURL) + XCTAssertFalse(filter.allows(fileURL)) + } + + func testExclusionOverrulesInclusion() { + let included: Set = ["example.com"] + let excluded: Set = ["http://api.example.com"] + let filter = URLFilter(includedHosts: included, excludedURLs: excluded) + + let includedURL = URL(string: "http://example.com")! + XCTAssertTrue(filter.allows(includedURL)) + + let includedSubdomainURL = URL(string: "http://www.example.com")! + XCTAssertTrue(filter.allows(includedSubdomainURL)) + + let excludedSubdomainURL = URL(string: "http://api.example.com")! + XCTAssertFalse(filter.allows(excludedSubdomainURL)) + + let excludedSubdomainURLwithPath = URL(string: "http://api.example.com/some/path")! + XCTAssertFalse(filter.allows(excludedSubdomainURLwithPath)) + } +} diff --git a/Tests/DatadogTests/DatadogObjc/DDConfigurationTests.swift b/Tests/DatadogTests/DatadogObjc/DDConfigurationTests.swift index b3b735d326..352058b057 100644 --- a/Tests/DatadogTests/DatadogObjc/DDConfigurationTests.swift +++ b/Tests/DatadogTests/DatadogObjc/DDConfigurationTests.swift @@ -19,6 +19,17 @@ extension Datadog.Configuration.LogsEndpoint: Equatable { } } +extension Datadog.Configuration.TracesEndpoint: Equatable { + public static func == (_ lhs: Datadog.Configuration.TracesEndpoint, _ rhs: Datadog.Configuration.TracesEndpoint) -> Bool { + switch (lhs, rhs) { + case (.us, .us): return true + case (.eu, .eu): return true + case let (.custom(lhsURL), .custom(rhsURL)): return lhsURL == rhsURL + default: return false + } + } +} + /// This tests verify that objc-compatible `DatadogObjc` wrapper properly interacts with`Datadog` public API (swift). class DDConfigurationTests: XCTestCase { func testItFowardsInitializationToSwift() { @@ -26,24 +37,45 @@ class DDConfigurationTests: XCTestCase { let swiftConfigurationDefault = objcBuilder.build().sdkConfiguration XCTAssertEqual(swiftConfigurationDefault.clientToken, "abc-123") + XCTAssertTrue(swiftConfigurationDefault.loggingEnabled) + XCTAssertTrue(swiftConfigurationDefault.tracingEnabled) XCTAssertEqual(swiftConfigurationDefault.logsEndpoint, .us) + XCTAssertEqual(swiftConfigurationDefault.tracesEndpoint, .us) XCTAssertEqual(swiftConfigurationDefault.environment, "tests") XCTAssertNil(swiftConfigurationDefault.serviceName) - objcBuilder.set(endpoint: .eu()) + objcBuilder.enableLogging(false) + let swiftConfigurationLoggingDisabled = objcBuilder.build().sdkConfiguration + XCTAssertFalse(swiftConfigurationLoggingDisabled.loggingEnabled) + + objcBuilder.enableTracing(false) + let swiftConfigurationTracingDisabled = objcBuilder.build().sdkConfiguration + XCTAssertFalse(swiftConfigurationTracingDisabled.tracingEnabled) + + objcBuilder.set(logsEndpoint: .eu()) + objcBuilder.set(tracesEndpoint: .eu()) let swiftConfigurationEU = objcBuilder.build().sdkConfiguration XCTAssertEqual(swiftConfigurationEU.logsEndpoint, .eu) + XCTAssertEqual(swiftConfigurationEU.tracesEndpoint, .eu) - objcBuilder.set(endpoint: .us()) + objcBuilder.set(logsEndpoint: .us()) + objcBuilder.set(tracesEndpoint: .us()) let swiftConfigurationUS = objcBuilder.build().sdkConfiguration XCTAssertEqual(swiftConfigurationUS.logsEndpoint, .us) + XCTAssertEqual(swiftConfigurationUS.tracesEndpoint, .us) - objcBuilder.set(endpoint: .custom(url: "https://api.example.com/v1/logs")) + objcBuilder.set(logsEndpoint: .custom(url: "https://api.example.com/v1/logs")) + objcBuilder.set(tracesEndpoint: .custom(url: "https://api.example.com/v1/logs")) let swiftConfigurationCustom = objcBuilder.build().sdkConfiguration XCTAssertEqual(swiftConfigurationCustom.logsEndpoint, .custom(url: "https://api.example.com/v1/logs")) + XCTAssertEqual(swiftConfigurationCustom.tracesEndpoint, .custom(url: "https://api.example.com/v1/logs")) objcBuilder.set(serviceName: "service-name") let swiftConfigurationServiceName = objcBuilder.build().sdkConfiguration XCTAssertEqual(swiftConfigurationServiceName.serviceName, "service-name") + + objcBuilder.set(tracedHosts: ["example.com"]) + let swiftConfigurationTracedHosts = objcBuilder.build().sdkConfiguration + XCTAssertEqual(swiftConfigurationTracedHosts.tracedHosts, ["example.com"]) } } diff --git a/Tests/DatadogTests/DatadogObjc/DDDatadogTests.swift b/Tests/DatadogTests/DatadogObjc/DDDatadogTests.swift index 842b7e8ad5..24fa9ae024 100644 --- a/Tests/DatadogTests/DatadogObjc/DDDatadogTests.swift +++ b/Tests/DatadogTests/DatadogObjc/DDDatadogTests.swift @@ -14,26 +14,33 @@ class DDDatadogTests: XCTestCase { super.setUp() XCTAssertNil(Datadog.instance) XCTAssertNil(LoggingFeature.instance) + XCTAssertNil(TracingAutoInstrumentation.instance) } override func tearDown() { XCTAssertNil(Datadog.instance) XCTAssertNil(LoggingFeature.instance) + XCTAssertNil(TracingAutoInstrumentation.instance) super.tearDown() } // MARK: - Initializing with configuration func testItFowardsInitializationToSwift() throws { + let configBuilder = DDConfiguration.builder(clientToken: "abcefghi", environment: "tests") + configBuilder.set(tracedHosts: ["example.com"]) + DDDatadog.initialize( appContext: DDAppContext(mainBundle: BundleMock.mockWith(CFBundleExecutable: "app-name")), - configuration: DDConfiguration.builder(clientToken: "abcefghi", environment: "tests").build() + configuration: configBuilder.build() ) XCTAssertNotNil(Datadog.instance) XCTAssertEqual(LoggingFeature.instance?.configuration.applicationName, "app-name") XCTAssertEqual(LoggingFeature.instance?.configuration.environment, "tests") + XCTAssertNotNil(TracingAutoInstrumentation.instance) + TracingAutoInstrumentation.instance?.swizzler.unswizzle() try Datadog.deinitializeOrThrow() } diff --git a/Tests/DatadogTests/DatadogObjc/DDLoggerTests.swift b/Tests/DatadogTests/DatadogObjc/DDLoggerTests.swift index 6476b38c9a..12afc21143 100644 --- a/Tests/DatadogTests/DatadogObjc/DDLoggerTests.swift +++ b/Tests/DatadogTests/DatadogObjc/DDLoggerTests.swift @@ -23,7 +23,7 @@ class DDLoggerTests: XCTestCase { super.tearDown() } - func testSendingLogWithCustomizedLogger() throws { + func testSendingLogsWithDifferentLevels() throws { let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) LoggingFeature.instance = .mockWorkingFeatureWith( server: server, @@ -31,21 +31,25 @@ class DDLoggerTests: XCTestCase { ) defer { LoggingFeature.instance = nil } - let objcBuilder = DDLogger.builder() - objcBuilder.set(serviceName: "objc-service-name") - objcBuilder.set(loggerName: "objc-logger-name") - objcBuilder.sendLogsToDatadog(true) - objcBuilder.printLogsToConsole(false) + let objcLogger = DDLogger.builder().build() - let objcLogger = objcBuilder.build() objcLogger.debug("message") + objcLogger.info("message") + objcLogger.notice("message") + objcLogger.warn("message") + objcLogger.error("message") + objcLogger.critical("message") - let logMatcher = try server.waitAndReturnLogMatchers(count: 1)[0] - logMatcher.assertServiceName(equals: "objc-service-name") - logMatcher.assertLoggerName(equals: "objc-logger-name") + let logMatchers = try server.waitAndReturnLogMatchers(count: 6) + logMatchers[0].assertStatus(equals: "debug") + logMatchers[1].assertStatus(equals: "info") + logMatchers[2].assertStatus(equals: "notice") + logMatchers[3].assertStatus(equals: "warn") + logMatchers[4].assertStatus(equals: "error") + logMatchers[5].assertStatus(equals: "critical") } - func testSendingLogsWithDifferentLevels() throws { + func testSendingMessageAttributes() throws { let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) LoggingFeature.instance = .mockWorkingFeatureWith( server: server, @@ -55,24 +59,25 @@ class DDLoggerTests: XCTestCase { let objcLogger = DDLogger.builder().build() - objcLogger.debug("message") - objcLogger.info("message") - objcLogger.notice("message") - objcLogger.warn("message") - objcLogger.error("message") - objcLogger.critical("message") + objcLogger.debug("message", attributes: ["foo": "bar"]) + objcLogger.info("message", attributes: ["foo": "bar"]) + objcLogger.notice("message", attributes: ["foo": "bar"]) + objcLogger.warn("message", attributes: ["foo": "bar"]) + objcLogger.error("message", attributes: ["foo": "bar"]) + objcLogger.critical("message", attributes: ["foo": "bar"]) let logMatchers = try server.waitAndReturnLogMatchers(count: 6) - logMatchers[0].assertStatus(equals: "DEBUG") - logMatchers[1].assertStatus(equals: "INFO") - logMatchers[2].assertStatus(equals: "NOTICE") - logMatchers[3].assertStatus(equals: "WARN") - logMatchers[4].assertStatus(equals: "ERROR") - logMatchers[5].assertStatus(equals: "CRITICAL") + logMatchers[0].assertStatus(equals: "debug") + logMatchers[1].assertStatus(equals: "info") + logMatchers[2].assertStatus(equals: "notice") + logMatchers[3].assertStatus(equals: "warn") + logMatchers[4].assertStatus(equals: "error") + logMatchers[5].assertStatus(equals: "critical") + logMatchers.forEach { matcher in + matcher.assertAttributes(equal: ["foo": "bar"]) + } } - // MARK: - Sending attributes - func testSendingLoggerAttributes() throws { let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) LoggingFeature.instance = .mockWorkingFeatureWith( @@ -113,6 +118,37 @@ class DDLoggerTests: XCTestCase { logMatcher.assertValue(forKeyPath: "nsdictionary-of-date.date1", equals: "2019-12-15T10:00:00.000Z") logMatcher.assertValue(forKeyPath: "nsdictionary-of-date.date2", equals: "2019-12-15T11:00:00.000Z") } + + func testSettingTagsAndAttributes() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + LoggingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + configuration: .mockWith(environment: "test") + ) + defer { LoggingFeature.instance = nil } + + let objcLogger = DDLogger.builder().build() + + objcLogger.addAttribute(forKey: "foo", value: "bar") + objcLogger.addAttribute(forKey: "bizz", value: "buzz") + objcLogger.removeAttribute(forKey: "bizz") + + objcLogger.addTag(withKey: "foo", value: "bar") + objcLogger.addTag(withKey: "bizz", value: "buzz") + objcLogger.removeTag(withKey: "bizz") + + objcLogger.add(tag: "foobar") + objcLogger.add(tag: "bizzbuzz") + objcLogger.remove(tag: "bizzbuzz") + + objcLogger.info(.mockAny()) + + let logMatcher = try server.waitAndReturnLogMatchers(count: 1)[0] + logMatcher.assertValue(forKeyPath: "foo", equals: "bar") + logMatcher.assertNoValue(forKey: "bizz") + logMatcher.assertTags(equal: ["foo:bar", "foobar", "env:test"]) + } } // swiftlint:enable multiline_arguments_brackets // swiftlint:enable compiler_protocol_init diff --git a/Tests/DatadogTests/DatadogObjc/DDTracerConfigurationTests.swift b/Tests/DatadogTests/DatadogObjc/DDTracerConfigurationTests.swift new file mode 100644 index 0000000000..d8a088222b --- /dev/null +++ b/Tests/DatadogTests/DatadogObjc/DDTracerConfigurationTests.swift @@ -0,0 +1,21 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog +@testable import DatadogObjc + +class DDTracerConfigurationTests: XCTestCase { + func testItFowardsConfigurationToSwift() { + let objcConfiguration = DDTracerConfiguration() + objcConfiguration.set(serviceName: "service-name") + objcConfiguration.sendNetworkInfo(true) + + let swiftConfiguration = objcConfiguration.swiftConfiguration + XCTAssertEqual(swiftConfiguration.serviceName, "service-name") + XCTAssertTrue(swiftConfiguration.sendNetworkInfo) + } +} diff --git a/Tests/DatadogTests/DatadogObjc/DDTracerTests.swift b/Tests/DatadogTests/DatadogObjc/DDTracerTests.swift new file mode 100644 index 0000000000..6ea39820be --- /dev/null +++ b/Tests/DatadogTests/DatadogObjc/DDTracerTests.swift @@ -0,0 +1,260 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog +@testable import DatadogObjc + +class DDTracerTests: XCTestCase { + override func setUp() { + super.setUp() + XCTAssertNil(TracingFeature.instance) + temporaryDirectory.create() + } + + override func tearDown() { + XCTAssertNil(TracingFeature.instance) + temporaryDirectory.delete() + super.tearDown() + } + + func testSendingCustomizedSpans() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory + ) + defer { TracingFeature.instance = nil } + + let objcTracer = DDTracer.initialize(configuration: DDTracerConfiguration()).dd! + + let objcSpan1 = objcTracer.startSpan("operation") + let objcSpan2 = objcTracer.startSpan( + "operation", + tags: NSDictionary(dictionary: ["tag1": NSString(string: "value1"), "tag2": NSInteger(integerLiteral: 123)]) + ) + let objcSpan3 = objcTracer.startSpan( + "operation", + childOf: objcSpan1.context + ) + let objcSpan4 = objcTracer.startSpan( + "operation", + childOf: objcSpan1.context, + tags: NSDictionary(dictionary: ["tag1": NSString(string: "value1"), "tag2": NSInteger(integerLiteral: 123)]) + ) + let objcSpan5 = objcTracer.startSpan( + "operation", + childOf: objcSpan1.context, + tags: NSDictionary( + dictionary: [ + "tag1": NSString(string: "value1"), + "tag2": NSInteger(integerLiteral: 123), + "nsurlTag": NSURL(string: "https://example.com/image.png")! + ] + ), + startTime: .mockDecember15th2019At10AMUTC() + ) + + objcSpan5.setOperationName("updated operation name") + objcSpan5.setTag("nsstringTag", value: NSString(string: "string value")) + objcSpan5.setTag("nsnumberTag", numberValue: NSNumber(value: 10.5)) + objcSpan5.setTag("nsboolTag", boolValue: true) + + _ = objcSpan5.setBaggageItem("item", value: "value") + XCTAssertEqual(objcSpan5.getBaggageItem("item"), "value") + + var baggageItems: [(key: String, value: String)] = [] + objcSpan5.context.forEachBaggageItem { itemKey, itemValue in + baggageItems.append((key: itemKey, value: itemValue)) + return false + } + XCTAssertEqual(baggageItems.count, 1) + XCTAssertEqual(baggageItems[0].key, "item") + XCTAssertEqual(baggageItems[0].value, "value") + + objcSpan1.finish() + objcSpan2.finish() + objcSpan3.finish() + objcSpan4.finishWithTime(nil) + objcSpan5.finishWithTime(.mockDecember15th2019At10AMUTC(addingTimeInterval: 0.5)) + + [objcSpan1, objcSpan2, objcSpan3, objcSpan4, objcSpan5].forEach { span in + XCTAssertTrue(span.tracer === objcTracer) + } + + let spanMatchers = try server.waitAndReturnSpanMatchers(count: 5) + + // assert operation name + try spanMatchers[0...3].forEach { spanMatcher in + XCTAssertEqual(try spanMatcher.operationName(), "operation") + } + XCTAssertEqual(try spanMatchers[4].operationName(), "updated operation name") + + // assert parent-child relationship + try spanMatchers[2...4].forEach { spanMatcher in + XCTAssertEqual(try spanMatcher.traceID(), try spanMatchers[0].traceID()) + XCTAssertEqual(try spanMatcher.parentSpanID(), try spanMatchers[0].spanID()) + } + + // assert tags + try [spanMatchers[1], spanMatchers[3], spanMatchers[4]].forEach { spanMatcher in + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.tag1"), "value1") + XCTAssertEqual(try spanMatcher.meta.custom(keyPath: "meta.tag2"), "123") + } + XCTAssertEqual(try spanMatchers[4].meta.custom(keyPath: "meta.nsurlTag"), "https://example.com/image.png") + XCTAssertEqual(try spanMatchers[4].meta.custom(keyPath: "meta.nsstringTag"), "string value") + XCTAssertEqual(try spanMatchers[4].meta.custom(keyPath: "meta.nsnumberTag"), "10.5") + XCTAssertEqual(try spanMatchers[4].meta.custom(keyPath: "meta.nsboolTag"), "true") + + // assert baggage item + XCTAssertEqual(try spanMatchers[4].meta.custom(keyPath: "meta.item"), "value") + + // assert timing + XCTAssertEqual(try spanMatchers[4].startTime(), 1_576_404_000_000_000_000) + XCTAssertEqual(try spanMatchers[4].duration(), 500_000_000) + } + + func testSendingSpanLogs() throws { + let server = ServerMock(delivery: .success(response: .mockResponseWith(statusCode: 200))) + let loggingFeature = LoggingFeature.mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + performance: .combining(storagePerformance: .readAllFiles, uploadPerformance: .veryQuick) + ) + TracingFeature.instance = .mockWorkingFeatureWith( + server: server, + directory: temporaryDirectory, + performance: .combining(storagePerformance: .noOp, uploadPerformance: .noOp), + loggingFeature: loggingFeature + ) + defer { TracingFeature.instance = nil } + + let objcTracer = DDTracer(configuration: DDTracerConfiguration()) + + let objcSpan = objcTracer.startSpan("operation") + objcSpan.log(["foo": NSString(string: "bar")], timestamp: Date.mockDecember15th2019At10AMUTC()) + objcSpan.log(["bizz": NSNumber(10.5)]) + objcSpan.log(["buzz": NSURL(string: "https://example.com/image.png")!], timestamp: nil) + + let logMatchers = try server.waitAndReturnLogMatchers(count: 3) + + logMatchers[0].assertValue(forKey: "foo", equals: "bar") + logMatchers[1].assertValue(forKey: "bizz", equals: 10.5) + logMatchers[2].assertValue(forKey: "buzz", equals: "https://example.com/image.png") + } + + func testInjectingSpanContextToValidCarrierAndFormat() throws { + let objcTracer = DDTracer(swiftTracer: Tracer.mockAny()) + let objcSpanContext = DDSpanContextObjc( + swiftSpanContext: DDSpanContext.mockWith(traceID: 1, spanID: 2) + ) + + let objcWriter = DDHTTPHeadersWriter() + try objcTracer.inject(objcSpanContext, format: OTFormatHTTPHeaders, carrier: objcWriter) + + let expectedHTTPHeaders = [ + "x-datadog-trace-id": "1", + "x-datadog-parent-id": "2", + ] + let swiftWritter = objcWriter.swiftHTTPHeadersWriter + XCTAssertEqual(swiftWritter.tracePropagationHTTPHeaders, expectedHTTPHeaders) + } + + func testInjectingSpanContextToInvalidCarrierOrFormat() throws { + let objcTracer = DDTracer(swiftTracer: Tracer.mockAny()) + let objcSpanContext = DDSpanContextObjc(swiftSpanContext: DDSpanContext.mockWith(traceID: 1, spanID: 2)) + + let objcValidWriter = DDHTTPHeadersWriter() + let objcInvalidFormat = "foo" + XCTAssertThrowsError( + try objcTracer.inject(objcSpanContext, format: objcInvalidFormat, carrier: objcValidWriter) + ) + + let objcInvalidWriter = NSObject() + let objcValidFormat = OTFormatHTTPHeaders + XCTAssertThrowsError( + try objcTracer.inject(objcSpanContext, format: objcValidFormat, carrier: objcInvalidWriter) + ) + } + + func testWhenSettingGlobalTracer_itSetsSwiftTracerAswell() { + XCTAssertTrue(OTGlobal.sharedTracer === noopTracer) + + let swiftTracer = Tracer.mockAny() + let objcTracer = DDTracer(swiftTracer: swiftTracer) + + let previousObjcTracer = OTGlobal.sharedTracer + let previousSwiftTracer = Global.sharedTracer + OTGlobal.initSharedTracer(objcTracer) + defer { + OTGlobal.sharedTracer = previousObjcTracer + Global.sharedTracer = previousSwiftTracer + } + + XCTAssertTrue(OTGlobal.sharedTracer === objcTracer) + XCTAssertTrue(Global.sharedTracer as? Tracer === swiftTracer) + } + + // MARK: - Usage errors + + func testsWhenUsingUnexpectedOTTracer() throws { + let previousObjcTracer = OTGlobal.sharedTracer + + OTGlobal.initSharedTracer(noopTracer) + + XCTAssertTrue(OTGlobal.sharedTracer === previousObjcTracer) + XCTAssertFalse(Global.sharedTracer is Tracer) + } + + func testsWhenUsingUnexpectedOTSpanContext() throws { + let objcTracer = DDTracer(swiftTracer: Tracer.mockAny()) + + XCTAssertNil(objcTracer.startSpan(.mockAny(), childOf: noopSpanContext).dd!.swiftSpan.dd.ddContext.parentSpanID) + XCTAssertNil(objcTracer.startSpan(.mockAny(), childOf: noopSpanContext, tags: NSDictionary()).dd!.swiftSpan.dd.ddContext.parentSpanID) + XCTAssertNil(objcTracer.startSpan(.mockAny(), childOf: noopSpanContext, tags: NSDictionary(), startTime: .mockAny()).dd!.swiftSpan.dd.ddContext.parentSpanID) + + let objcWriter = DDHTTPHeadersWriter() + try objcTracer.inject(noopSpanContext, format: OTFormatHTTPHeaders, carrier: objcWriter) + XCTAssertEqual(objcWriter.swiftHTTPHeadersWriter.tracePropagationHTTPHeaders.count, 0) + } + + func testsWhenUsingUnexpectedTagsDictionary() throws { + let objcTracer = DDTracer(swiftTracer: Tracer.mockAny()) + + let tags = NSDictionary(dictionary: [1: "string"]) + let objcSpan = objcTracer.startSpan(.mockAny(), tags: tags) + + XCTAssertEqual(objcSpan.dd?.swiftSpan.dd.tags.count, 0) + } + + func testUsingNoopTracerIsSafe() { + // noop Tracer + XCTAssertTrue(noopTracer.startSpan(.mockAny()) === noopSpan) + XCTAssertTrue(noopTracer.startSpan(.mockAny(), tags: nil) === noopSpan) + XCTAssertTrue(noopTracer.startSpan(.mockAny(), childOf: nil) === noopSpan) + XCTAssertTrue(noopTracer.startSpan(.mockAny(), childOf: nil, tags: nil) === noopSpan) + XCTAssertTrue(noopTracer.startSpan(.mockAny(), childOf: nil, tags: nil, startTime: nil) === noopSpan) + XCTAssertNoThrow(try noopTracer.inject(noopSpanContext, format: .mockAny(), carrier: NSObject())) + XCTAssertNoThrow(try noopTracer.extractWithFormat(.mockAny(), carrier: NSObject())) + + // noop Span + XCTAssertTrue(noopSpan.context === noopSpanContext) + XCTAssertTrue(noopSpan.tracer === noopTracer) + noopSpan.setOperationName(.mockAny()) + noopSpan.setTag(.mockAny(), value: "") + noopSpan.setTag(.mockAny(), numberValue: 0) + noopSpan.setTag(.mockAny(), boolValue: false) + noopSpan.log([:]) + noopSpan.log([:], timestamp: nil) + _ = noopSpan.setBaggageItem(.mockAny(), value: .mockAny()) + _ = noopSpan.getBaggageItem(.mockAny()) + noopSpan.finish() + noopSpan.finishWithTime(nil) + + // noop SpanContext + noopSpanContext.forEachBaggageItem { _, _ in false } + } +} diff --git a/Tests/DatadogTests/Helpers/DatadogExtensions.swift b/Tests/DatadogTests/Helpers/DatadogExtensions.swift index d212dc0f23..0dd7c05add 100644 --- a/Tests/DatadogTests/Helpers/DatadogExtensions.swift +++ b/Tests/DatadogTests/Helpers/DatadogExtensions.swift @@ -19,8 +19,12 @@ extension Date { } } -extension EncodableValue: Equatable { - public static func == (lhs: EncodableValue, rhs: EncodableValue) -> Bool { - return String(describing: lhs) == String(describing: rhs) +extension File { + func makeReadonly() throws { + try FileManager.default.setAttributes([.immutable: true], ofItemAtPath: url.path) + } + + func makeReadWrite() throws { + try FileManager.default.setAttributes([.immutable: false], ofItemAtPath: url.path) } } diff --git a/Tests/DatadogTests/Helpers/Encoding.swift b/Tests/DatadogTests/Helpers/Encoding.swift new file mode 100644 index 0000000000..e2a7944878 --- /dev/null +++ b/Tests/DatadogTests/Helpers/Encoding.swift @@ -0,0 +1,25 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +@testable import Datadog + +extension EncodableValue: Equatable { + public static func == (lhs: EncodableValue, rhs: EncodableValue) -> Bool { + return String(describing: lhs) == String(describing: rhs) + } +} + +/// Prior to `iOS13.0`, the `JSONEncoder` supports only object or array as the root type. +/// Hence we can't test encoding `Encodable` values directly and we need as support of this `EncodingContainer` container. +/// +/// Reference: https://bugs.swift.org/browse/SR-6163 +struct EncodingContainer: Encodable { + let value: Value + + init(_ value: Value) { + self.value = value + } +} diff --git a/Tests/DatadogTests/Helpers/SwiftExtensions.swift b/Tests/DatadogTests/Helpers/SwiftExtensions.swift index e5c16eed41..c872f46954 100644 --- a/Tests/DatadogTests/Helpers/SwiftExtensions.swift +++ b/Tests/DatadogTests/Helpers/SwiftExtensions.swift @@ -33,9 +33,9 @@ extension Date { } extension TimeZone { - static var UTC: TimeZone { - return TimeZone(abbreviation: "UTC")! - } + static var UTC: TimeZone { TimeZone(abbreviation: "UTC")! } + static var EET: TimeZone { TimeZone(abbreviation: "EET")! } + static func mockAny() -> TimeZone { .EET } } extension Calendar { diff --git a/Tests/DatadogTests/Helpers/TestsDirectory.swift b/Tests/DatadogTests/Helpers/TestsDirectory.swift index 6d735d6b55..0ed0c4dbbd 100644 --- a/Tests/DatadogTests/Helpers/TestsDirectory.swift +++ b/Tests/DatadogTests/Helpers/TestsDirectory.swift @@ -21,10 +21,6 @@ func obtainUniqueTemporaryDirectory() -> Directory { /// The subfolder does not exist and can be created and deleted by calling `.create()` and `.delete()`. let temporaryDirectory = obtainUniqueTemporaryDirectory() -/// Default URL where logs persist in -/// logsDirectory.delete() can be useful in tests when logs need to be cleared -let logsDirectory = try! obtainLoggingFeatureDirectory() - /// Extends `Directory` with set of utilities for convenient work with files in tests. /// Provides handy methods to create / delete files and directires. extension Directory { diff --git a/Tests/DatadogTests/Matchers/JSONDataMatcher.swift b/Tests/DatadogTests/Matchers/JSONDataMatcher.swift new file mode 100644 index 0000000000..c0603e7205 --- /dev/null +++ b/Tests/DatadogTests/Matchers/JSONDataMatcher.swift @@ -0,0 +1,127 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation +import XCTest + +/// Provides set of assertions for single JSON object or collection of JSON objects. +/// Note: this file is individually referenced by integration tests project, so no dependency on other source files should be introduced. +internal class JSONDataMatcher { + let json: [String: Any] + + // MARK: - Initialization + + init(from jsonObject: [String: Any]) { + self.json = jsonObject + } + + // MARK: - Full match + + func assertItFullyMatches(jsonString: String, file: StaticString = #file, line: UInt = #line) throws { + let thisJSON = json as NSDictionary + let theirJSON = try jsonString.data(using: .utf8)! + .toJSONObject(file: file, line: line) as NSDictionary // swiftlint:disable:this force_unwrapping + + XCTAssertEqual(thisJSON, theirJSON, file: file, line: line) + } + + // MARK: - Generic matches + + func assertValue(forKey key: String, equals value: T, file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual(json[key] as? T, value, file: file, line: line) + } + + func assertNoValue(forKey key: String, file: StaticString = #file, line: UInt = #line) { + XCTAssertNil(json[key], file: file, line: line) + } + + func assertValue(forKeyPath keyPath: String, equals value: T, file: StaticString = #file, line: UInt = #line) { + let dictionary = json as NSDictionary + let dictionaryValue = dictionary.value(forKeyPath: keyPath) + guard let jsonValue = dictionaryValue as? T else { + XCTFail("Value at key path `\(keyPath)` is not of type `\(type(of: value))`: \(String(describing: dictionaryValue))", file: file, line: line) + return + } + XCTAssertEqual(jsonValue, value, file: file, line: line) + } + + func assertNoValue(forKeyPath keyPath: String, file: StaticString = #file, line: UInt = #line) { + let dictionary = json as NSDictionary + XCTAssertNil(dictionary.value(forKeyPath: keyPath), file: file, line: line) + } + + func assertValue( + forKeyPath keyPath: String, + matches matcherClosure: (T) -> Bool, + file: StaticString = #file, + line: UInt = #line + ) { + let dictionary = json as NSDictionary + let dictionaryValue = dictionary.value(forKeyPath: keyPath) + guard let jsonValue = dictionaryValue as? T else { + XCTFail( + "Can't cast value at key path `\(keyPath)` to expected type: \(String(describing: dictionaryValue))", + file: file, + line: line + ) + return + } + + XCTAssertTrue(matcherClosure(jsonValue), file: file, line: line) + } + + func assertValue( + forKeyPath keyPath: String, + isTypeOf type: T.Type, + file: StaticString = #file, + line: UInt = #line + ) { + let dictionary = json as NSDictionary + let dictionaryValue = dictionary.value(forKeyPath: keyPath) + XCTAssertTrue((dictionaryValue as? T) != nil, file: file, line: line) + } + + // MARK: - Values extraction + + internal struct Exception: Error { + let description: String + } + + func value(forKeyPath keyPath: String) throws -> T { + let dictionary = json as NSDictionary + guard let anyValue = dictionary.value(forKeyPath: keyPath) else { + throw Exception( + description: "No value for key path `\(keyPath)`" + ) + } + guard let tValue = anyValue as? T else { + throw Exception( + description: "Cannot cast value for key path `\(keyPath)` to type `\(T.self)`: \(String(describing: anyValue))" + ) + } + return tValue + } +} + +internal extension Data { + func toArrayOfJSONObjects(file: StaticString = #file, line: UInt = #line) throws -> [[String: Any]] { + guard let jsonArray = try? JSONSerialization.jsonObject(with: self, options: []) as? [[String: Any]] else { + XCTFail("Cannot decode array of JSON objects from data.", file: file, line: line) + return [] + } + + return jsonArray + } + + func toJSONObject(file: StaticString = #file, line: UInt = #line) throws -> [String: Any] { + guard let jsonObject = try? JSONSerialization.jsonObject(with: self, options: []) as? [String: Any] else { + XCTFail("Cannot decode JSON object from given data.", file: file, line: line) + return [:] + } + + return jsonObject + } +} diff --git a/Tests/DatadogTests/Matchers/LogMatcher.swift b/Tests/DatadogTests/Matchers/LogMatcher.swift index de5ace3fd5..970aa51628 100644 --- a/Tests/DatadogTests/Matchers/LogMatcher.swift +++ b/Tests/DatadogTests/Matchers/LogMatcher.swift @@ -6,17 +6,9 @@ import XCTest -/// Provides set of assertions for Log JSON object. +/// Provides set of assertions for single `Log` JSON object or collection of `[Log]`. /// Note: this file is individually referenced by integration tests project, so no dependency on other source files should be introduced. -struct LogMatcher { - private static let dateFormatter: ISO8601DateFormatter = { - let formatter = ISO8601DateFormatter() - if #available(iOS 11.2, *) { - formatter.formatOptions.insert(.withFractionalSeconds) - } - return formatter - }() - +internal class LogMatcher: JSONDataMatcher { /// Log JSON keys. struct JSONKey { static let date = "date" @@ -63,30 +55,19 @@ struct LogMatcher { /// Allowed values for `network.client.reachability` attribute. static let allowedNetworkReachabilityValues: Set = ["yes", "no", "maybe"] - private let json: [String: Any] - // MARK: - Initialization - static func fromJSONObjectData(_ data: Data) throws -> LogMatcher { - return self.init(from: try data.toJSONObject()) - } - - static func fromArrayOfJSONObjectsData(_ data: Data) throws -> [LogMatcher] { - return try data.toArrayOfJSONObjects() - .map { jsonObject in self.init(from: jsonObject) } + class func fromJSONObjectData(_ data: Data, file: StaticString = #file, line: UInt = #line) throws -> LogMatcher { + return LogMatcher(from: try data.toJSONObject(file: file, line: line)) } - private init(from jsonObject: [String: Any]) { - self.json = jsonObject + class func fromArrayOfJSONObjectsData(_ data: Data, file: StaticString = #file, line: UInt = #line) throws -> [LogMatcher] { + return try data.toArrayOfJSONObjects(file: file, line: line) + .map { LogMatcher(from: $0) } } - // MARK: Full match - - func assertItFullyMatches(jsonString: String, file: StaticString = #file, line: UInt = #line) throws { - let thisJSON = json as NSDictionary - let theirJSON = try jsonString.data(using: .utf8)!.toJSONObject() as NSDictionary // swiftlint:disable:this force_unwrapping - - XCTAssertEqual(thisJSON, theirJSON, file: file, line: line) + override private init(from jsonObject: [String: Any]) { + super.init(from: jsonObject) } // MARK: Partial matches @@ -96,7 +77,7 @@ struct LogMatcher { XCTFail("Cannot decode date from log JSON: \(json).", file: file, line: line) return } - guard let date = LogMatcher.dateFormatter.date(from: dateString) else { + guard let date = date(fromISO8601FormattedString: dateString) else { XCTFail("Date has invalid format: \(dateString).", file: file, line: line) return } @@ -132,7 +113,7 @@ struct LogMatcher { } func assertUserInfo(equals userInfo: (id: String?, name: String?, email: String?)?, file: StaticString = #file, line: UInt = #line) { - if let id = userInfo?.id { // swiftlint:disable:this identifier_name + if let id = userInfo?.id { assertValue(forKey: JSONKey.userId, equals: id, file: file, line: line) } else { assertNoValue(forKey: JSONKey.userId, file: file, line: line) @@ -177,80 +158,13 @@ struct LogMatcher { XCTAssertEqual(matcherTags, logTags, file: file, line: line) } - - // MARK: - Generic matches - - func assertValue(forKey key: String, equals value: T, file: StaticString = #file, line: UInt = #line) { - XCTAssertEqual(json[key] as? T, value, file: file, line: line) - } - - func assertNoValue(forKey key: String, file: StaticString = #file, line: UInt = #line) { - XCTAssertNil(json[key], file: file, line: line) - } - - func assertValue(forKeyPath keyPath: String, equals value: T, file: StaticString = #file, line: UInt = #line) { - let dictionary = json as NSDictionary - let dictionaryValue = dictionary.value(forKeyPath: keyPath) - guard let jsonValue = dictionaryValue as? T else { - XCTFail("Value at key path `\(keyPath)` is not of type `\(type(of: value))`: \(String(describing: dictionaryValue))", file: file, line: line) - return - } - XCTAssertEqual(jsonValue, value, file: file, line: line) - } - - func assertNoValue(forKeyPath keyPath: String, file: StaticString = #file, line: UInt = #line) { - let dictionary = json as NSDictionary - XCTAssertNil(dictionary.value(forKeyPath: keyPath), file: file, line: line) - } - - func assertValue( - forKeyPath keyPath: String, - matches matcherClosure: (T) -> Bool, - file: StaticString = #file, - line: UInt = #line - ) { - let dictionary = json as NSDictionary - let dictionaryValue = dictionary.value(forKeyPath: keyPath) - guard let jsonValue = dictionaryValue as? T else { - XCTFail( - "Can't cast value at key path `\(keyPath)` to expected type: \(String(describing: dictionaryValue))", - file: file, - line: line - ) - return - } - - XCTAssertTrue(matcherClosure(jsonValue), file: file, line: line) - } - - func assertValue( - forKeyPath keyPath: String, - isTypeOf type: T.Type, - file: StaticString = #file, - line: UInt = #line - ) { - let dictionary = json as NSDictionary - let dictionaryValue = dictionary.value(forKeyPath: keyPath) - XCTAssertTrue((dictionaryValue as? T) != nil, file: file, line: line) - } } -private extension Data { - func toArrayOfJSONObjects(file: StaticString = #file, line: UInt = #line) throws -> [[String: Any]] { - guard let jsonArray = try? JSONSerialization.jsonObject(with: self, options: []) as? [[String: Any]] else { - XCTFail("Cannot decode array of JSON objects from data.", file: file, line: line) - return [] - } - - return jsonArray - } - - func toJSONObject(file: StaticString = #file, line: UInt = #line) throws -> [String: Any] { - guard let jsonObject = try? JSONSerialization.jsonObject(with: self, options: []) as? [String: Any] else { - XCTFail("Cannot decode JSON object from given data.", file: file, line: line) - return [:] - } - - return jsonObject - } +func date(fromISO8601FormattedString: String) -> Date? { + let iso8601Formatter = DateFormatter() + iso8601Formatter.locale = Locale(identifier: "en_US_POSIX") + iso8601Formatter.timeZone = TimeZone(abbreviation: "UTC")! + iso8601Formatter.calendar = Calendar(identifier: .gregorian) + iso8601Formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" // ISO8601 format + return iso8601Formatter.date(from: fromISO8601FormattedString) } diff --git a/Tests/DatadogTests/Matchers/SpanMatcher.swift b/Tests/DatadogTests/Matchers/SpanMatcher.swift new file mode 100644 index 0000000000..10f5042b39 --- /dev/null +++ b/Tests/DatadogTests/Matchers/SpanMatcher.swift @@ -0,0 +1,154 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation + +/// Implemented by types allowed to represent span attribute `.*` value in JSON. +protocol AllowedSpanAttributeValue {} +/// Implemented by types allowed to represent span `metrics.*` value in JSON. +protocol AllowedSpanMetricValue {} +/// Implemented by types allowed to represent span `meta.*` value in JSON. +protocol AllowedSpanMetaValue {} + +// All JSON-convertible values are allowed for `span.*`. +extension String: AllowedSpanAttributeValue {} +extension UInt64: AllowedSpanAttributeValue {} +extension Int: AllowedSpanAttributeValue {} + +// Only numeric values are allowed for `span.metrics.*`. +extension Int: AllowedSpanMetricValue {} + +// Only string values are allowed for `span.meta.*`. +extension String: AllowedSpanMetaValue {} + +/// Provides set of assertions for single `Span` JSON object or collection of `[Span]`. +/// Note: this file is individually referenced by integration tests project, so no dependency on other source files should be introduced. +internal class SpanMatcher { + // MARK: - Initialization + + /// Returns "Span A" matcher for data representing string: + /// + /// { "spans": [ { /* Span A json */ } ] } + /// + /// **NOTE:** If `spans` array contains more than one span JSON, only the first one will be described by the matcher. + /// Current implementation of `SpanEnvelope` doesn't allow for more than one span in the array. + /// + /// **See Also**: `SpanEnvelope` + /// + class func fromJSONObjectData(_ data: Data) throws -> SpanMatcher { + return try SpanMatcher(from: try data.toJSONObject()) + } + + /// Returns array containing Span A, Span B and Span C matchers for data representing string: + /// + /// ``` + /// { "spans": [ { /* Span A json */ } ] } + /// { "spans": [ { /* Span B json */ } ] } + /// { "spans": [ { /* Span C json */ } ] } + /// ``` + /// + /// **See Also** `SpanMatcher.fromJSONObjectData(_:)` + /// + class func fromNewlineSeparatedJSONObjectsData(_ data: Data) throws -> [SpanMatcher] { + let separator = "\n".data(using: .utf8)![0] + let spansData = data.split(separator: separator).map { Data($0) } + return try spansData.map { spanJSONData in try SpanMatcher.fromJSONObjectData(spanJSONData) } + } + + /// Matcher for the whole `SpanEnvelope`. + private let envelope: JSONDataMatcher + /// Matcher for the first `Span` from envelope. + private let span: JSONDataMatcher + + private init(from jsonObject: [String: Any]) throws { + self.envelope = JSONDataMatcher(from: jsonObject) + self.span = JSONDataMatcher(from: try self.envelope.value(forKeyPath: "spans.@firstObject")) + } + + // MARK: - Full match + + func assertItFullyMatches(jsonString: String, file: StaticString = #file, line: UInt = #line) throws { + try self.envelope.assertItFullyMatches(jsonString: jsonString, file: file, line: line) + } + + // MARK: - Attributes matching + + func traceID() throws -> String { try attribute(forKeyPath: "trace_id") } + func spanID() throws -> String { try attribute(forKeyPath: "span_id") } + func parentSpanID() throws -> String { try attribute(forKeyPath: "parent_id") } + func operationName() throws -> String { try attribute(forKeyPath: "name") } + func serviceName() throws -> String { try attribute(forKeyPath: "service") } + func resource() throws -> String { try attribute(forKeyPath: "resource") } + func type() throws -> String { try attribute(forKeyPath: "type") } + func startTime() throws -> UInt64 { try attribute(forKeyPath: "start") } + func duration() throws -> UInt64 { try attribute(forKeyPath: "duration") } + func isError() throws -> Int { try attribute(forKeyPath: "error") } + func environment() throws -> String { try envelope.value(forKeyPath: "env") } + + // MARK: - Metrics matching + + var metrics: Metrics { Metrics(matcher: self) } + + struct Metrics { + fileprivate let matcher: SpanMatcher + + func isRootSpan() throws -> Int { try matcher.metric(forKeyPath: "metrics._top_level") } + func samplingPriority() throws -> Int { try matcher.metric(forKeyPath: "metrics._sampling_priority_v1") } + } + + // MARK: - Meta matching + + var meta: Meta { Meta(matcher: self) } + + struct Meta { + fileprivate let matcher: SpanMatcher + + func source() throws -> String { try matcher.meta(forKeyPath: "meta._dd.source") } + func applicationVersion() throws -> String { try matcher.meta(forKeyPath: "meta.version") } + func tracerVersion() throws -> String { try matcher.meta(forKeyPath: "meta.tracer.version") } + + func userID() throws -> String { try matcher.meta(forKeyPath: "meta.usr.id") } + func userName() throws -> String { try matcher.meta(forKeyPath: "meta.usr.name") } + func userEmail() throws -> String { try matcher.meta(forKeyPath: "meta.usr.email") } + + func networkReachability() throws -> String { try matcher.meta(forKeyPath: "meta.network.client.reachability") } + func networkAvailableInterfaces() throws -> String { try matcher.meta(forKeyPath: "meta.network.client.available_interfaces") } + func networkConnectionSupportsIPv4() throws -> String { try matcher.meta(forKeyPath: "meta.network.client.supports_ipv4") } + func networkConnectionSupportsIPv6() throws -> String { try matcher.meta(forKeyPath: "meta.network.client.supports_ipv6") } + func networkConnectionIsExpensive() throws -> String { try matcher.meta(forKeyPath: "meta.network.client.is_expensive") } + func networkConnectionIsConstrained() throws -> String { try matcher.meta(forKeyPath: "meta.network.client.is_constrained") } + + func mobileNetworkCarrierName() throws -> String { try matcher.meta(forKeyPath: "meta.network.client.sim_carrier.name") } + func mobileNetworkCarrierISOCountryCode() throws -> String { try matcher.meta(forKeyPath: "meta.network.client.sim_carrier.iso_country") } + func mobileNetworkCarrierRadioTechnology() throws -> String { try matcher.meta(forKeyPath: "meta.network.client.sim_carrier.technology") } + func mobileNetworkCarrierAllowsVoIP() throws -> String { try matcher.meta(forKeyPath: "meta.network.client.sim_carrier.allows_voip") } + + func custom(keyPath: String) throws -> String { try matcher.meta(forKeyPath: keyPath) } + } + + /// Allowed values for `meta.network.client.available_interfaces` attribute. + static let allowedNetworkAvailableInterfacesValues: Set = ["wifi", "wiredEthernet", "cellular", "loopback", "other"] + /// Allowed values for `meta.network.client.reachability` attribute. + static let allowedNetworkReachabilityValues: Set = ["yes", "no", "maybe"] + + // MARK: - Private + + private func attribute(forKeyPath keyPath: String) throws -> T { + precondition(!keyPath.hasPrefix("metrics."), "use specialized `metric(forKeyPath:)`") + precondition(!keyPath.hasPrefix("meta."), "use specialized `meta(forKeyPath:)`") + return try span.value(forKeyPath: keyPath) + } + + private func metric(forKeyPath keyPath: String) throws -> T { + precondition(keyPath.hasPrefix("metrics.")) + return try span.value(forKeyPath: keyPath) + } + + private func meta(forKeyPath keyPath: String) throws -> T { + precondition(keyPath.hasPrefix("meta.")) + return try span.value(forKeyPath: keyPath) + } +} diff --git a/Tests/DatadogTests/OpenTracing/OTGlobalTests.swift b/Tests/DatadogTests/OpenTracing/OTGlobalTests.swift new file mode 100644 index 0000000000..6872d7129f --- /dev/null +++ b/Tests/DatadogTests/OpenTracing/OTGlobalTests.swift @@ -0,0 +1,32 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import XCTest +@testable import Datadog + +class OTGlobalTests: XCTestCase { + func testWhenUsingDefaultGlobalTracer_itDoesNothing() { + let noOpTracer = Global.sharedTracer + XCTAssertTrue(noOpTracer is DDNoopTracer) + + let noOpSpan = Global.sharedTracer.startSpan(operationName: .mockAny()) + XCTAssertTrue(noOpSpan is DDNoopSpan) + XCTAssertTrue(noOpSpan.tracer() is DDNoopTracer) + XCTAssertTrue(noOpSpan.context is DDNoopSpanContext) + + noOpSpan.setOperationName(.mockAny()) + noOpSpan.setTag(key: .mockAny(), value: String.mockAny()) + noOpSpan.setBaggageItem(key: .mockAny(), value: .mockAny()) + _ = noOpSpan.baggageItem(withKey: .mockAny()) + _ = noOpSpan.context.forEachBaggageItem { _, _ in return false } + noOpSpan.log(fields: [.mockAny(): String.mockAny()]) + noOpSpan.finish() + + let headersWriter = HTTPHeadersWriter() + noOpTracer.inject(spanContext: noOpSpan.context, writer: headersWriter) + XCTAssertEqual(headersWriter.tracePropagationHTTPHeaders.count, 0) + } +} diff --git a/api-surface-objc b/api-surface-objc new file mode 100644 index 0000000000..6fddc26c7d --- /dev/null +++ b/api-surface-objc @@ -0,0 +1,104 @@ +public class DDAppContext: NSObject + public init(mainBundle: Bundle) + override public init() +public class DDDatadog: NSObject + public static func initialize(appContext: DDAppContext, configuration: DDConfiguration) + public static func setVerbosityLevel(_ verbosityLevel: DDSDKVerbosityLevel) + public static func verbosityLevel() -> DDSDKVerbosityLevel + public static func setUserInfo(id: String? = nil, name: String? = nil, email: String? = nil) +public class DDLogsEndpoint: NSObject + public static func eu() -> DDLogsEndpoint + public static func us() -> DDLogsEndpoint + public static func custom(url: String) -> DDLogsEndpoint +public class DDTracesEndpoint: NSObject + public static func eu() -> DDTracesEndpoint + public static func us() -> DDTracesEndpoint + public static func custom(url: String) -> DDTracesEndpoint +public class DDConfiguration: NSObject + public static func builder(clientToken: String, environment: String) -> DDConfigurationBuilder +public class DDConfigurationBuilder: NSObject + public func set(endpoint: DDLogsEndpoint) + public func enableLogging(_ enabled: Bool) + public func enableTracing(_ enabled: Bool) + public func set(logsEndpoint: DDLogsEndpoint) + public func set(tracesEndpoint: DDTracesEndpoint) + public func set(tracedHosts: Set) + public func set(serviceName: String) + public func build() -> DDConfiguration +public enum DDSDKVerbosityLevel: Int + case none + case debug + case info + case notice + case warn + case error + case critical +public class DDLogger: NSObject + public func debug(_ message: String) + public func debug(_ message: String, attributes: [String: Any]) + public func info(_ message: String) + public func info(_ message: String, attributes: [String: Any]) + public func notice(_ message: String) + public func notice(_ message: String, attributes: [String: Any]) + public func warn(_ message: String) + public func warn(_ message: String, attributes: [String: Any]) + public func error(_ message: String) + public func error(_ message: String, attributes: [String: Any]) + public func critical(_ message: String) + public func critical(_ message: String, attributes: [String: Any]) + public func addAttribute(forKey key: String, value: Any) + public func removeAttribute(forKey key: String) + public func addTag(withKey key: String, value: String) + public func removeTag(withKey key: String) + public func add(tag: String) + public func remove(tag: String) + public static func builder() -> DDLoggerBuilder +public class DDLoggerBuilder: NSObject + public func set(serviceName: String) + public func set(loggerName: String) + public func sendNetworkInfo(_ enabled: Bool) + public func sendLogsToDatadog(_ enabled: Bool) + public func printLogsToConsole(_ enabled: Bool) + public func build() -> DDLogger +public class OTGlobal: NSObject + public static func initSharedTracer(_ tracer: OTTracer) + public internal(set) static var sharedTracer: OTTracer = noopTracer +public protocol OTSpan + var context: OTSpanContext + var tracer: OTTracer + func setOperationName(_ operationName: String) + func setTag(_ key: String, value: NSString) + func setTag(_ key: String, numberValue: NSNumber) + func setTag(_ key: String, boolValue: Bool) + func log(_ fields: [String: NSObject]) + func log(_ fields: [String: NSObject], timestamp: Date?) + func setBaggageItem(_ key: String, value: String) -> OTSpan + func getBaggageItem(_ key: String) -> String? + func finish() + func finishWithTime(_ finishTime: Date?) +public protocol OTSpanContext + func forEachBaggageItem(_ callback: (_ key: String, _ value: String) -> Bool) +public let OTFormatHTTPHeaders = "OTFormatHTTPHeaders" +public protocol OTTracer + func startSpan(_ operationName: String) -> OTSpan + func startSpan(_ operationName: String, tags: NSDictionary?) -> OTSpan + func startSpan(_ operationName: String, childOf parent: OTSpanContext?) -> OTSpan + func startSpan(_ operationName: String, childOf parent: OTSpanContext?, tags: NSDictionary?) -> OTSpan + func startSpan(_ operationName: String, childOf parent: OTSpanContext?, tags: NSDictionary?, startTime: Date?) -> OTSpan + func inject(_ spanContext: OTSpanContext, format: String, carrier: Any) throws + func extractWithFormat(_ format: String, carrier: Any) throws +public class DDTracer: DatadogObjc.OTTracer + public static func initialize(configuration: DDTracerConfiguration) -> DatadogObjc.OTTracer + public func startSpan(_ operationName: String) -> OTSpan + public func startSpan(_ operationName: String, tags: NSDictionary?) -> OTSpan + public func startSpan(_ operationName: String, childOf parent: OTSpanContext?) -> OTSpan + public func startSpan(_ operationName: String,childOf parent: OTSpanContext?,tags: NSDictionary?) -> OTSpan + public func startSpan(_ operationName: String,childOf parent: OTSpanContext?,tags: NSDictionary?,startTime: Date?) -> OTSpan + public func inject(_ spanContext: OTSpanContext, format: String, carrier: Any) throws + public func extractWithFormat(_ format: String, carrier: Any) throws +public class DDTracerConfiguration: NSObject + override public init() + public func set(serviceName: String) + public func sendNetworkInfo(_ enabled: Bool) +public class DDHTTPHeadersWriter: NSObject + override public init() diff --git a/api-surface-swift b/api-surface-swift new file mode 100644 index 0000000000..70f7a01b84 --- /dev/null +++ b/api-surface-swift @@ -0,0 +1,140 @@ +public class Datadog + public struct AppContext + public init(mainBundle: Bundle = Bundle.main) + public static func initialize(appContext: AppContext, configuration: Configuration) + public static var verbosityLevel: LogLevel? = nil + public static func setUserInfo(id: String? = nil,name: String? = nil,email: String? = nil) + public struct Configuration + public enum LogsEndpoint + case us + case eu + case custom(url: String) + public enum TracesEndpoint + case us + case eu + case custom(url: String) + public static func builderUsing(clientToken: String, environment: String) -> Builder + public class Builder + public func enableLogging(_ enabled: Bool) -> Builder + public func enableTracing(_ enabled: Bool) -> Builder + public func set(tracedHosts: Set) -> Builder + public func set(logsEndpoint: LogsEndpoint) -> Builder + public func set(tracesEndpoint: TracesEndpoint) -> Builder + public func set(serviceName: String) -> Builder + public func build() -> Configuration +public enum LogLevel: Int, Codable + case debug + case info + case notice + case warn + case error + case critical +public typealias AttributeKey = String +public typealias AttributeValue = Encodable +public typealias DDLogger = Logger +public class Logger + public func debug(_ message: String, attributes: [AttributeKey: AttributeValue]? = nil) + public func info(_ message: String, attributes: [AttributeKey: AttributeValue]? = nil) + public func notice(_ message: String, attributes: [AttributeKey: AttributeValue]? = nil) + public func warn(_ message: String, attributes: [AttributeKey: AttributeValue]? = nil) + public func error(_ message: String, attributes: [AttributeKey: AttributeValue]? = nil) + public func critical(_ message: String, attributes: [AttributeKey: AttributeValue]? = nil) + public func addAttribute(forKey key: AttributeKey, value: AttributeValue) + public func removeAttribute(forKey key: AttributeKey) + public func addTag(withKey key: String, value: String) + public func removeTag(withKey key: String) + public func add(tag: String) + public func remove(tag: String) + public static var builder: Builder + public class Builder + public func set(serviceName: String) -> Builder + public func set(loggerName: String) -> Builder + public func sendNetworkInfo(_ enabled: Bool) -> Builder + public func sendLogsToDatadog(_ enabled: Bool) -> Builder + public enum ConsoleLogFormat + case short + case shortWith(prefix: String) + case json + case jsonWith(prefix: String) + public func printLogsToConsole(_ enabled: Bool, usingFormat format: ConsoleLogFormat = .short) -> Builder + public func build() -> Logger +public struct OTTags + public static let component = "component" + public static let dbInstance = "db.instance" + public static let dbStatement = "db.statement" + public static let dbType = "db.type" + public static let dbUser = "db.user" + public static let error = "error" + public static let httpMethod = "http.method" + public static let httpStatusCode = "http.status_code" + public static let httpUrl = "http.url" + public static let messageBusDestination = "message_bus.destination" + public static let peerAddress = "peer.address" + public static let peerHostname = "peer.hostname" + public static let peerIPv4 = "peer.ipv4" + public static let peerIPv6 = "peer.ipv6" + public static let peerPort = "peer.port" + public static let peerService = "peer.service" + public static let samplingPriority = "sampling.priority" + public static let spanKind = "span.kind" +public struct OTLogFields + public static let errorKind = "error.kind" + public static let event = "event" + public static let message = "message" + public static let stack = "stack" +public protocol OTFormatReader: OTCustomFormatReader +public protocol OTFormatWriter: OTCustomFormatWriter +public protocol OTTextMapReader: OTFormatReader +public protocol OTTextMapWriter: OTFormatWriter +public protocol OTHTTPHeadersReader: OTTextMapReader +public protocol OTHTTPHeadersWriter: OTTextMapWriter +public protocol OTCustomFormatReader + func extract() -> OTSpanContext? +public protocol OTCustomFormatWriter + func inject(spanContext: OTSpanContext) +public struct Global + public static var sharedTracer: OTTracer = DDNoopGlobals.tracer +public struct OTReference + public let type: OTReferenceType + public let context: OTSpanContext + public static func child(of parent: OTSpanContext) -> OTReference + public static func follows(from precedingContext: OTSpanContext) -> OTReference +public enum OTReferenceType: String + case childOf = "CHILD_OF" + case followsFrom = "FOLLOWS_FROM" +public protocol OTSpan + var context: OTSpanContext + func tracer() -> OTTracer + func setOperationName(_ operationName: String) + func setTag(key: String, value: Encodable) + func log(fields: [String: Encodable], timestamp: Date) + func setBaggageItem(key: String, value: String) + func baggageItem(withKey key: String) -> String? + func finish(at time: Date) +public extension OTSpan + func log(fields: [String: Encodable]) + func finish() +public protocol OTSpanContext + func forEachBaggageItem(callback: (_ key: String, _ value: String) -> Bool) +public protocol OTTracer + func startSpan(operationName: String,references: [OTReference]?,tags: [String: Encodable]?,startTime: Date?) -> OTSpan + func inject(spanContext: OTSpanContext, writer: OTFormatWriter) + func extract(reader: OTFormatReader) -> OTSpanContext? +public extension OTTracer + func startSpan(operationName: String,childOf parent: OTSpanContext? = nil,tags: [String: Encodable]? = nil,startTime: Date? = nil) -> OTSpan +public struct DDTags + public static let resource = "resource.name" +public typealias DDTracer = Tracer +public class Tracer: OTTracer + public static func initialize(configuration: Configuration) -> OTTracer + public func startSpan(operationName: String, references: [OTReference]? = nil, tags: [String: Encodable]? = nil, startTime: Date? = nil) -> OTSpan + public func inject(spanContext: OTSpanContext, writer: OTFormatWriter) + public func extract(reader: OTFormatReader) -> OTSpanContext? + public struct Configuration + public var serviceName: String? + public var sendNetworkInfo: Bool + public init(serviceName: String? = nil,sendNetworkInfo: Bool = false) +public class HTTPHeadersWriter: OTHTTPHeadersWriter + public init() + public private(set) var tracePropagationHTTPHeaders: [String: String] = [:] + public func inject(spanContext: OTSpanContext) diff --git a/bitrise.yml b/bitrise.yml index 7bca3ac2df..abc96f3315 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -12,8 +12,8 @@ workflows: - _make_dependencies - run_linter - run_unit_tests - - run_ui_tests - run_integration_tests + - run_ui_tests - check_dependency_managers - _deploy_artifacts @@ -39,14 +39,14 @@ workflows: description: |- Runs swiftlint and license check for all source and test files. steps: - - swiftlint@0.4.2: + - swiftlint@0.7.0: title: Lint Sources/* inputs: - strict: 'yes' - lint_config_file: "$BITRISE_SOURCE_DIR/tools/lint/sources.swiftlint.yml" - linting_path: "$BITRISE_SOURCE_DIR" - reporter: emoji - - swiftlint@0.4.2: + - swiftlint@0.7.0: title: Lint Tests/* is_always_run: true inputs: @@ -66,6 +66,7 @@ workflows: run_unit_tests: description: |- Runs unit tests for SDK on iOS Simulator. + Runs benchmarks for SDK on iOS Simulator. Runs unit tests for HTTPServerMock package on macOS. steps: - xcode-test@2.4.5: @@ -77,6 +78,16 @@ workflows: - generate_code_coverage_files: 'yes' - project_path: Datadog.xcworkspace - xcpretty_test_options: --color --report html --output "${BITRISE_DEPLOY_DIR}/Unit-tests.html" + - xcode-test: + title: Run benchmarks - DatadogBenchmarkTests on iOS Simulator + inputs: + - scheme: DatadogBenchmarkTests + - simulator_device: iPhone 11 + - should_build_before_test: 'no' + - is_clean_build: 'no' + - generate_code_coverage_files: 'yes' + - project_path: Datadog.xcworkspace + - xcpretty_test_options: --color --report html --output "${BITRISE_DEPLOY_DIR}/Benchmark-tests.html" - script@1.1.6: title: Generate HTTPServerMock.xcodeproj inputs: @@ -101,7 +112,7 @@ workflows: - content: |- #!/usr/bin/env bash set -e - cd Shopist && make + echo 'DATADOG_CLIENT_TOKEN="fake-client-token-for-shopist"' > xcconfigs/Datadog.local.xcconfig - xcode-test@2.4.5: title: Run UI tests for Shopist - iOS Simulator inputs: @@ -125,6 +136,7 @@ workflows: - generate_code_coverage_files: 'yes' - project_path: Datadog.xcworkspace - xcpretty_test_options: --color --report html --output "${BITRISE_DEPLOY_DIR}/Integration-tests.html" + check_dependency_managers: description: |- Uses supported dependency managers to fetch, install and link the SDK diff --git a/dependency-manager-tests/.gitignore b/dependency-manager-tests/.gitignore index 6a6b301abd..ffc5a47759 100644 --- a/dependency-manager-tests/.gitignore +++ b/dependency-manager-tests/.gitignore @@ -11,6 +11,7 @@ xcuserdata # SPM test # - ignore `SPMProject.xcodeproj` as it will be re-created for every test run /spm/SPMProject.xcodeproj +/spm/**/Package.resolved # Cocoapods test # - ignore `Podfile.lock` and `Podfile` as they will be re-created for every test run diff --git a/dependency-manager-tests/carthage/CTProject.xcodeproj/project.pbxproj b/dependency-manager-tests/carthage/CTProject.xcodeproj/project.pbxproj index e7888a005f..a8b45c1fb9 100644 --- a/dependency-manager-tests/carthage/CTProject.xcodeproj/project.pbxproj +++ b/dependency-manager-tests/carthage/CTProject.xcodeproj/project.pbxproj @@ -15,7 +15,6 @@ 61C36430243752A600C4D4E6 /* CTProjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3642F243752A600C4D4E6 /* CTProjectTests.swift */; }; 61C3643B243752A600C4D4E6 /* CTProjectUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C3643A243752A600C4D4E6 /* CTProjectUITests.swift */; }; 61C3644B2437547A00C4D4E6 /* Datadog.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61C3644A2437547A00C4D4E6 /* Datadog.framework */; }; - 61C3644C2437547A00C4D4E6 /* Datadog.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 61C3644A2437547A00C4D4E6 /* Datadog.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -35,21 +34,10 @@ }; /* End PBXContainerItemProxy section */ -/* Begin PBXCopyFilesBuildPhase section */ - 61C3644D2437547A00C4D4E6 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 61C3644C2437547A00C4D4E6 /* Datadog.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ + 615519322461CDB4002A85CF /* Datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.xcconfig; sourceTree = ""; }; + 615519332461CDB4002A85CF /* Datadog.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.local.xcconfig; sourceTree = ""; }; + 615519342461D121002A85CF /* OpenTracing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenTracing.framework; path = Carthage/Build/iOS/OpenTracing.framework; sourceTree = ""; }; 61C36415243752A500C4D4E6 /* CTProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CTProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; 61C36418243752A500C4D4E6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 61C3641A243752A500C4D4E6 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -64,9 +52,6 @@ 61C3643A243752A600C4D4E6 /* CTProjectUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CTProjectUITests.swift; sourceTree = ""; }; 61C3643C243752A600C4D4E6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 61C3644A2437547A00C4D4E6 /* Datadog.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Datadog.framework; path = Carthage/Build/iOS/Datadog.framework; sourceTree = ""; }; - 61C3646224377D8A00C4D4E6 /* app-target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "app-target.xcconfig"; sourceTree = ""; }; - 61C3646324377D8A00C4D4E6 /* unit-tests-target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "unit-tests-target.xcconfig"; sourceTree = ""; }; - 61C364642437816500C4D4E6 /* datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = datadog.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -95,10 +80,20 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 615519312461CDB4002A85CF /* xcconfigs */ = { + isa = PBXGroup; + children = ( + 615519322461CDB4002A85CF /* Datadog.xcconfig */, + 615519332461CDB4002A85CF /* Datadog.local.xcconfig */, + ); + name = xcconfigs; + path = ../../xcconfigs; + sourceTree = ""; + }; 61C3640C243752A500C4D4E6 = { isa = PBXGroup; children = ( - 61C3646124377D8A00C4D4E6 /* xcconfigs */, + 615519312461CDB4002A85CF /* xcconfigs */, 61C36417243752A500C4D4E6 /* CTProject */, 61C3642E243752A600C4D4E6 /* CTProjectTests */, 61C36439243752A600C4D4E6 /* CTProjectUITests */, @@ -151,22 +146,12 @@ 61C364492437547A00C4D4E6 /* Frameworks */ = { isa = PBXGroup; children = ( + 615519342461D121002A85CF /* OpenTracing.framework */, 61C3644A2437547A00C4D4E6 /* Datadog.framework */, ); name = Frameworks; sourceTree = ""; }; - 61C3646124377D8A00C4D4E6 /* xcconfigs */ = { - isa = PBXGroup; - children = ( - 61C364642437816500C4D4E6 /* datadog.xcconfig */, - 61C3646224377D8A00C4D4E6 /* app-target.xcconfig */, - 61C3646324377D8A00C4D4E6 /* unit-tests-target.xcconfig */, - ); - name = xcconfigs; - path = ../xcconfigs; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -178,7 +163,6 @@ 61C36412243752A500C4D4E6 /* Frameworks */, 61C36413243752A500C4D4E6 /* Resources */, 61C364482437544F00C4D4E6 /* โš™๏ธ Carthage */, - 61C3644D2437547A00C4D4E6 /* Embed Frameworks */, 61C3645C243768FC00C4D4E6 /* โš™๏ธ Run linter */, ); buildRules = ( @@ -401,7 +385,7 @@ /* Begin XCBuildConfiguration section */ 61C3643D243752A600C4D4E6 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 61C3646224377D8A00C4D4E6 /* app-target.xcconfig */; + baseConfigurationReference = 615519322461CDB4002A85CF /* Datadog.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -462,7 +446,7 @@ }; 61C3643E243752A600C4D4E6 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 61C3646224377D8A00C4D4E6 /* app-target.xcconfig */; + baseConfigurationReference = 615519322461CDB4002A85CF /* Datadog.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -519,7 +503,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", @@ -531,7 +515,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.datadogqh.CTProject; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "Wildcard Development"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -541,7 +524,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", @@ -560,11 +543,10 @@ }; 61C36443243752A600C4D4E6 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 61C3646324377D8A00C4D4E6 /* unit-tests-target.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = CTProjectTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -581,11 +563,10 @@ }; 61C36444243752A600C4D4E6 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 61C3646324377D8A00C4D4E6 /* unit-tests-target.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = CTProjectTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -604,7 +585,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = CTProjectUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -623,7 +604,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = CTProjectUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/dependency-manager-tests/carthage/CTProject/ViewController.swift b/dependency-manager-tests/carthage/CTProject/ViewController.swift index 2bb83a6529..fa398ad026 100644 --- a/dependency-manager-tests/carthage/CTProject/ViewController.swift +++ b/dependency-manager-tests/carthage/CTProject/ViewController.swift @@ -25,6 +25,11 @@ internal class ViewController: UIViewController { .printLogsToConsole(true) .build() + Global.sharedTracer = Tracer.initialize(configuration: .init()) + logger.info("It works") + + // Start span, but never finish it (no upload) + _ = Global.sharedTracer.startSpan(operationName: "This too") } } diff --git a/dependency-manager-tests/carthage/Makefile b/dependency-manager-tests/carthage/Makefile index 3be84654d3..e95617da4e 100644 --- a/dependency-manager-tests/carthage/Makefile +++ b/dependency-manager-tests/carthage/Makefile @@ -4,9 +4,15 @@ else GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) endif +PWD := $(shell pwd) + test: @echo "โš™๏ธ Configuring CTProject with remote branch: '${GIT_BRANCH}'..." @sed "s|REMOTE_GIT_BRANCH|${GIT_BRANCH}|g" Cartfile.src > Cartfile @rm -rf Carthage/ - carthage update - @echo "OK ๐Ÿ‘Œ" + @echo "๐Ÿงช Run 'carthage update'" + @carthage update --platform iOS + @echo "๐Ÿงช Check if expected frameworks exist in $(PWD)/Carthage/Build/iOS" + @[ -e "Carthage/Build/iOS/Datadog.framework" ] && echo "Datadog.framework - OK" || { echo "Datadog.framework - missing"; false; } + @[ -e "Carthage/Build/iOS/DatadogObjc.framework" ] && echo "DatadogObjc.framework - OK" || { echo "DatadogObjc.framework - missing"; false; } + @echo "๐Ÿงช SUCCEEDED" diff --git a/dependency-manager-tests/cocoapods/CPProject.xcodeproj/project.pbxproj b/dependency-manager-tests/cocoapods/CPProject.xcodeproj/project.pbxproj index 8a23165890..d58937c251 100644 --- a/dependency-manager-tests/cocoapods/CPProject.xcodeproj/project.pbxproj +++ b/dependency-manager-tests/cocoapods/CPProject.xcodeproj/project.pbxproj @@ -49,9 +49,8 @@ 61C364522437568300C4D4E6 /* CPProjectTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CPProjectTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 61C364542437568300C4D4E6 /* CPProjectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPProjectTests.swift; sourceTree = ""; }; 61C364562437568300C4D4E6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 61C36466243782B000C4D4E6 /* datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = datadog.xcconfig; sourceTree = ""; }; - 61C36467243782B000C4D4E6 /* app-target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "app-target.xcconfig"; sourceTree = ""; }; - 61C36468243782B000C4D4E6 /* unit-tests-target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "unit-tests-target.xcconfig"; sourceTree = ""; }; + 61ED26E42461D29E00752D66 /* Datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.xcconfig; sourceTree = ""; }; + 61ED26E52461D29E00752D66 /* Datadog.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.local.xcconfig; sourceTree = ""; }; A18949628C7A45A21789F71F /* Pods_CPProject.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CPProject.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C7DE97263B77DC87F0FC27F0 /* Pods-CPProject.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CPProject.release.xcconfig"; path = "Target Support Files/Pods-CPProject/Pods-CPProject.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -94,7 +93,7 @@ 61C363862436318E00C4D4E6 = { isa = PBXGroup; children = ( - 61C36465243782B000C4D4E6 /* xcconfigs */, + 61ED26E32461D29E00752D66 /* xcconfigs */, 61C363912436318E00C4D4E6 /* CPProject */, 61C364532437568300C4D4E6 /* CPProjectTests */, 61C363A82436319000C4D4E6 /* CPProjectUITests */, @@ -145,15 +144,14 @@ path = CPProjectTests; sourceTree = ""; }; - 61C36465243782B000C4D4E6 /* xcconfigs */ = { + 61ED26E32461D29E00752D66 /* xcconfigs */ = { isa = PBXGroup; children = ( - 61C36466243782B000C4D4E6 /* datadog.xcconfig */, - 61C36467243782B000C4D4E6 /* app-target.xcconfig */, - 61C36468243782B000C4D4E6 /* unit-tests-target.xcconfig */, + 61ED26E42461D29E00752D66 /* Datadog.xcconfig */, + 61ED26E52461D29E00752D66 /* Datadog.local.xcconfig */, ); name = xcconfigs; - path = ../xcconfigs; + path = ../../xcconfigs; sourceTree = ""; }; A0508E6A3FA3F45E84391D24 /* Frameworks */ = { @@ -417,7 +415,7 @@ /* Begin XCBuildConfiguration section */ 61C363AC2436319000C4D4E6 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 61C36467243782B000C4D4E6 /* app-target.xcconfig */; + baseConfigurationReference = 61ED26E42461D29E00752D66 /* Datadog.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -478,7 +476,7 @@ }; 61C363AD2436319000C4D4E6 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 61C36467243782B000C4D4E6 /* app-target.xcconfig */; + baseConfigurationReference = 61ED26E42461D29E00752D66 /* Datadog.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -536,7 +534,7 @@ baseConfigurationReference = 5AD49F84C2EA2EE5CB99586F /* Pods-CPProject.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = CPProject/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -554,7 +552,7 @@ baseConfigurationReference = C7DE97263B77DC87F0FC27F0 /* Pods-CPProject.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = CPProject/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -571,7 +569,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = CPProjectUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -590,7 +588,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = CPProjectUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -607,10 +605,9 @@ }; 61C364592437568300C4D4E6 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 61C36468243782B000C4D4E6 /* unit-tests-target.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = CPProjectTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -627,10 +624,9 @@ }; 61C3645A2437568300C4D4E6 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 61C36468243782B000C4D4E6 /* unit-tests-target.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = CPProjectTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/dependency-manager-tests/cocoapods/CPProject/ViewController.swift b/dependency-manager-tests/cocoapods/CPProject/ViewController.swift index 2bb83a6529..fa398ad026 100644 --- a/dependency-manager-tests/cocoapods/CPProject/ViewController.swift +++ b/dependency-manager-tests/cocoapods/CPProject/ViewController.swift @@ -25,6 +25,11 @@ internal class ViewController: UIViewController { .printLogsToConsole(true) .build() + Global.sharedTracer = Tracer.initialize(configuration: .init()) + logger.info("It works") + + // Start span, but never finish it (no upload) + _ = Global.sharedTracer.startSpan(operationName: "This too") } } diff --git a/dependency-manager-tests/spm/SPMProject.xcodeproj.src/project.pbxproj b/dependency-manager-tests/spm/SPMProject.xcodeproj.src/project.pbxproj index 2cac7a3d3a..6a397dd899 100644 --- a/dependency-manager-tests/spm/SPMProject.xcodeproj.src/project.pbxproj +++ b/dependency-manager-tests/spm/SPMProject.xcodeproj.src/project.pbxproj @@ -63,9 +63,8 @@ 61C363F724374D6100C4D4E6 /* SPMProjectUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SPMProjectUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 61C363FB24374D6100C4D4E6 /* SPMProjectUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SPMProjectUITests.swift; sourceTree = ""; }; 61C363FD24374D6100C4D4E6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 61C3646B2437839300C4D4E6 /* datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = datadog.xcconfig; sourceTree = ""; }; - 61C3646C2437839300C4D4E6 /* app-target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "app-target.xcconfig"; sourceTree = ""; }; - 61C3646D2437839300C4D4E6 /* unit-tests-target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "unit-tests-target.xcconfig"; sourceTree = ""; }; + 61CE5FD52461D3C2005EA621 /* Datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.xcconfig; sourceTree = ""; }; + 61CE5FD62461D3C2005EA621 /* Datadog.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.local.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -97,7 +96,7 @@ 61C363CD24374D5F00C4D4E6 = { isa = PBXGroup; children = ( - 61C3646A2437839300C4D4E6 /* xcconfigs */, + 61CE5FD42461D3C2005EA621 /* xcconfigs */, 61C363D824374D5F00C4D4E6 /* SPMProject */, 61C363EF24374D6100C4D4E6 /* SPMProjectTests */, 61C363FA24374D6100C4D4E6 /* SPMProjectUITests */, @@ -146,15 +145,14 @@ path = SPMProjectUITests; sourceTree = ""; }; - 61C3646A2437839300C4D4E6 /* xcconfigs */ = { + 61CE5FD42461D3C2005EA621 /* xcconfigs */ = { isa = PBXGroup; children = ( - 61C3646B2437839300C4D4E6 /* datadog.xcconfig */, - 61C3646C2437839300C4D4E6 /* app-target.xcconfig */, - 61C3646D2437839300C4D4E6 /* unit-tests-target.xcconfig */, + 61CE5FD52461D3C2005EA621 /* Datadog.xcconfig */, + 61CE5FD62461D3C2005EA621 /* Datadog.local.xcconfig */, ); name = xcconfigs; - path = ../xcconfigs; + path = ../../xcconfigs; sourceTree = ""; }; /* End PBXGroup section */ @@ -376,7 +374,7 @@ /* Begin XCBuildConfiguration section */ 61C363FE24374D6100C4D4E6 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 61C3646C2437839300C4D4E6 /* app-target.xcconfig */; + baseConfigurationReference = 61CE5FD52461D3C2005EA621 /* Datadog.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -437,7 +435,7 @@ }; 61C363FF24374D6100C4D4E6 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 61C3646C2437839300C4D4E6 /* app-target.xcconfig */; + baseConfigurationReference = 61CE5FD52461D3C2005EA621 /* Datadog.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -494,7 +492,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = SPMProject/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -511,7 +509,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = SPMProject/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -526,11 +524,10 @@ }; 61C3640424374D6100C4D4E6 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 61C3646D2437839300C4D4E6 /* unit-tests-target.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = SPMProjectTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -547,11 +544,10 @@ }; 61C3640524374D6100C4D4E6 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 61C3646D2437839300C4D4E6 /* unit-tests-target.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = SPMProjectTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -570,7 +566,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = SPMProjectUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -589,7 +585,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = SPMProjectUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/dependency-manager-tests/spm/SPMProject.xcodeproj.src/xcshareddata/xcschemes/SPMProject.xcscheme b/dependency-manager-tests/spm/SPMProject.xcodeproj.src/xcshareddata/xcschemes/SPMProject.xcscheme new file mode 100644 index 0000000000..5df5eb0262 --- /dev/null +++ b/dependency-manager-tests/spm/SPMProject.xcodeproj.src/xcshareddata/xcschemes/SPMProject.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dependency-manager-tests/spm/SPMProject/ViewController.swift b/dependency-manager-tests/spm/SPMProject/ViewController.swift index 2bb83a6529..fa398ad026 100644 --- a/dependency-manager-tests/spm/SPMProject/ViewController.swift +++ b/dependency-manager-tests/spm/SPMProject/ViewController.swift @@ -25,6 +25,11 @@ internal class ViewController: UIViewController { .printLogsToConsole(true) .build() + Global.sharedTracer = Tracer.initialize(configuration: .init()) + logger.info("It works") + + // Start span, but never finish it (no upload) + _ = Global.sharedTracer.startSpan(operationName: "This too") } } diff --git a/dependency-manager-tests/xcconfigs/app-target.xcconfig b/dependency-manager-tests/xcconfigs/app-target.xcconfig deleted file mode 100644 index 831ef2c9f9..0000000000 --- a/dependency-manager-tests/xcconfigs/app-target.xcconfig +++ /dev/null @@ -1,6 +0,0 @@ -DEVELOPMENT_TEAM[sdk=iphoneos*]=// use your own Development Team -CODE_SIGN_IDENTITY[sdk=iphoneos*]=// use your own Sign Identity -PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]=// use your own Provisioning Profile - -// If `datadog.xcconfig` is present, overwrite all settings -#include? "datadog.local.xcconfig" diff --git a/dependency-manager-tests/xcconfigs/unit-tests-target.xcconfig b/dependency-manager-tests/xcconfigs/unit-tests-target.xcconfig deleted file mode 100644 index 4452f945be..0000000000 --- a/dependency-manager-tests/xcconfigs/unit-tests-target.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "app-target.xcconfig" -PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]=// this line overrides app-target.xcconfig and sets provisioning profile to Auto diff --git a/docs/README.md b/docs/README.md index a33f661f8c..c3934967df 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,3 +3,4 @@ Find in this folder dedicated documentation for: * [Collecting and sending logs from your iOS application to Datadog](log_collection.md). +* [Collecting and sending traces from your iOS application to Datadog](trace_collection.md). diff --git a/docs/images/logging.png b/docs/images/logging.png new file mode 100644 index 0000000000..9bf2b28915 Binary files /dev/null and b/docs/images/logging.png differ diff --git a/docs/images/tracing.png b/docs/images/tracing.png new file mode 100644 index 0000000000..9ac5d8ea63 Binary files /dev/null and b/docs/images/tracing.png differ diff --git a/docs/log_collection.md b/docs/log_collection.md index c0782aea5c..c7a5f1751f 100644 --- a/docs/log_collection.md +++ b/docs/log_collection.md @@ -3,9 +3,9 @@ Send logs to Datadog from your iOS applications with [Datadog's `dd-sdk-ios` client-side logging library][1] and leverage the following features: * Log to Datadog in JSON format natively. -* Add `context` and extra custom attributes to each log sent. +* Use default and add custom attributes to each log sent. * Record real client IP addresses and User-Agents. -* Optimized network usage with automatic bulk posts. +* Leverage optimized network usage with automatic bulk posts. **Note**: The `dd-sdk-ios` library supports all iOS versions 11+. @@ -53,7 +53,8 @@ github "DataDog/dd-sdk-ios" Datadog.initialize( appContext: .init(), configuration: Datadog.Configuration - .builderUsing(clientToken: config.clientToken) + .builderUsing(clientToken: "", environment: "") + .set(serviceName: "app-name") .build() ) ``` @@ -65,7 +66,8 @@ Datadog.initialize( Datadog.initialize( appContext: .init(), configuration: Datadog.Configuration - .builderUsing(clientToken: config.clientToken) + .builderUsing(clientToken: "", environment: "") + .set(serviceName: "app-name") .set(logsEndpoint: .eu) .build() ) @@ -74,17 +76,17 @@ Datadog.initialize( {{% /tab %}} {{< /tabs >}} - When writing your application, you can enable development logs. All internal messages in the library with a priority equal to or higher than the provided level are then logged to console logs. + When writing your application, you can enable development logs. All internal messages in the SDK with a priority equal to or higher than the provided level are then logged to console logs. ```swift Datadog.verbosityLevel = .debug ``` -3. Configure the iOS Logger: +3. Configure the `Logger`: ```swift logger = Logger.builder - .set(serviceName: "ios-sdk-example-app") + .sendNetworkInfo(true) .printLogsToConsole(true, usingFormat: .shortWith(prefix: "[iOS App] ")) .build() ``` @@ -114,11 +116,11 @@ The following methods in `Logger.Builder` can be used when initializing the logg | Method | Description | |----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `sendNetworkInfo(_ enabled: true)` | Add `network.client.connectivity` attribute to all logs. The data logged by default is `reachability` (`yes`, `no`, `maybe`...), `available_interfaces` (`wifi`, `cellular`...), `sim_carrier` (`AT&T - US`), `sim_carrier.technology` (`3G`, `LTE`) and `sim_carrier.iso_country` (`US`). | -| `set(serviceName: )` | Set `` as the value for the `service` [standard attribute][4] attached to all logs sent to Datadog. | -| `printLogsToConsole(_ enabled: true, usingFormat format: ConsoleLogFormat = .short)` | Set to `true` to send logs to the debugger console. | -| `sendLogsToDatadog(_ enabled: true)` | Set to `true` to send logs to Datadog. | -| `set(loggerName: )` | Set `` as the value for the `logger.name` attribute attached to all logs sent to Datadog. | +| `sendNetworkInfo(true)` | Add `network.client.*` attributes to all logs. The data logged by default is: `reachability` (`yes`, `no`, `maybe`), `available_interfaces` (`wifi`, `cellular`, ...), `sim_carrier.name` (e.x. `AT&T - US`), `sim_carrier.technology` (`3G`, `LTE`, ...) and `sim_carrier.iso_country` (e.x. `US`). | +| `set(serviceName: "")` | Set `` as the value for the `service` [standard attribute][4] attached to all logs sent to Datadog. | +| `printLogsToConsole(true)` | Set to `true` to send logs to the debugger console. | +| `sendLogsToDatadog(true)` | Set to `true` to send logs to Datadog. | +| `set(loggerName: "")` | Set `` as the value for the `logger.name` attribute attached to all logs sent to Datadog. | | `build()` | Build a new logger instance with all options set. | ### Global configuration @@ -129,22 +131,22 @@ Find below methods to add/remove tags and attributes to all logs sent by a given ##### Add Tags -Use the `addTag(withKey: "", value: "")` method to add tags to all logs sent by a specific logger: +Use the `addTag(withKey:value:)` method to add tags to all logs sent by a specific logger: ```swift // This adds a tag "build_configuration:debug" logger.addTag(withKey: "build_configuration", value: "debug") ``` -**Note**: `` must be a String. +**Note**: `` must be a `String`. ##### Remove Tags -Use the `removeTag(withKey key: "")` method to remove tags from all logs sent by a specific logger: +Use the `removeTag(withKey:)` method to remove tags from all logs sent by a specific logger: ```swift // This removes any tag starting with "build_configuration" -logger.removeTag(withKey key: "build_configuration") +logger.removeTag(withKey: "build_configuration") ``` [Learn more about Datadog tags][5]. @@ -159,24 +161,25 @@ By default, the following attributes are added to all logs sent by a logger: * `network.client.ip` and its extracted geographical properties (`country`, `city`) * `logger.version`, Datadog SDK version * `logger.thread_name`, (`main`, `background`) -* `application.version`, client's app version extracted from `Info.plist` +* `version`, client's app version extracted from `Info.plist` +* `environment`, the environment name used to initialize the SDK -Use the `addAttribute(forKey key: "", value: "")` method to add a custom attribute to all logs sent by a specific logger: +Use the `addAttribute(forKey:value:)` method to add a custom attribute to all logs sent by a specific logger: ```swift // This adds an attribute "device-model" with a string value logger.addAttribute(forKey: "device-model", value: UIDevice.current.model) ``` -**Note**: `` can be anything conforming to `Encodable` (String, Date, custom `Codable` data model, ...). +**Note**: `` can be anything conforming to `Encodable` (`String`, `Date`, custom `Codable` data model, ...). ##### Remove attributes -Use the `removeAttribute(forKey key: "")` method to remove a custom attribute from all logs sent by a specific logger: +Use the `removeAttribute(forKey:)` method to remove a custom attribute from all logs sent by a specific logger: ```swift // This removes the attribute "device-model" from all further log send. -logger.removeAttribute("device-model") +logger.removeAttribute(forKey: "device-model") ``` diff --git a/docs/trace_collection.md b/docs/trace_collection.md new file mode 100644 index 0000000000..73e0e209db --- /dev/null +++ b/docs/trace_collection.md @@ -0,0 +1,174 @@ +# iOS Trace Collection + +
The iOS Trace collection is in public beta. If you have any questions, contact our support team.
+ +Send [traces][1] to Datadog from your iOS applications with [Datadog's `dd-sdk-ios` client-side tracing library][2] and leverage the following features: + +* Create custom [spans][3] for various operations in your app. +* Send logs for each span individually. +* Use default and add custom attributes to each span. +* Leverage optimized network usage with automatic bulk posts. + +## Setup + +1. Declare the library as a dependency depending on your package manager: + + {{< tabs >}} + {{% tab "CocoaPods" %}} + +You can use [CocoaPods][4] to install `dd-sdk-ios`: +``` +pod 'DatadogSDK' +``` + +[4]: https://cocoapods.org/ + + {{% /tab %}} + {{% tab "Swift Package Manager (SPM)" %}} + +To integrate using Apple's Swift Package Manager, add the following as a dependency to your `Package.swift`: +```swift +.package(url: "https://github.com/Datadog/dd-sdk-ios.git", .upToNextMajor(from: "1.0.0")) +``` + + {{% /tab %}} + {{% tab "Carthage" %}} + +You can use [Carthage][5] to install `dd-sdk-ios`: +``` +github "DataDog/dd-sdk-ios" +``` + +[5]: https://github.com/Carthage/Carthage + + {{% /tab %}} + {{< /tabs >}} + +2. Initialize the library with your application context and your [Datadog client token][6]. For security reasons, you must use a client token: you cannot use [Datadog API keys][7] to configure the `dd-sdk-ios` library as they would be exposed client-side in the iOS application IPA byte code. For more information about setting up a client token, see the [client token documentation][6]. + + {{< tabs >}} + {{% tab "US" %}} + +```swift +Datadog.initialize( + appContext: .init(), + configuration: Datadog.Configuration + .builderUsing(clientToken: "", environment: "") + .set(serviceName: "app-name") + .build() +) +``` + + {{% /tab %}} + {{% tab "EU" %}} + +```swift +Datadog.initialize( + appContext: .init(), + configuration: Datadog.Configuration + .builderUsing(clientToken: "", environment: "") + .set(serviceName: "app-name") + .set(tracesEndpoint: .eu) + .build() +) +``` + + {{% /tab %}} + {{< /tabs >}} + + When writing your application, you can enable development logs. All internal messages in the SDK with a priority equal to or higher than the provided level are then logged to console logs. + + ```swift + Datadog.verbosityLevel = .debug + ``` + +3. Datadog tracer implements the [Open Tracing standard][8]. Configure and register the `Tracer` globally as Open Tracing `Global.sharedTracer`. You only need to do it once, usually in your `AppDelegate` code: + + ```swift + import Datadog + + Global.sharedTracer = Tracer.initialize( + configuration: Tracer.Configuration( + sendNetworkInfo: true + ) + ) + ``` + +4. Instrument your code using the following methods: + + ```swift + let span = Global.sharedTracer.startSpan(operationName: "") + // do something you want to measure ... + // ... then, when the operation is finished: + span.finish() + ``` + +5. (Optional) - Set child-parent relationship between your spans: + + ```swift + let responseDecodingSpan = Global.sharedTracer.startSpan( + operationName: "response decoding", + childOf: networkRequestSpan.context // make it a child of `networkRequestSpan` + ) + // ... decode HTTP response data ... + responseDecodingSpan.finish() + ``` + +6. (Optional) - Provide additional tags alongside your span: + + ```swift + span.setTag(key: "http.url", value: url) + ``` + +7. (Optional) Attach an error to a span - you can do so by logging the error information using the [standard Open Tracing log fields][9]: + + ```swift + span.log( + fields: [ + OTLogFields.event: "error", + OTLogFields.errorKind: "I/O Exception", + OTLogFields.message: "File not found", + OTLogFields.stack: "FileReader.swift:42", + ] + ) + ``` + +8. (Optional) To distribute traces between your environments, for example frontend - backend, inject tracer context in the client request: + + ```swift + import Datadog + + var request: URLRequest = ... // the request to your API + + let span = Global.sharedTracer.startSpan(operationName: "network request") + + let headersWritter = HTTPHeadersWriter() + Global.sharedTracer.inject(spanContext: span.context, writer: headersWritter) + + for (headerField, value) in headersWritter.tracePropagationHTTPHeaders { + request.addValue(value, forHTTPHeaderField: headerField) + } + ``` + This will set additional tracing headers on your request, so that your backend can extract it and continue distributed tracing. If your backend is also instrumented with [Datadog APM & Distributed Tracing][10] you will see the entire front-to-back trace in Datadog dashboard. Once the request is done, within a completion handler, call `span.finish()`. + + +## Batch collection + +All the spans are first stored on the local device in batches. Each batch follows the intake specification. They are sent periodically if network is available, and the battery is high enough to ensure the Datadog SDK does not impact the end user's experience. If the network is not available while your application is in the foreground, or if an upload of data fails, the batch is kept until it can be sent successfully. + +This means that even if users open your application while being offline, no data will be lost. + +The data on disk will automatically be discarded if it gets too old to ensure the SDK doesn't use too much disk space. + +## Further Reading + +{{< partial name="whats-next/whats-next.html" >}} + +[1]: https://docs.datadoghq.com/tracing/visualization/#trace +[2]: https://github.com/DataDog/dd-sdk-ios +[3]: https://docs.datadoghq.com/tracing/visualization/#spans +[6]: https://docs.datadoghq.com/account_management/api-app-keys/#client-tokens +[7]: https://docs.datadoghq.com/account_management/api-app-keys/#api-keys +[8]: https://opentracing.io +[9]: https://github.com/opentracing/specification/blob/master/semantic_conventions.md#log-fields-table +[10]: https://docs.datadoghq.com/tracing/ diff --git a/instrumented-tests/.gitignore b/instrumented-tests/.gitignore deleted file mode 100644 index 97efd0fe41..0000000000 --- a/instrumented-tests/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# `http-server-mock.xcconfig` file is generated automatically before running instrumented tests -http-server-mock.xcconfig diff --git a/instrumented-tests/http-server-mock/Package.swift b/instrumented-tests/http-server-mock/Package.swift index 7d01efe97e..42f20d0287 100644 --- a/instrumented-tests/http-server-mock/Package.swift +++ b/instrumented-tests/http-server-mock/Package.swift @@ -5,6 +5,7 @@ import PackageDescription let package = Package( name: "HTTPServerMock", + platforms: [.macOS(.v10_12)], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( diff --git a/instrumented-tests/http-server-mock/README.md b/instrumented-tests/http-server-mock/README.md index c8ea913e93..eef4de3db9 100644 --- a/instrumented-tests/http-server-mock/README.md +++ b/instrumented-tests/http-server-mock/README.md @@ -38,3 +38,7 @@ XCTAssertEqual(recordedRequests[0].httpBody, "hello world".data(using: .utf8)!) ``` By obtaining separate `ServerSession` with `server.obtainUniqueRecordingSession()` for each test, there is no need to restart the server each time to reset its state. + +## License + +[Apache License, v2.0](../../LICENSE) diff --git a/instrumented-tests/http-server-mock/Sources/HTTPServerMock/ServerMock.swift b/instrumented-tests/http-server-mock/Sources/HTTPServerMock/ServerMock.swift index f05fa1257c..4da14e7c3c 100644 --- a/instrumented-tests/http-server-mock/Sources/HTTPServerMock/ServerMock.swift +++ b/instrumented-tests/http-server-mock/Sources/HTTPServerMock/ServerMock.swift @@ -28,13 +28,16 @@ public class ServerMock { let path: String /// HTTP method of this request. let httpMethod: String + /// Follow-up path to fetch HTTP headers associated with this request. + let httpHeadersInspectionPath: String /// Follow-up path to fetch HTTP body associated with this request. let httpBodyInspectionPath: String enum CodingKeys: String, CodingKey { case path = "request_path" case httpMethod = "request_method" - case httpBodyInspectionPath = "inspection_path" + case httpHeadersInspectionPath = "headers_inspection_path" + case httpBodyInspectionPath = "body_inspection_path" } } @@ -52,4 +55,11 @@ public class ServerMock { let bodyURL = baseURL.appendingPathComponent(requestInfo.httpBodyInspectionPath) return try Data(contentsOf: bodyURL) } + + /// Fetches HTTP headers of particular request recorded by the server. + internal func getRecordedRequestHeaders(_ requestInfo: RequestInfo) throws -> [String] { + let headersURL = baseURL.appendingPathComponent(requestInfo.httpHeadersInspectionPath) + let headersString = try String(data: Data(contentsOf: headersURL), encoding: .utf8) + return headersString?.split(separator: "\r\n").map { String($0) } ?? [] + } } diff --git a/instrumented-tests/http-server-mock/Sources/HTTPServerMock/ServerSession.swift b/instrumented-tests/http-server-mock/Sources/HTTPServerMock/ServerSession.swift index 53a6bb895c..2bd5d5c306 100644 --- a/instrumented-tests/http-server-mock/Sources/HTTPServerMock/ServerSession.swift +++ b/instrumented-tests/http-server-mock/Sources/HTTPServerMock/ServerSession.swift @@ -12,15 +12,21 @@ public class ServerSession { public struct POSTRequestDetails { /// Original path of the request, i.e. `/something/1` for `POST /something/1`. public let path: String + /// Original http headers of this request. + public let httpHeaders: [String] /// Original body of this request. public let httpBody: Data } + internal struct Exception: Error, CustomStringConvertible { + let description: String + } + private let server: ServerMock private let sessionIdentifier: String /// Unique session URL. `POST` requests send using this base URL can be later retrieved - /// using `getRecordedPOSTRequests()`. + /// using `getRecordedPOSTRequests() or pullRecordedPOSTRequests()`. public let recordingURL: URL internal init(server: ServerMock) { @@ -37,8 +43,42 @@ public class ServerSession { .map { requestInfo in return POSTRequestDetails( path: requestInfo.path, + httpHeaders: try server.getRecordedRequestHeaders(requestInfo), httpBody: try server.getRecordedRequestBody(requestInfo) ) } } + + /// Actively fetches 'POST` requests recorded by the server until a desired count is found, or timeouts returning current recorded requests + public func pullRecordedPOSTRequests(count: Int, timeout: TimeInterval) throws -> [POSTRequestDetails] { + var currentRequests = [ServerMock.RequestInfo]() + + let timeoutTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.global()) + timeoutTimer.setEventHandler { timeoutTimer.cancel() } + timeoutTimer.schedule(deadline: .now() + timeout, leeway: .nanoseconds(0)) + if #available(iOS 10.0, *) { + timeoutTimer.activate() + } + + repeat { + currentRequests = try server + .getRecordedPOSTRequestsInfo() + .filter { requestInfo in requestInfo.path.contains(sessionIdentifier) } + Thread.sleep(forTimeInterval: 0.2) + } while !timeoutTimer.isCancelled && currentRequests.count < count + + if timeoutTimer.isCancelled { + throw Exception(description: "Exceeded \(timeout)s timeout by receiving only \(currentRequests.count) requests, where \(count) were expected.") + } else { + timeoutTimer.cancel() + } + + return try currentRequests.map { requestInfo in + return POSTRequestDetails( + path: requestInfo.path, + httpHeaders: try server.getRecordedRequestHeaders(requestInfo), + httpBody: try server.getRecordedRequestBody(requestInfo) + ) + } + } } diff --git a/instrumented-tests/http-server-mock/Tests/HTTPServerMockTests/HTTPServerMockTests.swift b/instrumented-tests/http-server-mock/Tests/HTTPServerMockTests/HTTPServerMockTests.swift index 69e34b59f8..0641701a3b 100644 --- a/instrumented-tests/http-server-mock/Tests/HTTPServerMockTests/HTTPServerMockTests.swift +++ b/instrumented-tests/http-server-mock/Tests/HTTPServerMockTests/HTTPServerMockTests.swift @@ -5,7 +5,7 @@ */ import XCTest -import HTTPServerMock +@testable import HTTPServerMock final class HTTPServerMockTests: XCTestCase { #if os(macOS) @@ -52,6 +52,70 @@ final class HTTPServerMockTests: XCTestCase { XCTAssertTrue(recordedRequests[1].path.hasSuffix("/resource/2")) XCTAssertEqual(recordedRequests[1].httpBody, "2nd request body".data(using: .utf8)!) } + + func testItReturnspullRecordedPOSTRequests() throws { + let runner = ServerProcessRunner(serverURL: URL(string: "http://127.0.0.1:8000")!) + guard let serverProces = runner.waitUntilServerIsReachable() else { + XCTFail("Failed to connect with the server.") + return + } + + let server = ServerMock(serverProcess: serverProces) + let session = server.obtainUniqueRecordingSession() + + let initialTime = Date() + DispatchQueue.global(qos: .userInitiated).async { + Thread.sleep(forTimeInterval: 0.5) + sendPOSTRequestAsynchronouslyTo( + url: session.recordingURL.appendingPathComponent("/resource/1"), + body: "1st request body".data(using: .utf8)! + ) + Thread.sleep(forTimeInterval: 0.5) + sendPOSTRequestAsynchronouslyTo( + url: session.recordingURL.appendingPathComponent("/resource/2"), + body: "2nd request body".data(using: .utf8)! + ) + } + let timeoutTime: TimeInterval = 2 + let recordedRequests = try session.pullRecordedPOSTRequests(count: 2, timeout: timeoutTime) + XCTAssertLessThan(Date(), initialTime.addingTimeInterval(timeoutTime)) + XCTAssertEqual(recordedRequests.count, 2) + XCTAssertTrue(recordedRequests[0].path.hasSuffix("/resource/1")) + XCTAssertEqual(recordedRequests[0].httpBody, "1st request body".data(using: .utf8)!) + XCTAssertTrue(recordedRequests[1].path.hasSuffix("/resource/2")) + XCTAssertEqual(recordedRequests[1].httpBody, "2nd request body".data(using: .utf8)!) + } + + func testWhenPullingRecordedPOSTRequestExceedsTimeout_itThrownsAnError() throws { + let runner = ServerProcessRunner(serverURL: URL(string: "http://127.0.0.1:8000")!) + guard let serverProces = runner.waitUntilServerIsReachable() else { + XCTFail("Failed to connect with the server.") + return + } + + let server = ServerMock(serverProcess: serverProces) + let session = server.obtainUniqueRecordingSession() + + DispatchQueue.global(qos: .userInitiated).async { + Thread.sleep(forTimeInterval: 2) + sendPOSTRequestAsynchronouslyTo( + url: session.recordingURL.appendingPathComponent("/resource/1"), + body: "1st request body".data(using: .utf8)! + ) + Thread.sleep(forTimeInterval: 2) + sendPOSTRequestAsynchronouslyTo( + url: session.recordingURL.appendingPathComponent("/resource/2"), + body: "2nd request body".data(using: .utf8)! + ) + } + + let timeoutTime: TimeInterval = 2 + var thrownError: Error? + XCTAssertThrowsError(try session.pullRecordedPOSTRequests(count: 2, timeout: timeoutTime)) { + thrownError = $0 + } + XCTAssertTrue(thrownError is ServerSession.Exception, "Unexpected error type: \(type(of: thrownError))") + } } // MARK: - Helpers @@ -90,3 +154,14 @@ private func sendPOSTRequestSynchronouslyTo(url: URL, body: Data) { task.resume() } + +private func sendPOSTRequestAsynchronouslyTo(url: URL, body: Data) { + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = body + + let task = URLSession.shared.dataTask(with: request) { _, _, error in + XCTAssertNil(error) + } + task.resume() +} diff --git a/instrumented-tests/http-server-mock/python/start_mock_server.py b/instrumented-tests/http-server-mock/python/start_mock_server.py index 62fb534281..43fb041ee2 100755 --- a/instrumented-tests/http-server-mock/python/start_mock_server.py +++ b/instrumented-tests/http-server-mock/python/start_mock_server.py @@ -27,10 +27,13 @@ class HTTPMockServer(BaseHTTPRequestHandler): GET /inspect - Endpoint listing history of recorded generic requests. It provides information - for each request to access its HTTP body with `GET /inspect/`. + for each request to access its details, e.g. HTTP body with `GET /inspect//body`. - GET /inspect/ - - Endpoint returning HTTP body of specific generic request. + GET /inspect//body + - Endpoint returning HTTP body of specific generic request. + + GET /inspect//headers + - Endpoint returning HTTP headers of specific generic request. """ def do_POST(self): @@ -47,7 +50,8 @@ def do_GET(self): """ self.__route([ (r"/inspect$", self.__GET_inspect), - (r"/inspect/([0-9]+)$", self.__GET_inspect_request), + (r"/inspect/([0-9]+)/body$", self.__GET_inspect_request_body), + (r"/inspect/([0-9]+)/headers$", self.__GET_inspect_request_headers) ]) def __POST_any(self, parameters): @@ -59,7 +63,7 @@ def __POST_any(self, parameters): global history request_path = parameters[0] request_body = self.rfile.read(int(self.headers['Content-Length'])) - request = GenericRequest("POST", request_path, request_body) + request = GenericRequest("POST", request_path, self.headers, request_body) history.add_request(request) return "{}" @@ -75,13 +79,14 @@ def __GET_inspect(self, parameters): inspection_info.append({ "request_method": request.http_method, "request_path": request.path, - "inspection_path": "/inspect/{request_id}".format( request_id = request.id ) + "body_inspection_path": "/inspect/{request_id}/body".format( request_id = request.id ), + "headers_inspection_path": "/inspect/{request_id}/headers".format( request_id = request.id ) }) return json.dumps(inspection_info) - def __GET_inspect_request(self, parameters): + def __GET_inspect_request_body(self, parameters): """ - GET /inspect/ + GET /inspect//body Returns http body of a generic requests with given id. """ @@ -89,6 +94,17 @@ def __GET_inspect_request(self, parameters): request_id = parameters[0] return history.request(request_id).http_body + + def __GET_inspect_request_headers(self, parameters): + """ + GET /inspect//headers + + Returns http headers of a generic requests with given id. + """ + global history + request_id = parameters[0] + return history.request(request_id).http_headers + def __route(self, routes): try: for url_regexp, method in routes: @@ -113,10 +129,11 @@ class GenericRequest: Represents data of request sent to generic endponit. """ - def __init__(self, http_method, path, http_body): + def __init__(self, http_method, path, http_headers, http_body): self.id = None # set later by `GenericRequestsHistory` self.path = path self.http_method = http_method + self.http_headers = http_headers self.http_body = http_body class GenericRequestsHistory: diff --git a/tools/all-platform-tests/bitrise-test-all-platforms.yml b/tools/all-platform-tests/bitrise-test-all-platforms.yml new file mode 100644 index 0000000000..2f75ecad55 --- /dev/null +++ b/tools/all-platform-tests/bitrise-test-all-platforms.yml @@ -0,0 +1,149 @@ +--- +format_version: '8' +default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git +project_type: other + +# Stack specification: +# https://github.com/bitrise-io/bitrise.io/blob/master/system_reports/osx-xcode-11.5.x.log +# +# This stack is configured for the `trigger_all_platform_tests` workflow +# defined in Workflow Editor on Bitrise.io +# +# To properly use this stack, environment variables need to be picked up carefully: +# - SIMULATOR_DEVICE - pick one listed in `== Device Types ==` +# - SIMULATOR_OS_VERSION - pick one listed in `== Runtimes ==` + +app: + envs: + - PROJECT_PATH: Datadog.xcworkspace + - PROJECT_SCHEME: Datadog + +workflows: + run_all: + after_run: + - _make_dependencies + - _run_unit_tests_iOS12.0 + - _run_unit_tests_iOS12.1 + - _run_unit_tests_iOS12.2 + - _run_unit_tests_iOS12.4 + - _run_unit_tests_iOS13.0 + - _run_unit_tests_iOS13.1 + - _run_unit_tests_iOS13.2 + - _run_unit_tests_iOS13.3 + - _run_unit_tests_iOS13.4 + - _run_unit_tests_iOS13.5 + - _deploy_artifacts + + # Platform-specific workflows: + + _run_unit_tests_iOS12.0: + envs: + - SIMULATOR_DEVICE: iPhone X + - SIMULATOR_OS_VERSION: '12.0' + after_run: + - _run_unit_tests + + _run_unit_tests_iOS12.1: + envs: + - SIMULATOR_DEVICE: iPhone X + - SIMULATOR_OS_VERSION: '12.1' + after_run: + - _run_unit_tests + + _run_unit_tests_iOS12.2: + envs: + - SIMULATOR_DEVICE: iPhone X + - SIMULATOR_OS_VERSION: '12.2' + after_run: + - _run_unit_tests + + _run_unit_tests_iOS12.4: + envs: + - SIMULATOR_DEVICE: iPhone X + - SIMULATOR_OS_VERSION: '12.4' + after_run: + - _run_unit_tests + + _run_unit_tests_iOS13.0: + envs: + - SIMULATOR_DEVICE: iPhone 11 + - SIMULATOR_OS_VERSION: '13.0' + after_run: + - _run_unit_tests + + _run_unit_tests_iOS13.1: + envs: + - SIMULATOR_DEVICE: iPhone 11 + - SIMULATOR_OS_VERSION: '13.1' + after_run: + - _run_unit_tests + + _run_unit_tests_iOS13.2: + envs: + - SIMULATOR_DEVICE: iPhone 11 + - SIMULATOR_OS_VERSION: '13.2' + after_run: + - _run_unit_tests + + _run_unit_tests_iOS13.3: + envs: + - SIMULATOR_DEVICE: iPhone 11 + - SIMULATOR_OS_VERSION: '13.3' + after_run: + - _run_unit_tests + + _run_unit_tests_iOS13.4: + envs: + - SIMULATOR_DEVICE: iPhone 11 + - SIMULATOR_OS_VERSION: '13.4' + after_run: + - _run_unit_tests + + _run_unit_tests_iOS13.5: + envs: + - SIMULATOR_DEVICE: iPhone 11 + - SIMULATOR_OS_VERSION: '13.5' + after_run: + - _run_unit_tests + + # Platform-agnostic workflows: + + _make_dependencies: + description: |- + Does `make dependencies` to prepare source code in repo for building and testing. + steps: + - script@1.1.6: + title: Do `make dependencies`. + inputs: + - content: |- + #!/usr/bin/env bash + set -e + make dependencies + + _deploy_artifacts: + description: |- + Uploads artifacts to associate them with build log on Bitrise.io. + steps: + - deploy-to-bitrise-io: {} + + _run_unit_tests: + steps: + - script@1.1.6: + inputs: + - content: |- + #!/usr/bin/env bash + echo "+------------------------------------------------------------------------------+" + printf '| %-78s |\n' "๐Ÿงช Runing unit tests for ${PROJECT_SCHEME} on ${SIMULATOR_DEVICE} (${SIMULATOR_OS_VERSION})" + echo "+------------------------------------------------------------------------------+" + - xcode-test@2.4.5: + title: Run unit tests for given platform + is_always_run: true # continue next tests if some failed + inputs: + - project_path: $PROJECT_PATH + - scheme: $PROJECT_SCHEME + - simulator_device: $SIMULATOR_DEVICE + - simulator_os_version: $SIMULATOR_OS_VERSION + - is_clean_build: 'no' + - should_retry_test_on_fail: 'yes' # retry once to mitigate flakiness + - generate_code_coverage_files: 'yes' + - xcpretty_test_options: --color --report html --output "${BITRISE_DEPLOY_DIR}/Unit-tests-${PROJECT_SCHEME}-${SIMULATOR_DEVICE} (${SIMULATOR_OS_VERSION}).html" diff --git a/tools/api-surface/.gitignore b/tools/api-surface/.gitignore new file mode 100644 index 0000000000..95c4320919 --- /dev/null +++ b/tools/api-surface/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ diff --git a/tools/api-surface/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/tools/api-surface/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/tools/api-surface/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/tools/api-surface/Fixtures/.gitignore b/tools/api-surface/Fixtures/.gitignore new file mode 100644 index 0000000000..95c4320919 --- /dev/null +++ b/tools/api-surface/Fixtures/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ diff --git a/tools/api-surface/Fixtures/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/tools/api-surface/Fixtures/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/tools/api-surface/Fixtures/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/tools/api-surface/Fixtures/Package.swift b/tools/api-surface/Fixtures/Package.swift new file mode 100644 index 0000000000..112a153285 --- /dev/null +++ b/tools/api-surface/Fixtures/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version:5.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Fixtures", + products: [ + .library(name: "Fixtures", targets: ["Fixtures"]) + ], + dependencies: [], + targets: [ + .target(name: "Fixtures", dependencies: []) + ] +) diff --git a/tools/api-surface/Fixtures/Sources/Fixtures/Fixture1.swift b/tools/api-surface/Fixtures/Sources/Fixtures/Fixture1.swift new file mode 100644 index 0000000000..34a96d87de --- /dev/null +++ b/tools/api-surface/Fixtures/Sources/Fixtures/Fixture1.swift @@ -0,0 +1,35 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +/// This is a fixture file used by `api-surface` tests. + +import Foundation + +public class Car { + public enum Manufacturer: String { + case manufacturer1 + case manufacturer2 + case manufacturer3 + } + + private let engine = Engine() + + public init( + manufacturer: Manufacturer + ) {} + + public func startEngine() -> Bool { engine.start() } + public func stopEngine() -> Bool { engine.stop() } +} + +internal struct Engine { + func start() -> Bool { true } + func stop() -> Bool { true } +} + +public extension Car { + var price: Int { 100 } +} diff --git a/tools/api-surface/Package.resolved b/tools/api-surface/Package.resolved new file mode 100644 index 0000000000..f8e43fb415 --- /dev/null +++ b/tools/api-surface/Package.resolved @@ -0,0 +1,70 @@ +{ + "object": { + "pins": [ + { + "package": "Commandant", + "repositoryURL": "https://github.com/Carthage/Commandant.git", + "state": { + "branch": null, + "revision": "ab68611013dec67413628ac87c1f29e8427bc8e4", + "version": "0.17.0" + } + }, + { + "package": "Nimble", + "repositoryURL": "https://github.com/Quick/Nimble.git", + "state": { + "branch": null, + "revision": "2b1809051b4a65c1d7f5233331daa24572cd7fca", + "version": "8.1.1" + } + }, + { + "package": "Quick", + "repositoryURL": "https://github.com/Quick/Quick.git", + "state": { + "branch": null, + "revision": "09b3becb37cb2163919a3842a4c5fa6ec7130792", + "version": "2.2.1" + } + }, + { + "package": "SourceKitten", + "repositoryURL": "https://github.com/jpsim/SourceKitten.git", + "state": { + "branch": null, + "revision": "77a4dbbb477a8110eb8765e3c44c70fb4929098f", + "version": "0.29.0" + } + }, + { + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser", + "state": { + "branch": null, + "revision": "eb51f949cdd0c9d88abba9ce79d37eb7ea1231d0", + "version": "0.2.0" + } + }, + { + "package": "SWXMLHash", + "repositoryURL": "https://github.com/drmohundro/SWXMLHash.git", + "state": { + "branch": null, + "revision": "a4931e5c3bafbedeb1601d3bb76bbe835c6d475a", + "version": "5.0.1" + } + }, + { + "package": "Yams", + "repositoryURL": "https://github.com/jpsim/Yams.git", + "state": { + "branch": null, + "revision": "c947a306d2e80ecb2c0859047b35c73b8e1ca27f", + "version": "2.0.0" + } + } + ] + }, + "version": 1 +} diff --git a/tools/api-surface/Package.swift b/tools/api-surface/Package.swift new file mode 100644 index 0000000000..85575ae3f9 --- /dev/null +++ b/tools/api-surface/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version:5.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "api-surface", + platforms: [.macOS(.v10_15)], + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser", from: "0.2.0"), + .package(url: "https://github.com/jpsim/SourceKitten.git", .exact("0.29.0")), + ], + targets: [ + .target( + name: "api-surface", + dependencies: [ + "APISurfaceCore", + .product(name: "ArgumentParser", package: "swift-argument-parser") + ] + ), + .target( + name: "APISurfaceCore", + dependencies: [ + .product(name: "SourceKittenFramework", package: "SourceKitten") + ] + ), + .testTarget( + name: "api-surfaceTests", + dependencies: ["api-surface"] + ) + ] +) diff --git a/tools/api-surface/README.md b/tools/api-surface/README.md new file mode 100644 index 0000000000..4512b3d689 --- /dev/null +++ b/tools/api-surface/README.md @@ -0,0 +1,65 @@ +# api-surface + +> A command-line utility for listing public API interface for Swift modules. + +This package provides a command-line tool for listing `public` interface of a Swift module. + +## Usage + +``` +$ api-surface spm --module-name Foo --path ./Foo +``` +or +``` +$ api-surface workspace --workspace-name Foo.xcworkspace --scheme Foo --path . +``` +Check `api-surface help` for full overview. + +## What is API surface? + +API surface is a list of all public APIs exposed from a module. Given following Swift file: +```swift +import Foundation + +public class Car { + public enum Manufacturer: String { + case manufacturer1 + case manufacturer2 + case manufacturer3 + } + + private let engine = Engine() + + public init( + manufacturer: Manufacturer + ) {} + + public func startEngine() -> Bool { engine.start() } + public func stopEngine() -> Bool { engine.stop() } +} + +internal struct Engine { + func start() -> Bool { true } + func stop() -> Bool { true } +} + +public extension Car { + var price: Int { 100 } +} +``` +It's API surface is: +``` +public class Car + public enum Manufacturer: String + case manufacturer1 + case manufacturer2 + case manufacturer3 + public init(manufacturer: Manufacturer) + public func startEngine() -> Bool + public func stopEngine() -> Bool +public extension Car + var price: Int +``` +## License + +[Apache License, v2.0](../../LICENSE) diff --git a/tools/api-surface/Sources/APISurfaceCore/APISurface.swift b/tools/api-surface/Sources/APISurfaceCore/APISurface.swift new file mode 100644 index 0000000000..f05e2eb89d --- /dev/null +++ b/tools/api-surface/Sources/APISurfaceCore/APISurface.swift @@ -0,0 +1,52 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-2020 Datadog, Inc. +*/ + +import SourceKittenFramework + +public struct APISurfaceError: Error, CustomStringConvertible { + public let description: String +} + +public class APISurface { + private let moduleInterface: ModuleInterface + private let printer = ModuleInterfacePrinter() + + // MARK: - Initialization + + public convenience init(forWorkspaceNamed workspaceName: String, scheme: String, inPath path: String) throws { + try self.init( + module: Module( + xcodeBuildArguments: [ + "-workspace", workspaceName, + "-scheme", scheme + ], + inPath: path + ) + ) + } + + public convenience init(forSPMModuleNamed spmModuleName: String, inPath path: String) throws { + try self.init( + module: Module( + spmName: spmModuleName, + inPath: path + ) + ) + } + + private init(module: Module?) throws { + guard let module = module else { + throw APISurfaceError(description: "Failed to generate module interface.") + } + self.moduleInterface = try ModuleInterface(module: module) + } + + // MARK: - Output + + public func print() -> String { + printer.print(moduleInterface: moduleInterface) + } +} diff --git a/tools/api-surface/Sources/APISurfaceCore/ModuleInterface.swift b/tools/api-surface/Sources/APISurfaceCore/ModuleInterface.swift new file mode 100644 index 0000000000..eec3990d5b --- /dev/null +++ b/tools/api-surface/Sources/APISurfaceCore/ModuleInterface.swift @@ -0,0 +1,41 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-2020 Datadog, Inc. + */ + +import Foundation +import SourceKittenFramework + +/// A single interface item, e.g.: class or method declaration. +internal struct InterfaceItem { + /// Code declaration, + /// e.g. `class Car` or `case manufacturer1` for enum. + let declaration: String + + /// The level of nesting this item, + /// e.g. `2` for `struct City` nested in `struct Address`. + let nestingLevel: Int +} + +/// An interface of the entire module. +internal struct ModuleInterface { + /// List of file interfaces in this module. + let fileInterfaces: [FileInterface] + + init(module: Module) throws { + self.fileInterfaces = try module.docs.map { fileDocs in + try FileInterface(docs: fileDocs) + } + } +} + +/// An interface of a single source file. +internal struct FileInterface { + /// List of public interface items in this file. + let publicInterface: [InterfaceItem] + + fileprivate init(docs: SwiftDocs) throws { + self.publicInterface = try getPublicInterfaceItems(from: docs) + } +} diff --git a/tools/api-surface/Sources/APISurfaceCore/Parsing.swift b/tools/api-surface/Sources/APISurfaceCore/Parsing.swift new file mode 100644 index 0000000000..d61a28e604 --- /dev/null +++ b/tools/api-surface/Sources/APISurfaceCore/Parsing.swift @@ -0,0 +1,87 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-2020 Datadog, Inc. +*/ + +import Foundation +import SourceKittenFramework + +/// Finds public `InterfaceItems` in given `SwiftDocs`. +internal func getPublicInterfaceItems(from docs: SwiftDocs) throws -> [InterfaceItem] { + let docsJSONObject = toNSDictionary(docs.docsDictionary) + let docsJSONData = try JSONSerialization.data( + withJSONObject: docsJSONObject, + options: [.prettyPrinted, .sortedKeys] + ) + + let skCode = try decoder.decode(SKCode.self, from: docsJSONData) + + var items: [InterfaceItem] = [] + recursivelySearchForPublicInterfaceItems(in: skCode.substructures, result: &items) + return items +} + +// MARK: - Parsing SourceKitten-generated documentation + +/// Decodable `SourceKitten's` representation of a file documentation. +private class SKCode: Decodable { + enum CodingKeys: String, CodingKey { + case substructures = "key.substructure" + } + + let substructures: [SKSubstructure] +} + +/// Decodable `SourceKitten's` representation of a code construct documentation. +/// Code construct may nest other code constructs. +private class SKSubstructure: Decodable { + enum CodingKeys: String, CodingKey { + case accessibility = "key.accessibility" + case declaration = "key.parsed_declaration" + case substructures = "key.substructure" + } + + /// e.g. `source.lang.swift.accessibility.public` + let accessibility: String? + /// e.g. `public class Car` + let declaration: String? + let substructures: [SKSubstructure]? +} + +private let decoder = JSONDecoder() + +/// Inspects `SKCode` and fills the `result` array with public `InterfaceItems`. +private func recursivelySearchForPublicInterfaceItems( + in skSubstructures: [SKSubstructure], + result: inout [InterfaceItem], + recursionLevel: Int = 0 +) { + skSubstructures + .compactMap { $0 } + .forEach { structure in + if structure.accessibility == "source.lang.swift.accessibility.public" { + if let declaration = structure.declaration { + let item = InterfaceItem( + declaration: declaration, + nestingLevel: recursionLevel + ) + result.append(item) + } + } + + /// Some `substructures` parsed by `SourceKitten` are simple containers without any declaration. + let hasDeclaration = structure.declaration != nil + + if let substructures = structure.substructures { + // Structures can nest other structures, e.g. `enum` may nest + // its `case` substructures. We do head recursion to + // list them in the correct order. + recursivelySearchForPublicInterfaceItems( + in: substructures, + result: &result, + recursionLevel: recursionLevel + (hasDeclaration ? 1 : 0) + ) + } + } +} diff --git a/tools/api-surface/Sources/APISurfaceCore/Printing.swift b/tools/api-surface/Sources/APISurfaceCore/Printing.swift new file mode 100644 index 0000000000..1cef59c6d7 --- /dev/null +++ b/tools/api-surface/Sources/APISurfaceCore/Printing.swift @@ -0,0 +1,38 @@ +/* +* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. +* This product includes software developed at Datadog (https://www.datadoghq.com/). +* Copyright 2019-2020 Datadog, Inc. +*/ + +import Foundation +import SourceKittenFramework + +internal struct ModuleInterfacePrinter { + func print(moduleInterface: ModuleInterface) -> String { + return moduleInterface + .fileInterfaces + .filter { fileInterface in !fileInterface.publicInterface.isEmpty } + .map { fileInterface in self.print(fileInterface: fileInterface) } + .joined(separator: "\n") + } + + private func print(fileInterface: FileInterface) -> String { + return fileInterface + .publicInterface + .map { self.print(interfaceItem: $0) } + .joined(separator: "\n") + } + + private func print(interfaceItem: InterfaceItem) -> String { + let inlinedDeclaration = interfaceItem.declaration + .split(separator: "\n") + .map { String($0).removingCommonLeadingWhitespaceFromLines() } + .joined() + + let indentation = (0.. Bool + public func stopEngine() -> Bool + public extension Car + var price: Int + """ + +final class api_surfaceTests: XCTestCase { + func testApiSurfaceCommandLineInterface() throws { + // Run `swift build` for `Fixtures` package + buildFixturesPackage() + + // Run `api-surface spm --module-name Fixtures --path ./Fixtures` + let output = try executeBinary( + withArguments: ["spm", "--module-name", "Fixtures", "--path", resolveFixturesPackageFolder().path] + ) + + XCTAssertEqual(output, expectedFixturesAPISurface + "\n") + } + + private func executeBinary(withArguments arguments: [String]) throws -> String { + let process = Process() + process.executableURL = productsDirectory.appendingPathComponent("api-surface") + process.arguments = arguments + + let pipe = Pipe() + process.standardOutput = pipe + + try process.run() + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + return String(data: data, encoding: .utf8) ?? "" + } +} + +final class APISurfaceTests: XCTestCase { + func testGeneratingAPISurfaceForFixturesPackage() throws { + // Run `swift build` for `Fixtures` package + buildFixturesPackage() + + let surface = try APISurface( + forSPMModuleNamed: "Fixtures", + inPath: resolveFixturesPackageFolder().path + ) + + XCTAssertEqual(surface.print(), expectedFixturesAPISurface) + } + + /// NOTE: Use this test to debug (CMD+U) API surface for Datadog.xcworkspace +// func testGeneratingAPISurfaceForDatadogWorkspace() throws { +// let surface = try APISurface( +// forWorkspaceNamed: "Datadog.xcworkspace", +// scheme: "Datadog", +// inPath: resolveSwiftPackageFolder().appendingPathComponent("../..").path +// ) +// +// print(surface.print()) +// } +} + +// MARK: - Helpers + +/// Returns path to the built products directory. +private let productsDirectory: URL = { + for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { + return bundle.bundleURL.deletingLastPathComponent() + } + fatalError("couldn't find the products directory") +}() + +/// Runs `swift build` for `Fixtures` package. +/// This generates necessary `.build/debug.yaml` file required by SourceKitten to parse docs for SPM module. +private func buildFixturesPackage() { + let process = Process() + process.currentDirectoryURL = resolveFixturesPackageFolder() + process.launchPath = "/bin/bash" + process.arguments = ["-c", "swift build"] + process.launch() + process.waitUntilExit() +} + +private func resolveFixturesPackageFolder() -> URL { + resolveSwiftPackageFolder().appendingPathComponent("Fixtures") +} + +/// Resolves the url to the folder containing `Package.swift` +private func resolveSwiftPackageFolder() -> URL { + var currentFolder = URL(fileURLWithPath: #file).deletingLastPathComponent() + + while currentFolder.pathComponents.count > 0 { + if FileManager.default.fileExists(atPath: currentFolder.appendingPathComponent("Package.swift").path) { + return currentFolder + } else { + currentFolder.deleteLastPathComponent() + } + } + + fatalError("Cannot resolve the URL to folder containing `Package.swif`.") +} diff --git a/tools/config/generate-http-server-mock-config.sh b/tools/config/generate-http-server-mock-config.sh index d2983618e6..201ee49358 100755 --- a/tools/config/generate-http-server-mock-config.sh +++ b/tools/config/generate-http-server-mock-config.sh @@ -5,6 +5,6 @@ if [ ! -f "Package.swift" ]; then fi SERVER_ADDRESS=$(./instrumented-tests/http-server-mock/python/server_address.py) -XCCONFIG_FILE="./Datadog/TargetSupport/DatadogIntegrationTests/MockServerAddress.local.xcconfig" +XCCONFIG_FILE="./xcconfigs/MockServerAddress.local.xcconfig" echo "MOCK_SERVER_ADDRESS=${SERVER_ADDRESS}" > "${XCCONFIG_FILE}" diff --git a/tools/license/check-license.sh b/tools/license/check-license.sh index d0224afbcc..9e6b54486a 100755 --- a/tools/license/check-license.sh +++ b/tools/license/check-license.sh @@ -4,21 +4,39 @@ if [ ! -f "Package.swift" ]; then echo "\`check-license.sh\` must be run in repository root folder: \`./tools/license/check-license.sh\`"; exit 1 fi +IFS=$'\n' + +# Lists all files requiring the license header. function files { + # Exclude all auto-generated and 3rd party files. find -E . \ - -iregex '.*\.(swift|h|m)$' \ + -iregex '.*\.(swift|h|m|py)$' \ -type f \( ! -name "Package.swift" \) \ -not -path "*/.build/*" \ -not -path "*Pods*" \ - -print0 + -not -path "*Carthage/Build/*" \ + -not -path "*Carthage/Checkouts/*" \ + -not -name "OTGlobal.swift" \ + -not -name "OTSpan.swift" \ + -not -name "OTFormat.swift" \ + -not -name "OTTracer.swift" \ + -not -name "OTReference.swift" \ + -not -name "OTSpanContext.swift" \ + -not -name "Versioning.swift" } -files | while IFS= read -r -d '' file; do - if ! grep -q "Apache License Version 2.0" "$file" - then - echo "No license in $file" - exit 1 - fi +FILES_WITH_MISSING_LICENSE="" + +for file in $(files); do + if ! grep -q "Apache License Version 2.0" "$file"; then + FILES_WITH_MISSING_LICENSE="${FILES_WITH_MISSING_LICENSE}\n${file}" + fi done -exit 0 +if [ -z "$FILES_WITH_MISSING_LICENSE" ]; then + echo "โœ… All files include the license header" + exit 0 +else + echo -e "๐Ÿ”ฅ Missing the license header in files: $FILES_WITH_MISSING_LICENSE" + exit 1 +fi diff --git a/tools/lint/sources.swiftlint.yml b/tools/lint/sources.swiftlint.yml index 9650fda84f..f5231de7ef 100644 --- a/tools/lint/sources.swiftlint.yml +++ b/tools/lint/sources.swiftlint.yml @@ -30,7 +30,6 @@ whitelist_rules: # we enable lint rules explicitly - only the ones listed below - force_try - force_unwrapping - function_default_parameter_at_end - - identifier_name - implicitly_unwrapped_optional - last_where - leading_whitespace @@ -84,6 +83,8 @@ custom_rules: included: - Sources - instrumented-tests/http-server-mock/Sources + - tools/api-surface/Sources + - tools/api-surface/Fixtures/Sources - dependency-manager-tests/carthage/CTProject - dependency-manager-tests/cocoapods/CPProject - dependency-manager-tests/spm/SPMProject diff --git a/tools/lint/tests.swiftlint.yml b/tools/lint/tests.swiftlint.yml index f7a3cf2a7f..910934a15b 100644 --- a/tools/lint/tests.swiftlint.yml +++ b/tools/lint/tests.swiftlint.yml @@ -25,7 +25,6 @@ whitelist_rules: # we enable lint rules explicitly - only the ones listed below - first_where - for_where - function_default_parameter_at_end - - identifier_name - implicitly_unwrapped_optional - last_where - leading_whitespace @@ -77,7 +76,8 @@ custom_rules: included: - Tests - - instrumented-tests/http-server-mock/Test + - instrumented-tests/http-server-mock/Tests + - tools/api-surface/Tests - dependency-manager-tests/carthage/CTProjectTests - dependency-manager-tests/carthage/CTProjectUITests - dependency-manager-tests/cocoapods/CTProjectTests diff --git a/xcconfigs/Datadog.xcconfig b/xcconfigs/Datadog.xcconfig new file mode 100644 index 0000000000..29ffb2f7cc --- /dev/null +++ b/xcconfigs/Datadog.xcconfig @@ -0,0 +1,5 @@ +DATADOG_CLIENT_TOKEN=// use your own Client Token obtained on datadoghq.com +DEVELOPMENT_TEAM[sdk=iphoneos*]=// use your own Development Team + +// Overwrite with secrets +#include? "Datadog.local.xcconfig"