XRay


Introducing XRay

This script provides an example of using Xray to trace the interaction between two HTTP services - service A and B. Service A calls service B as part of performing its operation and propagates the trace.

  1: 
  2: 
  3: 
  4: 
  5: 
  6: 
  7: 
  8: 
  9: 
 10: 
 11: 
 12: 
 13: 
 14: 
 15: 
 16: 
 17: 
 18: 
 19: 
 20: 
 21: 
 22: 
 23: 
 24: 
 25: 
 26: 
 27: 
 28: 
 29: 
 30: 
 31: 
 32: 
 33: 
 34: 
 35: 
 36: 
 37: 
 38: 
 39: 
 40: 
 41: 
 42: 
 43: 
 44: 
 45: 
 46: 
 47: 
 48: 
 49: 
 50: 
 51: 
 52: 
 53: 
 54: 
 55: 
 56: 
 57: 
 58: 
 59: 
 60: 
 61: 
 62: 
 63: 
 64: 
 65: 
 66: 
 67: 
 68: 
 69: 
 70: 
 71: 
 72: 
 73: 
 74: 
 75: 
 76: 
 77: 
 78: 
 79: 
 80: 
 81: 
 82: 
 83: 
 84: 
 85: 
 86: 
 87: 
 88: 
 89: 
 90: 
 91: 
 92: 
 93: 
 94: 
 95: 
 96: 
 97: 
 98: 
 99: 
100: 
101: 
102: 
103: 
104: 
105: 
106: 
107: 
108: 
109: 
110: 
111: 
112: 
113: 
114: 
115: 
116: 
117: 
118: 
119: 
120: 
121: 
122: 
123: 
124: 
125: 
126: 
127: 
128: 
129: 
130: 
131: 
132: 
#r "XRay.dll"
#load "../../src/XRay/Samples/Prelude.fsx"

open System.Net
open XRay
open Prelude

// Inject Codec (Using Fleece - Newtonsoft Json Library)

open Fleece.Newtonsoft
let CodecHttp =
    CtxCodecNewtonsoft.jsonValueCodecT
    |> Codec.compose jsonValueToTextCodec
    |> Codec.Http

open System

let telemetryEventDecoder (m: TelemetryEvent) =
    ([|
        jpairWith  JsonEncode.dateTimeOffset         "ts"   m.Timestamp
        jpairWith  JsonEncode.string                 "et"   m.EventType
        jpairWith  JsonEncode.string                 "tid"  m.TraceId
        jpairWith (JsonEncode.map JsonEncode.string) "tags" (TraceTags.toMap m.Tags)
    |] |> jobj).ToString Newtonsoft.Json.Formatting.None

let te = {
    TraceId = "42"
    EventType = ""
    Timestamp = DateTimeOffset.MinValue
    Tags = TraceTags.ofSeq [ "e\"scape","the" ; "special", "chars" ] }

let sb = System.Text.StringBuilder ()
let telemetryEventWriter (x: TelemetryEvent) (sb: System.Text.StringBuilder) =
    sb.Append (telemetryEventDecoder x) |> ignore
telemetryEventWriter te sb


/// This is the code for service A
let serviceA (host:string, serviceBHost:string)  = async {

  // create the Xray log
  let xray = TracePublisher.createDefault "msku_service" (fun x -> printfn "%s" x) telemetryEventWriter

  // the name of the operation performed by this service
  let serverSpanName = "import_msku"

  // the name of the client operation
  let clientSpanName = "get_sku_info"

  // client
  let serviceB =
    HttpClient.baseAddress serviceBHost    
  
  // handles HTTP requests
  let handle (req:HttpReq) : Async<HttpRes> = async {

    // start a (server) span by first attempting to decode a trace context from the incoming request.
    let req,span = req |> Span.decodeStart xray id serverSpanName (CodecHttp.Req |> Codec.decoder) 
    logInfo "received_request|trace_id=%s" span.TraceId

    // start client call sequence -----------------------------------------------------------
    logInfo "calling_service|caller=serviceA callee=serviceB"

    // construct a request to the client
    let clientReq = HttpReq.post () |> HttpReq.withBodyString "hello B"

    // start a client span and embed the trace into the client request, thereby propagating it
    let clientSpan,clientReq =
      clientReq |> Span.startPropagate xray id span clientSpanName (CodecHttp.Req |> Codec.encoder)

    // call the client for service B
    let! res' = serviceB clientReq
    
    // complete the client span
    let res',clientTraceContext =
      Span.completeAck xray clientSpan (CodecHttp.Res |> Codec.decoder) res'

    logInfo "calling_service_completed|caller=serviceA callee=serviceB"

    // end client call sequence ---------------------------------------------------

    // construct the server response
    let res =
      HttpRes.ofStatusCode HttpStatusCode.OK
      |> HttpRes.withBodyStringUTF8 "hello" "text/plain"

    // complete the server span and embed span completion data into the response.
    let res = Span.completeEcho xray span (CodecHttp.Res |> Codec.encoder) res

    return res }
    
  do! HttpHost.hostAsync host handle

}


/// This is the code for serviceB
let serviceB (host:string)  = async {

  let xray = TracePublisher.createDefault "sku_service" (fun x -> printfn "%s" x) telemetryEventWriter

  let serverSpanName = "import_msku"

  // handles HTTP requests
  let handle (req:HttpReq) : Async<HttpRes> = async {

    let req,span = req |> Span.decodeStart xray id serverSpanName (CodecHttp.Req |> Codec.decoder) 

    logInfo "received_request|service=serviceB trace_id=%s op=%s" span.TraceId "op_b"
    
    do! Async.Sleep 1000 // TODO: service logic

    let res =
      HttpRes.ofStatusCode HttpStatusCode.OK
      |> HttpRes.withBodyStringUTF8 "hello" "text/plain"

    let res = Span.completeEcho xray span (CodecHttp.Res |> Codec.encoder) res

    logInfo "sending_response|service=serviceB trace_id=%s" span.TraceId

    return res }
    
  do! HttpHost.hostAsync host handle

}

let serviceAHost = "http://localhost:8080/"
let serviceBHost = "http://localhost:8081/"

Async.Parallel
  [ serviceA (serviceAHost, serviceBHost) ; serviceB serviceBHost ]
  |> Async.RunSynchronously
namespace System
namespace System.Net
module Prelude
val CodecHttp : obj
module CtxCodecNewtonsoft

from Prelude
val jsonValueCodecT : ('a -> 'b) * ('c -> 'd)
val telemetryEventDecoder : m:'a -> 'b
val m : 'a
val te : obj
Multiple items
type DateTimeOffset =
  struct
    new : dateTime:DateTime -> DateTimeOffset + 5 overloads
    member Add : timeSpan:TimeSpan -> DateTimeOffset
    member AddDays : days:float -> DateTimeOffset
    member AddHours : hours:float -> DateTimeOffset
    member AddMilliseconds : milliseconds:float -> DateTimeOffset
    member AddMinutes : minutes:float -> DateTimeOffset
    member AddMonths : months:int -> DateTimeOffset
    member AddSeconds : seconds:float -> DateTimeOffset
    member AddTicks : ticks:int64 -> DateTimeOffset
    member AddYears : years:int -> DateTimeOffset
    ...
  end

--------------------
DateTimeOffset ()
DateTimeOffset(dateTime: DateTime) : DateTimeOffset
DateTimeOffset(ticks: int64, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(dateTime: DateTime, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, calendar: Globalization.Calendar, offset: TimeSpan) : DateTimeOffset
field DateTimeOffset.MinValue: DateTimeOffset
val sb : Text.StringBuilder
namespace System.Text
Multiple items
type StringBuilder =
  new : unit -> StringBuilder + 5 overloads
  member Append : value:string -> StringBuilder + 19 overloads
  member AppendFormat : format:string * arg0:obj -> StringBuilder + 7 overloads
  member AppendLine : unit -> StringBuilder + 1 overload
  member Capacity : int with get, set
  member Chars : int -> char with get, set
  member Clear : unit -> StringBuilder
  member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit
  member EnsureCapacity : capacity:int -> int
  member Equals : sb:StringBuilder -> bool
  ...

--------------------
Text.StringBuilder() : Text.StringBuilder
Text.StringBuilder(capacity: int) : Text.StringBuilder
Text.StringBuilder(value: string) : Text.StringBuilder
Text.StringBuilder(value: string, capacity: int) : Text.StringBuilder
Text.StringBuilder(capacity: int, maxCapacity: int) : Text.StringBuilder
Text.StringBuilder(value: string, startIndex: int, length: int, capacity: int) : Text.StringBuilder
val telemetryEventWriter : x:'a -> sb:Text.StringBuilder -> unit
val x : 'a
Text.StringBuilder.Append(value: char []) : Text.StringBuilder
   (+0 other overloads)
Text.StringBuilder.Append(value: obj) : Text.StringBuilder
   (+0 other overloads)
Text.StringBuilder.Append(value: uint64) : Text.StringBuilder
   (+0 other overloads)
Text.StringBuilder.Append(value: uint32) : Text.StringBuilder
   (+0 other overloads)
Text.StringBuilder.Append(value: uint16) : Text.StringBuilder
   (+0 other overloads)
Text.StringBuilder.Append(value: decimal) : Text.StringBuilder
   (+0 other overloads)
Text.StringBuilder.Append(value: float) : Text.StringBuilder
   (+0 other overloads)
Text.StringBuilder.Append(value: float32) : Text.StringBuilder
   (+0 other overloads)
Text.StringBuilder.Append(value: int64) : Text.StringBuilder
   (+0 other overloads)
Text.StringBuilder.Append(value: int) : Text.StringBuilder
   (+0 other overloads)
val ignore : value:'T -> unit
val serviceA : host:string * serviceBHost:string -> Async<unit>


 This is the code for service A
val host : string
Multiple items
val string : value:'T -> string

--------------------
type string = String
val serviceBHost : string
val async : AsyncBuilder
val xray : obj
val printfn : format:Printf.TextWriterFormat<'T> -> 'T
val serverSpanName : string
val clientSpanName : string
val serviceB : (HttpReq -> Async<HttpRes>)
module HttpClient

from Prelude
val baseAddress : host:string -> (HttpReq -> Async<HttpRes>)


 Creates an HTTP client for the specified host url.
val handle : (HttpReq -> Async<HttpRes>)
val req : HttpReq
Multiple items
module HttpReq

from Prelude

--------------------
type HttpReq = Http.HttpRequestMessage
Multiple items
type Async =
  static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
  static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
  static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
  static member AwaitTask : task:Task -> Async<unit>
  static member AwaitTask : task:Task<'T> -> Async<'T>
  static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
  static member CancelDefaultToken : unit -> unit
  static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
  static member Choice : computations:seq<Async<'T option>> -> Async<'T option>
  static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
  ...

--------------------
type Async<'T> =
Multiple items
module HttpRes

from Prelude

--------------------
type HttpRes = Http.HttpResponseMessage
val req : obj
val span : obj
val id : x:'T -> 'T
val logInfo : x:Printf.TextWriterFormat<'a> -> 'a
val clientReq : HttpReq
val post : unit -> HttpReq


 Creates an HTTP POST request.
val withBodyString : s:string -> req:HttpReq -> HttpReq


 Assigns a string body to an HTTP request.
val clientSpan : obj
val res' : HttpRes
val res' : obj
val clientTraceContext : obj
val res : HttpRes
val ofStatusCode : s:HttpStatusCode -> HttpRes
type HttpStatusCode =
  | Continue = 100
  | SwitchingProtocols = 101
  | OK = 200
  | Created = 201
  | Accepted = 202
  | NonAuthoritativeInformation = 203
  | NoContent = 204
  | ResetContent = 205
  | PartialContent = 206
  | MultipleChoices = 300
  ...
field HttpStatusCode.OK: HttpStatusCode = 200
val withBodyStringUTF8 : s:string -> mediaType:string -> (HttpRes -> HttpRes)
module HttpHost

from Prelude
val hostAsync : uriPrefix:string -> service:(HttpReq -> Async<HttpRes>) -> Async<unit>
val serviceB : host:string -> Async<unit>


 This is the code for serviceB
static member Async.Sleep : millisecondsDueTime:int -> Async<unit>
val serviceAHost : string
static member Async.Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member Async.RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:Threading.CancellationToken -> 'T
Fork me on GitHub