Skip to content

Instantly share code, notes, and snippets.

@feifanzhou
Last active June 22, 2024 04:05
Show Gist options
  • Save feifanzhou/51d87821127d5094a057864b4ae5807d to your computer and use it in GitHub Desktop.
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
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