Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ExampleApp/ExampleApp/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ let config = { () -> LDConfig in
config.plugins = [
Observability(
options: .init(
serviceName: "MyApp (ExampleApp)",
// otlpEndpoint: "http://localhost:4318",
sessionBackgroundTimeout: 3,
isDebug: true,
Expand Down
12 changes: 12 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ let package = Package(
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
]
),
.target(name: "System"),
.target(
name: "SystemLive",
dependencies: [
"System",
"Common",
"Instrumentation",
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
]
),
.target(
name: "Observability",
dependencies: [
Expand All @@ -92,6 +102,8 @@ let package = Package(
"Sampling",
"SamplingLive",
"Instrumentation",
"System",
"SystemLive",
.product(name: "OpenTelemetrySdk", package: "opentelemetry-swift"),
.product(name: "OpenTelemetryApi", package: "opentelemetry-swift"),
.product(name: "OpenTelemetryProtocolExporterHTTP", package: "opentelemetry-swift"),
Expand Down
5 changes: 5 additions & 0 deletions Sources/Common/SemanticConventionLD.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ public enum SemanticConvention {

public enum LDSemanticAttribute {
public static let ATTR_SAMPLING_RATIO = "launchdarkly.sampling.ratio"
public enum System {
public static let systemCpuUtilization = "system.cpu.utilization"
public static let cpuLogicalNumber = "cpu.logical_number"
public static let cpuMode = "cpu.mode"
}
}
11 changes: 10 additions & 1 deletion Sources/LaunchDarklyObservability/Plugin/Observability.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,22 @@ public final class Observability: Plugin {
resourceAttributes["launchdarkly.sdk.version"] = .string(String(format: "%@/%@", metadata.sdkMetadata.name, metadata.sdkMetadata.version))
resourceAttributes["highlight.project_id"] = .string(sdkKey)

let containsProjectId = options.customHeaders.contains { (key, value) in
key == "highlight.project_id"
}
var customHeaders = options.customHeaders
if !containsProjectId {
customHeaders.append(("highlight.project_id", sdkKey))
}


let options = Options(
serviceName: options.serviceName,
serviceVersion: options.serviceVersion,
otlpEndpoint: options.otlpEndpoint,
backendUrl: options.backendUrl,
resourceAttributes: options.resourceAttributes.merging(resourceAttributes) { (old, _) in old },
customHeaders: options.customHeaders,
customHeaders: customHeaders,
sessionBackgroundTimeout: options.sessionBackgroundTimeout,
isDebug: options.isDebug,
disableErrorTracking: options.disableErrorTracking,
Expand Down
30 changes: 14 additions & 16 deletions Sources/Observability/InstrumentationLive.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,20 @@ extension Instrumentation {
static let logsPath = "/v1/logs"
static let metricsPath = "/v1/metrics"

static func noOp() -> Self {
Self(
recordMetric: { _ in },
recordCount: { _ in },
recordIncr: { _ in },
recordHistogram: { _ in },
recordUpDownCounter: { _ in },
recordError: { _, _ in },
recordLog: { _, _, _ in },
startSpan: { _, _ in
/// No-op implementation of the Tracer
DefaultTracer.instance.spanBuilder(spanName: "").startSpan()
},
flush: { true }
)
}
static let noOp: Self = .init(
recordMetric: { _ in },
recordCount: { _ in },
recordIncr: { _ in },
recordHistogram: { _ in },
recordUpDownCounter: { _ in },
recordError: { _, _ in },
recordLog: { _, _, _ in },
startSpan: { _, _ in
/// No-op implementation of the Tracer
DefaultTracer.instance.spanBuilder(spanName: "").startSpan()
},
flush: { true }
)

static func build(
context: ObservabilityContext,
Expand Down
56 changes: 28 additions & 28 deletions Sources/Observability/InstrumentationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ final class InstrumentationManager {

private let sampler = ExportSampler.customSampler()
private var otelLogger: (any OpenTelemetryApi.Logger)
private var otelTracer: Tracer?
private var otelTracer: Tracer
private var otelMeter: (any Meter)

private var urlSessionInstrumentation: URLSessionInstrumentation?
Expand Down Expand Up @@ -105,7 +105,7 @@ final class InstrumentationManager {
let exporter = SamplingTraceExporterDecorator(
exporter: OtlpHttpTraceExporter(
endpoint: url,
envVarHeaders: context.options.customHeaders
config: .init(headers: context.options.customHeaders)
),
sampler: sampler
)
Expand Down Expand Up @@ -161,7 +161,7 @@ final class InstrumentationManager {
SamplingLogExporterDecorator(
exporter: OtlpHttpLogExporter(
endpoint: url,
envVarHeaders: context.options.customHeaders
config: .init(headers: context.options.customHeaders)
),
sampler: sampler
)
Expand Down Expand Up @@ -217,7 +217,7 @@ final class InstrumentationManager {
if let url = URL(string: context.options.otlpEndpoint)?.appendingPathComponent(Instrumentation.metricsPath) {
let exporter = OtlpHttpMetricExporter(
endpoint: url,
envVarHeaders: context.options.customHeaders
config: .init(headers: context.options.customHeaders)
)

let reader = PeriodicMetricReaderBuilder(exporter: exporter)
Expand All @@ -228,14 +228,22 @@ final class InstrumentationManager {
reader.forceFlush()
}


let provider = MeterProviderSdk.builder()
.registerView(
selector: InstrumentSelector.builder().setInstrument(name: context.options.serviceName).build(),
view: View.builder().build()
)
.registerMetricReader(
reader: reader
)
.registerView(
selector: InstrumentSelector
.builder()
.setInstrument(name: ".*")
.setMeter(name: options.serviceName)
.build(),
view: View
.builder()
.withName(name: options.serviceName)
.build()
)
.build()

/// Register custom meter
Expand Down Expand Up @@ -320,7 +328,9 @@ final class InstrumentationManager {
.build()
cachedGauges[metric.name] = gauge
}
gauge?.record(value: metric.value, attributes: metric.attributes)
let attributes = metric.attributes.merging(context.options.resourceAttributes, uniquingKeysWith: { current, _ in current })
gauge?.record(value: metric.value, attributes: attributes)
// gauge?.record(value: metric.value, attributes: attributes)
}

func recordCount(metric: Metric) {
Expand Down Expand Up @@ -375,41 +385,31 @@ final class InstrumentationManager {

func recordError(error: Error, attributes: [String: AttributeValue]) {
var attributes = attributes
let builder = otelTracer?.spanBuilder(spanName: "highlight.error")
let builder = otelTracer.spanBuilder(spanName: "highlight.error")

if let parent = OpenTelemetry.instance.contextProvider.activeSpan {
builder?.setParent(parent)
builder.setParent(parent)
}

attributes.forEach {
builder?.setAttribute(key: $0.key, value: $0.value)
builder.setAttribute(key: $0.key, value: $0.value)
}

let sessionId = sessionManager.sessionInfo.id
if !sessionId.isEmpty {
builder?.setAttribute(key: SemanticConvention.highlightSessionId, value: sessionId)
builder.setAttribute(key: SemanticConvention.highlightSessionId, value: sessionId)
attributes[SemanticConvention.highlightSessionId] = .string(sessionId)
}


let span = builder?.startSpan()
span?.setAttributes(attributes)
span?.recordException(ErrorSpanException(error: error), attributes: attributes)
span?.end()
let span = builder.startSpan()
span.setAttributes(attributes)
span.recordException(ErrorSpanException(error: error), attributes: attributes)
span.end()
}

func startSpan(name: String, attributes: [String: AttributeValue]) -> any Span {
let tracer: Tracer
if let otelTracer {
tracer = otelTracer
} else {
tracer = OpenTelemetry.instance.tracerProvider.get(
instrumentationName: context.options.serviceName,
instrumentationVersion: context.options.serviceVersion
)
}

let builder = tracer.spanBuilder(spanName: name)
let builder = otelTracer.spanBuilder(spanName: name)

if let parent = OpenTelemetry.instance.contextProvider.activeSpan {
builder.setParent(parent)
Expand Down
18 changes: 16 additions & 2 deletions Sources/Observability/ObservabilityClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import Instrumentation
import Common
import CrashReporter
import CrashReporterLive
import System
import SystemLive

public final class ObservabilityClient: Observe {
private let instrumentationManager: Instrumentation
private let sessionManager: SessionManager
private let systemInfoManager: SystemInfo
private let context: ObservabilityContext

private var cachedSpans = AtomicDictionary<String, Span>()
Expand All @@ -21,12 +24,23 @@ public final class ObservabilityClient: Observe {
let sessionManager = SessionManager(options: .init(timeout: context.options.sessionBackgroundTimeout))

do {
self.instrumentationManager = try Instrumentation.build(
let instrumentation = try Instrumentation.build(
context: context,
sessionManager: sessionManager
)

let systemInfoManager = SystemInfo.build(
monitoringInterval: 5,
instrumentation: instrumentation,
logger: context.logger
)

self.instrumentationManager = instrumentation
self.systemInfoManager = systemInfoManager
systemInfoManager.startMonitoring()
} catch {
self.instrumentationManager = Instrumentation.noOp()
self.instrumentationManager = Instrumentation.noOp
self.systemInfoManager = SystemInfo.noOp
os_log("%{public}@", log: context.logger.log, type: .error, "Failed to initialize Instrumentation manager with error: \(error)")
}

Expand Down
15 changes: 15 additions & 0 deletions Sources/System/CPUStatistics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
public struct CPUStatistics {
public let user: Double
public let system: Double
public let idle: Double
public let nice: Double
public let total: Double

public init(user: Double, system: Double, idle: Double, nice: Double, total: Double) {
self.user = user
self.system = system
self.idle = idle
self.nice = nice
self.total = total
}
}
9 changes: 9 additions & 0 deletions Sources/System/SystemInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
public struct SystemInfo {
public var startMonitoring: () -> Void
public var stopMonitoring: () -> Void

public init(startMonitoring: @escaping () -> Void, stopMonitoring: @escaping () -> Void) {
self.startMonitoring = startMonitoring
self.stopMonitoring = stopMonitoring
}
}
4 changes: 4 additions & 0 deletions Sources/System/SystemMetricError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public enum SystemMetricError: Error {
case cpuLoadInfoFetchFailed
case cpuLoadInfoFailed
}
61 changes: 61 additions & 0 deletions Sources/SystemLive/CPULoad.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import MachO

import System

public struct CPULoad {
private static let machHost = mach_host_self()
private var loadPrevious = host_cpu_load_info()

public init() {}

private func hostCPULoadInfo() throws -> host_cpu_load_info {
let hostCpuLoadInfoCount = MemoryLayout<host_cpu_load_info>.stride/MemoryLayout<integer_t>.stride
var size = mach_msg_type_number_t(hostCpuLoadInfoCount)
var cpuLoadInfo = host_cpu_load_info()

let result = withUnsafeMutablePointer(to: &cpuLoadInfo) {
$0.withMemoryRebound(to: integer_t.self, capacity: hostCpuLoadInfoCount) {
host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &size)
}
}
guard result == KERN_SUCCESS else {
print("Error - \(#file): \(#function) - kern_result_t = \(result)")
throw SystemMetricError.cpuLoadInfoFetchFailed
}
return cpuLoadInfo
}

/// Get CPU usage (system, user, idle, nice). Determined by the delta between the current and previous invocations.
public func cpuUsage() throws -> CPUStatistics {
do {
let load = try hostCPULoadInfo();
let userDiff: Double = Double(load.cpu_ticks.0 - loadPrevious.cpu_ticks.0);
let systemDiff = Double(load.cpu_ticks.1 - loadPrevious.cpu_ticks.1);
let idleDiff = Double(load.cpu_ticks.2 - loadPrevious.cpu_ticks.2);
let niceDiff = Double(load.cpu_ticks.3 - loadPrevious.cpu_ticks.3);

let totalTicks = userDiff + systemDiff + idleDiff + niceDiff
let system = systemDiff / totalTicks * 100.0
let user = userDiff / totalTicks * 100.0
let idle = idleDiff / totalTicks * 100.0
let nice = niceDiff / totalTicks * 100.0

return .init(
user: user,
system: system,
idle: idle,
nice: nice,
total: totalTicks
)
} catch {
throw error
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: CPU Load Calculation Stuck on Initial Values

The cpuUsage() method calculates CPU load deltas using loadPrevious, but loadPrevious is never updated with the current measurement. This causes all CPU usage calculations after the initial call to be incorrect, as they always compare against the stale, initial values.

Fix in Cursor Fix in Web


public func physicalCoresCount() -> UInt {
var size: size_t = MemoryLayout<UInt>.size
var coresCount: UInt = 0
sysctlbyname("hw.physicalcpu", &coresCount, &size, nil, 0)
return coresCount
}
}
Loading