Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create an AsyncStream based OTel Logger w/ HB2 example #102

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 26 additions & 13 deletions Examples/Server/Sources/ServerExample/ServerExample.swift
Expand Up @@ -16,18 +16,14 @@ import Logging
import Metrics
@_spi(Metrics) import OTel
@_spi(Metrics) import OTLPGRPC
@_spi(Logging) import OTel
@_spi(Logging) import OTLPGRPC
import ServiceLifecycle
import Tracing

@main
enum ServerMiddlewareExample {
static func main() async throws {
// Bootstrap the logging backend with the OTel metadata provider which includes span IDs in logging messages.
LoggingSystem.bootstrap { label in
var handler = StreamLogHandler.standardError(label: label, metadataProvider: .otel)
handler.logLevel = .trace
return handler
}

// Configure OTel resource detection to automatically apply helpful attributes to events.
let environment = OTelEnvironment.detected()
let resourceDetection = OTelResourceDetection(detectors: [
Expand All @@ -51,14 +47,28 @@ enum ServerMiddlewareExample {
)
MetricsSystem.bootstrap(OTLPMetricsFactory(registry: registry))

// Bootstrap the logging backend with the OTel metadata provider which includes span IDs in logging messages.
let logExporter = try OTLPGRPCLogExporter(
configuration: .init(environment: environment)
)

let logger = OTelStreamingLogger(
resource: resource,
exporter: logExporter,
logLevel: .trace
)
LoggingSystem.bootstrap { label in
return logger
}

// Bootstrap the tracing backend to export traces periodically in OTLP/gRPC.
let exporter = try OTLPGRPCSpanExporter(configuration: .init(environment: environment))
let processor = OTelBatchSpanProcessor(exporter: exporter, configuration: .init(environment: environment))
let traceExporter = try OTLPGRPCSpanExporter(configuration: .init(environment: environment))
let traceProcessor = OTelBatchSpanProcessor(exporter: traceExporter, configuration: .init(environment: environment))
let tracer = OTelTracer(
idGenerator: OTelRandomIDGenerator(),
sampler: OTelConstantSampler(isOn: true),
propagator: OTelW3CPropagator(),
processor: processor,
processor: traceProcessor,
environment: environment,
resource: resource
)
Expand All @@ -69,11 +79,14 @@ enum ServerMiddlewareExample {
router.middlewares.add(HBTracingMiddleware())
router.middlewares.add(HBMetricsMiddleware())
router.middlewares.add(HBLogRequestsMiddleware(.info))
router.get("hello") { _, _ in "hello" }
router.get("hello") { _, context in
context.logger.info("Someone visited me, at last!")
return "hello"
}
var app = HBApplication(router: router)

// Add the tracer lifecycle service to the HTTP server service group and start the application.
app.addServices(metrics, tracer)
// Add the logger, metrics and tracer lifecycle services to the HTTP server service group and start the application.
app.addServices(logger, metrics, tracer)
try await app.runService()
}
}
11 changes: 8 additions & 3 deletions Examples/Server/docker/otel-collector-config.yaml
Expand Up @@ -5,8 +5,8 @@ receivers:
endpoint: "otel-collector:4317"

exporters:
debug: # Data sources: traces, metrics, logs
verbosity: detailed
logging:
loglevel: debug

prometheus: # Data sources: metrics
endpoint: "otel-collector:7070"
Expand All @@ -24,6 +24,11 @@ service:
exporters: [prometheus, debug]
traces:
receivers: [otlp]
exporters: [otlp/jaeger, debug]
exporters: [otlp/jaeger]

logs:
receivers: [otlp]
processors: []
exporters: [logging]

# yaml-language-server: $schema=https://raw.githubusercontent.com/srikanthccv/otelcol-jsonschema/main/schema.json
175 changes: 175 additions & 0 deletions Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift
@@ -0,0 +1,175 @@
//===----------------------------------------------------------------------===//
//
// 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 GRPC
import Logging
import NIO
import NIOHPACK
import NIOSSL
@_spi(Logging) import OTel
@_spi(Logging) import OTLPCore

/// Exports logs to an OTel collector using OTLP/gRPC.
@_spi(Logging)
public final class OTLPGRPCLogEntryExporter: OTelLogEntryExporter {
private let configuration: OTLPGRPCLogEntryExporterConfiguration
private let connection: ClientConnection
private let client: Opentelemetry_Proto_Collector_Logs_V1_LogsServiceAsyncClient
private let logger = Logger(label: String(describing: OTLPGRPCLogEntryExporter.self))

Check warning on line 28 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L28

Added line #L28 was not covered by tests

public init(
configuration: OTLPGRPCLogEntryExporterConfiguration,
group: EventLoopGroup = MultiThreadedEventLoopGroup.singleton,
requestLogger: Logger = ._otelDisabled,
backgroundActivityLogger: Logger = ._otelDisabled
) {
self.configuration = configuration

Check warning on line 36 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L35-L36

Added lines #L35 - L36 were not covered by tests

var connectionConfiguration = ClientConnection.Configuration.default(
target: .host(configuration.endpoint.host, port: configuration.endpoint.port),
eventLoopGroup: group
)

Check warning on line 41 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L38-L41

Added lines #L38 - L41 were not covered by tests

if configuration.endpoint.isInsecure {
logger.debug("Using insecure connection.", metadata: [
"host": "\(configuration.endpoint.host)",
"port": "\(configuration.endpoint.port)",
])

Check warning on line 47 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L43-L47

Added lines #L43 - L47 were not covered by tests
}

// TODO: Support OTEL_EXPORTER_OTLP_CERTIFICATE
// TODO: Support OTEL_EXPORTER_OTLP_CLIENT_KEY
// TODO: Support OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE

Check warning on line 52 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L50-L52

Added lines #L50 - L52 were not covered by tests

var headers = configuration.headers
if !headers.isEmpty {
logger.trace("Configured custom request headers.", metadata: [
"keys": .array(headers.map { "\($0.name)" }),
])

Check warning on line 58 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L54-L58

Added lines #L54 - L58 were not covered by tests
}
headers.replaceOrAdd(name: "user-agent", value: "OTel-OTLP-Exporter-Swift/\(OTelLibrary.version)")

Check warning on line 60 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L60

Added line #L60 was not covered by tests

connectionConfiguration.backgroundActivityLogger = backgroundActivityLogger
connection = ClientConnection(configuration: connectionConfiguration)

Check warning on line 63 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L62-L63

Added lines #L62 - L63 were not covered by tests

client = Opentelemetry_Proto_Collector_Logs_V1_LogsServiceAsyncClient(
channel: connection,
defaultCallOptions: .init(customMetadata: headers, logger: requestLogger)
)

Check warning on line 68 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L65-L68

Added lines #L65 - L68 were not covered by tests
}

public func export(_ batch: some Collection<OTelLogEntry> & Sendable) async throws {
if case .shutdown = connection.connectivity.state {
throw OTelLogEntryExporterAlreadyShutDownError()

Check warning on line 73 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L71-L73

Added lines #L71 - L73 were not covered by tests
}

guard !batch.isEmpty else { return }

Check warning on line 76 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L76

Added line #L76 was not covered by tests

let request = Opentelemetry_Proto_Collector_Logs_V1_ExportLogsServiceRequest.with { request in
request.resourceLogs = [
Opentelemetry_Proto_Logs_V1_ResourceLogs.with { resourceLog in
resourceLog.scopeLogs = [
Opentelemetry_Proto_Logs_V1_ScopeLogs.with { scopeLog in
scopeLog.logRecords = batch.map { log in
Opentelemetry_Proto_Logs_V1_LogRecord.with { logRecord in
logRecord.timeUnixNano = log.timeNanosecondsSinceEpoch
logRecord.observedTimeUnixNano = log.timeNanosecondsSinceEpoch
logRecord.severityNumber = switch log.level {
case .trace: .trace
case .debug: .debug
case .info: .info
case .notice: .info4
case .warning: .warn
case .error: .error
case .critical: .fatal

Check warning on line 94 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L78-L94

Added lines #L78 - L94 were not covered by tests
}
logRecord.severityText = switch log.level {
case .trace: "TRACE"
case .debug: "DEUG"
case .info: "INFO"
case .notice: "NOTICE"
case .warning: "WARNING"
case .error: "ERROR"
case .critical: "CRITICAL"

Check warning on line 103 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L96-L103

Added lines #L96 - L103 were not covered by tests
}
if let metadata = log.metadata {
logRecord.attributes = .init(metadata)

Check warning on line 106 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L105-L106

Added lines #L105 - L106 were not covered by tests
}
logRecord.body = .with { body in
body.stringValue = log.body

Check warning on line 109 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L108-L109

Added lines #L108 - L109 were not covered by tests
}
}
}
}
]

Check warning on line 114 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L114

Added line #L114 was not covered by tests
}
]

Check warning on line 116 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L116

Added line #L116 was not covered by tests
}

_ = try await client.export(request)

Check warning on line 119 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L119

Added line #L119 was not covered by tests
}

public func forceFlush() async throws {}

Check warning on line 122 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L122

Added line #L122 was not covered by tests

public func shutdown() async {
let promise = connection.eventLoop.makePromise(of: Void.self)

Check warning on line 125 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L124-L125

Added lines #L124 - L125 were not covered by tests

connection.closeGracefully(deadline: .now() + .milliseconds(500), promise: promise)

Check warning on line 127 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L127

Added line #L127 was not covered by tests

try? await promise.futureResult.get()

Check warning on line 129 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L129

Added line #L129 was not covered by tests
}
}

@_spi(Logging)
extension [Opentelemetry_Proto_Common_V1_KeyValue] {
package init(_ metadata: Logger.Metadata) {
self = metadata.map { key, value in
return .with { attribute in
attribute.key = key
attribute.value = .init(value)

Check warning on line 139 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L135-L139

Added lines #L135 - L139 were not covered by tests
}
}
}
}

@_spi(Logging)
extension Opentelemetry_Proto_Common_V1_KeyValueList {
package init(_ metadata: Logger.Metadata) {
self = .with { keyValueList in
keyValueList.values = .init(metadata)

Check warning on line 149 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L147-L149

Added lines #L147 - L149 were not covered by tests
}
}
}

@_spi(Logging)
extension Opentelemetry_Proto_Common_V1_AnyValue {
package init(_ value: Logger.Metadata.Value) {
self = .with { attributeValue in
attributeValue.value = switch value {
case .string(let string): .stringValue(string)
case .stringConvertible(let stringConvertible): .stringValue(stringConvertible.description)
case .dictionary(let metadata): .kvlistValue(.init(metadata))
case .array(let values): .arrayValue(.init(values))

Check warning on line 162 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L156-L162

Added lines #L156 - L162 were not covered by tests
}
}
}
}

@_spi(Logging)
extension Opentelemetry_Proto_Common_V1_ArrayValue {
package init(_ values: [Logger.Metadata.Value]) {
self = .with { valueList in
valueList.values = values.map(Opentelemetry_Proto_Common_V1_AnyValue.init)

Check warning on line 172 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporter.swift#L170-L172

Added lines #L170 - L172 were not covered by tests
}
}
}
@@ -0,0 +1,70 @@
//===----------------------------------------------------------------------===//
//
// 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 NIOHPACK
import OTel

@_spi(Logging)
public struct OTLPGRPCLogEntryExporterConfiguration: Sendable {
let endpoint: OTLPGRPCEndpoint
let headers: HPACKHeaders

/// Create a configuration for an ``OTLPGRPCLogEntryExporter``.
///
/// - Parameters:
/// - environment: The environment variables.
/// - endpoint: An optional endpoint string that takes precedence over any environment values. Defaults to `localhost:4317` if `nil`.
/// - shouldUseAnInsecureConnection: Whether to use an insecure connection in the absence of a scheme inside an endpoint configuration value.
/// - headers: Optional headers that take precedence over any headers configured via environment values.
public init(
environment: OTelEnvironment,
endpoint: String? = nil,
shouldUseAnInsecureConnection: Bool? = nil,
headers: HPACKHeaders? = nil
) throws {
let shouldUseAnInsecureConnection = try environment.value(
programmaticOverride: shouldUseAnInsecureConnection,
signalSpecificKey: "OTEL_EXPORTER_OTLP_LOGGING_INSECURE",
sharedKey: "OTEL_EXPORTER_OTLP_INSECURE"
) ?? false

Check warning on line 39 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporterConfiguration.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporterConfiguration.swift#L34-L39

Added lines #L34 - L39 were not covered by tests

let programmaticEndpoint: OTLPGRPCEndpoint? = try {
guard let endpoint else { return nil }
return try OTLPGRPCEndpoint(urlString: endpoint, isInsecure: shouldUseAnInsecureConnection)
}()

Check warning on line 44 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporterConfiguration.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporterConfiguration.swift#L41-L44

Added lines #L41 - L44 were not covered by tests

self.endpoint = try environment.value(
programmaticOverride: programmaticEndpoint,
signalSpecificKey: "OTEL_EXPORTER_OTLP_LOGGING_ENDPOINT",
sharedKey: "OTEL_EXPORTER_OTLP_ENDPOINT",
transformValue: { value in
do {
return try OTLPGRPCEndpoint(urlString: value, isInsecure: shouldUseAnInsecureConnection)
} catch {
// TODO: Log
return nil

Check warning on line 55 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporterConfiguration.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporterConfiguration.swift#L46-L55

Added lines #L46 - L55 were not covered by tests
}
}
) ?? .default

Check warning on line 58 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporterConfiguration.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporterConfiguration.swift#L58

Added line #L58 was not covered by tests

self.headers = try environment.value(
programmaticOverride: headers,
signalSpecificKey: "OTEL_EXPORTER_OTLP_LOGGING_HEADERS",
sharedKey: "OTEL_EXPORTER_OTLP_HEADERS",
transformValue: { value in
guard let keyValuePairs = OTelEnvironment.headers(parsingValue: value) else { return nil }
return HPACKHeaders(keyValuePairs)

Check warning on line 66 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporterConfiguration.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporterConfiguration.swift#L60-L66

Added lines #L60 - L66 were not covered by tests
}
) ?? [:]

Check warning on line 68 in Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporterConfiguration.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTLPGRPC/Logging/OTLPGRPCLogEntryExporterConfiguration.swift#L68

Added line #L68 was not covered by tests
}
}
40 changes: 40 additions & 0 deletions Sources/OTel/Logging/Exporting/OTelLogEntryExporter.swift
@@ -0,0 +1,40 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// A span exporter receives batches of processed spans to export them, e.g. by sending them over the network.
///
/// [OpenTelemetry specification: Span exporter](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/trace/sdk.md#span-exporter)
@_spi(Logging)
public protocol OTelLogEntryExporter: Sendable {
/// Export the given batch of spans.
///
/// - Parameter batch: A batch of spans to export.
func export(_ batch: some Collection<OTelLogEntry> & Sendable) async throws

/// Force the span exporter to export any previously received spans as soon as possible.
func forceFlush() async throws

/// Shut down the span exporter.
///a
/// This method gives exporters a chance to wrap up existing work such as finishing in-flight exports while not allowing new ones anymore.
/// Once this method returns, the exporter is to be considered shut down and further invocations of ``export(_:)``
/// are expected to fail.
func shutdown() async
}

/// An error indicating that a given exporter has already been shut down while receiving an additional batch of spans to export.
@_spi(Logging)
public struct OTelLogEntryExporterAlreadyShutDownError: Error {
/// Initialize the error.
public init() {}

Check warning on line 39 in Sources/OTel/Logging/Exporting/OTelLogEntryExporter.swift

View check run for this annotation

Codecov / codecov/patch

Sources/OTel/Logging/Exporting/OTelLogEntryExporter.swift#L39

Added line #L39 was not covered by tests
}