Skip to content

Commit

Permalink
First idea of the telemetry API
Browse files Browse the repository at this point in the history
  • Loading branch information
MaximBazarov committed Aug 17, 2023
1 parent 4d6ba0b commit ee0814c
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 10 deletions.
10 changes: 8 additions & 2 deletions Decide/Accessor/Default/DefaultBind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ import Foundation
@DefaultEnvironment var environment

private let propertyKeyPath: KeyPath<S, Property<Value>>
let context: Context

public init(
_ keyPath: KeyPath<S, Mutable<Value>>
_ keyPath: KeyPath<S, Mutable<Value>>,
file: String = #fileID,
line: Int = #line
) {
propertyKeyPath = keyPath.appending(path: \.wrappedValue)
let context = Context(file: file, line: line)
self.context = context
self.propertyKeyPath = keyPath.appending(path: \.wrappedValue)
}

public static subscript<EnclosingObject: EnvironmentObservingObject>(
Expand All @@ -46,6 +51,7 @@ import Foundation
let environment = instance.environment
storage.environment = environment
environment.setValue(newValue, propertyKeyPath)
environment.telemetry.log(event: UnstructuredMutation(context: storage.context, keyPath: "\(propertyKeyPath)", value: newValue))
}
}

Expand Down
18 changes: 14 additions & 4 deletions Decide/Accessor/Default/DefaultBindKeyed.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ import Foundation

private let propertyKeyPath: KeyPath<S, Property<Value>>
private var valueBinding: KeyedValueBinding<I, S, Value>?
let context: Context

public init(
_ keyPath: KeyPath<S, Mutable<Value>>
_ keyPath: KeyPath<S, Mutable<Value>>,
file: String = #fileID,
line: Int = #line
) {
propertyKeyPath = keyPath.appending(path: \.wrappedValue)
let context = Context(file: file, line: line)
self.context = context
self.propertyKeyPath = keyPath.appending(path: \.wrappedValue)
}

public static subscript<EnclosingObject: EnvironmentObservingObject>(
Expand All @@ -43,7 +48,8 @@ import Foundation
storage.valueBinding = KeyedValueBinding(
bind: propertyKeyPath,
observer: observer,
environment: environment
environment: environment,
context: storage.context
)
}
return storage.valueBinding!
Expand All @@ -65,12 +71,15 @@ import Foundation

let observer: Observer
let propertyKeyPath: KeyPath<S, Property<Value>>
let context: Context

init(
bind propertyKeyPath: KeyPath<S, Property<Value>>,
observer: Observer,
environment: ApplicationEnvironment
environment: ApplicationEnvironment,
context: Context
) {
self.context = context
self.propertyKeyPath = propertyKeyPath
self.observer = observer
self.environment = environment
Expand All @@ -83,6 +92,7 @@ import Foundation
}
set {
environment.setValue(newValue, propertyKeyPath, at: identifier)
environment.telemetry.log(event: UnstructuredMutation(context: context, keyPath: "\(propertyKeyPath):\(identifier)", value: newValue))
}
}
}
6 changes: 5 additions & 1 deletion Decide/Accessor/SwiftUI/Bind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ import SwiftUI
@MainActor public struct Bind<S: AtomicState, Value>: DynamicProperty {
@SwiftUI.Environment(\.stateEnvironment) var environment
@ObservedObject var observer = ObservedObjectWillChangeNotification()
let context: Context

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

public init(_ propertyKeyPath: KeyPath<S, Mutable<Value>>) {
public init(_ propertyKeyPath: KeyPath<S, Mutable<Value>>, file: String = #fileID, line: Int = #line) {
let context = Context(file: file, line: line)
self.context = context
self.propertyKeyPath = propertyKeyPath.appending(path: \.wrappedValue)
}

Expand All @@ -34,6 +37,7 @@ import SwiftUI
}
nonmutating set {
environment.setValue(newValue, propertyKeyPath)
environment.telemetry.log(event: UnstructuredMutation(context: context, keyPath: "\(propertyKeyPath)", value: newValue))
}
}

Expand Down
9 changes: 7 additions & 2 deletions Decide/Accessor/SwiftUI/BindKeyed.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ import SwiftUI

@SwiftUI.Environment(\.stateEnvironment) var environment
@ObservedObject var observer = ObservedObjectWillChangeNotification()
let context: Context


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

public init(_ propertyKeyPath: KeyPath<S, Mutable<Value>>) {
public init(_ propertyKeyPath: KeyPath<S, Mutable<Value>>, file: String = #fileID, line: Int = #line) {
let context = Context(file: file, line: line)
self.context = context
self.propertyKeyPath = propertyKeyPath.appending(path: \.wrappedValue)
}

Expand All @@ -37,7 +41,8 @@ import SwiftUI
return environment.getValue(propertyKeyPath, at: identifier)
},
set: {
return environment.setValue($0, propertyKeyPath, at: identifier)
environment.setValue($0, propertyKeyPath, at: identifier)
environment.telemetry.log(event: UnstructuredMutation(context: context, keyPath: "\(propertyKeyPath):\(identifier)", value: $0))
}
)
}
Expand Down
15 changes: 15 additions & 0 deletions Decide/Environment/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ import Foundation
static let `default` = ApplicationEnvironment()

var storage: [Key: Any] = [:]
let telemetry: Telemetry = {
guard let config = ProcessInfo
.processInfo
.environment["DECIDE_TRACER"]
else {
return Telemetry(observer: OSLogTelemetryObserver()) // .noTelemetry
}

if config.replacingOccurrences(of: " ", with: "").lowercased() == "oslog" {
return Telemetry(observer: OSLogTelemetryObserver())
}

// OSLog by default
return Telemetry(observer: OSLogTelemetryObserver()) // .noTelemetry
}()

subscript<S: ValueContainerStorage>(_ key: Key) -> S {
if let state = storage[key] as? S { return state }
Expand Down
2 changes: 1 addition & 1 deletion Decide/Telemetry/Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ public final class Context: Sendable {

extension Context: CustomDebugStringConvertible {
public var debugDescription: String {
"(\(file):\(line)"
"\(file):\(line)"
}
}
35 changes: 35 additions & 0 deletions Decide/Telemetry/Events/UnstructuredMutation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Decide package open source project
//
// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package
// open source project authors
// Licensed under MIT
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: MIT
//
//===----------------------------------------------------------------------===//

import OSLog

final class UnstructuredMutation<V>: TelemetryEvent {
let category: String = "Unstructured State Mutation"
let name: String = "Property updated:"
let logLevel: OSLogType = .debug
let context: Decide.Context

let keyPath: String
let value: V

init(context: Decide.Context, keyPath: String, value: V) {
self.keyPath = keyPath
self.context = context
self.value = value
}

func message() -> String {
"\(keyPath) -> \(String(reflecting: value))"
}
}
85 changes: 85 additions & 0 deletions Decide/Telemetry/LoggerObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Decide package open source project
//
// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package
// open source project authors
// Licensed under MIT
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: MIT
//
//===----------------------------------------------------------------------===//

import OSLog

final class OSLogTelemetryObserver: TelemetryObserver {

static let subsystem = "State and Side-effects (Decide)"

static let unsafeTracingEnabled: Bool = {
guard let config: String = ProcessInfo
.processInfo
.environment["DECIDE_UNSAFE_TRACING_ENABLED"]
else {
return false
}

if let intValue = Int(config) {
return intValue == 1 ? true : false
}

if config.replacingOccurrences(of: " ", with: "").lowercased() == "true" {
return true
}

if config.replacingOccurrences(of: " ", with: "").lowercased() == "yes" {
return true
}

return false
}()

func eventDidOccur<E>(_ event: E) where E : TelemetryEvent {
let logger = Logger(subsystem: Self.subsystem, category: event.category)
if Self.unsafeTracingEnabled {
unsafeTrace(event: event, logger: logger)
} else {
trace(event: event, logger: logger)
}

}

func trace<E>(event: E, logger: Logger) where E : TelemetryEvent {
switch event.logLevel {
case .debug:
logger.debug("\(event.name): \(event.message(), privacy: .sensitive)\n context: \(event.context.debugDescription)")
case .info:
logger.info("\(event.message(), privacy: .sensitive)")
case .error:
logger.error("\(event.message(), privacy: .sensitive)")
case .fault:
logger.fault("\(event.message(), privacy: .sensitive)")
default:
logger.log("\(event.message(), privacy: .sensitive)")
}
}

func unsafeTrace<E>(event: E, logger: Logger) where E : TelemetryEvent {
switch event.logLevel {
case .debug:
logger.debug("\(event.name): \(event.message(), privacy: .sensitive)\n context: \(event.context.debugDescription)")
case .info:
logger.info("\(event.message(), privacy: .public)")
case .error:
logger.error("\(event.message(), privacy: .public)")
case .fault:
logger.fault("\(event.message(), privacy: .public)")
default:
logger.log("\(event.message(), privacy: .public)")
}
}
}


59 changes: 59 additions & 0 deletions Decide/Telemetry/Telemetry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Decide package open source project
//
// Copyright (c) 2020-2023 Maxim Bazarov and the Decide package
// open source project authors
// Licensed under MIT
//
// See LICENSE.txt for license information
//
// SPDX-License-Identifier: MIT
//
//===----------------------------------------------------------------------===//

import Foundation
import OSLog

final class Telemetry {

/// Minimum log level to log.
/// [Choose the Appropriate Log Level for Each Message](https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code#3665947)
var logLevel: OSLogType = .default

let observer: TelemetryObserver

init(observer: TelemetryObserver) {
self.observer = observer
}

func log<E: TelemetryEvent>(event: E) {
guard event.logLevel.rawValue >= self.logLevel.rawValue
else { return }
observer.eventDidOccur(event)
}
}

final class DoNotObserve: TelemetryObserver {
func eventDidOccur<E>(_ event: E) where E : TelemetryEvent {}
}
extension Telemetry {
static let noTelemetry = Telemetry(observer: DoNotObserve())
}

/// [Choose the Appropriate Log Level for Each Message](https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code#3665947)
protocol TelemetryEvent {
var category: String { get }
var name: String { get }
var context: Context { get }
var logLevel: OSLogType { get }

func message() -> String
}

protocol TelemetryObserver {
/// Called every time an event with debug level
/// equal or greater than current occur.
func eventDidOccur<E: TelemetryEvent>(_ event: E)
}

24 changes: 24 additions & 0 deletions DecideTesting/SetValue+Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
//===----------------------------------------------------------------------===//

@testable import Decide
import OSLog

public extension ApplicationEnvironment {
/// Set value at ``Mutable`` KeyPath on ``AtomicState``.
func setValue<S: AtomicState, Value>(_ newValue: Value, _ keyPath: KeyPath<S, Mutable<Value>>) {
setValue(newValue, keyPath.appending(path: \.wrappedValue))
telemetry.log(event: TestingMutation(context: .init(), keyPath: "\(keyPath)", value: newValue))
}

/// Set value at ``Mutable`` KeyPath on ``AtomicState``.
Expand All @@ -27,5 +29,27 @@ public extension ApplicationEnvironment {
at identifier: I
) {
setValue(newValue, keyPath.appending(path: \.wrappedValue), at: identifier)
telemetry.log(event: TestingMutation(context: .init(), keyPath: "\(keyPath):\(identifier)", value: newValue))
}
}


final class TestingMutation<V>: TelemetryEvent {
let category: String = "Testing: State Mutation"
let name: String = "Property updated:"
let logLevel: OSLogType = .debug
let context: Decide.Context

let keyPath: String
let value: V

init(context: Decide.Context, keyPath: String, value: V) {
self.keyPath = keyPath
self.context = context
self.value = value
}

func message() -> String {
"\(keyPath) -> \(String(reflecting: value))"
}
}

0 comments on commit ee0814c

Please sign in to comment.