14
14
15
15
package zipkin .autoconfigure .metrics ;
16
16
17
+ import io .prometheus .client .Histogram ;
17
18
import io .prometheus .client .hotspot .DefaultExports ;
18
19
import io .prometheus .client .spring .boot .EnablePrometheusEndpoint ;
19
20
import io .prometheus .client .spring .boot .EnableSpringBootMetricsCollector ;
20
21
import io .prometheus .client .spring .web .EnablePrometheusTiming ;
22
+ import java .io .IOException ;
23
+ import javax .servlet .AsyncContext ;
24
+ import javax .servlet .AsyncEvent ;
25
+ import javax .servlet .AsyncListener ;
21
26
import javax .servlet .Filter ;
27
+ import javax .servlet .FilterChain ;
28
+ import javax .servlet .FilterConfig ;
22
29
import javax .servlet .ServletException ;
30
+ import javax .servlet .ServletRequest ;
31
+ import javax .servlet .ServletResponse ;
32
+ import javax .servlet .http .HttpServletRequest ;
23
33
import org .springframework .context .annotation .Bean ;
24
34
import org .springframework .context .annotation .Configuration ;
25
35
@@ -32,11 +42,108 @@ public class PrometheusMetricsAutoConfiguration {
32
42
DefaultExports .initialize ();
33
43
}
34
44
35
- @ Bean
36
- public Filter prometheusMetricsFilter () throws ServletException {
37
- return new io .prometheus .client .filter .MetricsFilter ("http_request_duration_seconds" ,
38
- "Response time histogram" ,
39
- 0 ,
40
- null );
45
+ // Obviates the state bug in MetricsFilter which implicitly registers and hides something you
46
+ // can't create twice
47
+ static final Histogram http_request_duration_seconds = Histogram .build ()
48
+ .labelNames ("path" , "method" )
49
+ .help ("Response time histogram" )
50
+ .name ("http_request_duration_seconds" )
51
+ .register ();
52
+
53
+ @ Bean ("http_request_duration_seconds" ) Histogram http_request_duration_seconds () {
54
+ return http_request_duration_seconds ;
55
+ }
56
+
57
+ @ Bean public Filter prometheusMetricsFilter () {
58
+ return new PrometheusDurationFilter ();
59
+ }
60
+
61
+ /**
62
+ * The normal prometheus metrics filter implicitly registers a histogram which is hidden in a
63
+ * field and not deregistered on destroy. A registration of any second instance of that filter
64
+ * fails trying to re-register the same collector by design (by brian-brazil). The rationale is
65
+ * that you are not supposed to recreate the same histogram. However, this design prevents us from
66
+ * doing that. brian-bazil's hard stance on this makes the filter unusable for applications who
67
+ * run tests.
68
+ *
69
+ * <p>This filter replaces the normal prometheus filter, correcting the design flaw by allowing us
70
+ * to re-use the JVM singleton. It also corrects a major flaw in the upstream filter which results
71
+ * in double-counting of requests when they are performed asynchronously. When the culture changes
72
+ * in the prometheus project such that bugs are fixable, please submit this so that it can help
73
+ * others. For more info, see "brian's bomb" https://github.com/openzipkin/zipkin/issues/1811
74
+ */
75
+ static final class PrometheusDurationFilter implements Filter {
76
+ @ Override public void init (FilterConfig filterConfig ) {
77
+ }
78
+
79
+ /**
80
+ * Note that upstream also has a problem which is that it doesn't handle async properly.
81
+ * MetricsFilter results in double-counting, which this implementation avoids.
82
+ */
83
+ @ Override public void doFilter (ServletRequest servletRequest , ServletResponse servletResponse ,
84
+ FilterChain filterChain ) throws IOException , ServletException {
85
+
86
+ HttpServletRequest request = (HttpServletRequest ) servletRequest ;
87
+
88
+ // async servlets will enter the filter twice
89
+ if (request .getAttribute ("PrometheusDurationFilter" ) != null ) {
90
+ filterChain .doFilter (request , servletResponse );
91
+ return ;
92
+ }
93
+
94
+ request .setAttribute ("PrometheusDurationFilter" , "true" );
95
+
96
+ Histogram .Timer timer = http_request_duration_seconds
97
+ .labels (request .getRequestURI (), request .getMethod ())
98
+ .startTimer ();
99
+
100
+ try {
101
+ filterChain .doFilter (servletRequest , servletResponse );
102
+ } finally {
103
+ if (request .isAsyncStarted ()) { // we don't have the actual response, handle later
104
+ request .getAsyncContext ().addListener (new CompleteTimer (timer ));
105
+ } else { // we have a synchronous response, so we can finish the recording
106
+ timer .observeDuration ();
107
+ }
108
+ }
109
+ }
110
+
111
+ @ Override public void destroy () {
112
+ }
113
+ }
114
+
115
+ /** Inspired by WingtipsRequestSpanCompletionAsyncListener */
116
+ static final class CompleteTimer implements AsyncListener {
117
+ final Histogram .Timer timer ;
118
+ volatile boolean completed = false ;
119
+
120
+ CompleteTimer (Histogram .Timer timer ) {
121
+ this .timer = timer ;
122
+ }
123
+
124
+ @ Override public void onComplete (AsyncEvent e ) {
125
+ tryComplete ();
126
+ }
127
+
128
+ @ Override public void onTimeout (AsyncEvent e ) {
129
+ tryComplete ();
130
+ }
131
+
132
+ @ Override public void onError (AsyncEvent e ) {
133
+ tryComplete ();
134
+ }
135
+
136
+ /** Only observes the first completion event */
137
+ void tryComplete () {
138
+ if (completed ) return ;
139
+ timer .observeDuration ();
140
+ completed = true ;
141
+ }
142
+
143
+ /** If another async is created (ex via asyncContext.dispatch), this needs to be re-attached */
144
+ @ Override public void onStartAsync (AsyncEvent event ) {
145
+ AsyncContext eventAsyncContext = event .getAsyncContext ();
146
+ if (eventAsyncContext != null ) eventAsyncContext .addListener (this );
147
+ }
41
148
}
42
149
}
0 commit comments