Skip to content

Commit ac30a72

Browse files
authored
Merge pull request #158 from NthPortal/attribute-provider/PR
Redesign middleware abstractions
2 parents 1700b32 + 48ef30b commit ac30a72

File tree

42 files changed

+3588
-1284
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+3588
-1284
lines changed

build.sbt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import com.typesafe.tools.mima.core._
22

3-
ThisBuild / tlBaseVersion := "0.10" // your current series x.y
3+
ThisBuild / tlBaseVersion := "0.11" // your current series x.y
44

55
ThisBuild / licenses := Seq(License.Apache2)
66
ThisBuild / developers += tlGitHubDev("rossabaker", "Ross A. Baker")
@@ -49,6 +49,7 @@ lazy val `http4s-otel4s-middleware` = tlCrossRootProject
4949
lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform)
5050
.crossType(CrossType.Pure)
5151
.in(file("core"))
52+
.enablePlugins(BuildInfoPlugin)
5253
.settings(sharedSettings)
5354
.settings(
5455
name := s"$baseName-core",
@@ -57,6 +58,9 @@ lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform)
5758
"org.typelevel" %%% "otel4s-core-common" % otel4sV,
5859
"org.typelevel" %%% "otel4s-semconv" % otel4sV,
5960
),
61+
buildInfoKeys := Seq(version),
62+
buildInfoPackage := "org.http4s.otel4s.middleware",
63+
buildInfoOptions += BuildInfoOption.PackagePrivate,
6064
)
6165

6266
lazy val metrics = crossProject(JVMPlatform, JSPlatform, NativePlatform)
@@ -114,6 +118,7 @@ lazy val `trace-server` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
114118
"org.typelevel" %%% "otel4s-core-common" % otel4sV,
115119
"org.typelevel" %%% "otel4s-core-trace" % otel4sV,
116120
"org.typelevel" %%% "otel4s-semconv" % otel4sV,
121+
"org.http4s" %%% "http4s-dsl" % http4sV % Test,
117122
),
118123
)
119124

core/src/main/scala/org/http4s/otel4s/middleware/TypedAttributes.scala

Lines changed: 89 additions & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package org.http4s
1818
package otel4s.middleware
1919

2020
import com.comcast.ip4s.IpAddress
21-
import org.http4s.headers.Host
21+
import com.comcast.ip4s.Port
2222
import org.http4s.headers.`User-Agent`
2323
import org.typelevel.ci.CIString
2424
import org.typelevel.otel4s.Attribute
@@ -27,22 +27,39 @@ import org.typelevel.otel4s.Attributes
2727
import org.typelevel.otel4s.semconv.attributes.ErrorAttributes
2828
import org.typelevel.otel4s.semconv.attributes.HttpAttributes
2929
import org.typelevel.otel4s.semconv.attributes.NetworkAttributes
30-
import org.typelevel.otel4s.semconv.attributes.ServerAttributes
31-
import org.typelevel.otel4s.semconv.attributes.UrlAttributes
3230
import org.typelevel.otel4s.semconv.attributes.UserAgentAttributes
3331

3432
import java.util.Locale
3533

3634
/** Methods for creating appropriate `Attribute`s from typed HTTP objects. */
3735
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}")
3854

3955
/** @return the `http.request.method` `Attribute` */
4056
def httpRequestMethod(method: Method): Attribute[String] =
41-
HttpAttributes.HttpRequestMethod(method.name)
57+
if (knownMethods.contains(method)) HttpAttributes.HttpRequestMethod(method.name)
58+
else httpRequestMethodOther
4259

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)
4663

4764
/** @return the `http.response.status_code` `Attribute` */
4865
def httpResponseStatusCode(status: Status): Attribute[Long] =
@@ -52,195 +69,75 @@ object TypedAttributes {
5269
def networkPeerAddress(ip: IpAddress): Attribute[String] =
5370
NetworkAttributes.NetworkPeerAddress(ip.toString)
5471

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"
7781
}
82+
NetworkAttributes.NetworkProtocolVersion(rendered)
83+
}
7884

7985
/** @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))
8290

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 */
9092

9193
/** 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()
246143
}

0 commit comments

Comments
 (0)