Skip to main content

Network instrumentation

Network instrumentation​

While Embrace's automatic instrumentation captures URLSession requests out of the box, manual network instrumentation allows you to track requests made by third-party libraries like gRPC that don't use URLSession.

When to use manual network instrumentation​

Consider manual network instrumentation when:

  • Using gRPC or other RPC frameworks
  • Using third-party networking libraries (Alamofire, AFNetworking, etc.)
  • Implementing custom network protocols
  • Adding business-specific context to network requests
  • Tracking network operations that bypass URLSession

Required imports​

import Foundation
import EmbraceIO

gRPC instrumentation​

Since gRPC libraries like grpc-swift don't use URLSession, their network requests aren't automatically captured. You can manually create spans that appear as HTTP requests in the networking section of the Embrace dashboard.

Basic gRPC span creation​

class GRPCInstrumentation {
func recordGRPCRequest(
service: String,
method: String,
startTime: Date,
endTime: Date,
statusCode: Int? = nil,
requestSize: Int? = nil,
responseSize: Int? = nil,
error: Error? = nil
) {
// Create a URL-like identifier for the gRPC call
let grpcURL = "grpc://\(service)/\(method)"

// Build attributes using HTTP semantic conventions
var attributes: [String: String] = [
"url.full": grpcURL,
"http.request.method": "POST", // gRPC uses POST
"rpc.service": service,
"rpc.method": method
]

if let requestSize = requestSize {
attributes["http.request.body.size"] = String(requestSize)
}

if let statusCode = statusCode {
attributes["http.response.status_code"] = String(statusCode)
}

if let responseSize = responseSize {
attributes["http.response.body.size"] = String(responseSize)
}

if let error = error {
let nsError = error as NSError
attributes["error.type"] = nsError.domain
attributes["error.code"] = String(nsError.code)
attributes["error.message"] = error.localizedDescription
}

// Record the span
EmbraceIO.shared.recordCompletedSpan(
name: "POST /\(service)/\(method)",
type: .networkRequest,
parent: nil,
startTime: startTime,
endTime: endTime,
attributes: attributes,
events: [],
errorCode: error != nil ? .failure : nil
)
}
}

Real-time gRPC span creation​

For ongoing gRPC requests, create spans that you can update during the call:

func startGRPCSpan(service: String, method: String) -> Span? {
let grpcURL = "grpc://\(service)/\(method)"
let attributes = [
"url.full": grpcURL,
"http.request.method": "POST",
"rpc.service": service,
"rpc.method": method
]

return EmbraceIO.shared.buildSpan(
name: "POST /\(service)/\(method)",
type: .networkRequest,
attributes: attributes
).startSpan()
}

// Usage example
let span = startGRPCSpan(service: "UserService", method: "GetUser")
// ... perform gRPC call ...
span?.setAttribute(key: "http.response.status_code", value: "200")
span?.setAttribute(key: "http.response.body.size", value: "1024")
span?.end()

Integration with grpc-swift​

Here's how to integrate with the grpc-swift library:

import GRPC
import EmbraceIO

extension GRPCClient {
func makeInstrumentedCall<Request, Response>(
path: String,
request: Request,
callOptions: CallOptions? = nil,
handler: @escaping (Response) -> Void
) {
let startTime = Date()
let span = startGRPCSpan(service: "YourService", method: path)

// Make the actual gRPC call
let call = makeUnaryCall(
path: path,
request: request,
callOptions: callOptions
)

call.response.whenComplete { result in
let endTime = Date()

switch result {
case .success(let response):
span?.setAttribute(key: "http.response.status_code", value: "200")
span?.end()
handler(response)

case .failure(let error):
span?.setAttribute(key: "error.type", value: String(describing: type(of: error)))
span?.setAttribute(key: "error.message", value: error.localizedDescription)
span?.end(errorCode: .failure)
}
}
}
}

Third-party library instrumentation​

Alamofire integration​

import Alamofire
import EmbraceIO

extension Session {
func instrumentedRequest(
_ url: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil
) -> DataRequest {
let span = EmbraceIO.shared.buildSpan(
name: "\(method.rawValue) \(url)",
type: .networkRequest
)?.startSpan()

let request = self.request(
url,
method: method,
parameters: parameters,
encoding: encoding,
headers: headers
)

return request.responseData { response in
// Add response details to span
if let statusCode = response.response?.statusCode {
span?.setAttribute(key: "http.response.status_code", value: String(statusCode))
}

if let error = response.error {
span?.setAttribute(key: "error.message", value: error.localizedDescription)
span?.end(errorCode: .failure)
} else {
span?.end()
}
}
}
}

Required information for manual networking​

To ensure your network spans appear as network requests in the Embrace dashboard, you should add all required information listed below. Additionally, you can include some optional attributes to the network spans to enrich the telemetry you gather.

Required information​

Span type must be .networkRequest for these spans to show as network requests. This is required for proper categorization on Embrace's backend.

Required attributes​

Additionally, the following attributes are required on the span for your network request:

  • url.full - Complete URL or service identifier
  • http.request.method - HTTP method (GET, POST, etc.)
  • http.response.status_code - Response status code (200, 403, etc.)

Optional attributes​

  • http.request.body.size - Request payload size
  • http.response.body.size - Response payload size
  • error.type - Error type for failed requests
  • error.code - Error code
  • error.message - Error description

Naming convention​

For the span's name, use the format: "{METHOD} {path}" (e.g., GET /api/users, POST /UserService/GetUser)

Best practices​

1. Consistent naming​

Use the same service and method names as your API definitions:

// Good
span?.name = "POST /UserService/GetUser"

// Avoid
span?.name = "user_fetch_operation"

2. Include timing​

Always record accurate start and end times:

let startTime = Date()
// ... perform network operation ...
let endTime = Date()

embrace.recordCompletedSpan(
name: "POST /api/users",
type: .networkRequest,
parent: nil,
startTime: startTime,
endTime: endTime,
attributes: attributes,
events: [],
errorCode: nil
)

3. Error handling​

Capture both network errors and business logic errors:

// Network error
if let networkError = error as? URLError {
attributes["error.type"] = "URLError"
attributes["error.code"] = String(networkError.code.rawValue)
attributes["error.message"] = networkError.localizedDescription
}

// Business logic error (e.g., 404, 500)
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode >= 400 {
attributes["http.response.status_code"] = String(httpResponse.statusCode)
errorCode = .failure
}

4. Request/response sizes​

Include payload sizes for performance analysis:

// Before sending request
attributes["http.request.body.size"] = String(requestData.count)

// After receiving response
attributes["http.response.body.size"] = String(responseData.count)

5. Parent spans​

Create parent spans for complex operations with multiple network calls:

// Parent span for the entire user profile loading operation
let parentSpan = embrace.buildSpan(
name: "load_user_profile",
type: .performance
).startSpan()

// Child spans for individual network requests
let userSpan = embrace.buildSpan(
name: "GET /api/user",
type: .networkRequest
).setParent(parentSpan).startSpan()

let preferencesSpan = embrace.buildSpan(
name: "GET /api/preferences",
type: .networkRequest
).setParent(parentSpan).startSpan()

Common patterns​

Request wrapper function​

func trackNetworkRequest<T>(
name: String,
url: String,
method: String,
operation: @escaping () async throws -> T
) async throws -> T {
let span = EmbraceIO.shared.buildSpan(
name: name,
type: .networkRequest,
attributes: [
"url.full": url,
"http.request.method": method
]
)?.startSpan()

do {
let result = try await operation()
span?.setAttribute(key: "http.response.status_code", value: "200")
span?.end()
return result
} catch {
span?.setAttribute(key: "error.message", value: error.localizedDescription)
span?.end(errorCode: .failure)
throw error
}
}

// Usage
let userData = try await trackNetworkRequest(
name: "GET /api/user",
url: "https://api.example.com/user/123",
method: "GET"
) {
return try await fetchUserData()
}

Custom protocol instrumentation​

For WebSocket or other custom protocols:

class WebSocketInstrumentation {
private var connectionSpan: Span?

func startConnection(url: String) {
connectionSpan = EmbraceIO.shared.buildSpan(
name: "WebSocket Connection",
type: .networkRequest,
attributes: [
"url.full": url,
"protocol": "websocket"
]
)?.startSpan()
}

func recordMessage(direction: String, size: Int) {
connectionSpan?.addEvent(
name: "websocket.message",
attributes: [
"direction": direction,
"size": String(size)
]
)
}

func endConnection(error: Error? = nil) {
if let error = error {
connectionSpan?.setAttribute(key: "error.message", value: error.localizedDescription)
connectionSpan?.end(errorCode: .failure)
} else {
connectionSpan?.end()
}
}
}

Summary​

By following these patterns and examples, you can instrument network requests from any third-party library to appear alongside your automatically-captured URLSession requests in the Embrace dashboard.