-
-
Save feifanzhou/51d87821127d5094a057864b4ae5807d to your computer and use it in GitHub Desktop.
Example code for using opentelemetry-swift + swift-log, sending logs to HyperDX. https://feifan.blog/posts/streaming-logs-in-swift-to-the-cloud
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
// Yes, you need to import all of these modules individually | |
import GRPC | |
import Logging | |
import NIO | |
import NIOHPACK | |
import OpenTelemetryApi | |
import OpenTelemetrySdk | |
import OpenTelemetryProtocolExporterCommon | |
import OpenTelemetryProtocolExporterGrpc | |
// Implement a swift-log LogHandler so I can use the swift-log API as the frontend interface for callsites: | |
// https://github.com/apple/swift-log?tab=readme-ov-file#on-the-implementation-of-a-logging-backend-a-loghandler | |
struct OTLog: Logging.LogHandler { | |
static func register() { | |
let configuration = ClientConnection.Configuration( | |
target: .hostAndPort("in-otel.hyperdx.io", 4317), | |
eventLoopGroup: MultiThreadedEventLoopGroup(numberOfThreads: 1), | |
// Enable TLS to get through App Transport Security | |
tls: .init(), | |
backgroundActivityLogger: Logging.Logger(label: "io.grpc", factory: { StreamLogHandler.standardOutput(label: $0) }) | |
) | |
#if DEBUG | |
let deploymentEnvironment = "debug" | |
#else | |
let deploymentEnvironment = "release" | |
#endif | |
// Default metadata attributes that will be included with every log line | |
let resourceAttributes = [ | |
ResourceAttributes.deploymentEnvironment.rawValue: AttributeValue.string(deploymentEnvironment), | |
ResourceAttributes.hostName.rawValue: | |
AttributeValue.string(ProcessInfo.processInfo.hostName), | |
ResourceAttributes.osVersion.rawValue: | |
AttributeValue.string(ProcessInfo.processInfo.operatingSystemVersionString), | |
ResourceAttributes.serviceName.rawValue: AttributeValue.string("\(ProcessInfo.processInfo.processName)"), | |
ResourceAttributes.telemetrySdkName.rawValue: AttributeValue.string("opentelemetry"), | |
ResourceAttributes.telemetrySdkLanguage.rawValue: AttributeValue.string("swift"), | |
ResourceAttributes.telemetrySdkVersion.rawValue: AttributeValue.string(Resource.OTEL_SWIFT_SDK_VERSION) | |
] | |
OpenTelemetry.registerLoggerProvider( | |
loggerProvider: LoggerProviderBuilder() | |
.with( | |
processors: [ | |
BatchLogRecordProcessor( | |
logRecordExporter: GzippedOtlpLogExporter( | |
channel: ClientConnection( | |
configuration: configuration | |
), | |
// Set your API Key via the Authorization header | |
config: OtlpConfiguration(headers: [("authorization", "<YOUR_API_KEY>")]), | |
logger: Logging.Logger(label: "io.grpc", factory: { StreamLogHandler.standardOutput(label: $0) }) | |
) | |
) | |
] | |
) | |
.with(resource: Resource(attributes: resourceAttributes)) | |
.build() | |
) | |
LoggingSystem.bootstrap(Self.init) | |
} | |
static func `default`() -> Logging.Logger { | |
return Logging.Logger(label: "default") | |
} | |
private var eventProvider: any OpenTelemetryApi.Logger | |
// MARK: - | |
// MARK: swift-log LogHandler protocol | |
var metadata: Logging.Logger.Metadata | |
var logLevel: Logging.Logger.Level | |
init(label: String) { | |
self.eventProvider = OpenTelemetry.instance.loggerProvider | |
.loggerBuilder( | |
instrumentationScopeName: "<YOUR_PROJECT_NAME>" | |
) | |
.setEventDomain(label) | |
.setIncludeTraceContext(true) | |
.build() | |
self.metadata = .init() | |
self.logLevel = .debug | |
} | |
subscript(metadataKey key: String) -> Logging.Logger.Metadata.Value? { | |
get { | |
self.metadata[key] | |
} | |
set(newValue) { | |
self.metadata[key] = newValue | |
} | |
} | |
func log( | |
level: Logging.Logger.Level, | |
message: Logging.Logger.Message, | |
metadata: Logging.Logger.Metadata?, | |
source: String, | |
file: String, | |
function: String, | |
line: UInt) { | |
var attributes: [String: AttributeValue] = [:] | |
if let nonNilMetadata = metadata { | |
for (key, value) in nonNilMetadata { | |
switch value { | |
case .string(let str): | |
attributes[key] = AttributeValue(str) | |
default: | |
// TODO: Figure out other attribute values | |
continue | |
} | |
} | |
} | |
// Convert severity from swift-log's enum to opentelemetry's enum | |
var severity: OpenTelemetryApi.Severity = .debug | |
switch level { | |
case .critical: | |
severity = .fatal | |
case .debug: | |
severity = .debug | |
case .error: | |
severity = .error | |
case .info: | |
severity = .info | |
case .notice: | |
severity = .info2 | |
case .trace: | |
severity = .trace | |
case .warning: | |
severity = .warn | |
} | |
self.eventProvider.logRecordBuilder() | |
.setAttributes(attributes) | |
.setSeverity(severity) | |
.setObservedTimestamp(Date.now) | |
.setBody(AttributeValue(message.description)) | |
.emit() | |
} | |
// Copied from the built-in OtlpLogExporter, except with Gzip compression specified | |
public class GzippedOtlpLogExporter: LogRecordExporter { | |
let channel: GRPCChannel | |
var logClient: Opentelemetry_Proto_Collector_Logs_V1_LogsServiceNIOClient | |
let config: OtlpConfiguration | |
var callOptions: CallOptions | |
public init(channel: GRPCChannel, | |
config: OtlpConfiguration = OtlpConfiguration(), | |
logger: Logging.Logger = Logging.Logger(label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() }), | |
envVarHeaders: [(String, String)]? = EnvVarHeaders.attributes) { | |
self.channel = channel | |
logClient = Opentelemetry_Proto_Collector_Logs_V1_LogsServiceNIOClient(channel: channel) | |
self.config = config | |
let userAgentHeader = (Constants.HTTP.userAgent, Headers.getUserAgentHeader()) | |
if let headers = envVarHeaders { | |
var updatedHeaders = headers | |
updatedHeaders.append(userAgentHeader) | |
callOptions = CallOptions( | |
customMetadata: HPACKHeaders(updatedHeaders), | |
messageEncoding: .enabled(ClientMessageEncoding.Configuration(forRequests: .gzip, decompressionLimit: .ratio(100))), | |
logger: logger | |
) | |
} else if let headers = config.headers { | |
var updatedHeaders = headers | |
updatedHeaders.append(userAgentHeader) | |
callOptions = CallOptions( | |
customMetadata: HPACKHeaders(updatedHeaders), | |
messageEncoding: .enabled(ClientMessageEncoding.Configuration(forRequests: .gzip, decompressionLimit: .ratio(100))), | |
logger: logger | |
) | |
} else { | |
var headers = [(String, String)]() | |
headers.append(userAgentHeader) | |
callOptions = CallOptions( | |
customMetadata: HPACKHeaders(headers), | |
messageEncoding: .enabled(ClientMessageEncoding.Configuration(forRequests: .gzip, decompressionLimit: .ratio(100))), | |
logger: logger | |
) | |
} | |
} | |
public func export(logRecords: [ReadableLogRecord], explicitTimeout: TimeInterval? = nil) -> ExportResult { | |
let logRequest = Opentelemetry_Proto_Collector_Logs_V1_ExportLogsServiceRequest.with { request in | |
request.resourceLogs = LogRecordAdapter.toProtoResourceRecordLog(logRecordList: logRecords) | |
} | |
let timeout = min(explicitTimeout ?? TimeInterval.greatestFiniteMagnitude, config.timeout) | |
if timeout > 0 { | |
callOptions.timeLimit = TimeLimit.timeout(TimeAmount.nanoseconds(Int64(timeout.toNanoseconds))) | |
} | |
let export = logClient.export(logRequest, callOptions: callOptions) | |
do { | |
_ = try export.response.wait() | |
return .success | |
} catch { | |
return .failure | |
} | |
} | |
public func shutdown(explicitTimeout: TimeInterval? = nil) { | |
_ = channel.close() | |
} | |
public func forceFlush(explicitTimeout: TimeInterval? = nil) -> ExportResult { | |
.success | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment