@@ -18,7 +18,7 @@ package org.http4s
18
18
package otel4s .middleware
19
19
20
20
import com .comcast .ip4s .IpAddress
21
- import org . http4s . headers . Host
21
+ import com . comcast . ip4s . Port
22
22
import org .http4s .headers .`User-Agent`
23
23
import org .typelevel .ci .CIString
24
24
import org .typelevel .otel4s .Attribute
@@ -27,22 +27,39 @@ import org.typelevel.otel4s.Attributes
27
27
import org .typelevel .otel4s .semconv .attributes .ErrorAttributes
28
28
import org .typelevel .otel4s .semconv .attributes .HttpAttributes
29
29
import org .typelevel .otel4s .semconv .attributes .NetworkAttributes
30
- import org .typelevel .otel4s .semconv .attributes .ServerAttributes
31
- import org .typelevel .otel4s .semconv .attributes .UrlAttributes
32
30
import org .typelevel .otel4s .semconv .attributes .UserAgentAttributes
33
31
34
32
import java .util .Locale
35
33
36
34
/** Methods for creating appropriate `Attribute`s from typed HTTP objects. */
37
35
object TypedAttributes {
36
+ private [this ] lazy val knownMethods : Set [Method ] = Method .all.toSet
37
+ private [middleware] val middlewareVersion : Attribute [String ] =
38
+ Attribute (
39
+ " org.http4s.otel4s.middleware.version" ,
40
+ org.http4s.otel4s.middleware.BuildInfo .version,
41
+ )
42
+
43
+ /** The http.request.method `Attribute` with the special value _OTHER */
44
+ val httpRequestMethodOther : Attribute [String ] =
45
+ HttpAttributes .HttpRequestMethod (" _OTHER" )
46
+
47
+ /** @return the `error.type` `Attribute` */
48
+ def errorType (cause : Throwable ): Attribute [String ] =
49
+ ErrorAttributes .ErrorType (cause.getClass.getName)
50
+
51
+ /** @return the `error.type` `Attribute` */
52
+ def errorType (status : Status ): Attribute [String ] =
53
+ ErrorAttributes .ErrorType (s " ${status.code}" )
38
54
39
55
/** @return the `http.request.method` `Attribute` */
40
56
def httpRequestMethod (method : Method ): Attribute [String ] =
41
- HttpAttributes .HttpRequestMethod (method.name)
57
+ if (knownMethods.contains(method)) HttpAttributes .HttpRequestMethod (method.name)
58
+ else httpRequestMethodOther
42
59
43
- /** @return the `http.request.resend_count ` `Attribute` */
44
- def httpRequestResendCount ( count : Long ): Attribute [Long ] =
45
- HttpAttributes .HttpRequestResendCount (count )
60
+ /** @return the `http.request.method_original ` `Attribute` */
61
+ def httpRequestMethodOriginal ( method : Method ): Attribute [String ] =
62
+ HttpAttributes .HttpRequestMethodOriginal (method.name )
46
63
47
64
/** @return the `http.response.status_code` `Attribute` */
48
65
def httpResponseStatusCode (status : Status ): Attribute [Long ] =
@@ -52,195 +69,75 @@ object TypedAttributes {
52
69
def networkPeerAddress (ip : IpAddress ): Attribute [String ] =
53
70
NetworkAttributes .NetworkPeerAddress (ip.toString)
54
71
55
- /** @return the `server.address` `Attribute` */
56
- def serverAddress (host : Host ): Attribute [String ] =
57
- ServerAttributes .ServerAddress (Host .headerInstance.value(host))
58
-
59
- /** Returns of the following `Attribute`s when their corresponding values are
60
- * present in the URL and not redacted by the provided [[`UriRedactor` ]]:
61
- *
62
- * - `url.full`
63
- * - `url.scheme`
64
- * - `url.path`
65
- * - `url.query`
66
- * - `url.fragment` (extremely unlikely to be present)
67
- */
68
- def url (unredacted : Uri , redactor : UriRedactor ): Attributes =
69
- redactor.redact(unredacted).fold(Attributes .empty) { url =>
70
- val b = Attributes .newBuilder
71
- b += UrlAttributes .UrlFull (url.renderString)
72
- url.scheme.foreach(scheme => b += UrlAttributes .UrlScheme (scheme.value))
73
- if (url.path != Uri .Path .empty) b += UrlAttributes .UrlPath (url.path.renderString)
74
- if (url.query.nonEmpty) b += UrlAttributes .UrlQuery (url.query.renderString)
75
- url.fragment.foreach(b += UrlAttributes .UrlFragment (_))
76
- b.result()
72
+ /** @return the `network.peer.port` `Attribute` */
73
+ def networkPeerPort (port : Port ): Attribute [Long ] =
74
+ NetworkAttributes .NetworkPeerPort (port.value.toLong)
75
+
76
+ /** @return the `network.protocol.version` `Attribute` */
77
+ def networkProtocolVersion (version : HttpVersion ): Attribute [String ] = {
78
+ val rendered = version.major match {
79
+ case m if m <= 1 => s " $m. ${version.minor}"
80
+ case m /* if m >= 2 */ => s " $m"
77
81
}
82
+ NetworkAttributes .NetworkProtocolVersion (rendered)
83
+ }
78
84
79
85
/** @return the `user_agent.original` `Attribute` */
80
- def userAgentOriginal (userAgent : `User-Agent`): Attribute [String ] =
81
- UserAgentAttributes .UserAgentOriginal (`User-Agent`.headerInstance.value(userAgent))
86
+ def userAgentOriginal (headers : Headers ): Option [Attribute [String ]] =
87
+ headers
88
+ .get(`User-Agent`.name)
89
+ .map(nel => UserAgentAttributes .UserAgentOriginal (nel.head.value))
82
90
83
- /** @return the `error.type` `Attribute` */
84
- def errorType (cause : Throwable ): Attribute [String ] =
85
- ErrorAttributes .ErrorType (cause.getClass.getName)
86
-
87
- /** @return the `error.type` `Attribute` */
88
- def errorType (status : Status ): Attribute [String ] =
89
- ErrorAttributes .ErrorType (status.code.toString)
91
+ /* header stuff here, because it's long */
90
92
91
93
/** Methods for creating appropriate `Attribute`s from typed HTTP headers. */
92
- object Headers {
93
- private [this ] def generic (
94
- headers : Headers ,
95
- allowedHeaders : Set [CIString ],
96
- prefixKey : AttributeKey [Seq [String ]],
97
- ): Attributes =
98
- headers
99
- .redactSensitive()
100
- .headers
101
- .groupMap(_.name)(_.value)
102
- .view
103
- .collect {
104
- case (name, values) if allowedHeaders.contains(name) =>
105
- val key =
106
- prefixKey
107
- .transformName(_ + " ." + name.toString.toLowerCase(Locale .ROOT ))
108
- Attribute (key, values)
109
- }
110
- .to(Attributes )
111
-
112
- /** @return `http.request.header.<lowercase name>` `Attribute`s for
113
- * all headers in `allowedHeaders`
114
- */
115
- def request (headers : Headers , allowedHeaders : Set [CIString ]): Attributes =
116
- generic(headers, allowedHeaders, HttpAttributes .HttpRequestHeader )
117
-
118
- /** @return `http.response.header.<lowercase name>` `Attribute`s for
119
- * all headers in `allowedHeaders`
120
- */
121
- def response (headers : Headers , allowedHeaders : Set [CIString ]): Attributes =
122
- generic(headers, allowedHeaders, HttpAttributes .HttpResponseHeader )
123
-
124
- /** The default set of headers allowed to be turned into `Attribute`s. */
125
- lazy val defaultAllowedHeaders : Set [CIString ] = Set (
126
- " Accept" ,
127
- " Accept-CH" ,
128
- " Accept-Charset" ,
129
- " Accept-CH-Lifetime" ,
130
- " Accept-Encoding" ,
131
- " Accept-Language" ,
132
- " Accept-Ranges" ,
133
- " Access-Control-Allow-Credentials" ,
134
- " Access-Control-Allow-Headers" ,
135
- " Access-Control-Allow-Origin" ,
136
- " Access-Control-Expose-Methods" ,
137
- " Access-Control-Max-Age" ,
138
- " Access-Control-Request-Headers" ,
139
- " Access-Control-Request-Method" ,
140
- " Age" ,
141
- " Allow" ,
142
- " Alt-Svc" ,
143
- " B3" ,
144
- " Cache-Control" ,
145
- " Clear-Site-Data" ,
146
- " Connection" ,
147
- " Content-Disposition" ,
148
- " Content-Encoding" ,
149
- " Content-Language" ,
150
- " Content-Length" ,
151
- " Content-Location" ,
152
- " Content-Range" ,
153
- " Content-Security-Policy" ,
154
- " Content-Security-Policy-Report-Only" ,
155
- " Content-Type" ,
156
- " Cross-Origin-Embedder-Policy" ,
157
- " Cross-Origin-Opener-Policy" ,
158
- " Cross-Origin-Resource-Policy" ,
159
- " Date" ,
160
- " Deprecation" ,
161
- " Device-Memory" ,
162
- " DNT" ,
163
- " Early-Data" ,
164
- " ETag" ,
165
- " Expect" ,
166
- " Expect-CT" ,
167
- " Expires" ,
168
- " Feature-Policy" ,
169
- " Forwarded" ,
170
- " From" ,
171
- " Host" ,
172
- " If-Match" ,
173
- " If-Modified-Since" ,
174
- " If-None-Match" ,
175
- " If-Range" ,
176
- " If-Unmodified-Since" ,
177
- " Keep-Alive" ,
178
- " Large-Allocation" ,
179
- " Last-Modified" ,
180
- " Link" ,
181
- " Location" ,
182
- " Max-Forwards" ,
183
- " Origin" ,
184
- " Pragma" ,
185
- " Proxy-Authenticate" ,
186
- " Public-Key-Pins" ,
187
- " Public-Key-Pins-Report-Only" ,
188
- " Range" ,
189
- " Referer" ,
190
- " Referer-Policy" ,
191
- " Retry-After" ,
192
- " Save-Data" ,
193
- " Sec-CH-UA" ,
194
- " Sec-CH-UA-Arch" ,
195
- " Sec-CH-UA-Bitness" ,
196
- " Sec-CH-UA-Full-Version" ,
197
- " Sec-CH-UA-Full-Version-List" ,
198
- " Sec-CH-UA-Mobile" ,
199
- " Sec-CH-UA-Model" ,
200
- " Sec-CH-UA-Platform" ,
201
- " Sec-CH-UA-Platform-Version" ,
202
- " Sec-Fetch-Dest" ,
203
- " Sec-Fetch-Mode" ,
204
- " Sec-Fetch-Site" ,
205
- " Sec-Fetch-User" ,
206
- " Server" ,
207
- " Server-Timing" ,
208
- " SourceMap" ,
209
- " Strict-Transport-Security" ,
210
- " TE" ,
211
- " Timing-Allow-Origin" ,
212
- " Tk" ,
213
- " Trailer" ,
214
- " Transfer-Encoding" ,
215
- " Upgrade" ,
216
- " User-Agent" ,
217
- " Vary" ,
218
- " Via" ,
219
- " Viewport-Width" ,
220
- " Warning" ,
221
- " Width" ,
222
- " WWW-Authenticate" ,
223
- " X-B3-Sampled" ,
224
- " X-B3-SpanId" ,
225
- " X-B3-TraceId" ,
226
- " X-Content-Type-Options" ,
227
- " X-DNS-Prefetch-Control" ,
228
- " X-Download-Options" ,
229
- " X-Forwarded-For" ,
230
- " X-Forwarded-Host" ,
231
- " X-Forwarded-Port" ,
232
- " X-Forwarded-Proto" ,
233
- " X-Forwarded-Scheme" ,
234
- " X-Frame-Options" ,
235
- " X-Permitted-Cross-Domain-Policies" ,
236
- " X-Powered-By" ,
237
- " X-Real-Ip" ,
238
- " X-Request-Id" ,
239
- " X-Request-Start" ,
240
- " X-Runtime" ,
241
- " X-Scheme" ,
242
- " X-SourceMap" ,
243
- " X-XSS-Protection" ,
244
- ).map(CIString (_))
245
- }
94
+ private [this ] def genericHttpHeaders (
95
+ headers : Headers ,
96
+ allowedHeaders : Set [CIString ],
97
+ prefixKey : AttributeKey [Seq [String ]],
98
+ )(b : Attributes .Builder ): b.type =
99
+ b ++= headers
100
+ .redactSensitive()
101
+ .headers
102
+ .groupMap(_.name)(_.value)
103
+ .view
104
+ .collect {
105
+ case (name, values) if allowedHeaders.contains(name) =>
106
+ val key =
107
+ prefixKey
108
+ .transformName(_ + " ." + name.toString.toLowerCase(Locale .ROOT ))
109
+ Attribute (key, values)
110
+ }
111
+
112
+ /** Adds the `http.request.header.<lowercase name>` `Attribute`s for all
113
+ * headers in `allowedHeaders` to the provided builder.
114
+ */
115
+ def httpRequestHeadersForBuilder (headers : Headers , allowedHeaders : Set [CIString ])(
116
+ b : Attributes .Builder
117
+ ): b.type =
118
+ if (allowedHeaders.isEmpty) b
119
+ else genericHttpHeaders(headers, allowedHeaders, HttpAttributes .HttpRequestHeader )(b)
120
+
121
+ /** @return `http.request.header.<lowercase name>` `Attributes` for
122
+ * all headers in `allowedHeaders`
123
+ */
124
+ def httpRequestHeaders (headers : Headers , allowedHeaders : Set [CIString ]): Attributes =
125
+ if (allowedHeaders.isEmpty) Attributes .empty
126
+ else httpRequestHeadersForBuilder(headers, allowedHeaders)(Attributes .newBuilder).result()
127
+
128
+ /** Adds the `http.response.header.<lowercase name>` `Attribute`s for all
129
+ * headers in `allowedHeaders` to the provided builder.
130
+ */
131
+ def httpResponseHeadersForBuilder (headers : Headers , allowedHeaders : Set [CIString ])(
132
+ b : Attributes .Builder
133
+ ): b.type =
134
+ if (allowedHeaders.isEmpty) b
135
+ else genericHttpHeaders(headers, allowedHeaders, HttpAttributes .HttpResponseHeader )(b)
136
+
137
+ /** @return `http.response.header.<lowercase name>` `Attribute`s for
138
+ * all headers in `allowedHeaders`
139
+ */
140
+ def httpResponseHeaders (headers : Headers , allowedHeaders : Set [CIString ]): Attributes =
141
+ if (allowedHeaders.isEmpty) Attributes .empty
142
+ else httpResponseHeadersForBuilder(headers, allowedHeaders)(Attributes .newBuilder).result()
246
143
}
0 commit comments