Repository: beatlabs/patron (go)

Issue:
Patron service set up is broken
<!--
Welcome to the patron framework project.

- Please search for existing issues to avoid creating duplicate bugs/feature requests.
- Please be respectful and considerate of others when commenting on issues.
- Please provide as much information as possible, so we all understand the issue.

-->

## Is your feature request related to a problem? Please describe

With the removal of the builder pattern, the setup of Patron has become a one-liner with the `New()` function.
Previously though, by using the builder, we created a builder first which set up everything and then we created the service and ran it.

The result is that logging and tracing are not set up correctly and the whole main function in the service is using the default logger.

## Is this a bug? Please provide steps to reproduce, a failing test etc.

Just by creating a service, the problem is apparent.

## Describe the solution

In order to avoid reintroducing the builder pattern again, we could keep the `New()` function which sets up a service (logging, tracing etc.) at the beginning of the main and provide the components to run in the `Run()` method. This would give us the flexibility of the functional option pattern without the extra builder struct. Another benefit is that we are now decoupling the HTTP component from the service. If the end user would like to set up a worker without an HTTP endpoint this was not possible because Patron by default created an HTTP service. Somebody might want to start without or even instead HTTP start a gRPC one.


Pull request:
Service refactoring
<!--
Thanks for taking precious time for making a PR.

Before creating a pull request, please make sure:
- Your PR solves one problem for which an issue exist and a solution has been discussed
- You have read the guide for contributing
  - See https://github.com/beatlabs/patron/blob/master/README.md#how-to-contribute
- You signed all your commits (otherwise we won't be able to merge the PR)
  - See https://github.com/beatlabs/patron/blob/master/SIGNYOURWORK.md
- You added unit tests for the new functionality
- You mention in the PR description which issue it is addressing, e.g. "Resolves #123"
-->

## Which problem is this PR solving?

Closes #597.

## Short description of the changes

- 


Patch:
diff --git a/component/http/component.go b/component/http/component.go
index f74ffca71..79588e419 100644
--- a/component/http/component.go
+++ b/component/http/component.go
@@ -5,6 +5,8 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"os"
+	"strconv"
 	"sync"
 	"time"
 
@@ -15,12 +17,12 @@ import (
 )
 
 const (
-	httpPort            = 50000
-	httpReadTimeout     = 30 * time.Second
-	httpWriteTimeout    = 60 * time.Second
-	httpIdleTimeout     = 240 * time.Second
-	shutdownGracePeriod = 5 * time.Second
-	deflateLevel        = 6
+	defaultPort                = 50000
+	defaultReadTimeout         = 30 * time.Second
+	defaultWriteTimeout        = 60 * time.Second
+	defaultIdleTimeout         = 240 * time.Second
+	defaultShutdownGracePeriod = 5 * time.Second
+	defaultDeflateLevel        = 6
 )
 
 var (
@@ -109,11 +111,67 @@ func (c *Component) createHTTPServer() *http.Server {
 		Addr:         fmt.Sprintf(":%d", c.httpPort),
 		ReadTimeout:  c.httpReadTimeout,
 		WriteTimeout: c.httpWriteTimeout,
-		IdleTimeout:  httpIdleTimeout,
+		IdleTimeout:  defaultIdleTimeout,
 		Handler:      routerAfterMiddleware,
 	}
 }
 
+func port() (int, error) {
+	port, ok := os.LookupEnv("PATRON_HTTP_DEFAULT_PORT")
+	if !ok {
+		log.Debugf("using default port %d", defaultPort)
+		return defaultPort, nil
+	}
+	portVal, err := strconv.ParseInt(port, 10, 32)
+	if err != nil {
+		return 0, fmt.Errorf("env var for HTTP default port is not valid: %w", err)
+	}
+	log.Debugf("using port %d", portVal)
+	return int(portVal), nil
+}
+
+func readTimeout() (time.Duration, error) {
+	httpTimeout, ok := os.LookupEnv("PATRON_HTTP_READ_TIMEOUT")
+	if !ok {
+		log.Debugf("using default read timeout %s", defaultReadTimeout)
+		return defaultReadTimeout, nil
+	}
+	timeout, err := time.ParseDuration(httpTimeout)
+	if err != nil {
+		return 0, fmt.Errorf("env var for HTTP read timeout is not valid: %w", err)
+	}
+	log.Debugf("using read timeout %s", timeout)
+	return timeout, nil
+}
+
+func writeTimeout() (time.Duration, error) {
+	httpTimeout, ok := os.LookupEnv("PATRON_HTTP_WRITE_TIMEOUT")
+	if !ok {
+		log.Debugf("using default write timeout %s", defaultWriteTimeout)
+		return defaultWriteTimeout, nil
+	}
+	timeout, err := time.ParseDuration(httpTimeout)
+	if err != nil {
+		return 0, fmt.Errorf("env var for HTTP write timeout is not valid: %w", err)
+	}
+	log.Debugf("using write timeout %s", timeout)
+	return timeout, nil
+}
+
+func deflateLevel() (int, error) {
+	deflateLevel, ok := os.LookupEnv("PATRON_COMPRESSION_DEFLATE_LEVEL")
+	if !ok {
+		log.Debugf("using default deflate level %d", defaultDeflateLevel)
+		return defaultDeflateLevel, nil
+	}
+	deflateLevelInt, err := strconv.Atoi(deflateLevel)
+	if err != nil {
+		return 0, fmt.Errorf("env var for HTTP deflate level is not valid: %w", err)
+	}
+	log.Debugf("using deflate level %d", deflateLevelInt)
+	return deflateLevelInt, nil
+}
+
 // Builder gathers all required and optional properties, in order
 // to construct an HTTP component.
 //
@@ -142,18 +200,39 @@ type Builder struct {
 // Deprecated: Please use the new v2 package.
 // This package is frozen and no new functionality will be added.
 func NewBuilder() *Builder {
-	var errs []error
+	var ee []error
+
+	port, err := port()
+	if err != nil {
+		ee = append(ee, err)
+	}
+
+	readTimeout, err := readTimeout()
+	if err != nil {
+		ee = append(ee, err)
+	}
+
+	writeTimeout, err := writeTimeout()
+	if err != nil {
+		ee = append(ee, err)
+	}
+
+	deflateLevel, err := deflateLevel()
+	if err != nil {
+		ee = append(ee, err)
+	}
+
 	return &Builder{
 		ac:                  DefaultAliveCheck,
 		rc:                  DefaultReadyCheck,
-		httpPort:            httpPort,
-		httpReadTimeout:     httpReadTimeout,
-		httpWriteTimeout:    httpWriteTimeout,
+		httpPort:            port,
+		httpReadTimeout:     readTimeout,
+		httpWriteTimeout:    writeTimeout,
 		deflateLevel:        deflateLevel,
 		uncompressedPaths:   []string{MetricsPath, AlivePath, ReadyPath},
-		shutdownGracePeriod: shutdownGracePeriod,
+		shutdownGracePeriod: defaultShutdownGracePeriod,
 		routesBuilder:       NewRoutesBuilder(),
-		errors:              errs,
+		errors:              ee,
 	}
 }
 
diff --git a/component/http/v2/component.go b/component/http/v2/component.go
index fea322f4c..4d6d8e467 100644
--- a/component/http/v2/component.go
+++ b/component/http/v2/component.go
@@ -6,6 +6,8 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"os"
+	"strconv"
 	"sync"
 	"time"
 
@@ -13,14 +15,56 @@ import (
 )
 
 const (
-	port                = 50000
-	readTimeout         = 30 * time.Second
-	writeTimeout        = 60 * time.Second
-	idleTimeout         = 240 * time.Second
-	handlerTimeout      = 59 * time.Second // should be smaller than write timeout
-	shutdownGracePeriod = 5 * time.Second
+	defaultPort                = 50000
+	defaultReadTimeout         = 30 * time.Second
+	defaultWriteTimeout        = 60 * time.Second
+	defaultIdleTimeout         = 240 * time.Second
+	defaultHandlerTimeout      = 59 * time.Second // should be smaller than write timeout
+	defaultShutdownGracePeriod = 5 * time.Second
 )
 
+func port() (int, error) {
+	port, ok := os.LookupEnv("PATRON_HTTP_DEFAULT_PORT")
+	if !ok {
+		log.Debugf("using default port %d", defaultPort)
+		return defaultPort, nil
+	}
+	portVal, err := strconv.ParseInt(port, 10, 32)
+	if err != nil {
+		return 0, fmt.Errorf("env var for HTTP default port is not valid: %w", err)
+	}
+	log.Debugf("using port %d", portVal)
+	return int(portVal), nil
+}
+
+func readTimeout() (time.Duration, error) {
+	httpTimeout, ok := os.LookupEnv("PATRON_HTTP_READ_TIMEOUT")
+	if !ok {
+		log.Debugf("using default read timeout %s", defaultReadTimeout)
+		return defaultReadTimeout, nil
+	}
+	timeout, err := time.ParseDuration(httpTimeout)
+	if err != nil {
+		return 0, fmt.Errorf("env var for HTTP read timeout is not valid: %w", err)
+	}
+	log.Debugf("using read timeout %s", timeout)
+	return timeout, nil
+}
+
+func writeTimeout() (time.Duration, error) {
+	httpTimeout, ok := os.LookupEnv("PATRON_HTTP_WRITE_TIMEOUT")
+	if !ok {
+		log.Debugf("using default write timeout %s", defaultWriteTimeout)
+		return defaultWriteTimeout, nil
+	}
+	timeout, err := time.ParseDuration(httpTimeout)
+	if err != nil {
+		return 0, fmt.Errorf("env var for HTTP write timeout is not valid: %w", err)
+	}
+	log.Debugf("using write timeout %s", timeout)
+	return timeout, nil
+}
+
 // Component implementation of an HTTP router.
 type Component struct {
 	port                int
@@ -40,12 +84,27 @@ func New(handler http.Handler, oo ...OptionFunc) (*Component, error) {
 		return nil, errors.New("handler is nil")
 	}
 
+	port, err := port()
+	if err != nil {
+		return nil, err
+	}
+
+	readTimeout, err := readTimeout()
+	if err != nil {
+		return nil, err
+	}
+
+	writeTimeout, err := writeTimeout()
+	if err != nil {
+		return nil, err
+	}
+
 	cmp := &Component{
 		port:                port,
 		readTimeout:         readTimeout,
 		writeTimeout:        writeTimeout,
-		shutdownGracePeriod: shutdownGracePeriod,
-		handlerTimeout:      handlerTimeout,
+		shutdownGracePeriod: defaultShutdownGracePeriod,
+		handlerTimeout:      defaultHandlerTimeout,
 		handler:             handler,
 	}
 
@@ -83,7 +142,7 @@ func (c *Component) createHTTPServer() *http.Server {
 		Addr:         fmt.Sprintf(":%d", c.port),
 		ReadTimeout:  c.readTimeout,
 		WriteTimeout: c.writeTimeout,
-		IdleTimeout:  idleTimeout,
+		IdleTimeout:  defaultIdleTimeout,
 		Handler:      http.TimeoutHandler(c.handler, c.handlerTimeout, ""),
 	}
 }
diff --git a/component/sqs/component.go b/component/sqs/component.go
index acdcf9e18..41dc35f03 100644
--- a/component/sqs/component.go
+++ b/component/sqs/component.go
@@ -243,7 +243,7 @@ func (c *Component) consume(ctx context.Context, chErr chan error) {
 			return
 		}
 
-		logger.Debugf("Consume: received %d messages", len(output.Messages))
+		logger.Debugf("consume: received %d messages", len(output.Messages))
 		messageCountInc(c.queue.name, fetchedMessageState, false, len(output.Messages))
 
 		if len(output.Messages) == 0 {
diff --git a/examples/service/http.go b/examples/service/http.go
index a6415fbb1..61e071653 100644
--- a/examples/service/http.go
+++ b/examples/service/http.go
@@ -5,12 +5,13 @@ import (
 	"io"
 	"net/http"
 
+	"github.com/beatlabs/patron"
 	v2 "github.com/beatlabs/patron/component/http/v2"
 	"github.com/beatlabs/patron/component/http/v2/router/httprouter"
 	"github.com/beatlabs/patron/log"
 )
 
-func createHttpRouter() (http.Handler, error) {
+func createHttpRouter() (patron.Component, error) {
 	handler := func(rw http.ResponseWriter, req *http.Request) {
 		body, err := io.ReadAll(req.Body)
 		if err != nil {
@@ -36,5 +37,5 @@ func createHttpRouter() (http.Handler, error) {
 		return nil, fmt.Errorf("failed to create http router: %w", err)
 	}
 
-	return router, err
+	return v2.New(router)
 }
diff --git a/examples/service/main.go b/examples/service/main.go
index 1ba30cf9e..9739f04a8 100644
--- a/examples/service/main.go
+++ b/examples/service/main.go
@@ -15,7 +15,7 @@ const (
 )
 
 func init() {
-	err := os.Setenv("PATRON_LOG_LEVEL", "info")
+	err := os.Setenv("PATRON_LOG_LEVEL", "debug")
 	if err != nil {
 		log.Fatalf("failed to set log level env var: %v", err)
 	}
@@ -30,25 +30,30 @@ func init() {
 }
 
 func main() {
-	var options []patron.OptionFunc
+	ctx := context.Background()
+
+	service, err := patron.New(name, version, patron.WithTextLogger())
+	if err != nil {
+		log.Fatalf("failed to set up service: %v", err)
+	}
 
-	options = append(options, patron.WithTextLogger())
+	var components []patron.Component
 
 	// Setup HTTP
-	router, err := createHttpRouter()
+	cmp, err := createHttpRouter()
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	options = append(options, patron.WithRouter(router))
+	components = append(components, cmp)
 
 	// Setup gRPC
-	cmp, err := createGrpcServer()
+	cmp, err = createGrpcServer()
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	options = append(options, patron.WithComponents(cmp))
+	components = append(components, cmp)
 
 	// Setup Kafka
 	cmp, err = createKafkaConsumer()
@@ -56,7 +61,7 @@ func main() {
 		log.Fatal(err)
 	}
 
-	options = append(options, patron.WithComponents(cmp))
+	components = append(components, cmp)
 
 	// Setup SQS
 	cmp, err = createSQSConsumer()
@@ -64,7 +69,7 @@ func main() {
 		log.Fatal(err)
 	}
 
-	options = append(options, patron.WithComponents(cmp))
+	components = append(components, cmp)
 
 	// Setup AMQP
 	cmp, err = createAMQPConsumer()
@@ -72,16 +77,9 @@ func main() {
 		log.Fatal(err)
 	}
 
-	options = append(options, patron.WithComponents(cmp))
-
-	ctx := context.Background()
-
-	service, err := patron.New(name, version, options...)
-	if err != nil {
-		log.Fatalf("failed to set up service: %v", err)
-	}
+	components = append(components, cmp)
 
-	err = service.Run(ctx)
+	err = service.Run(ctx, components...)
 	if err != nil {
 		log.Fatalf("failed to create and run service %v", err)
 	}
diff --git a/options.go b/options.go
index 99f136b14..1913af6fd 100644
--- a/options.go
+++ b/options.go
@@ -2,7 +2,6 @@ package patron
 
 import (
 	"errors"
-	"net/http"
 	"os"
 
 	"github.com/beatlabs/patron/log"
@@ -11,34 +10,6 @@ import (
 
 type OptionFunc func(svc *Service) error
 
-// WithRouter replaces the default v1 HTTP component with a new component v2 based on http.Handler.
-func WithRouter(handler http.Handler) OptionFunc {
-	return func(svc *Service) error {
-		if handler == nil {
-			return errors.New("provided router is nil")
-		}
-
-		log.Debug("router will be used with the v2 HTTP component")
-		svc.httpRouter = handler
-
-		return nil
-	}
-}
-
-// WithComponents adds custom components to the Patron Service.
-func WithComponents(cc ...Component) OptionFunc {
-	return func(svc *Service) error {
-		if len(cc) == 0 {
-			return errors.New("provided components slice was empty")
-		}
-
-		log.Debug("setting components")
-		svc.cps = append(svc.cps, cc...)
-
-		return nil
-	}
-}
-
 // WithSIGHUP adds a custom handler for handling WithSIGHUP.
 func WithSIGHUP(handler func()) OptionFunc {
 	return func(svc *Service) error {
diff --git a/prometheus.yml b/prometheus.yml
deleted file mode 100644
index 76dddadaa..000000000
--- a/prometheus.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-global:
-  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
-  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
-  # scrape_timeout is set to the global default (10s).
-
-# Alertmanager configuration
-alerting:
-  alertmanagers:
-  - static_configs:
-    - targets:
-      # - alertmanager:9093
-
-# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
-rule_files:
-  # - "first_rules.yml"
-  # - "second_rules.yml"
-
-# A scrape configuration for prometheus itself, second/main.go and thirs/main.go
-# Use the following command to reload the configuration
-# docker kill --signal="SIGHUP" <pod-id>
-scrape_configs:
-  - job_name: 'prometheus'
-    static_configs:
-    - targets: ['localhost:9090']
-  - job_name: 'kafka'
-    scrape_interval: 5s
-    static_configs:
-    - targets: ['host.docker.internal:50001']
-  - job_name: 'amqp'
-    scrape_interval: 5s
-    static_configs:
-    - targets: ['host.docker.internal:50002']
diff --git a/service.go b/service.go
index 350b1feae..8427baf56 100644
--- a/service.go
+++ b/service.go
@@ -4,17 +4,13 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"net/http"
 	"os"
 	"os/signal"
 	"strconv"
 	"strings"
 	"sync"
 	"syscall"
-	"time"
 
-	v2 "github.com/beatlabs/patron/component/http/v2"
-	"github.com/beatlabs/patron/component/http/v2/router/httprouter"
 	patronErrors "github.com/beatlabs/patron/errors"
 	"github.com/beatlabs/patron/log"
 	patronzerolog "github.com/beatlabs/patron/log/zerolog"
@@ -36,142 +32,100 @@ type Component interface {
 // Service is responsible for managing and setting up everything.
 // The Service will start by default an HTTP component in order to host management endpoint.
 type Service struct {
-	name              string
-	version           string
-	cps               []Component
-	termSig           chan os.Signal
-	sighupHandler     func()
-	uncompressedPaths []string
-	httpRouter        http.Handler
-	config            config
+	name          string
+	version       string
+	termSig       chan os.Signal
+	sighupHandler func()
+	config        config
 }
 
-func (s *Service) setupOSSignal() {
-	signal.Notify(s.termSig, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
-}
-
-func (s *Service) Run(ctx context.Context) error {
-	defer func() {
-		err := trace.Close()
-		if err != nil {
-			log.Errorf("failed to close trace %v", err)
-		}
-	}()
-	cctx, cnl := context.WithCancel(ctx)
-	chErr := make(chan error, len(s.cps))
-	wg := sync.WaitGroup{}
-	wg.Add(len(s.cps))
-	for _, cp := range s.cps {
-		go func(c Component) {
-			defer wg.Done()
-			chErr <- c.Run(cctx)
-		}(cp)
-	}
-
-	log.FromContext(ctx).Infof("Service %s started", s.name)
-	ee := make([]error, 0, len(s.cps))
-	ee = append(ee, s.waitTermination(chErr))
-	cnl()
-
-	wg.Wait()
-	close(chErr)
-
-	for err := range chErr {
-		ee = append(ee, err)
-	}
-	return patronErrors.Aggregate(ee...)
-}
-
-func (s *Service) createHTTPComponent() (Component, error) {
-	var err error
-	portVal := int64(50000)
-	port, ok := os.LookupEnv("PATRON_HTTP_DEFAULT_PORT")
-	if ok {
-		portVal, err = strconv.ParseInt(port, 10, 32)
-		if err != nil {
-			return nil, fmt.Errorf("env var for HTTP default port is not valid: %w", err)
-		}
+func New(name, version string, options ...OptionFunc) (*Service, error) {
+	if name == "" {
+		return nil, errors.New("name is required")
 	}
-	log.Debugf("creating default HTTP component at port %d", portVal)
-
-	deflateLevel, err := getHTTPDeflateLevel()
-	if err != nil {
-		return nil, err
+	if version == "" {
+		version = "dev"
 	}
 
-	readTimeout, err := getHTTPReadTimeout()
-	if err != nil {
-		return nil, err
+	s := &Service{
+		name:    name,
+		version: version,
+		termSig: make(chan os.Signal, 1),
+		sighupHandler: func() {
+			log.Debug("WithSIGHUP received: nothing setup")
+		},
+		config: config{
+			logger: patronzerolog.New(os.Stderr, getLogLevel(), nil),
+			fields: defaultLogFields(name, version),
+		},
 	}
 
-	writeTimeout, err := getHTTPWriteTimeout()
+	var err error
+	err = setupJaegerTracing(name, version)
 	if err != nil {
 		return nil, err
 	}
 
-	if s.httpRouter == nil {
-		routerOptions := make([]httprouter.OptionFunc, 0)
-		if deflateLevel != nil {
-			routerOptions = append(routerOptions, httprouter.WithDeflateLevel(*deflateLevel))
-		}
-
-		s.httpRouter, err = httprouter.New(routerOptions...)
+	optionErrors := make([]error, 0)
+	for _, option := range options {
+		err = option(s)
 		if err != nil {
-			return nil, err
+			optionErrors = append(optionErrors, err)
 		}
 	}
 
-	return s.createHTTPv2(int(portVal), readTimeout, writeTimeout)
-}
-
-func getHTTPReadTimeout() (*time.Duration, error) {
-	httpTimeout, ok := os.LookupEnv("PATRON_HTTP_READ_TIMEOUT")
-	if !ok {
-		return nil, nil
-	}
-	timeout, err := time.ParseDuration(httpTimeout)
-	if err != nil {
-		return nil, fmt.Errorf("env var for HTTP read timeout is not valid: %w", err)
+	if len(optionErrors) > 0 {
+		return nil, patronErrors.Aggregate(optionErrors...)
 	}
-	return &timeout, nil
-}
 
-func getHTTPWriteTimeout() (*time.Duration, error) {
-	httpTimeout, ok := os.LookupEnv("PATRON_HTTP_WRITE_TIMEOUT")
-	if !ok {
-		return nil, nil
-	}
-	timeout, err := time.ParseDuration(httpTimeout)
+	err = setupLogging(s.config.fields, s.config.logger)
 	if err != nil {
-		return nil, fmt.Errorf("env var for HTTP write timeout is not valid: %w", err)
+		return nil, err
 	}
-	return &timeout, nil
+
+	s.setupOSSignal()
+
+	return s, nil
 }
 
-func getHTTPDeflateLevel() (*int, error) {
-	deflateLevel, ok := os.LookupEnv("PATRON_COMPRESSION_DEFLATE_LEVEL")
-	if !ok {
-		return nil, nil
+func (s *Service) Run(ctx context.Context, components ...Component) error {
+	if len(components) == 0 || components[0] == nil {
+		return errors.New("components are empty or nil")
 	}
-	deflateLevelInt, err := strconv.Atoi(deflateLevel)
-	if err != nil {
-		return nil, fmt.Errorf("env var for HTTP deflate level is not valid: %w", err)
+
+	defer func() {
+		err := trace.Close()
+		if err != nil {
+			log.Errorf("failed to close trace %v", err)
+		}
+	}()
+	ctx, cnl := context.WithCancel(ctx)
+	chErr := make(chan error, len(components))
+	wg := sync.WaitGroup{}
+	wg.Add(len(components))
+	for _, cp := range components {
+		go func(c Component) {
+			defer wg.Done()
+			chErr <- c.Run(ctx)
+		}(cp)
 	}
-	return &deflateLevelInt, nil
-}
 
-func (s *Service) createHTTPv2(port int, readTimeout, writeTimeout *time.Duration) (Component, error) {
-	oo := []v2.OptionFunc{v2.WithPort(port)}
+	log.FromContext(ctx).Infof("service %s started", s.name)
+	ee := make([]error, 0, len(components))
+	ee = append(ee, s.waitTermination(chErr))
+	cnl()
 
-	if readTimeout != nil {
-		oo = append(oo, v2.WithReadTimeout(*readTimeout))
-	}
+	wg.Wait()
+	close(chErr)
 
-	if writeTimeout != nil {
-		oo = append(oo, v2.WithWriteTimeout(*writeTimeout))
+	for err := range chErr {
+		ee = append(ee, err)
 	}
+	return patronErrors.Aggregate(ee...)
+}
 
-	return v2.New(s.httpRouter, oo...)
+func (s *Service) setupOSSignal() {
+	signal.Notify(s.termSig, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
 }
 
 func (s *Service) waitTermination(chErr <-chan error) error {
@@ -268,62 +222,3 @@ func setupJaegerTracing(name, version string) error {
 	log.Debugf("setting up default tracing %s, %s with param %f", agent, tp, prmVal)
 	return trace.Setup(name, version, agent, tp, prmVal, buckets)
 }
-
-func New(name, version string, options ...OptionFunc) (*Service, error) {
-	if name == "" {
-		return nil, errors.New("name is required")
-	}
-	if version == "" {
-		version = "dev"
-	}
-
-	// default config with structured logger and default fields.
-	cfg := config{
-		logger: patronzerolog.New(os.Stderr, getLogLevel(), nil),
-		fields: defaultLogFields(name, version),
-	}
-
-	s := &Service{
-		name:    name,
-		version: version,
-		termSig: make(chan os.Signal, 1),
-		sighupHandler: func() {
-			log.Debug("WithSIGHUP received: nothing setup")
-		},
-		config: cfg,
-		cps:    make([]Component, 0),
-	}
-
-	var err error
-	err = setupJaegerTracing(name, version)
-	if err != nil {
-		return nil, err
-	}
-
-	optionErrors := make([]error, 0)
-	for _, option := range options {
-		err = option(s)
-		if err != nil {
-			optionErrors = append(optionErrors, err)
-		}
-	}
-
-	if len(optionErrors) > 0 {
-		return nil, patronErrors.Aggregate(optionErrors...)
-	}
-
-	err = setupLogging(cfg.fields, cfg.logger)
-	if err != nil {
-		return nil, err
-	}
-
-	httpCp, err := s.createHTTPComponent()
-	if err != nil {
-		return nil, err
-	}
-
-	s.cps = append(s.cps, httpCp)
-	s.setupOSSignal()
-
-	return s, nil
-}


Test patch:
diff --git a/component/async/kafka/group/integration_test.go b/component/async/kafka/group/integration_test.go
index 0d09547c7..769270285 100644
--- a/component/async/kafka/group/integration_test.go
+++ b/component/async/kafka/group/integration_test.go
@@ -154,11 +154,11 @@ func TestKafkaAsyncPackageComponent_Success(t *testing.T) {
 	patronContext, patronCancel := context.WithCancel(context.Background())
 	var patronWG sync.WaitGroup
 	patronWG.Add(1)
-	svc, err := patron.New(successTopic1, "0", patron.WithTextLogger(), patron.WithComponents(component))
+	svc, err := patron.New(successTopic1, "0", patron.WithTextLogger())
 	require.NoError(t, err)
 
 	go func() {
-		err = svc.Run(patronContext)
+		err = svc.Run(patronContext, component)
 		require.NoError(t, err)
 		patronWG.Done()
 	}()
@@ -233,9 +233,9 @@ func TestKafkaAsyncPackageComponent_FailAllRetries(t *testing.T) {
 	}()
 
 	// Run Patron with the component - no need for goroutine since we expect it to stop after the retries fail
-	svc, err := patron.New(failAllRetriesTopic1, "0", patron.WithTextLogger(), patron.WithComponents(component))
+	svc, err := patron.New(failAllRetriesTopic1, "0", patron.WithTextLogger())
 	require.NoError(t, err)
-	err = svc.Run(context.Background())
+	err = svc.Run(context.Background(), component)
 	assert.Error(t, err)
 
 	// Wait for the producer & consumer to finish
@@ -292,9 +292,9 @@ func TestKafkaAsyncPackageComponent_FailOnceAndRetry(t *testing.T) {
 	var patronWG sync.WaitGroup
 	patronWG.Add(1)
 	go func() {
-		svc, err := patron.New(failAndRetryTopic1, "0", patron.WithTextLogger(), patron.WithComponents(component))
+		svc, err := patron.New(failAndRetryTopic1, "0", patron.WithTextLogger())
 		require.NoError(t, err)
-		err = svc.Run(patronContext)
+		err = svc.Run(patronContext, component)
 		require.NoError(t, err)
 		patronWG.Done()
 	}()
diff --git a/component/http/component_test.go b/component/http/component_test.go
index 965a35eea..461771345 100644
--- a/component/http/component_test.go
+++ b/component/http/component_test.go
@@ -133,11 +133,11 @@ func Test_createHTTPServerUsingBuilder(t *testing.T) {
 		"success": {
 			acf: DefaultAliveCheck,
 			rcf: DefaultReadyCheck,
-			p:   httpPort,
-			rt:  httpReadTimeout,
-			wt:  httpIdleTimeout,
-			dl:  deflateLevel,
-			gp:  shutdownGracePeriod,
+			p:   defaultPort,
+			rt:  defaultReadTimeout,
+			wt:  defaultIdleTimeout,
+			dl:  defaultDeflateLevel,
+			gp:  defaultShutdownGracePeriod,
 			rb:  rb,
 			mm: []middleware.Func{
 				middleware.NewRecovery(),
diff --git a/component/http/v2/component_test.go b/component/http/v2/component_test.go
index ddc76dd62..d2ed420bc 100644
--- a/component/http/v2/component_test.go
+++ b/component/http/v2/component_test.go
@@ -19,39 +19,98 @@ func (s stubHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) {
 }
 
 func TestNew(t *testing.T) {
-	t.Parallel()
+	hnd := &stubHandler{}
 	type args struct {
-		handler http.Handler
-		oo      []OptionFunc
+		handler      http.Handler
+		oo           []OptionFunc
+		port         string
+		readTimeout  string
+		writeTimeout string
 	}
 	tests := map[string]struct {
 		args        args
+		expected    *Component
 		expectedErr string
 	}{
-		"success": {args: args{
-			handler: &stubHandler{},
-			oo:      []OptionFunc{WithPort(50000)},
-		}},
-		"missing handler": {args: args{
-			handler: nil,
-			oo:      []OptionFunc{WithPort(50000)},
-		}, expectedErr: "handler is nil"},
-		"option error": {args: args{
-			handler: &stubHandler{},
-			oo:      []OptionFunc{WithPort(500000)},
-		}, expectedErr: "invalid HTTP Port provided"},
+		"success": {
+			args: args{handler: hnd},
+			expected: &Component{
+				port:                defaultPort,
+				readTimeout:         defaultReadTimeout,
+				writeTimeout:        defaultWriteTimeout,
+				shutdownGracePeriod: defaultShutdownGracePeriod,
+				handlerTimeout:      defaultHandlerTimeout,
+				handler:             hnd,
+			},
+		},
+		"success, env vars": {
+			args: args{
+				handler:      hnd,
+				port:         "8080",
+				readTimeout:  "10s",
+				writeTimeout: "11s",
+			},
+			expected: &Component{
+				port:                8080,
+				readTimeout:         10 * time.Second,
+				writeTimeout:        11 * time.Second,
+				shutdownGracePeriod: defaultShutdownGracePeriod,
+				handlerTimeout:      defaultHandlerTimeout,
+				handler:             hnd,
+			},
+		},
+		"failure, port env vars": {
+			args: args{
+				handler: hnd,
+				port:    "aaa",
+			},
+			expectedErr: `env var for HTTP default port is not valid: strconv.ParseInt: parsing "aaa": invalid syntax`,
+		},
+		"failure, read timeout env vars": {
+			args: args{
+				handler:     hnd,
+				readTimeout: "aaa",
+			},
+			expectedErr: `env var for HTTP read timeout is not valid: time: invalid duration "aaa"`,
+		},
+		"failure, write timeout env vars": {
+			args: args{
+				handler:      hnd,
+				writeTimeout: "aaa",
+			},
+			expectedErr: `env var for HTTP write timeout is not valid: time: invalid duration "aaa"`,
+		},
+		"missing handler": {
+			args:        args{handler: nil},
+			expectedErr: "handler is nil",
+		},
+		"option error": {
+			args:        args{handler: &stubHandler{}, oo: []OptionFunc{WithPort(500000)}},
+			expectedErr: "invalid HTTP Port provided",
+		},
 	}
 	for name, tt := range tests {
 		tt := tt
 		t.Run(name, func(t *testing.T) {
-			t.Parallel()
+			if tt.args.port != "" {
+				t.Setenv("PATRON_HTTP_DEFAULT_PORT", tt.args.port)
+			}
+
+			if tt.args.readTimeout != "" {
+				t.Setenv("PATRON_HTTP_READ_TIMEOUT", tt.args.readTimeout)
+			}
+
+			if tt.args.writeTimeout != "" {
+				t.Setenv("PATRON_HTTP_WRITE_TIMEOUT", tt.args.writeTimeout)
+			}
+
 			got, err := New(tt.args.handler, tt.args.oo...)
 			if tt.expectedErr != "" {
 				assert.EqualError(t, err, tt.expectedErr)
 				assert.Nil(t, got)
 			} else {
 				assert.NoError(t, err)
-				assert.NotNil(t, got)
+				assert.Equal(t, tt.expected, got)
 			}
 		})
 	}
diff --git a/options_test.go b/options_test.go
index 318f2035b..dfe9689f7 100644
--- a/options_test.go
+++ b/options_test.go
@@ -2,7 +2,6 @@ package patron
 
 import (
 	"errors"
-	"net/http"
 	"os"
 	"testing"
 
@@ -10,31 +9,6 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestComponents(t *testing.T) {
-	comp := &testComponent{}
-
-	type args struct {
-		cc []Component
-	}
-	tests := map[string]struct {
-		args           args
-		wantComponents []Component
-		wantError      error
-	}{
-		"no components provided": {args: args{cc: nil}, wantComponents: nil, wantError: errors.New("provided components slice was empty")},
-		"components provided":    {args: args{cc: []Component{comp}}, wantComponents: []Component{comp}, wantError: nil},
-	}
-	for name, tt := range tests {
-		temp := tt
-		t.Run(name, func(t *testing.T) {
-			svc := &Service{}
-			err := WithComponents(temp.args.cc...)(svc)
-			assert.Equal(t, temp.wantError, err)
-			assert.Equal(t, temp.wantComponents, svc.cps)
-		})
-	}
-}
-
 func TestLogFields(t *testing.T) {
 	defaultFields := defaultLogFields("test", "1.0")
 	fields := map[string]interface{}{"key": "value"}
@@ -86,37 +60,6 @@ func TestLogger(t *testing.T) {
 	assert.Equal(t, logger, svc.config.logger)
 }
 
-func TestRouter(t *testing.T) {
-	type args struct {
-		handler http.Handler
-	}
-
-	tests := map[string]struct {
-		args        args
-		wantHandler http.Handler
-		wantError   error
-	}{
-		"empty value for handler":     {args: args{handler: nil}, wantHandler: nil, wantError: errors.New("provided router is nil")},
-		"non empty value for handler": {args: args{handler: noopHTTPHandler{}}, wantHandler: noopHTTPHandler{}, wantError: nil},
-	}
-
-	for name, tt := range tests {
-		t.Run(name, func(t *testing.T) {
-			temp := tt
-			svc := &Service{}
-
-			err := WithRouter(temp.args.handler)(svc)
-			assert.Equal(t, temp.wantError, err)
-			assert.Equal(t, temp.wantHandler, svc.httpRouter)
-		})
-	}
-}
-
-type noopHTTPHandler struct{}
-
-func (noopHTTPHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
-}
-
 func TestSIGHUP(t *testing.T) {
 	t.Parallel()
 
diff --git a/service_test.go b/service_test.go
index d97700bc3..c730774bb 100644
--- a/service_test.go
+++ b/service_test.go
@@ -3,7 +3,6 @@ package patron
 import (
 	"context"
 	"errors"
-	"net/http"
 	"os"
 	"testing"
 
@@ -12,45 +11,38 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestNewServer(t *testing.T) {
-	mw := func(h http.Handler) http.Handler {
-		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			h.ServeHTTP(w, r)
-		})
-	}
-
-	httpBuilderAllErrors := "provided components slice was empty\n" +
-		"provided WithSIGHUP handler was nil\n" +
-		"provided router is nil\n"
+func TestNew(t *testing.T) {
+	httpBuilderAllErrors := "provided WithSIGHUP handler was nil\n"
 
 	tests := map[string]struct {
+		name              string
 		fields            map[string]interface{}
-		cps               []Component
 		sighupHandler     func()
 		uncompressedPaths []string
-		handler           http.Handler
 		wantErr           string
 	}{
 		"success": {
+			name:              "name",
 			fields:            map[string]interface{}{"env": "dev"},
-			cps:               []Component{&testComponent{}, &testComponent{}},
 			sighupHandler:     func() { log.Info("WithSIGHUP received: nothing setup") },
 			uncompressedPaths: []string{"/foo", "/bar"},
-			handler:           mw(nil),
 			wantErr:           "",
 		},
+		"name missing": {
+			sighupHandler:     nil,
+			uncompressedPaths: nil,
+			wantErr:           "name is required",
+		},
 		"nil inputs steps": {
-			cps:               nil,
+			name:              "name",
 			sighupHandler:     nil,
 			uncompressedPaths: nil,
-			handler:           nil,
 			wantErr:           httpBuilderAllErrors,
 		},
 		"error in all builder steps": {
-			cps:               []Component{},
+			name:              "name",
 			sighupHandler:     nil,
 			uncompressedPaths: []string{},
-			handler:           nil,
 			wantErr:           httpBuilderAllErrors,
 		},
 	}
@@ -58,8 +50,8 @@ func TestNewServer(t *testing.T) {
 	for name, tt := range tests {
 		temp := tt
 		t.Run(name, func(t *testing.T) {
-			gotService, gotErr := New("name", "1.0", WithLogFields(temp.fields), WithTextLogger(),
-				WithComponents(temp.cps...), WithSIGHUP(temp.sighupHandler), WithRouter(temp.handler))
+			gotService, gotErr := New(tt.name, "1.0", WithLogFields(temp.fields), WithTextLogger(),
+				WithSIGHUP(temp.sighupHandler))
 
 			if temp.wantErr != "" {
 				assert.EqualError(t, gotErr, temp.wantErr)
@@ -68,14 +60,8 @@ func TestNewServer(t *testing.T) {
 				assert.Nil(t, gotErr)
 				assert.NotNil(t, gotService)
 				assert.IsType(t, &Service{}, gotService)
-
-				assert.NotEmpty(t, gotService.cps)
 				assert.NotNil(t, gotService.termSig)
 				assert.NotNil(t, gotService.sighupHandler)
-
-				for _, comp := range temp.cps {
-					assert.Contains(t, gotService.cps, comp)
-				}
 			}
 		})
 	}
@@ -96,9 +82,9 @@ func TestServer_Run_Shutdown(t *testing.T) {
 				os.Clearenv()
 			}()
 			t.Setenv("PATRON_HTTP_DEFAULT_PORT", "50099")
-			svc, err := New("test", "", WithTextLogger(), WithComponents(temp.cp, temp.cp, temp.cp))
+			svc, err := New("test", "", WithTextLogger())
 			assert.NoError(t, err)
-			err = svc.Run(context.Background())
+			err = svc.Run(context.Background(), tt.cp)
 			if temp.wantErr {
 				assert.Error(t, err)
 			} else {
@@ -140,57 +126,44 @@ func TestServer_SetupTracing(t *testing.T) {
 				assert.NoError(t, err)
 			}
 
-			svc, err := New("test", "", WithTextLogger(), WithComponents(tt.cp, tt.cp, tt.cp))
+			svc, err := New("test", "", WithTextLogger())
 			assert.NoError(t, err)
 
-			err = svc.Run(context.Background())
+			err = svc.Run(context.Background(), tt.cp)
 			assert.NoError(t, err)
 		})
 	}
 }
 
-func TestNewServer_WithComponentsTwice(t *testing.T) {
-	svc, err := New("test", "", WithTextLogger(), WithComponents(&testComponent{}, &testComponent{}))
-	require.NoError(t, err)
-	assert.Len(t, svc.cps, 3)
-}
-
 func TestNewServer_FailingConditions(t *testing.T) {
 	tests := map[string]struct {
 		jaegerSamplerParam       string
-		port                     string
 		jaegerBuckets            string
 		expectedConstructorError string
 	}{
-		"failure with wrong w/ port":             {port: "foo", expectedConstructorError: "env var for HTTP default port is not valid: strconv.ParseInt: parsing \"foo\": invalid syntax"},
-		"success with wrong w/ overflowing port": {port: "153000", expectedConstructorError: "invalid HTTP Port provided"},
-		"failure w/ sampler param":               {jaegerSamplerParam: "foo", expectedConstructorError: "env var for jaeger sampler param is not valid: strconv.ParseFloat: parsing \"foo\": invalid syntax"},
-		"failure w/ overflowing sampler param":   {jaegerSamplerParam: "8", expectedConstructorError: "cannot initialize jaeger tracer: invalid Param for probabilistic sampler; expecting value between 0 and 1, received 8"},
-		"failure w/ custom default buckets":      {jaegerSamplerParam: "1", jaegerBuckets: "foo", expectedConstructorError: "env var for jaeger default buckets contains invalid value: strconv.ParseFloat: parsing \"foo\": invalid syntax"},
+		"failure w/ sampler param":             {jaegerSamplerParam: "foo", expectedConstructorError: "env var for jaeger sampler param is not valid: strconv.ParseFloat: parsing \"foo\": invalid syntax"},
+		"failure w/ overflowing sampler param": {jaegerSamplerParam: "8", expectedConstructorError: "cannot initialize jaeger tracer: invalid Param for probabilistic sampler; expecting value between 0 and 1, received 8"},
+		"failure w/ custom default buckets":    {jaegerSamplerParam: "1", jaegerBuckets: "foo", expectedConstructorError: "env var for jaeger default buckets contains invalid value: strconv.ParseFloat: parsing \"foo\": invalid syntax"},
 	}
 
 	for name, tt := range tests {
-		temp := tt
+		tt := tt
 		t.Run(name, func(t *testing.T) {
 			defer os.Clearenv()
 
-			if temp.port != "" {
-				err := os.Setenv("PATRON_HTTP_DEFAULT_PORT", temp.port)
-				require.NoError(t, err)
-			}
-			if temp.jaegerSamplerParam != "" {
-				err := os.Setenv("PATRON_JAEGER_SAMPLER_PARAM", temp.jaegerSamplerParam)
+			if tt.jaegerSamplerParam != "" {
+				err := os.Setenv("PATRON_JAEGER_SAMPLER_PARAM", tt.jaegerSamplerParam)
 				require.NoError(t, err)
 			}
-			if temp.jaegerBuckets != "" {
-				err := os.Setenv("PATRON_JAEGER_DEFAULT_BUCKETS", temp.jaegerBuckets)
+			if tt.jaegerBuckets != "" {
+				err := os.Setenv("PATRON_JAEGER_DEFAULT_BUCKETS", tt.jaegerBuckets)
 				require.NoError(t, err)
 			}
 
 			svc, err := New("test", "", WithTextLogger())
 
-			if temp.expectedConstructorError != "" {
-				require.EqualError(t, err, temp.expectedConstructorError)
+			if tt.expectedConstructorError != "" {
+				require.EqualError(t, err, tt.expectedConstructorError)
 				require.Nil(t, svc)
 
 				return
@@ -210,84 +183,6 @@ func TestNewServer_FailingConditions(t *testing.T) {
 	}
 }
 
-func TestServer_SetupReadWriteTimeouts(t *testing.T) {
-	tests := []struct {
-		name    string
-		cp      Component
-		ctx     context.Context
-		rt      string
-		wt      string
-		wantErr bool
-	}{
-		{name: "success wo/ setup read and write timeouts", cp: &testComponent{}, ctx: context.Background(), wantErr: false},
-		{name: "success w/ setup read and write timeouts", cp: &testComponent{}, ctx: context.Background(), rt: "60s", wt: "20s", wantErr: false},
-		{name: "failed w/ invalid write timeout", cp: &testComponent{}, ctx: context.Background(), wt: "invalid", wantErr: true},
-		{name: "failed w/ invalid read timeout", cp: &testComponent{}, ctx: context.Background(), rt: "invalid", wantErr: true},
-		{name: "failed w/ negative write timeout", cp: &testComponent{}, ctx: context.Background(), wt: "-100s", wantErr: true},
-		{name: "failed w/ zero read timeout", cp: &testComponent{}, ctx: context.Background(), rt: "0s", wantErr: true},
-	}
-	for _, tt := range tests {
-		temp := tt
-		t.Run(temp.name, func(t *testing.T) {
-			defer os.Clearenv()
-
-			if temp.rt != "" {
-				err := os.Setenv("PATRON_HTTP_READ_TIMEOUT", temp.rt)
-				assert.NoError(t, err)
-			}
-			if temp.wt != "" {
-				err := os.Setenv("PATRON_HTTP_WRITE_TIMEOUT", temp.wt)
-				assert.NoError(t, err)
-			}
-			_, err := New("test", "", WithTextLogger(), WithComponents(temp.cp, temp.cp, temp.cp))
-
-			if temp.wantErr {
-				assert.Error(t, err)
-			} else {
-				assert.NoError(t, err)
-			}
-		})
-	}
-}
-
-func TestServer_SetupDeflateLevel(t *testing.T) {
-	tests := []struct {
-		name      string
-		component Component
-		ctx       context.Context
-		level     string
-		wantErr   bool
-	}{
-		{name: "success without setup compression deflate level", component: &testComponent{}, ctx: context.Background(), wantErr: false},
-		{name: "success with setup compression deflate level = -2", component: &testComponent{}, ctx: context.Background(), level: "-2", wantErr: false},
-		{name: "success with setup compression deflate level = 2", component: &testComponent{}, ctx: context.Background(), level: "2", wantErr: false},
-		{name: "success with setup compression deflate level = 6", component: &testComponent{}, ctx: context.Background(), level: "6", wantErr: false},
-		{name: "success with setup compression deflate level = 9", component: &testComponent{}, ctx: context.Background(), level: "9", wantErr: false},
-		{name: "failed with too small compression deflate level", component: &testComponent{}, ctx: context.Background(), level: "-3", wantErr: true},
-		{name: "failed with too big compression deflate level", component: &testComponent{}, ctx: context.Background(), level: "10", wantErr: true},
-		{name: "failed with invalid compression deflate level", component: &testComponent{}, ctx: context.Background(), level: "blah", wantErr: true},
-	}
-	for _, tt := range tests {
-		temp := tt
-		t.Run(tt.name, func(t *testing.T) {
-			defer os.Clearenv()
-
-			if temp.level != "" {
-				err := os.Setenv("PATRON_COMPRESSION_DEFLATE_LEVEL", temp.level)
-				assert.NoError(t, err)
-			}
-
-			_, err := New("test", "", WithTextLogger(), WithComponents(temp.component, temp.component, temp.component))
-
-			if tt.wantErr {
-				assert.Error(t, err)
-			} else {
-				assert.NoError(t, err)
-			}
-		})
-	}
-}
-
 type testComponent struct {
 	errorRunning bool
 }