1// Copyright 2015 Prometheus Team
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package main
15
16import (
17	"context"
18	"fmt"
19	"net"
20	"net/http"
21	"net/url"
22	"os"
23	"os/signal"
24	"path/filepath"
25	"runtime"
26	"strings"
27	"sync"
28	"syscall"
29	"time"
30
31	"github.com/go-kit/log"
32	"github.com/go-kit/log/level"
33	"github.com/pkg/errors"
34	"github.com/prometheus/client_golang/prometheus"
35	"github.com/prometheus/client_golang/prometheus/promhttp"
36	"github.com/prometheus/common/model"
37	"github.com/prometheus/common/promlog"
38	promlogflag "github.com/prometheus/common/promlog/flag"
39	"github.com/prometheus/common/route"
40	"github.com/prometheus/common/version"
41	"github.com/prometheus/exporter-toolkit/web"
42	webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag"
43	"gopkg.in/alecthomas/kingpin.v2"
44
45	"github.com/prometheus/alertmanager/api"
46	"github.com/prometheus/alertmanager/cluster"
47	"github.com/prometheus/alertmanager/config"
48	"github.com/prometheus/alertmanager/dispatch"
49	"github.com/prometheus/alertmanager/inhibit"
50	"github.com/prometheus/alertmanager/nflog"
51	"github.com/prometheus/alertmanager/notify"
52	"github.com/prometheus/alertmanager/notify/email"
53	"github.com/prometheus/alertmanager/notify/opsgenie"
54	"github.com/prometheus/alertmanager/notify/pagerduty"
55	"github.com/prometheus/alertmanager/notify/pushover"
56	"github.com/prometheus/alertmanager/notify/slack"
57	"github.com/prometheus/alertmanager/notify/sns"
58	"github.com/prometheus/alertmanager/notify/victorops"
59	"github.com/prometheus/alertmanager/notify/webhook"
60	"github.com/prometheus/alertmanager/notify/wechat"
61	"github.com/prometheus/alertmanager/provider/mem"
62	"github.com/prometheus/alertmanager/silence"
63	"github.com/prometheus/alertmanager/template"
64	"github.com/prometheus/alertmanager/timeinterval"
65	"github.com/prometheus/alertmanager/types"
66	"github.com/prometheus/alertmanager/ui"
67)
68
69var (
70	requestDuration = prometheus.NewHistogramVec(
71		prometheus.HistogramOpts{
72			Name:    "alertmanager_http_request_duration_seconds",
73			Help:    "Histogram of latencies for HTTP requests.",
74			Buckets: []float64{.05, 0.1, .25, .5, .75, 1, 2, 5, 20, 60},
75		},
76		[]string{"handler", "method"},
77	)
78	responseSize = prometheus.NewHistogramVec(
79		prometheus.HistogramOpts{
80			Name:    "alertmanager_http_response_size_bytes",
81			Help:    "Histogram of response size for HTTP requests.",
82			Buckets: prometheus.ExponentialBuckets(100, 10, 7),
83		},
84		[]string{"handler", "method"},
85	)
86	clusterEnabled = prometheus.NewGauge(
87		prometheus.GaugeOpts{
88			Name: "alertmanager_cluster_enabled",
89			Help: "Indicates whether the clustering is enabled or not.",
90		},
91	)
92	configuredReceivers = prometheus.NewGauge(
93		prometheus.GaugeOpts{
94			Name: "alertmanager_receivers",
95			Help: "Number of configured receivers.",
96		},
97	)
98	configuredIntegrations = prometheus.NewGauge(
99		prometheus.GaugeOpts{
100			Name: "alertmanager_integrations",
101			Help: "Number of configured integrations.",
102		},
103	)
104	promlogConfig = promlog.Config{}
105)
106
107func init() {
108	prometheus.MustRegister(requestDuration)
109	prometheus.MustRegister(responseSize)
110	prometheus.MustRegister(clusterEnabled)
111	prometheus.MustRegister(configuredReceivers)
112	prometheus.MustRegister(configuredIntegrations)
113	prometheus.MustRegister(version.NewCollector("alertmanager"))
114}
115
116func instrumentHandler(handlerName string, handler http.HandlerFunc) http.HandlerFunc {
117	handlerLabel := prometheus.Labels{"handler": handlerName}
118	return promhttp.InstrumentHandlerDuration(
119		requestDuration.MustCurryWith(handlerLabel),
120		promhttp.InstrumentHandlerResponseSize(
121			responseSize.MustCurryWith(handlerLabel),
122			handler,
123		),
124	)
125}
126
127const defaultClusterAddr = "0.0.0.0:9094"
128
129// buildReceiverIntegrations builds a list of integration notifiers off of a
130// receiver config.
131func buildReceiverIntegrations(nc *config.Receiver, tmpl *template.Template, logger log.Logger) ([]notify.Integration, error) {
132	var (
133		errs         types.MultiError
134		integrations []notify.Integration
135		add          = func(name string, i int, rs notify.ResolvedSender, f func(l log.Logger) (notify.Notifier, error)) {
136			n, err := f(log.With(logger, "integration", name))
137			if err != nil {
138				errs.Add(err)
139				return
140			}
141			integrations = append(integrations, notify.NewIntegration(n, rs, name, i))
142		}
143	)
144
145	for i, c := range nc.WebhookConfigs {
146		add("webhook", i, c, func(l log.Logger) (notify.Notifier, error) { return webhook.New(c, tmpl, l) })
147	}
148	for i, c := range nc.EmailConfigs {
149		add("email", i, c, func(l log.Logger) (notify.Notifier, error) { return email.New(c, tmpl, l), nil })
150	}
151	for i, c := range nc.PagerdutyConfigs {
152		add("pagerduty", i, c, func(l log.Logger) (notify.Notifier, error) { return pagerduty.New(c, tmpl, l) })
153	}
154	for i, c := range nc.OpsGenieConfigs {
155		add("opsgenie", i, c, func(l log.Logger) (notify.Notifier, error) { return opsgenie.New(c, tmpl, l) })
156	}
157	for i, c := range nc.WechatConfigs {
158		add("wechat", i, c, func(l log.Logger) (notify.Notifier, error) { return wechat.New(c, tmpl, l) })
159	}
160	for i, c := range nc.SlackConfigs {
161		add("slack", i, c, func(l log.Logger) (notify.Notifier, error) { return slack.New(c, tmpl, l) })
162	}
163	for i, c := range nc.VictorOpsConfigs {
164		add("victorops", i, c, func(l log.Logger) (notify.Notifier, error) { return victorops.New(c, tmpl, l) })
165	}
166	for i, c := range nc.PushoverConfigs {
167		add("pushover", i, c, func(l log.Logger) (notify.Notifier, error) { return pushover.New(c, tmpl, l) })
168	}
169	for i, c := range nc.SNSConfigs {
170		add("sns", i, c, func(l log.Logger) (notify.Notifier, error) { return sns.New(c, tmpl, l) })
171	}
172	if errs.Len() > 0 {
173		return nil, &errs
174	}
175	return integrations, nil
176}
177
178func main() {
179	os.Exit(run())
180}
181
182func run() int {
183	if os.Getenv("DEBUG") != "" {
184		runtime.SetBlockProfileRate(20)
185		runtime.SetMutexProfileFraction(20)
186	}
187
188	var (
189		configFile      = kingpin.Flag("config.file", "Alertmanager configuration file name.").Default("alertmanager.yml").String()
190		dataDir         = kingpin.Flag("storage.path", "Base path for data storage.").Default("data/").String()
191		retention       = kingpin.Flag("data.retention", "How long to keep data for.").Default("120h").Duration()
192		alertGCInterval = kingpin.Flag("alerts.gc-interval", "Interval between alert GC.").Default("30m").Duration()
193
194		webConfig      = webflag.AddFlags(kingpin.CommandLine)
195		externalURL    = kingpin.Flag("web.external-url", "The URL under which Alertmanager is externally reachable (for example, if Alertmanager is served via a reverse proxy). Used for generating relative and absolute links back to Alertmanager itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by Alertmanager. If omitted, relevant URL components will be derived automatically.").String()
196		routePrefix    = kingpin.Flag("web.route-prefix", "Prefix for the internal routes of web endpoints. Defaults to path of --web.external-url.").String()
197		listenAddress  = kingpin.Flag("web.listen-address", "Address to listen on for the web interface and API.").Default(":9093").String()
198		getConcurrency = kingpin.Flag("web.get-concurrency", "Maximum number of GET requests processed concurrently. If negative or zero, the limit is GOMAXPROC or 8, whichever is larger.").Default("0").Int()
199		httpTimeout    = kingpin.Flag("web.timeout", "Timeout for HTTP requests. If negative or zero, no timeout is set.").Default("0").Duration()
200
201		clusterBindAddr = kingpin.Flag("cluster.listen-address", "Listen address for cluster. Set to empty string to disable HA mode.").
202				Default(defaultClusterAddr).String()
203		clusterAdvertiseAddr = kingpin.Flag("cluster.advertise-address", "Explicit address to advertise in cluster.").String()
204		peers                = kingpin.Flag("cluster.peer", "Initial peers (may be repeated).").Strings()
205		peerTimeout          = kingpin.Flag("cluster.peer-timeout", "Time to wait between peers to send notifications.").Default("15s").Duration()
206		gossipInterval       = kingpin.Flag("cluster.gossip-interval", "Interval between sending gossip messages. By lowering this value (more frequent) gossip messages are propagated across the cluster more quickly at the expense of increased bandwidth.").Default(cluster.DefaultGossipInterval.String()).Duration()
207		pushPullInterval     = kingpin.Flag("cluster.pushpull-interval", "Interval for gossip state syncs. Setting this interval lower (more frequent) will increase convergence speeds across larger clusters at the expense of increased bandwidth usage.").Default(cluster.DefaultPushPullInterval.String()).Duration()
208		tcpTimeout           = kingpin.Flag("cluster.tcp-timeout", "Timeout for establishing a stream connection with a remote node for a full state sync, and for stream read and write operations.").Default(cluster.DefaultTcpTimeout.String()).Duration()
209		probeTimeout         = kingpin.Flag("cluster.probe-timeout", "Timeout to wait for an ack from a probed node before assuming it is unhealthy. This should be set to 99-percentile of RTT (round-trip time) on your network.").Default(cluster.DefaultProbeTimeout.String()).Duration()
210		probeInterval        = kingpin.Flag("cluster.probe-interval", "Interval between random node probes. Setting this lower (more frequent) will cause the cluster to detect failed nodes more quickly at the expense of increased bandwidth usage.").Default(cluster.DefaultProbeInterval.String()).Duration()
211		settleTimeout        = kingpin.Flag("cluster.settle-timeout", "Maximum time to wait for cluster connections to settle before evaluating notifications.").Default(cluster.DefaultPushPullInterval.String()).Duration()
212		reconnectInterval    = kingpin.Flag("cluster.reconnect-interval", "Interval between attempting to reconnect to lost peers.").Default(cluster.DefaultReconnectInterval.String()).Duration()
213		peerReconnectTimeout = kingpin.Flag("cluster.reconnect-timeout", "Length of time to attempt to reconnect to a lost peer.").Default(cluster.DefaultReconnectTimeout.String()).Duration()
214	)
215
216	promlogflag.AddFlags(kingpin.CommandLine, &promlogConfig)
217	kingpin.CommandLine.UsageWriter(os.Stdout)
218
219	kingpin.Version(version.Print("alertmanager"))
220	kingpin.CommandLine.GetFlag("help").Short('h')
221	kingpin.Parse()
222
223	logger := promlog.New(&promlogConfig)
224
225	level.Info(logger).Log("msg", "Starting Alertmanager", "version", version.Info())
226	level.Info(logger).Log("build_context", version.BuildContext())
227
228	err := os.MkdirAll(*dataDir, 0777)
229	if err != nil {
230		level.Error(logger).Log("msg", "Unable to create data directory", "err", err)
231		return 1
232	}
233
234	var peer *cluster.Peer
235	if *clusterBindAddr != "" {
236		peer, err = cluster.Create(
237			log.With(logger, "component", "cluster"),
238			prometheus.DefaultRegisterer,
239			*clusterBindAddr,
240			*clusterAdvertiseAddr,
241			*peers,
242			true,
243			*pushPullInterval,
244			*gossipInterval,
245			*tcpTimeout,
246			*probeTimeout,
247			*probeInterval,
248		)
249		if err != nil {
250			level.Error(logger).Log("msg", "unable to initialize gossip mesh", "err", err)
251			return 1
252		}
253		clusterEnabled.Set(1)
254	}
255
256	stopc := make(chan struct{})
257	var wg sync.WaitGroup
258	wg.Add(1)
259
260	notificationLogOpts := []nflog.Option{
261		nflog.WithRetention(*retention),
262		nflog.WithSnapshot(filepath.Join(*dataDir, "nflog")),
263		nflog.WithMaintenance(15*time.Minute, stopc, wg.Done),
264		nflog.WithMetrics(prometheus.DefaultRegisterer),
265		nflog.WithLogger(log.With(logger, "component", "nflog")),
266	}
267
268	notificationLog, err := nflog.New(notificationLogOpts...)
269	if err != nil {
270		level.Error(logger).Log("err", err)
271		return 1
272	}
273	if peer != nil {
274		c := peer.AddState("nfl", notificationLog, prometheus.DefaultRegisterer)
275		notificationLog.SetBroadcast(c.Broadcast)
276	}
277
278	marker := types.NewMarker(prometheus.DefaultRegisterer)
279
280	silenceOpts := silence.Options{
281		SnapshotFile: filepath.Join(*dataDir, "silences"),
282		Retention:    *retention,
283		Logger:       log.With(logger, "component", "silences"),
284		Metrics:      prometheus.DefaultRegisterer,
285	}
286
287	silences, err := silence.New(silenceOpts)
288	if err != nil {
289		level.Error(logger).Log("err", err)
290		return 1
291	}
292	if peer != nil {
293		c := peer.AddState("sil", silences, prometheus.DefaultRegisterer)
294		silences.SetBroadcast(c.Broadcast)
295	}
296
297	// Start providers before router potentially sends updates.
298	wg.Add(1)
299	go func() {
300		silences.Maintenance(15*time.Minute, filepath.Join(*dataDir, "silences"), stopc)
301		wg.Done()
302	}()
303
304	defer func() {
305		close(stopc)
306		wg.Wait()
307	}()
308
309	// Peer state listeners have been registered, now we can join and get the initial state.
310	if peer != nil {
311		err = peer.Join(
312			*reconnectInterval,
313			*peerReconnectTimeout,
314		)
315		if err != nil {
316			level.Warn(logger).Log("msg", "unable to join gossip mesh", "err", err)
317		}
318		ctx, cancel := context.WithTimeout(context.Background(), *settleTimeout)
319		defer func() {
320			cancel()
321			if err := peer.Leave(10 * time.Second); err != nil {
322				level.Warn(logger).Log("msg", "unable to leave gossip mesh", "err", err)
323			}
324		}()
325		go peer.Settle(ctx, *gossipInterval*10)
326	}
327
328	alerts, err := mem.NewAlerts(context.Background(), marker, *alertGCInterval, nil, logger)
329	if err != nil {
330		level.Error(logger).Log("err", err)
331		return 1
332	}
333	defer alerts.Close()
334
335	var disp *dispatch.Dispatcher
336	defer disp.Stop()
337
338	groupFn := func(routeFilter func(*dispatch.Route) bool, alertFilter func(*types.Alert, time.Time) bool) (dispatch.AlertGroups, map[model.Fingerprint][]string) {
339		return disp.Groups(routeFilter, alertFilter)
340	}
341
342	// An interface value that holds a nil concrete value is non-nil.
343	// Therefore we explicly pass an empty interface, to detect if the
344	// cluster is not enabled in notify.
345	var clusterPeer cluster.ClusterPeer
346	if peer != nil {
347		clusterPeer = peer
348	}
349
350	api, err := api.New(api.Options{
351		Alerts:      alerts,
352		Silences:    silences,
353		StatusFunc:  marker.Status,
354		Peer:        clusterPeer,
355		Timeout:     *httpTimeout,
356		Concurrency: *getConcurrency,
357		Logger:      log.With(logger, "component", "api"),
358		Registry:    prometheus.DefaultRegisterer,
359		GroupFunc:   groupFn,
360	})
361
362	if err != nil {
363		level.Error(logger).Log("err", errors.Wrap(err, "failed to create API"))
364		return 1
365	}
366
367	amURL, err := extURL(logger, os.Hostname, *listenAddress, *externalURL)
368	if err != nil {
369		level.Error(logger).Log("msg", "failed to determine external URL", "err", err)
370		return 1
371	}
372	level.Debug(logger).Log("externalURL", amURL.String())
373
374	waitFunc := func() time.Duration { return 0 }
375	if peer != nil {
376		waitFunc = clusterWait(peer, *peerTimeout)
377	}
378	timeoutFunc := func(d time.Duration) time.Duration {
379		if d < notify.MinTimeout {
380			d = notify.MinTimeout
381		}
382		return d + waitFunc()
383	}
384
385	var (
386		inhibitor *inhibit.Inhibitor
387		tmpl      *template.Template
388	)
389
390	dispMetrics := dispatch.NewDispatcherMetrics(false, prometheus.DefaultRegisterer)
391	pipelineBuilder := notify.NewPipelineBuilder(prometheus.DefaultRegisterer)
392	configLogger := log.With(logger, "component", "configuration")
393	configCoordinator := config.NewCoordinator(
394		*configFile,
395		prometheus.DefaultRegisterer,
396		configLogger,
397	)
398	configCoordinator.Subscribe(func(conf *config.Config) error {
399		tmpl, err = template.FromGlobs(conf.Templates...)
400		if err != nil {
401			return errors.Wrap(err, "failed to parse templates")
402		}
403		tmpl.ExternalURL = amURL
404
405		// Build the routing tree and record which receivers are used.
406		routes := dispatch.NewRoute(conf.Route, nil)
407		activeReceivers := make(map[string]struct{})
408		routes.Walk(func(r *dispatch.Route) {
409			activeReceivers[r.RouteOpts.Receiver] = struct{}{}
410		})
411
412		// Build the map of receiver to integrations.
413		receivers := make(map[string][]notify.Integration, len(activeReceivers))
414		var integrationsNum int
415		for _, rcv := range conf.Receivers {
416			if _, found := activeReceivers[rcv.Name]; !found {
417				// No need to build a receiver if no route is using it.
418				level.Info(configLogger).Log("msg", "skipping creation of receiver not referenced by any route", "receiver", rcv.Name)
419				continue
420			}
421			integrations, err := buildReceiverIntegrations(rcv, tmpl, logger)
422			if err != nil {
423				return err
424			}
425			// rcv.Name is guaranteed to be unique across all receivers.
426			receivers[rcv.Name] = integrations
427			integrationsNum += len(integrations)
428		}
429
430		// Build the map of time interval names to mute time definitions.
431		muteTimes := make(map[string][]timeinterval.TimeInterval, len(conf.MuteTimeIntervals))
432		for _, ti := range conf.MuteTimeIntervals {
433			muteTimes[ti.Name] = ti.TimeIntervals
434		}
435
436		inhibitor.Stop()
437		disp.Stop()
438
439		inhibitor = inhibit.NewInhibitor(alerts, conf.InhibitRules, marker, logger)
440		silencer := silence.NewSilencer(silences, marker, logger)
441
442		// An interface value that holds a nil concrete value is non-nil.
443		// Therefore we explicly pass an empty interface, to detect if the
444		// cluster is not enabled in notify.
445		var pipelinePeer notify.Peer
446		if peer != nil {
447			pipelinePeer = peer
448		}
449
450		pipeline := pipelineBuilder.New(
451			receivers,
452			waitFunc,
453			inhibitor,
454			silencer,
455			muteTimes,
456			notificationLog,
457			pipelinePeer,
458		)
459		configuredReceivers.Set(float64(len(activeReceivers)))
460		configuredIntegrations.Set(float64(integrationsNum))
461
462		api.Update(conf, func(labels model.LabelSet) {
463			inhibitor.Mutes(labels)
464			silencer.Mutes(labels)
465		})
466
467		disp = dispatch.NewDispatcher(alerts, routes, pipeline, marker, timeoutFunc, nil, logger, dispMetrics)
468		routes.Walk(func(r *dispatch.Route) {
469			if r.RouteOpts.RepeatInterval > *retention {
470				level.Warn(configLogger).Log(
471					"msg",
472					"repeat_interval is greater than the data retention period. It can lead to notifications being repeated more often than expected.",
473					"repeat_interval",
474					r.RouteOpts.RepeatInterval,
475					"retention",
476					*retention,
477					"route",
478					r.Key(),
479				)
480			}
481		})
482
483		go disp.Run()
484		go inhibitor.Run()
485
486		return nil
487	})
488
489	if err := configCoordinator.Reload(); err != nil {
490		return 1
491	}
492
493	// Make routePrefix default to externalURL path if empty string.
494	if *routePrefix == "" {
495		*routePrefix = amURL.Path
496	}
497	*routePrefix = "/" + strings.Trim(*routePrefix, "/")
498	level.Debug(logger).Log("routePrefix", *routePrefix)
499
500	router := route.New().WithInstrumentation(instrumentHandler)
501	if *routePrefix != "/" {
502		router.Get("/", func(w http.ResponseWriter, r *http.Request) {
503			http.Redirect(w, r, *routePrefix, http.StatusFound)
504		})
505		router = router.WithPrefix(*routePrefix)
506	}
507
508	webReload := make(chan chan error)
509
510	ui.Register(router, webReload, logger)
511
512	mux := api.Register(router, *routePrefix)
513
514	srv := &http.Server{Addr: *listenAddress, Handler: mux}
515	srvc := make(chan struct{})
516
517	go func() {
518		level.Info(logger).Log("msg", "Listening", "address", *listenAddress)
519		if err := web.ListenAndServe(srv, *webConfig, logger); err != http.ErrServerClosed {
520			level.Error(logger).Log("msg", "Listen error", "err", err)
521			close(srvc)
522		}
523		defer func() {
524			if err := srv.Close(); err != nil {
525				level.Error(logger).Log("msg", "Error on closing the server", "err", err)
526			}
527		}()
528	}()
529
530	var (
531		hup      = make(chan os.Signal, 1)
532		hupReady = make(chan bool)
533		term     = make(chan os.Signal, 1)
534	)
535	signal.Notify(hup, syscall.SIGHUP)
536	signal.Notify(term, os.Interrupt, syscall.SIGTERM)
537
538	go func() {
539		<-hupReady
540		for {
541			select {
542			case <-hup:
543				// ignore error, already logged in `reload()`
544				_ = configCoordinator.Reload()
545			case errc := <-webReload:
546				errc <- configCoordinator.Reload()
547			}
548		}
549	}()
550
551	// Wait for reload or termination signals.
552	close(hupReady) // Unblock SIGHUP handler.
553
554	for {
555		select {
556		case <-term:
557			level.Info(logger).Log("msg", "Received SIGTERM, exiting gracefully...")
558			return 0
559		case <-srvc:
560			return 1
561		}
562	}
563}
564
565// clusterWait returns a function that inspects the current peer state and returns
566// a duration of one base timeout for each peer with a higher ID than ourselves.
567func clusterWait(p *cluster.Peer, timeout time.Duration) func() time.Duration {
568	return func() time.Duration {
569		return time.Duration(p.Position()) * timeout
570	}
571}
572
573func extURL(logger log.Logger, hostnamef func() (string, error), listen, external string) (*url.URL, error) {
574	if external == "" {
575		hostname, err := hostnamef()
576		if err != nil {
577			return nil, err
578		}
579		_, port, err := net.SplitHostPort(listen)
580		if err != nil {
581			return nil, err
582		}
583		if port == "" {
584			level.Warn(logger).Log("msg", "no port found for listen address", "address", listen)
585		}
586
587		external = fmt.Sprintf("http://%s:%s/", hostname, port)
588	}
589
590	u, err := url.Parse(external)
591	if err != nil {
592		return nil, err
593	}
594	if u.Scheme != "http" && u.Scheme != "https" {
595		return nil, errors.Errorf("%q: invalid %q scheme, only 'http' and 'https' are supported", u.String(), u.Scheme)
596	}
597
598	ppref := strings.TrimRight(u.Path, "/")
599	if ppref != "" && !strings.HasPrefix(ppref, "/") {
600		ppref = "/" + ppref
601	}
602	u.Path = ppref
603
604	return u, nil
605}
606