Skip to content
This repository was archived by the owner on Aug 22, 2025. It is now read-only.

Commit 3398413

Browse files
authored
Initial: Telemetry/Tracing interface (#34)
1 parent 734c836 commit 3398413

File tree

10 files changed

+298
-9
lines changed

10 files changed

+298
-9
lines changed

Decide/Accessor/Default/DefaultBind.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,16 @@ import Foundation
2020
@DefaultEnvironment var environment
2121

2222
private let propertyKeyPath: KeyPath<S, Property<Value>>
23+
let context: Context
2324

2425
public init(
25-
_ keyPath: KeyPath<S, Mutable<Value>>
26+
_ keyPath: KeyPath<S, Mutable<Value>>,
27+
file: String = #fileID,
28+
line: Int = #line
2629
) {
27-
propertyKeyPath = keyPath.appending(path: \.wrappedValue)
30+
let context = Context(file: file, line: line)
31+
self.context = context
32+
self.propertyKeyPath = keyPath.appending(path: \.wrappedValue)
2833
}
2934

3035
public static subscript<EnclosingObject: EnvironmentObservingObject>(
@@ -46,6 +51,7 @@ import Foundation
4651
let environment = instance.environment
4752
storage.environment = environment
4853
environment.setValue(newValue, propertyKeyPath)
54+
environment.telemetry.log(event: UnstructuredMutation(context: storage.context, keyPath: "\(propertyKeyPath)", value: newValue))
4955
}
5056
}
5157

Decide/Accessor/Default/DefaultBindKeyed.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,16 @@ import Foundation
2121

2222
private let propertyKeyPath: KeyPath<S, Property<Value>>
2323
private var valueBinding: KeyedValueBinding<I, S, Value>?
24+
let context: Context
2425

2526
public init(
26-
_ keyPath: KeyPath<S, Mutable<Value>>
27+
_ keyPath: KeyPath<S, Mutable<Value>>,
28+
file: String = #fileID,
29+
line: Int = #line
2730
) {
28-
propertyKeyPath = keyPath.appending(path: \.wrappedValue)
31+
let context = Context(file: file, line: line)
32+
self.context = context
33+
self.propertyKeyPath = keyPath.appending(path: \.wrappedValue)
2934
}
3035

3136
public static subscript<EnclosingObject: EnvironmentObservingObject>(
@@ -43,7 +48,8 @@ import Foundation
4348
storage.valueBinding = KeyedValueBinding(
4449
bind: propertyKeyPath,
4550
observer: observer,
46-
environment: environment
51+
environment: environment,
52+
context: storage.context
4753
)
4854
}
4955
return storage.valueBinding!
@@ -65,12 +71,15 @@ import Foundation
6571

6672
let observer: Observer
6773
let propertyKeyPath: KeyPath<S, Property<Value>>
74+
let context: Context
6875

6976
init(
7077
bind propertyKeyPath: KeyPath<S, Property<Value>>,
7178
observer: Observer,
72-
environment: ApplicationEnvironment
79+
environment: ApplicationEnvironment,
80+
context: Context
7381
) {
82+
self.context = context
7483
self.propertyKeyPath = propertyKeyPath
7584
self.observer = observer
7685
self.environment = environment
@@ -83,6 +92,7 @@ import Foundation
8392
}
8493
set {
8594
environment.setValue(newValue, propertyKeyPath, at: identifier)
95+
environment.telemetry.log(event: UnstructuredMutation(context: context, keyPath: "\(propertyKeyPath):\(identifier)", value: newValue))
8696
}
8797
}
8898
}

Decide/Accessor/SwiftUI/Bind.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ import SwiftUI
2020
@MainActor public struct Bind<S: AtomicState, Value>: DynamicProperty {
2121
@SwiftUI.Environment(\.stateEnvironment) var environment
2222
@ObservedObject var observer = ObservedObjectWillChangeNotification()
23+
let context: Context
2324

2425
let propertyKeyPath: KeyPath<S, Property<Value>>
2526

26-
public init(_ propertyKeyPath: KeyPath<S, Mutable<Value>>) {
27+
public init(_ propertyKeyPath: KeyPath<S, Mutable<Value>>, file: String = #fileID, line: Int = #line) {
28+
let context = Context(file: file, line: line)
29+
self.context = context
2730
self.propertyKeyPath = propertyKeyPath.appending(path: \.wrappedValue)
2831
}
2932

@@ -34,6 +37,7 @@ import SwiftUI
3437
}
3538
nonmutating set {
3639
environment.setValue(newValue, propertyKeyPath)
40+
environment.telemetry.log(event: UnstructuredMutation(context: context, keyPath: "\(propertyKeyPath)", value: newValue))
3741
}
3842
}
3943

Decide/Accessor/SwiftUI/BindKeyed.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ import SwiftUI
2020

2121
@SwiftUI.Environment(\.stateEnvironment) var environment
2222
@ObservedObject var observer = ObservedObjectWillChangeNotification()
23+
let context: Context
24+
2325

2426
let propertyKeyPath: KeyPath<S, Property<Value>>
2527

26-
public init(_ propertyKeyPath: KeyPath<S, Mutable<Value>>) {
28+
public init(_ propertyKeyPath: KeyPath<S, Mutable<Value>>, file: String = #fileID, line: Int = #line) {
29+
let context = Context(file: file, line: line)
30+
self.context = context
2731
self.propertyKeyPath = propertyKeyPath.appending(path: \.wrappedValue)
2832
}
2933

@@ -37,7 +41,8 @@ import SwiftUI
3741
return environment.getValue(propertyKeyPath, at: identifier)
3842
},
3943
set: {
40-
return environment.setValue($0, propertyKeyPath, at: identifier)
44+
environment.setValue($0, propertyKeyPath, at: identifier)
45+
environment.telemetry.log(event: UnstructuredMutation(context: context, keyPath: "\(propertyKeyPath):\(identifier)", value: $0))
4146
}
4247
)
4348
}

Decide/Environment/Environment.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ import Foundation
2828
static let `default` = ApplicationEnvironment()
2929

3030
var storage: [Key: Any] = [:]
31+
let telemetry: Telemetry = {
32+
guard let config = ProcessInfo
33+
.processInfo
34+
.environment["DECIDE_TRACER"]
35+
else {
36+
return Telemetry(observer: OSLogTelemetryObserver()) // .noTelemetry
37+
}
38+
39+
if config.replacingOccurrences(of: " ", with: "").lowercased() == "oslog" {
40+
return Telemetry(observer: OSLogTelemetryObserver())
41+
}
42+
43+
// OSLog by default
44+
return Telemetry(observer: OSLogTelemetryObserver()) // .noTelemetry
45+
}()
3146

3247
subscript<S: ValueContainerStorage>(_ key: Key) -> S {
3348
if let state = storage[key] as? S { return state }

Decide/Telemetry/Context.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Decide package open source project
4+
//
5+
// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package
6+
// open source project authors
7+
// Licensed under MIT
8+
//
9+
// See LICENSE.txt for license information
10+
//
11+
// SPDX-License-Identifier: MIT
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
17+
/// While enabling extreme modularity by decoupling components,
18+
/// we must not loose the context of execution while debugging
19+
/// or dealing with incidents.
20+
///
21+
/// Decide guaranties that every state mutation whether it's structured
22+
/// or unstructured can be traced to the point of changes origin.
23+
///
24+
/// `Context` represents this origin of the change by storing the information about
25+
/// the point of execution e.g. class or function
26+
/// as well as the location in the source code.
27+
///
28+
public final class Context: Sendable {
29+
30+
/// The file path where the execution happened.
31+
public let file: String
32+
33+
/// The line number where the execution happened.
34+
public let line: Int
35+
36+
public init(file: String = #fileID, line: Int = #line) {
37+
self.file = file
38+
self.line = line
39+
}
40+
}
41+
42+
extension Context: CustomDebugStringConvertible {
43+
public var debugDescription: String {
44+
"\(file):\(line)"
45+
}
46+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Decide package open source project
4+
//
5+
// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package
6+
// open source project authors
7+
// Licensed under MIT
8+
//
9+
// See LICENSE.txt for license information
10+
//
11+
// SPDX-License-Identifier: MIT
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import OSLog
16+
17+
final class UnstructuredMutation<V>: TelemetryEvent {
18+
let category: String = "Unstructured State Mutation"
19+
let name: String = "Property updated:"
20+
let logLevel: OSLogType = .debug
21+
let context: Decide.Context
22+
23+
let keyPath: String
24+
let value: V
25+
26+
init(context: Decide.Context, keyPath: String, value: V) {
27+
self.keyPath = keyPath
28+
self.context = context
29+
self.value = value
30+
}
31+
32+
func message() -> String {
33+
"\(keyPath) -> \(String(reflecting: value))"
34+
}
35+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Decide package open source project
4+
//
5+
// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package
6+
// open source project authors
7+
// Licensed under MIT
8+
//
9+
// See LICENSE.txt for license information
10+
//
11+
// SPDX-License-Identifier: MIT
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import OSLog
16+
17+
final class OSLogTelemetryObserver: TelemetryObserver {
18+
19+
static let subsystem = "State and Side-effects (Decide)"
20+
21+
static let unsafeTracingEnabled: Bool = {
22+
guard let config: String = ProcessInfo
23+
.processInfo
24+
.environment["DECIDE_UNSAFE_TRACING_ENABLED"]
25+
else {
26+
return false
27+
}
28+
29+
if let intValue = Int(config) {
30+
return intValue == 1 ? true : false
31+
}
32+
33+
if config.replacingOccurrences(of: " ", with: "").lowercased() == "true" {
34+
return true
35+
}
36+
37+
if config.replacingOccurrences(of: " ", with: "").lowercased() == "yes" {
38+
return true
39+
}
40+
41+
return false
42+
}()
43+
44+
func eventDidOccur<E>(_ event: E) where E : TelemetryEvent {
45+
let logger = Logger(subsystem: Self.subsystem, category: event.category)
46+
if Self.unsafeTracingEnabled {
47+
unsafeTrace(event: event, logger: logger)
48+
} else {
49+
trace(event: event, logger: logger)
50+
}
51+
52+
}
53+
54+
func trace<E>(event: E, logger: Logger) where E : TelemetryEvent {
55+
switch event.logLevel {
56+
case .debug:
57+
logger.debug("\(event.name): \(event.message(), privacy: .sensitive)\n context: \(event.context.debugDescription)")
58+
case .info:
59+
logger.info("\(event.message(), privacy: .sensitive)")
60+
case .error:
61+
logger.error("\(event.message(), privacy: .sensitive)")
62+
case .fault:
63+
logger.fault("\(event.message(), privacy: .sensitive)")
64+
default:
65+
logger.log("\(event.message(), privacy: .sensitive)")
66+
}
67+
}
68+
69+
func unsafeTrace<E>(event: E, logger: Logger) where E : TelemetryEvent {
70+
switch event.logLevel {
71+
case .debug:
72+
logger.debug("\(event.name): \(event.message(), privacy: .sensitive)\n context: \(event.context.debugDescription)")
73+
case .info:
74+
logger.info("\(event.message(), privacy: .public)")
75+
case .error:
76+
logger.error("\(event.message(), privacy: .public)")
77+
case .fault:
78+
logger.fault("\(event.message(), privacy: .public)")
79+
default:
80+
logger.log("\(event.message(), privacy: .public)")
81+
}
82+
}
83+
}
84+
85+

Decide/Telemetry/Telemetry.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Decide package open source project
4+
//
5+
// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package
6+
// open source project authors
7+
// Licensed under MIT
8+
//
9+
// See LICENSE.txt for license information
10+
//
11+
// SPDX-License-Identifier: MIT
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import OSLog
17+
18+
final class Telemetry {
19+
20+
/// Minimum log level to log.
21+
/// [Choose the Appropriate Log Level for Each Message](https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code#3665947)
22+
var logLevel: OSLogType = .default
23+
24+
let observer: TelemetryObserver
25+
26+
init(observer: TelemetryObserver) {
27+
self.observer = observer
28+
}
29+
30+
func log<E: TelemetryEvent>(event: E) {
31+
guard event.logLevel.rawValue >= self.logLevel.rawValue
32+
else { return }
33+
observer.eventDidOccur(event)
34+
}
35+
}
36+
37+
final class DoNotObserve: TelemetryObserver {
38+
func eventDidOccur<E>(_ event: E) where E : TelemetryEvent {}
39+
}
40+
extension Telemetry {
41+
static let noTelemetry = Telemetry(observer: DoNotObserve())
42+
}
43+
44+
/// [Choose the Appropriate Log Level for Each Message](https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code#3665947)
45+
protocol TelemetryEvent {
46+
var category: String { get }
47+
var name: String { get }
48+
var context: Context { get }
49+
var logLevel: OSLogType { get }
50+
51+
func message() -> String
52+
}
53+
54+
protocol TelemetryObserver {
55+
/// Called every time an event with debug level
56+
/// equal or greater than current occur.
57+
func eventDidOccur<E: TelemetryEvent>(_ event: E)
58+
}
59+

0 commit comments

Comments
 (0)