1 # bend-trace-context: native HTTP headers 2 # ======================================= 3 # 4 # The reference adapter for native HTTP on bend-kit's HTTP package 5 # (paymog/bend-kit, bend-kit-http on BendHub). bend-kit keeps the header 6 # fields of a request or a response in a Base Map from each field name to its 7 # values in arrival order; its parser lowercases the names and trims the 8 # values. This module turns such a map into the carrier of trace_context.bend 9 # and back, and gives a service its receive-to-send shortcuts around 10 # bend-kit's server and client on a caller's source. 11 # generation.bend gives the same shortcuts on the host's cryptographic source 12 # (NativeHttp.continue_or_start and NativeHttp.send there). 13 # 14 # The module decides nothing about Trace Context. Which fields are context 15 # fields, how they are read and what is written all come from 16 # trace_context.bend: the shortcuts are the package's operations on the 17 # carrier of a map (laws http_continue and http_send), the carrier keeps 18 # every value of every name (law http_carrier), and the map of a carrier 19 # holds every value of every name (law http_headers). It imports only Base 20 # and trace_context.bend, not bend-kit or the host's entropy, so it builds 21 # wherever the package builds and every def in it is pure; a service imports 22 # bend-kit for the transport. 23 # 24 # Names in the Headers namespace are internal: Bend lets any module import 25 # them, but they are not a compatibility contract. 26 27 import Base 28 import ./trace_context.bend as TC 29 30 # The header fields of a bend-kit request or response: each name, as bend-kit 31 # stores it, with its values in arrival order. 32 def HeaderMap() -> Data: 33 Map<&2, List<&2, String>> 34 35 # A request to send: the context fields the package gave it (`sent`), and 36 # the header map to send it with, which holds those fields. 37 type Outbound is Data: 38 Outbound{sent: TC.Sent, headers: Map<&2, List<&2, String>>} 39 40 # One field for each value of `name`, pushed onto `fields` in order, so that 41 # the last value ends up first. A loop: a received head of 64 KiB can hold 42 # thousands of values. 43 def Headers.push(+name: String, values: List<&2, String>, fields: List<&2, TC.Header>) -> List<&2, TC.Header>: 44 match values: 45 case Nil{}: 46 fields 47 case Con{value, rest}: 48 Headers.push(name, rest, Con{TC.Header{name, value}, fields}) 49 50 # The fields of every entry pushed onto `fields`, entry by entry. 51 def Headers.gather(entries: List<&2, Sigma<&2, &2, String, _ => List<&2, String>>>, fields: List<&2, TC.Header>) -> 52 List<&2, TC.Header>: 53 match entries: 54 case Nil{}: 55 fields 56 case Con{Tuple{name, values}, rest}: 57 Headers.gather(rest, Headers.push(name, values, fields)) 58 59 # The header fields of a map as a carrier for TC.Context.extract or 60 # TC.Context.send: every value of every name, in arrival order within each 61 # name (law http_carrier). The order between different names is the map's 62 # own, which no Trace Context rule depends on. 63 def carrier(headers: HeaderMap()) -> List<&2, TC.Header>: 64 List.reverse(&2, TC.Header, Headers.gather(Map.to_list(&2, List<&2, String>, headers), Nil{})) 65 66 # The values a map holds under `name`, none when it has no entry for it. 67 def Headers.values(+headers: HeaderMap(), name: String) -> List<&2, String>: 68 Pair.snd(HeaderMap(), List<&2, String>, Map.get(List<&2, String>, Nil{}, headers, name)) 69 70 # Put one field's value first among its name's values. 71 def Headers.add(+headers: HeaderMap(), header: TC.Header) -> HeaderMap(): 72 match header: 73 case TC.Header{+name, value}: 74 Map.set(&2, List<&2, String>, headers, name, Con{value, Headers.values(headers, name)}) 75 76 # Put every field of `fields` first among its name's values, in order. A 77 # loop, as Headers.push is. 78 def Headers.write(fields: List<&2, TC.Header>, +headers: HeaderMap()) -> HeaderMap(): 79 match fields: 80 case Nil{}: 81 headers 82 case Con{header, rest}: 83 Headers.write(rest, Headers.add(headers, header)) 84 85 # The header map of a carrier, such as the one TC.Sent.carrier gives for a 86 # message to send: under each name, the carrier's values of that name in 87 # order (law http_headers), as bend-kit's Http.add would add them one by one, 88 # with the names as the carrier spells them. The fields are put in from the 89 # last, each value first among its name's, which leaves every name's values 90 # in order. bend-kit writes one header line for each value. 91 def headers(carrier: List<&2, TC.Header>) -> HeaderMap(): 92 Headers.write(List.reverse(&2, TC.Header, carrier), Map.new(&2, List<&2, String>)) 93 94 # The service's own operation for a received message, from the header map of 95 # its request, on a caller's source: TC.Context.continue_or_start_with over 96 # the extraction of the map's carrier (law http_continue). `base` is the 97 # context to keep when the request carries no usable traceparent. 98 def continue_or_start_with(~S: Type, ~read: S -> IO(S & Result<&1, &1, U32 & String, U32>), source: S, 99 +limits: TC.Limits, headers: HeaderMap(), base: Maybe<&2, TC.BaseContext>, reception: TC.Reception, 100 sampling: TC.Sampling, +policy: TC.FailurePolicy) -> IO(S & TC.FailurePolicy.result(policy, TC.Service)): 101 TC.Context.continue_or_start_with(~S, ~read, source, TC.Context.extract(limits, carrier(headers), base), reception, 102 sampling, policy) 103 104 # The request to send for a message: the message with the map of the fields 105 # that TC.Sent.carrier gives. 106 def Headers.outbound(+sent: TC.Sent) -> Outbound: 107 Outbound{sent, headers(TC.Sent.carrier(sent))} 108 109 # The same for a strict result: a failure stays a failure. 110 def Headers.strict(result: Result<&2, &2, TC.GenerationError, TC.Sent>) -> 111 Result<&2, &2, TC.GenerationError, Outbound>: 112 match result: 113 case Fail{error}: 114 Fail{error} 115 case Done{sent}: 116 Done{Headers.outbound(sent)} 117 118 # A message sent under `policy`, with its header map. 119 def Headers.written(policy: TC.FailurePolicy, result: TC.FailurePolicy.result(policy, TC.Sent)) -> 120 TC.FailurePolicy.result(policy, Outbound): 121 match policy: 122 case TC.Lenient{}: 123 Headers.outbound(result) 124 case TC.Strict{}: 125 Headers.strict(result) 126 127 # The source's final state with a message sent under `policy` and its map. 128 def Headers.written.done(-S: Type, +policy: TC.FailurePolicy, done: S & TC.FailurePolicy.result(policy, TC.Sent)) -> 129 S & TC.FailurePolicy.result(policy, Outbound): 130 (source, result) = done 131 (source, Headers.written(policy, result)) 132 133 # Give one request that the service sends the context of a new operation, on 134 # a caller's source: TC.Context.send_with on the carrier of `headers`, the 135 # request's own header map, and the map of the fields it gives back (law 136 # http_send). The request's old context fields go, whatever the case of 137 # their names, and the new ones replace them; without a new operation, the 138 # map forwards the received pair or carries no context fields. 139 def send_with(~S: Type, ~read: S -> IO(S & Result<&1, &1, U32 & String, U32>), source: S, +limits: TC.Limits, 140 service: TC.Service, sampling: TC.Sampling, +policy: TC.FailurePolicy, headers: HeaderMap()) -> 141 IO(S & TC.FailurePolicy.result(policy, Outbound)): 142 IO.bind(S & TC.FailurePolicy.result(policy, TC.Sent), S & TC.FailurePolicy.result(policy, Outbound), 143 TC.Context.send_with(~S, ~read, source, limits, service, sampling, policy, carrier(headers)), 144 done => IO.pure(S & TC.FailurePolicy.result(policy, Outbound), Headers.written.done(S, policy, done))) 145 146 # The context fields that the package gave the request. 147 def Outbound.sent(outbound: Outbound) -> TC.Sent: 148 match outbound: 149 case Outbound{sent, headers}: 150 sent 151 152 # The header map to send the request with. 153 def Outbound.headers(outbound: Outbound) -> HeaderMap(): 154 match outbound: 155 case Outbound{sent, headers}: 156 headers