Skip to content

DEMRUM-773: Network Monitor Module #304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0032966
DEMRUM-773 Initial Network Info method
SMickelsn May 20, 2025
ed48d3c
DEMRUM-773 Network Info module support
SMickelsn May 27, 2025
a36e67a
DEMRUM-773 Network Info module support
SMickelsn May 27, 2025
2f2bd1f
DEMRUM-773 Network Info module support
SMickelsn May 27, 2025
eb2073f
DEMRUM-773 Network Info module support
SMickelsn May 27, 2025
5d32b2f
DEMRUM-773 Network Info module support
SMickelsn May 27, 2025
5b46617
DEMRUM-773 Address merge conflicts
SMickelsn Jun 2, 2025
8a36b8f
DEMRUM-773 Move various files
SMickelsn Jun 2, 2025
1387c06
DEMRUM-773 Move various files
SMickelsn Jun 2, 2025
877915b
DEMRUM-773 Connect Module to default pool
SMickelsn Jun 2, 2025
6277e4e
DEMRUM-773 Connect Module to SharedAppState
SMickelsn Jun 3, 2025
338e009
DEMRUM-773 Indicate VPN as a network.connection.type
SMickelsn Jun 4, 2025
b77a0f6
DEMRUM-773 Return network.status as string rather than boolean
SMickelsn Jun 4, 2025
7ef0816
DEMRUM-773 Remove status change span at app start
SMickelsn Jun 4, 2025
3206086
DEMRUM-773 Update package.swift re review comment
SMickelsn Jun 4, 2025
886d212
DEMRUM-773 Only signal change when relevant paramters change
SMickelsn Jun 4, 2025
8f1d9fc
DEMRUM-773 Unit Test
SMickelsn Jun 4, 2025
ecdccec
Added module config. Add subtype for connection. Renamed to Network M…
aditi-s3 Jun 17, 2025
82d0a03
Updated package.swift and resolved conflicts
aditi-s3 Jun 18, 2025
8426037
Updated comments
aditi-s3 Jun 18, 2025
138965f
Resolved merge conflicts in files
aditi-s3 Jun 18, 2025
4261cdb
DEMRUM-773: Update package.swift
SMickelsn Jun 18, 2025
32c4e83
DEMRUM-773: Update package.swift
SMickelsn Jun 18, 2025
945ded0
Merge branch 'feature/next-gen' into feature/DEMRUM-773-Network-Info-…
SMickelsn Jun 25, 2025
21b0ec1
DEMRUM-773: Updated per review comments
SMickelsn Jun 27, 2025
c5f0928
Merge branch 'feature/next-gen' into feature/DEMRUM-773-Network-Info-…
SMickelsn Jun 27, 2025
201ea0c
DEMRUM-773: Updated per review comments
SMickelsn Jul 1, 2025
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
24 changes: 20 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func generateMainTargets() -> [Target] {
"SplunkCrashReports",
"SplunkSessionReplayProxy",
"SplunkNetwork",
"SplunkNetworkInfo",
"SplunkSlowFrameDetector",
"SplunkOpenTelemetry",
"SplunkAppStart",
Expand Down Expand Up @@ -100,10 +101,25 @@ func generateMainTargets() -> [Target] {
dependencies: ["SplunkNetwork"],
path: "SplunkNetwork/Tests"
),


// MARK: - Splunk Common


// MARK: Splunk Network Info

.target(
name: "SplunkNetworkInfo",
dependencies: [
"SplunkCommon",
"SplunkOpenTelemetry"
],
path: "SplunkNetworkInfo/Sources"
),
.testTarget(
name: "SplunkNetworkInfoTests",
dependencies: ["SplunkNetworkInfo"],
path: "SplunkNetworkInfo/Tests"
),

// MARK: Splunk Common

.target(
name: "SplunkCommon",
path: "SplunkCommon/Sources"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ internal import SplunkCommon
internal import SplunkNetwork
#endif

#if canImport(SplunkNetworkInfo)
internal import SplunkNetworkInfo
#endif

#if canImport(CiscoSessionReplay)
internal import CiscoSessionReplay
internal import SplunkSessionReplayProxy
Expand Down Expand Up @@ -70,6 +74,11 @@ class DefaultModulesPool: AgentModulesPool {
knownModules.append(NetworkInstrumentation.self)
#endif

// Network Info
#if canImport(SplunkNetworkInfo)
knownModules.append(NetworkInfo.self)
#endif

// Slow Frame Detector
#if canImport(SplunkSlowFrameDetector)
knownModules.append(SlowFrameDetector.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Foundation
internal import CiscoSessionReplay
internal import SplunkAppStart
internal import SplunkNetwork
internal import SplunkNetworkInfo

#if canImport(SplunkCrashReports)
internal import SplunkCrashReports
Expand Down Expand Up @@ -53,6 +54,7 @@ extension SplunkRum {
customizeSessionReplay()
customizeNetwork()
customizeAppStart()
customizeNetworkInfo()
customizeWebViewInstrumentation()
}

Expand Down Expand Up @@ -113,6 +115,13 @@ extension SplunkRum {
appStartModule?.sharedState = sharedState
}

/// Configure NetworkInfo module
private func customizeNetworkInfo() {
let networkInfoModule = modulesManager?.module(ofType: SplunkNetworkInfo.NetworkInfo.self)

networkInfoModule?.sharedState = sharedState
}

/// Configure WebView intrumentation module
private func customizeWebViewInstrumentation() {
// Get WebViewInstrumentation module, set its sharedState
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
/*
Copyright 2025 Splunk Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import Foundation
import SplunkCommon


public struct NetworkInfoData: ModuleEventData {}

public struct NetworkInfoMetadata: ModuleEventMetadata {
public var timestamp = Date()
public var eventName: String = "network.change"
}

extension NetworkInfo: Module {

// MARK: - Module types

public typealias Configuration = NetworkInfoConfiguration
public typealias RemoteConfiguration = NetworkInfoRemoteConfiguration

public typealias EventMetadata = NetworkInfoMetadata
public typealias EventData = NetworkInfoData


// MARK: - Module methods

public func install(with configuration: (any ModuleConfiguration)?, remoteConfiguration: (any RemoteModuleConfiguration)?) {
startDetection()
}


// MARK: - Type transparency helpers

public func deleteData(for metadata: any ModuleEventMetadata) {}
public func onPublish(data: @escaping (NetworkInfoMetadata, NetworkInfoData) -> Void) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
/*
Copyright 2025 Splunk Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import Foundation
import SplunkCommon

/// NetworkInfo module configuration, minimal configuration for module conformance.
public struct NetworkInfoConfiguration: ModuleConfiguration {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
/*
Copyright 2025 Splunk Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import Foundation
import SplunkCommon

/// NetworkInfo remote configuration.
public struct NetworkInfoRemoteConfiguration: RemoteModuleConfiguration {

// MARK: - Internal decoding

struct NetworkInfo: Decodable {
let enabled: Bool
}

struct MRUMRoot: Decodable {
let networkInfo: NetworkInfo
}

struct Configuration: Decodable {
let mrum: MRUMRoot
}

struct Root: Decodable {
let configuration: Configuration
}


// MARK: - Public

public var enabled: Bool

public init?(from data: Data) {
guard let root = try? JSONDecoder().decode(Root.self, from: data) else {
return nil
}

enabled = root.configuration.mrum.networkInfo.enabled
}
}
103 changes: 103 additions & 0 deletions SplunkNetworkInfo/Sources/SplunkNetworkInfo/NetworkInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
/*
Copyright 2025 Splunk Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import Foundation
import Network
import SplunkCommon
import OpenTelemetryApi

public class NetworkInfo {
public enum ConnectionType: String {
case wifi
case cellular
case wiredEthernet
case other
case lost
}

/// An instance of the Agent shared state object, which is used to obtain agent's state, e.g. a session id.
public unowned var sharedState: AgentSharedState?

public static let shared = NetworkInfo()

private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitorQueue")

public private(set) var isConnected: Bool = false
public private(set) var connectionType: ConnectionType = .lost
public private(set) var isVPNActive: Bool = false

public var statusChangeHandler: ((Bool, ConnectionType, Bool) -> Void)?

// MARK: - Initialization

// Module conformance
public required init() { }

public func startDetection() {
monitor.pathUpdateHandler = { [weak self] path in
guard let self = self else { return }
self.isConnected = path.status == .satisfied
self.connectionType = self.getConnectionType(path)
self.isVPNActive = path.availableInterfaces.contains(where: { iface in
iface.type == .other &&
(iface.name.lowercased().contains("utun") ||
iface.name.lowercased().contains("ppp") ||
iface.name.lowercased().contains("ipsec"))
})
self.sendNetworkChangeSpan()
self.statusChangeHandler?(self.isConnected, self.connectionType, self.isVPNActive)
}
monitor.start(queue: queue)
// Send initial state span
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.sendNetworkChangeSpan()
}
}

private func getConnectionType(_ path: NWPath) -> ConnectionType {
if path.usesInterfaceType(.wifi) {
return .wifi
} else if path.usesInterfaceType(.cellular) {
return .cellular
} else if path.usesInterfaceType(.wiredEthernet) {
return .wiredEthernet
} else if path.status == .unsatisfied {
return .lost
} else {
return .other
}
}

private func sendNetworkChangeSpan() {

let tracer = OpenTelemetry.instance
.tracerProvider
.get(
instrumentationName: "NetworkInfo",
instrumentationVersion: sharedState?.agentVersion
)

let span = tracer.spanBuilder(spanName: "network.change")
.setStartTime(time: Date())
.startSpan()
span.setAttribute(key: "network.connected", value: isConnected)
span.setAttribute(key: "network.connection.type", value: connectionType.rawValue)
span.setAttribute(key: "network.vpn", value: isVPNActive)
span.end(time: Date())
}
}