-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHttpClient.fs
More file actions
230 lines (192 loc) · 8.54 KB
/
HttpClient.fs
File metadata and controls
230 lines (192 loc) · 8.54 KB
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
module Telebot.HttpClient
open System
open System.Diagnostics
open System.Net.Http
open Microsoft.Extensions.DependencyInjection
open Polly
open Polly.Extensions.Http
open Telebot.PrometheusMetrics
open Telebot.TelemetryService
module private Constants =
[<Literal>]
let UserAgent =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
[<Literal>]
let DefaultTimeoutSeconds = 30
// Retry policy for HTTP requests
let private retryPolicy =
HttpPolicyExtensions
.HandleTransientHttpError()
.Or<Polly.Timeout.TimeoutRejectedException>()
.WaitAndRetryAsync(
retryCount = 3,
sleepDurationProvider = fun retryAttempt -> TimeSpan.FromSeconds(Math.Pow(2.0, float retryAttempt))
)
// Circuit breaker policy
let private circuitBreakerPolicy =
HttpPolicyExtensions
.HandleTransientHttpError()
.Or<Polly.Timeout.TimeoutRejectedException>()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking = 5,
durationOfBreak = TimeSpan.FromSeconds(30.0)
)
// Timeout policy
let private timeoutPolicy =
Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(120.0))
// Combined policy
let private combinedPolicy =
Policy.WrapAsync(retryPolicy, circuitBreakerPolicy, timeoutPolicy)
// Service provider for dependency injection
let mutable private serviceProvider: ServiceProvider option = None
// Initialize HTTP client factory
let initializeHttpClientFactory () =
let services = ServiceCollection()
services
.AddHttpClient("telebot")
.AddPolicyHandler(combinedPolicy)
.ConfigureHttpClient(Action<HttpClient>(fun client -> client.Timeout <- TimeSpan.FromSeconds(30.0)))
.ConfigurePrimaryHttpMessageHandler(
Func<HttpMessageHandler>(fun () ->
let handler = new HttpClientHandler(CookieContainer = System.Net.CookieContainer())
handler :> HttpMessageHandler // Explicit upcast to HttpMessageHandler
)
) |> ignore
let provider = services.BuildServiceProvider()
serviceProvider <- Some provider
provider
// Get HTTP client from factory
let private getHttpClient () =
match serviceProvider with
| Some provider ->
let factory = provider.GetRequiredService<IHttpClientFactory>()
factory.CreateClient("telebot")
| None ->
let provider = initializeHttpClientFactory()
let factory = provider.GetRequiredService<IHttpClientFactory>()
factory.CreateClient("telebot")
// Record HTTP metrics
let private recordHttpMetrics (method: string) (uri: Uri) (responseMessage: HttpResponseMessage) (duration: TimeSpan) (requestSize: int64 option) (responseSize: int64 option) =
let host = uri.Host
let statusCode = responseMessage.StatusCode.ToString()
httpRequestsTotal.WithLabels([|method; host; statusCode|]).Inc()
httpRequestDuration.WithLabels([|method; host|]).Observe(duration.TotalSeconds)
requestSize |> Option.iter (fun size -> httpRequestSize.WithLabels([|method; host|]).Observe(float size))
responseSize |> Option.iter (fun size -> httpResponseSize.WithLabels([|method; host|]).Observe(float size))
// Async HTTP request with telemetry
let private executeHttpRequestAsync (request: HttpRequestMessage) : Async<HttpResponseMessage> =
async {
use client = getHttpClient()
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0")
client.DefaultRequestHeaders.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-US,en;q=0.5")
client.DefaultRequestHeaders.Add("Sec-Fetch-Dest", "document")
client.DefaultRequestHeaders.Add("Sec-Fetch-Mode", "navigate")
client.DefaultRequestHeaders.Add("Sec-Fetch-Site", "none")
client.DefaultRequestHeaders.Add("Sec-Fetch-User", "?1")
client.DefaultRequestHeaders.Add("Upgrade-Insecure-Requests", "1")
client.DefaultRequestHeaders.Add("DNT", "1")
let stopwatch = Stopwatch.StartNew()
try
activeConnectionsGauge.Inc()
let! response = client.SendAsync(request) |> Async.AwaitTask
stopwatch.Stop()
let requestSize =
match request.Content with
| null -> None
| content ->
match content.Headers.ContentLength with
| length when length.HasValue -> Some length.Value
| _ -> None
let responseSize =
match response.Content.Headers.ContentLength with
| length when length.HasValue -> Some length.Value
| _ -> None
recordHttpMetrics request.Method.Method request.RequestUri response stopwatch.Elapsed requestSize responseSize
return response
finally
activeConnectionsGauge.Dec()
stopwatch.Stop()
}
// GET request with telemetry
let getAsync (url: string) : Async<HttpResponseMessage> =
withOperationTelemetry "http_get" (fun scope ->
async {
TelemetryScope.addProperty "url" url scope |> ignore
TelemetryScope.logInfo $"Making GET request to {url}" scope
use request = new HttpRequestMessage(HttpMethod.Get, url)
let! response = executeHttpRequestAsync request
if response.IsSuccessStatusCode then
TelemetryScope.logInfo $"GET request successful: {response.StatusCode}" scope
else
TelemetryScope.logWarning $"GET request failed: {response.StatusCode}" scope
return response
}
)
// POST request with telemetry
let postAsync (url: string) (content: HttpContent) : Async<HttpResponseMessage> =
withOperationTelemetry "http_post" (fun scope ->
async {
TelemetryScope.addProperty "url" url scope |> ignore
TelemetryScope.logInfo $"Making POST request to {url}" scope
use request = new HttpRequestMessage(HttpMethod.Post, url)
request.Content <- content
let! response = executeHttpRequestAsync request
if response.IsSuccessStatusCode then
TelemetryScope.logInfo $"POST request successful: {response.StatusCode}" scope
else
TelemetryScope.logWarning $"POST request failed: {response.StatusCode}" scope
return response
}
)
// Execute custom request with telemetry
let executeRequestAsync (request: HttpRequestMessage) : Async<HttpResponseMessage> =
withOperationTelemetry "http_custom" (fun scope ->
async {
let url = request.RequestUri.ToString()
let method = request.Method.Method
TelemetryScope.addProperty "url" url scope |> ignore
TelemetryScope.addProperty "method" method scope |> ignore
TelemetryScope.logInfo $"Making {method} request to {url}" scope
let! response = executeHttpRequestAsync request
if response.IsSuccessStatusCode then
TelemetryScope.logInfo $"{method} request successful: {response.StatusCode}" scope
else
TelemetryScope.logWarning $"{method} request failed: {response.StatusCode}" scope
return response
}
)
// Download data as byte array
let downloadBytesAsync (url: string) : Async<byte[]> =
async {
let! response = getAsync url
response.EnsureSuccessStatusCode() |> ignore
return! response.Content.ReadAsByteArrayAsync() |> Async.AwaitTask
}
// Download data as string
let downloadStringAsync (url: string) : Async<string> =
async {
let! response = getAsync url
response.EnsureSuccessStatusCode() |> ignore
return! response.Content.ReadAsStringAsync() |> Async.AwaitTask
}
// Health check function
let healthCheckAsync () : Async<bool> =
async {
try
let! response = getAsync "https://httpbin.org/status/200"
let isHealthy = response.IsSuccessStatusCode
healthCheckStatus.WithLabels([|"http_client"|]).Set(if isHealthy then 1.0 else 0.0)
return isHealthy
with
| _ ->
healthCheckStatus.WithLabels([|"http_client"|]).Set(0.0)
return false
}
// Cleanup resources
let cleanup () =
match serviceProvider with
| Some provider ->
provider.Dispose()
serviceProvider <- None
| None -> ()