forked from CrunchyData/pg_tileserv
-
Notifications
You must be signed in to change notification settings - Fork 0
/
util.go
295 lines (254 loc) · 7.98 KB
/
util.go
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
package main
import (
"bytes"
"errors"
"fmt"
"math"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"text/template"
"time"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/theckman/httpforwarded"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
// cache SQL/HTML templates so repeated filesystem reads are not required
var globalTemplates map[string](*template.Template) = make(map[string](*template.Template))
var globalTemplatesMutex = &sync.Mutex{}
// formatBaseURL takes a hostname (baseHost) and a base path
// and joins them. Both are parsed as URLs (using net/url) and
// then joined to ensure a properly formed URL.
// net/url does not support parsing hostnames without a scheme
// (e.g. example.com is invalid; http://example.com is valid).
// serverURLHost ensures a scheme is added.
func formatBaseURL(baseHost string, basePath string) string {
urlHost, err := url.Parse(baseHost)
if err != nil {
log.Fatal(err)
}
urlPath, err := url.Parse(basePath)
if err != nil {
log.Fatal(err)
}
return strings.TrimRight(urlHost.ResolveReference(urlPath).String(), "/")
}
// serverURLBase returns the base server URL
// that the client used to access this service.
// All pg_tileserv routes are mounted relative to
// this URL (including path, if specified by the
// BasePath config option)
func serverURLBase(r *http.Request) string {
baseHost := serverURLHost(r)
basePath := viper.GetString("BasePath")
return formatBaseURL(baseHost, basePath)
}
// serverURLHost returns the host (and scheme)
// for this service.
// In the case of access via a proxy service, if
// the standard headers are set, we return that
// hostname. If necessary the automatic calculation
// can be over-ridden by setting the "UrlBase"
// configuration option.
func serverURLHost(r *http.Request) string {
// Use configuration file settings if we have them
configURL := viper.GetString("UrlBase")
if configURL != "" {
return configURL
}
// Preferred scheme
ps := "http"
if r.TLS != nil {
ps = "https"
}
// Preferred host:port
ph := strings.TrimRight(r.Host, "/")
// Check for the IETF standard "Forwarded" header
// for reverse proxy information
xf := http.CanonicalHeaderKey("Forwarded")
if f, ok := r.Header[xf]; ok {
if fm, err := httpforwarded.Parse(f); err == nil {
if len(fm["host"]) > 0 && len(fm["proto"]) > 0 {
ph = fm["host"][0]
ps = fm["proto"][0]
return fmt.Sprintf("%v://%v", ps, ph)
}
}
}
// Check the X-Forwarded-Host and X-Forwarded-Proto
// headers
xfh := http.CanonicalHeaderKey("X-Forwarded-Host")
if fh, ok := r.Header[xfh]; ok {
ph = fh[0]
}
xfp := http.CanonicalHeaderKey("X-Forwarded-Proto")
if fp, ok := r.Header[xfp]; ok {
ps = fp[0]
}
return fmt.Sprintf("%v://%v", ps, ph)
}
func getSQLTemplate(name string, tmpl string) *template.Template {
tp, ok := globalTemplates[name]
if ok {
return tp
}
t := template.New(name)
tp, err := t.Parse(tmpl)
if err != nil {
log.Fatal(err)
}
globalTemplatesMutex.Lock()
globalTemplates[name] = tp
globalTemplatesMutex.Unlock()
return tp
}
func renderSQLTemplate(name string, tmpl string, data interface{}) (string, error) {
var buf bytes.Buffer
t := getSQLTemplate(name, tmpl)
err := t.Execute(&buf, data)
if err != nil {
return string(buf.Bytes()), err
}
sql := string(buf.Bytes())
log.Debug(sql)
return sql, nil
}
/******************************************************************************/
func getServerBounds() (b *Bounds, e error) {
if globalServerBounds != nil {
return globalServerBounds, nil
}
srid := viper.GetInt("CoordinateSystem.SRID")
xmin := viper.GetFloat64("CoordinateSystem.Xmin")
ymin := viper.GetFloat64("CoordinateSystem.Ymin")
xmax := viper.GetFloat64("CoordinateSystem.Xmax")
ymax := viper.GetFloat64("CoordinateSystem.Ymax")
log.Infof("Using CoordinateSystem.SRID %d with bounds [%g, %g, %g, %g]",
srid, xmin, ymin, xmax, ymax)
width := xmax - xmin
height := ymax - ymin
size := math.Min(width, height)
/* Not square enough to just adjust */
if math.Abs(width-height) > 0.01*size {
return nil, errors.New("CoordinateSystem bounds must be square")
}
cx := xmin + width/2
cy := ymin + height/2
/* Perfectly square bounds please */
xmin = cx - size/2
ymin = cy - size/2
xmax = cx + size/2
ymax = cy + size/2
globalServerBounds = &Bounds{srid, xmin, ymin, xmax, ymax}
return globalServerBounds, nil
}
func getTTL() (ttl int) {
if globalTimeToLive < 0 {
globalTimeToLive = viper.GetInt("CacheTTL")
}
return globalTimeToLive
}
/*****************************************************************************/
//Prometheus metrics collection
var (
tilesProcessed = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "pg_tileserv_tile_requests_total",
Help: "The total number of tiles processed",
},
[]string{
"layer",
"status_code",
})
tilesDurationHistogram = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "pg_tileserv_tile_requests_duration",
Help: "Tile request processing duration distribution",
Buckets: []float64{.1, .25, .5, 1, 2.5, 5, 10},
},
[]string{
"layer",
},
)
)
// metricsResponseWriter helps capture the HTTP status code
// of responses.
// Credit to github.com/Boerworz for the sample code
type metricsResponseWriter struct {
http.ResponseWriter
StatusCode int
}
// NewMetricsResponseWriter instantiates and returns a metricsResponseWriter
func NewMetricsResponseWriter(w http.ResponseWriter) *metricsResponseWriter {
return &metricsResponseWriter{w, http.StatusOK}
}
func (mrw *metricsResponseWriter) WriteHeader(code int) {
mrw.StatusCode = code
mrw.ResponseWriter.WriteHeader(code)
}
// tileMetrics returns a middleware that collects metrics for tile set endpoints.
// If EnableMetrics = false, a blank middleware is returned. This is to avoid all the Prometheus
// metrics operations from occuring if metrics are disabled.
//
// Requests that return an HTTP status in the range 400-499 are considered bad
// requests and are not tracked. This includes layers that do not exist (404) and
// invalid tiles (400). Server errors (500) will still be tracked.
// 404 and 400 errors cannot be tracked because label values would no longer be
// constrained to valid layers.
func tileMetrics(h http.Handler) http.Handler {
if viper.GetBool("EnableMetrics") {
// log metrics URL at startup
basePath := viper.GetString("BasePath")
log.Infof("Prometheus metrics enabled at %s/metrics", formatBaseURL(fmt.Sprintf("http://%s:%d",
viper.GetString("HttpHost"), viper.GetInt("HttpPort")), basePath))
// create the handler that will track metrics for tile requests.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// start a timer for the duration histogram.
start := time.Now()
mrw := NewMetricsResponseWriter(w)
// get path variables from the router to determine the layer name
vars := mux.Vars(r)
layer := vars["name"]
// call the next handler
h.ServeHTTP(mrw, r)
// Do not track metrics for invalid user requests (4xx errors)
if mrw.StatusCode/100 == 4 {
return
}
// get the counter for this request and then increment it
counter, err := tilesProcessed.GetMetricWith(
map[string]string{
"layer": layer,
"status_code": strconv.Itoa(mrw.StatusCode),
},
)
if err != nil {
log.Warn("Unable to get tilesProcessed Prometheus counter.")
return
}
// get the histogram metric and make an observation of the
// response time.
histogram, err := tilesDurationHistogram.GetMetricWith(
map[string]string{
"layer": layer,
},
)
if err != nil {
log.Warn("Unable to get tilesDurationHistogram Prometheus histogram.")
return
}
counter.Inc()
duration := time.Since(start)
histogram.Observe(duration.Seconds())
})
}
// if metrics are disabled, return a handler without any of the
// metric operations.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.ServeHTTP(w, r)
})
}