diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 00000000..d0749a77 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,43 @@ +name: "Integration Tests" +on: [push, pull_request] +jobs: + integration_test: + name: Integration Tests + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + swift: ["5.9", "5.10", "latest"] + + steps: + - name: Install Swift + uses: vapor/swiftly-action@v0.1 + with: + toolchain: ${{ matrix.swift }} + env: + SWIFTLY_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout + uses: actions/checkout@v4.2.0 + + - name: Resolve Swift dependencies + run: | + cd IntegrationTests + swift package resolve + + - name: Start OTel Collector + run: | + cd IntegrationTests + docker compose -f docker/docker-compose.yaml up -d + + - name: Wait for OTel Collector + uses: iFaxity/wait-on-action@v1.2.1 + with: + resource: "file:./IntegrationTests/docker/otel-collector-output/output.jsonl" + timeout: 3000 + + - name: Run Integration Tests + run: | + cd IntegrationTests + OTEL_COLLECTOR_OUTPUT=$(pwd)/docker/otel-collector-output swift test diff --git a/.gitignore b/.gitignore index 70f7b5ef..8f9089ba 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ xcuserdata/ .protoc-grpc-swift-plugins.download/ swift-otel-workspace.xcworkspace/ .benchmarkBaselines +IntegrationTests/docker/otel-collector-output/ diff --git a/IntegrationTests/Package.swift b/IntegrationTests/Package.swift new file mode 100644 index 00000000..91cb8c7e --- /dev/null +++ b/IntegrationTests/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "swift-otel-integration-tests", + dependencies: [ + .package(path: "../"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.0.0"), + ], + targets: [ + .testTarget( + name: "IntegrationTests", + dependencies: [ + .product(name: "OTel", package: "swift-otel"), + .product(name: "OTLPGRPC", package: "swift-otel"), + .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), + ], + swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] + ), + ], + swiftLanguageVersions: [.version("6"), .v5] +) diff --git a/IntegrationTests/Tests/IntegrationTests/OTLPGRPCIntegrationTests.swift b/IntegrationTests/Tests/IntegrationTests/OTLPGRPCIntegrationTests.swift new file mode 100644 index 00000000..e24c6ce4 --- /dev/null +++ b/IntegrationTests/Tests/IntegrationTests/OTLPGRPCIntegrationTests.swift @@ -0,0 +1,133 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift OTel open source project +// +// Copyright (c) 2024 Moritz Lang and the Swift OTel project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +@testable import Instrumentation +@testable import Logging +import NIO +import OTel +import OTLPGRPC +import ServiceLifecycle +import W3CTraceContext +import XCTest + +final class OTLPGRPCIntegrationTests: XCTestCase, @unchecked Sendable { + func test_example() async throws { + LoggingSystem.bootstrapInternal { label in + var handler = StreamLogHandler.standardOutput(label: label) + handler.logLevel = .trace + return handler + } + let logger = Logger(label: "test") + let group = MultiThreadedEventLoopGroup.singleton + let exporter = try OTLPGRPCSpanExporter( + configuration: OTLPGRPCSpanExporterConfiguration(environment: [:]), + group: group, + requestLogger: logger, + backgroundActivityLogger: logger + ) + let processor = OTelBatchSpanProcessor( + exporter: exporter, + configuration: .init( + environment: [:], + scheduleDelay: .zero + ) + ) + let tracer = OTelTracer( + idGenerator: OTelRandomIDGenerator(), + sampler: OTelConstantSampler(isOn: true), + propagator: OTelW3CPropagator(), + processor: processor, + environment: [:], + resource: OTelResource(attributes: ["service.name": "IntegrationTests"]) + ) + + InstrumentationSystem.bootstrapInternal(tracer) + + let serviceGroup = ServiceGroup( + configuration: .init( + services: [ + .init(service: tracer), + .init(service: TestService(), successTerminationBehavior: .gracefullyShutdownGroup), + ], + logger: logger + ) + ) + try await serviceGroup.run() + } +} + +struct TestService: Service { + func run() async throws { + let otelCollectorOutputPath = try XCTUnwrap(ProcessInfo.processInfo.environment["OTEL_COLLECTOR_OUTPUT"]) + let outputFileURL = URL(fileURLWithPath: otelCollectorOutputPath).appendingPathComponent("output.jsonl") + XCTAssertTrue(FileManager.default.fileExists(atPath: outputFileURL.path), outputFileURL.path) + + let span = InstrumentationSystem.tracer.startSpan("test") + span.attributes["foo"] = "bar" + span.setStatus(.init(code: .ok)) + span.end() + + // wait for export + try await Task.sleep(for: .seconds(2)) + + let jsonDecoder = JSONDecoder() + let outputFileContents = try String(contentsOf: outputFileURL).trimmingCharacters(in: .whitespacesAndNewlines) + let lines = outputFileContents.components(separatedBy: .newlines) + let exportLine = try XCTUnwrap(lines.last) + let decodedExportLine = try jsonDecoder.decode(ExportLine.self, from: Data(exportLine.utf8)) + let resourceSpans = try XCTUnwrap(decodedExportLine.resourceSpans.first) + let scopeSpans = try XCTUnwrap(resourceSpans.scopeSpans.first) + let exportedSpan = try XCTUnwrap(scopeSpans.spans.first) + + XCTAssertEqual(exportedSpan.spanID, span.context.spanContext?.spanID.description) + XCTAssertEqual(exportedSpan.traceID, span.context.spanContext?.traceID.description) + XCTAssertEqual(exportedSpan.name, "test") + XCTAssertEqual(exportedSpan.attributes, [.init(key: "foo", value: .init(stringValue: "bar"))]) + } +} + +struct ExportLine: Decodable { + let resourceSpans: [ResourceSpan] + + struct ResourceSpan: Decodable { + let scopeSpans: [ScopeSpans] + + struct ScopeSpans: Decodable { + let spans: [Span] + + struct Span: Decodable { + let traceID: String + let spanID: String + let name: String + let attributes: [Attribute] + + private enum CodingKeys: String, CodingKey { + case traceID = "traceId" + case spanID = "spanId" + case name + case attributes + } + + struct Attribute: Decodable, Equatable { + let key: String + let value: Value + + struct Value: Decodable, Equatable { + let stringValue: String + } + } + } + } + } +} diff --git a/IntegrationTests/docker/docker-compose.yaml b/IntegrationTests/docker/docker-compose.yaml new file mode 100644 index 00000000..85d6c1d6 --- /dev/null +++ b/IntegrationTests/docker/docker-compose.yaml @@ -0,0 +1,12 @@ +name: swift-otel-integration-tests +services: + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + command: ["--config=/etc/otel-collector-config.yaml"] + volumes: + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml + - ./otel-collector-output:/etc/otel-collector-output + ports: + - "4317:4317" # OTLP gRPC receiver + +# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json diff --git a/IntegrationTests/docker/otel-collector-config.yaml b/IntegrationTests/docker/otel-collector-config.yaml new file mode 100644 index 00000000..02a3d570 --- /dev/null +++ b/IntegrationTests/docker/otel-collector-config.yaml @@ -0,0 +1,17 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: "otel-collector:4317" + +exporters: + file: + path: /etc/otel-collector-output/output.jsonl + +service: + pipelines: + traces: + receivers: [otlp] + exporters: [file] + +# yaml-language-server: $schema=https://raw.githubusercontent.com/srikanthccv/otelcol-jsonschema/main/schema.json diff --git a/Makefile b/Makefile index 885ef2b2..d4f7e741 100644 --- a/Makefile +++ b/Makefile @@ -141,6 +141,9 @@ define contents_xcworkspacedata + + + endef export contents_xcworkspacedata diff --git a/Sources/OTel/Tracing/Processing/Batch/OTelBatchSpanProcessor.swift b/Sources/OTel/Tracing/Processing/Batch/OTelBatchSpanProcessor.swift index 94b89b7c..21bfd59b 100644 --- a/Sources/OTel/Tracing/Processing/Batch/OTelBatchSpanProcessor.swift +++ b/Sources/OTel/Tracing/Processing/Batch/OTelBatchSpanProcessor.swift @@ -119,7 +119,6 @@ public actor OTelBatchSpanProcessor & Sendable) async { let batchID = batchID self.batchID += 1 - print(batchID) var exportLogger = logger exportLogger[metadataKey: "batch_id"] = "\(batchID)" diff --git a/scripts/validate_format.sh b/scripts/validate_format.sh index 106ac0d2..da56eb3b 100755 --- a/scripts/validate_format.sh +++ b/scripts/validate_format.sh @@ -36,7 +36,7 @@ printf "=> Checking format\n" FIRST_OUT="$(git status --porcelain)" # swiftformat does not scale so we loop ourselves shopt -u dotglob -find Sources/* Tests/* Examples/* Benchmarks/* -type d -not -path "*/Generated*" | while IFS= read -r d; do +find Sources/* Tests/* Examples/* Benchmarks/* IntegrationTests/* -type d -not -path "*/Generated*" | while IFS= read -r d; do printf " * checking $d... " out=$(mint run swiftformat -quiet $d 2>&1) if [[ $out == *$'\n' ]]; then diff --git a/scripts/validate_license_headers.sh b/scripts/validate_license_headers.sh index 6ea97cfc..e1a06299 100755 --- a/scripts/validate_license_headers.sh +++ b/scripts/validate_license_headers.sh @@ -112,6 +112,7 @@ EOF find . \ \( \! -path './.build/*' \) -a \ \( \! -path './Benchmarks/.build/*' \) -a \ + \( \! -path './IntegrationTests/.build/*' \) -a \ \( \! -path '*/Generated/*' \) -a \ \( "${matching_files[@]}" \) -a \ \( \! \( "${exceptions[@]}" \) \) | while read line; do