Skip to main content

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 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
) {
guard let embrace = Embrace.client else { return }

// 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
embrace.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? {
guard let embrace = Embrace.client else { return nil }

let grpcURL = "grpc://\(service)/\(method)"
let attributes = [
"url.full": grpcURL,
"http.request.method": "POST",
"rpc.service": service,
"rpc.method": method
]

return embrace.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 = Embrace.client?.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 Attributes for Dashboard Appearance

To ensure your network spans appear as HTTP requests in the Embrace dashboard:

Essential Attributes

  • url.full - Complete URL or service identifier
  • http.request.method - HTTP method (GET, POST, etc.)
  • http.response.status_code - Response status code
  • .networkRequest span type - Critical for proper categorization

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

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 = Embrace.client?.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 = Embrace.client?.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()
}
}
}

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