Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cut a v2 of cmdutil (& deps) that replaces logrus with slog #215

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
6 changes: 6 additions & 0 deletions cmdutil/v2/debug/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package debug

// Config describes the configurable parameters for debugging.
type Config struct {
Port int `env:"DEBUG_PORT,default=9999"`
}
74 changes: 74 additions & 0 deletions cmdutil/v2/debug/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Package debug wraps the gops agent for use as a cmdutil-compatible Server.
//
// The debug server will be started on DEBUG_PORT (default 9999). Get a stack
// trace, profile memory, etc. by running the gops command line connected to
// locahost:9999 like:
//
// $ gops stack localhost:9999
// goroutine 50 [running]:
// runtime/pprof.writeGoroutineStacks(0x4a18a20, 0xc000010138, 0x0, 0x0)
// /usr/local/Cellar/go/1.13.5/libexec/src/runtime/pprof/pprof.go:679 +0x9d
// runtime/pprof.writeGoroutine(0x4a18a20, 0xc000010138, 0x2, 0x0, 0x0)
// ...
//
// Learn more about gops at https://github.com/google/gops.
package debug

import (
"fmt"

"log/slog"

"github.com/google/gops/agent"
)

// New inializes a debug server listening on the provided port.
//
// Connect to the debug server with gops:
//
// gops stack localhost:PORT
func New(l *slog.Logger, port int) *Server {
return &Server{
logger: l,
addr: fmt.Sprintf("127.0.0.1:%d", port),
done: make(chan struct{}),
}
}

// Server wraps a gops server for easy use with oklog/group.
type Server struct {
logger *slog.Logger
addr string
done chan struct{}
}

// Run starts the debug server.
//
// It implements oklog group's runFn.
func (s *Server) Run() error {
s.logger.Info("",
"at", "binding",
"service", "debug",
"addr", s.addr,
)

opts := agent.Options{
Addr: s.addr,
ShutdownCleanup: false,
}
if err := agent.Listen(opts); err != nil {
return err
}

<-s.done
return nil
}

// Stop shuts down the debug server.
//
// It implements oklog group's interruptFn.
func (s *Server) Stop(_ error) {
agent.Close()

close(s.done)
}
8 changes: 8 additions & 0 deletions cmdutil/v2/health/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package health

// Config can be used in a service's main config struct to load the
// healthcheck port from the environment.
type Config struct {
Port int `env:"HEROKU_ROUTER_HEALTHCHECK_PORT,default=6000"`
MetricInterval int `env:"HEROKU_HEALTH_METRIC_INTERVAL,default=5"`
}
47 changes: 47 additions & 0 deletions cmdutil/v2/health/serve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Package health provides cmdutil-compatible healthcheck utilities.
package health

import (
"context"
"fmt"
"log/slog"
"time"

"github.com/heroku/x/cmdutil"
"github.com/heroku/x/go-kit/metrics"
"github.com/heroku/x/tickgroup"
"github.com/heroku/x/v2/healthcheck"
)

// NewTCPServer returns a cmdutil.Server which emits a health metric whenever a TCP
// connection is opened on the configured port.
func NewTCPServer(logger *slog.Logger, provider metrics.Provider, cfg Config) cmdutil.Server {
healthLogger := logger.With(slog.String("service", "healthcheck"))
healthLogger.With(
slog.String("at", "binding"),
slog.Int("port", cfg.Port),
).Info("")

return healthcheck.NewTCPServer(healthLogger, provider, fmt.Sprintf(":%d", cfg.Port))
}

// NewTickingServer returns a cmdutil.Server which emits a health metric every
// cfg.MetricInterval seconds.
func NewTickingServer(logger *slog.Logger, provider metrics.Provider, cfg Config) cmdutil.Server {
logger.With(
slog.String("service", "healthcheck-worker"),
slog.String("at", "starting"),
slog.Int("interval", cfg.MetricInterval),
).Info("")

c := provider.NewCounter("health")

return cmdutil.NewContextServer(func(ctx context.Context) error {
g := tickgroup.New(ctx)
g.Go(time.Duration(cfg.MetricInterval)*time.Second, func() error {
c.Add(1)
return nil
})
return g.Wait()
})
}
90 changes: 90 additions & 0 deletions cmdutil/v2/service/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package service

import (
"net/url"
"time"

"github.com/heroku/x/cmdutil/debug"
"github.com/heroku/x/cmdutil/metrics"
"github.com/heroku/x/cmdutil/oc"
"github.com/heroku/x/cmdutil/rollbar"
"github.com/heroku/x/cmdutil/v2/svclog"
)

// standardConfig is used when service.New is called.
type standardConfig struct {
Debug debug.Config
Logger svclog.Config
Metrics metrics.Config
Rollbar rollbar.Config
OpenCensus oc.Config
}

// platformConfig is used by HTTP and captures
// config related to running on the Heroku platform.
type platformConfig struct {
// Port is the primary port to listen on when running as a normal platform
// app.
Port int `env:"PORT"`

// AdditionalPort defines an additional port to listen on in addition to the
// primary port for use with dyno-dyno networking.
AdditionalPort int `env:"ADDITIONAL_PORT"`
}

// bypassConfig is used by HTTP and GRPC and captures
// config related to running with the router bypass
// feature on the Heroku platform.
type bypassConfig struct {
// The following ports, TLS, and ACM configurations are set when running with
// spaces-router-bypass enabled.
InsecurePort int `env:"HEROKU_ROUTER_HTTP_PORT"`
SecurePort int `env:"HEROKU_ROUTER_HTTPS_PORT"`
HealthPort int `env:"HEROKU_ROUTER_HEALTHCHECK_PORT"`
TLS tlsConfig
ACMEHTTPValidationURL *url.URL `env:"ACME_HTTP_VALIDATION_URL,default=https://va-acm.runtime.herokai.com/challenge"`
}

// tlsConfig is used by bypassConfig and captures config related to TLS
// when not running on the Heroku platform.
type tlsConfig struct {
// These environement variables are automatically set by Foundation in
// relation to Let's Encrypt certificates.
ServerCert string `env:"SERVER_CERT"`
ServerKey string `env:"SERVER_KEY"`

// Used by GRPC services, set by terraform.
ServerCACert string `env:"SERVER_CA_CERT"`

UseAutocert bool `env:"HTTPS_USE_AUTOCERT"`
}

// spaceCAConfig is used by grpcConfig and captures config related to
// common runtime services whose certs are generated using the
// spaceCA.
type spaceCAConfig struct {
// Used by GRPC services in new mTLS cert generation where services
// generate their certificates using the SpaceCA.
RootCACert string `env:"HEROKU_SPACE_CA_ROOT_CERT"`
SpaceCACert string `env:"HEROKU_SPACE_CA_CERT"`
SpaceCAKey string `env:"HEROKU_SPACE_CA_KEY"`

// RootCACertAlternate is set during a root certificate rotation and must be
// installed into the certificate pool to ensure that services are able to
// communicate while the rotation is in progress.
RootCACertAlternate string `env:"HEROKU_SPACE_CA_ROOT_CERT_ALTERNATE"`

// Switch which will determine whether an app generates their cert
// using the SpaceCA.
UseSpaceCA bool `env:"USE_SPACE_CA,default=false"`

// Domain of the service used in the generation of the cert
Domain string `env:"DOMAIN"`
}

type timeoutConfig struct {
Read time.Duration `env:"SERVER_READ_TIMEOUT"`
ReadHeader time.Duration `env:"SERVER_READ_HEADER_TIMEOUT,default=30s"`
Write time.Duration `env:"SERVER_WRITE_TIMEOUT"`
Idle time.Duration `env:"SERVER_IDLE_TIMEOUT"`
}
35 changes: 35 additions & 0 deletions cmdutil/v2/service/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package service

import (
"testing"

"github.com/joeshaw/envdecode"
)

// httpConfig should be decodable with nothing required.
//
// This isn't a perfect test as there may be something set
// in the test environment that is used by httpConfig but it
// should help ensure at least more specific items like
// SERVER_CA_CERT are not required.
func TestDecodeHTTPConfig(t *testing.T) {
var cfg httpConfig

if err := envdecode.StrictDecode(&cfg); err != nil {
t.Fatal(err)
}
}

// grpcConfig should be decodable with nothing required.
//
// This isn't a perfect test as there may be something set
// in the test environment that is used by grpcConfig but it
// should help ensure at least more specific items like
// SERVER_CA_CERT are not required.
func TestDecodeGRPCConfig(t *testing.T) {
var cfg grpcConfig

if err := envdecode.StrictDecode(&cfg); err != nil {
t.Fatal(err)
}
}
3 changes: 3 additions & 0 deletions cmdutil/v2/service/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package service provides standardized command and HTTP setup by smartly
// composing the other cmdutil packages based on environment variables.
package service
30 changes: 30 additions & 0 deletions cmdutil/v2/service/http_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package service_test

import (
"io"
"net/http"
"time"

"github.com/heroku/x/cmdutil/v2/service"
)

func ExampleWithHTTPServerHook() {
var cfg struct {
Hello string `env:"HELLO,default=hello"`
}
svc := service.New(&cfg)

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = io.WriteString(w, cfg.Hello)
})

configureHTTP := func(s *http.Server) {
s.ReadTimeout = 10 * time.Second
}

svc.Add(service.HTTP(svc.Logger, svc.MetricsProvider, handler,
service.WithHTTPServerHook(configureHTTP),
))

svc.Run()
}
38 changes: 38 additions & 0 deletions cmdutil/v2/service/integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//go:build integration
// +build integration

package service

import (
"os"
"testing"

"github.com/heroku/x/cmdutil"
"github.com/heroku/x/go-kit/metrics/l2met"
)

func TestPanicReporting(t *testing.T) {

os.Setenv("APP_NAME", "test-app")
os.Setenv("DEPLOY", "test")

t.Cleanup(func() {
os.Unsetenv("APP_NAME")
os.Unsetenv("DEPLOY")
})

var cfg struct {
Val string `env:"TEST_VAL,default=test"`
}

s := New(&cfg)

f := func() error {
panic("test panic")
return nil
}

s.Add(cmdutil.ServerFunc(f))
s.Run()

}
Loading
Loading