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