Skip to content

Commit

Permalink
Startup Trace and Automatic image switching (#8)
Browse files Browse the repository at this point in the history
* fix: Fixed startup tracing

* feat: Added automatic image switchover

* Updated go version on PR

* Fixed indentation

* fix: cleaned up some error handling

* fix: linter errors

* fix: capitalizations on error messages

* fix: Missed one
  • Loading branch information
GavinPower747 authored Aug 25, 2024
1 parent b760d77 commit 1529b79
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 45 deletions.
21 changes: 16 additions & 5 deletions .github/workflows/Deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:

jobs:

deploy:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand All @@ -28,9 +28,20 @@ jobs:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.REGISTRY_URL }}/gavinpower747/shortener:latest
tags: ${{ secrets.REGISTRY_URL }}/gavinpower747/shortener:${{ github.sha }}
build-args: |
GIT_COMMIT=${{github.sha}}
GIT_BRANCH=${{github.ref}}
GIT_COMMIT=${{ github.sha }}
GIT_BRANCH=${{ github.ref }}
BUILD_DATE=${{ github.event.repository.updated_at }}
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- name: 'Deploy new image to app service'
uses: azure/webapps-deploy@v2
with:
app-name: 'shortener'
slot-name: 'production'
publish_profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}
images: ${{ secrets.REGISTRY_URL }}/gavinpower747/shortener:${{ github.sha }}
2 changes: 1 addition & 1 deletion .github/workflows/PR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: 'Set up Go'
uses: actions/setup-go@v3
with:
go-version: '1.20'
go-version: '1.23'

- name: Verify dependencies
run: go mod verify
Expand Down
45 changes: 31 additions & 14 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,40 @@ func main() {
}
}

func run() (err error) {
const (
ServiceName = "gavs.at"
)

func run() (err error) {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

otelShutdown, err := observability.SetupOTelSDK(ctx)
otelShutdown, err := observability.SetupOTelSDK(ctx, ServiceName)
if err != nil {
log.Fatalf("Failed to setup otel: %v", err)
return
log.Printf("Failed to setup otel: %v", err)
return err
}

tracer := otel.GetTracerProvider().Tracer("gavs.at/shortener")
tracer := otel.GetTracerProvider().Tracer(ServiceName)

_, span := tracer.Start(ctx, "startup")
defer span.End()
ctx, span := tracer.Start(ctx, "ApplicationStartup")

defer func() {
err = errors.Join(err, otelShutdown(context.Background()))
if span.IsRecording() && err != nil {
span.RecordError(err)
span.End()
}

err = errors.Join(err, otelShutdown(ctx))
}()

listenAddr := ":80"

storageAccount, err := storage.NewStorageAccount()
storageAccount, err := storage.NewStorageAccount(ctx)
if err != nil {
log.Fatal(err)
log.Println(err)

return err
}

reqHandlers := handlers.NewHandlers(storageAccount)
Expand All @@ -68,27 +77,35 @@ func run() (err error) {
}

srvErr := make(chan error, 1)

span.End()

go func() {
log.Println("Listening on", listenAddr)
srvErr <- srv.ListenAndServe()
}()

select {
case err = <-srvErr:
return
if span.IsRecording() {
span.RecordError(err)
span.End()
}

return err
case <-ctx.Done():
stop()
}

err = srv.Shutdown(context.Background())
err = srv.Shutdown(ctx)

return
return nil
}

func configureRouter(reqHandlers *handlers.Handlers) *mux.Router {
r := mux.NewRouter()

r.Use(otelmux.Middleware("gavs.at"))
r.Use(otelmux.Middleware(ServiceName))
r.Use(middleware.RequestMetrics)

apiRouter := r.PathPrefix("/api").Subrouter()
Expand Down
2 changes: 1 addition & 1 deletion internal/handlers/create_redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func (h *Handlers) UpsertRedirect(w http.ResponseWriter, r *http.Request) {
Entity: aztables.Entity{PartitionKey: "pk001", RowKey: req.Slug},
}

err := h.storage.UpsertEntity(redirect)
err := h.storage.UpsertEntity(r.Context(), redirect)

if err != nil {
log.Printf("Error when upserting redirect: %s", err)
Expand Down
6 changes: 3 additions & 3 deletions internal/handlers/create_redirect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (m *MockStorage) QueryEntity(_ context.Context, _, _ string) ([]byte, error
return nil, nil
}

func (m *MockStorage) UpsertEntity(entity interface{}) error {
func (m *MockStorage) UpsertEntity(_ context.Context, entity interface{}) error {
if m.UpsertEntityFunc == nil {
return nil
}
Expand All @@ -34,7 +34,7 @@ func (m *MockStorage) UpsertEntity(entity interface{}) error {
func TestHandlers_UpsertRedirect_ValidRequest(t *testing.T) {
// Arrange
mockStorage := &MockStorage{
UpsertEntityFunc: func(entity interface{}) error {
UpsertEntityFunc: func(_ interface{}) error {
return nil
},
}
Expand Down Expand Up @@ -91,7 +91,7 @@ func TestHandlers_UpsertRedirect_InvalidRequestBody(t *testing.T) {
func TestHandlers_UpsertRedirect_StorageError(t *testing.T) {
// Arrange
mockStorage := &MockStorage{
UpsertEntityFunc: func(entity interface{}) error {
UpsertEntityFunc: func(_ interface{}) error {
return errors.New("test error")
},
}
Expand Down
4 changes: 2 additions & 2 deletions internal/handlers/redirect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type MockStorageAccount struct {
mock.Mock
}

func (m *MockStorageAccount) QueryEntity(ctx context.Context, partitionKey, rowKey string) ([]byte, error) {
func (m *MockStorageAccount) QueryEntity(_ context.Context, partitionKey, rowKey string) ([]byte, error) {
args := m.Called(partitionKey, rowKey)

if args.Get(0) == nil {
Expand All @@ -34,7 +34,7 @@ func (m *MockStorageAccount) QueryEntity(ctx context.Context, partitionKey, rowK
return args.Get(0).([]byte), args.Error(1)
}

func (m *MockStorageAccount) UpsertEntity(entity interface{}) error {
func (m *MockStorageAccount) UpsertEntity(_ context.Context, entity interface{}) error {
args := m.Called(entity)

return args.Error(0)
Expand Down
12 changes: 6 additions & 6 deletions internal/storage/storage_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (

type Account interface {
QueryEntity(ctx context.Context, partitionKey string, rowKey string) ([]byte, error)
UpsertEntity(entity interface{}) error
UpsertEntity(ctx context.Context, entity interface{}) error
}

type storageAccount struct {
Expand Down Expand Up @@ -53,7 +53,7 @@ func (sa *storageAccount) QueryEntity(ctx context.Context, partitionKey, rowKey
return nil, nil
}

func (sa *storageAccount) UpsertEntity(entity interface{}) error {
func (sa *storageAccount) UpsertEntity(ctx context.Context, entity interface{}) error {
tableClient := sa.serviceClient.NewClient(TableName)

jsonEntity, err := json.Marshal(entity)
Expand All @@ -62,12 +62,12 @@ func (sa *storageAccount) UpsertEntity(entity interface{}) error {
return err
}

_, err = tableClient.UpsertEntity(context.Background(), jsonEntity, nil)
_, err = tableClient.UpsertEntity(ctx, jsonEntity, nil)

return err
}

func NewStorageAccount() (Account, error) {
func NewStorageAccount(ctx context.Context) (Account, error) {
connectionString, found := os.LookupEnv("API_AzureStorageConnectionString")

if !found {
Expand All @@ -91,14 +91,14 @@ func NewStorageAccount() (Account, error) {
pager := sa.NewListTablesPager(pagerOptions)

for pager.More() {
resp, pageErr := pager.NextPage(context.Background())
resp, pageErr := pager.NextPage(ctx)

if pageErr != nil {
return nil, pageErr
}

if len(resp.Tables) == 0 {
_, err = sa.CreateTable(context.Background(), TableName, nil)
_, err = sa.CreateTable(ctx, TableName, nil)

if err != nil {
return nil, err
Expand Down
10 changes: 5 additions & 5 deletions pkg/middleware/basic_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@ func BasicAuth(next http.Handler) http.Handler {

if authHeader == "" {
web.NotAuthorized(w, "Missing Authorization Header")
span.RecordError(fmt.Errorf("Missing Authorization Header"))
span.RecordError(fmt.Errorf("missing authorization header"))

return
}

headerSections := strings.Split(authHeader, " ")

if !strings.HasPrefix(authHeader, "Basic ") {
web.NotAuthorized(w, fmt.Sprintf("Invalid Authorization Header, %s authentication scheme is not supported", headerSections[0]))
span.RecordError(fmt.Errorf("Invalid Authorization Header, %s authentication scheme is not supported", headerSections[0]))
web.NotAuthorized(w, fmt.Sprintf("invalid authorization header, %s authentication scheme is not supported", headerSections[0]))
span.RecordError(fmt.Errorf("invalid authorization header, %s authentication scheme is not supported", headerSections[0]))

return
}
Expand All @@ -52,7 +52,7 @@ func BasicAuth(next http.Handler) http.Handler {

if err != nil {
web.NotAuthorized(w, "Invalid Authorization Header")
span.RecordError(fmt.Errorf("Invalid Authorization Header"))
span.RecordError(fmt.Errorf("invalid authorization header"))

return
}
Expand All @@ -68,7 +68,7 @@ func BasicAuth(next http.Handler) http.Handler {

if username != expectedUsername || passwordHash != expectedPasswordHash {
web.NotAuthorized(w, "Invalid Credentials")
span.RecordError(fmt.Errorf("Invalid Credentials"))
span.RecordError(fmt.Errorf("invalid credentials"))

return
}
Expand Down
17 changes: 9 additions & 8 deletions pkg/observability/otel.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ import (
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)

func SetupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) {
func SetupOTelSDK(ctx context.Context, serviceName string) (shutdown func(context.Context) error, err error) {
var shutdownFuncs []func(context.Context) error

shutdown = func(ctx context.Context) error {
var err error
for _, fn := range shutdownFuncs {
if shutErr := fn(ctx); shutErr != nil {
err = errors.Join(err, shutErr)
}
}

return err
}

Expand All @@ -33,9 +33,9 @@ func SetupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, er
}
}

resource := resource.NewWithAttributes(
otelService := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("gavs.at"),
semconv.ServiceNameKey.String(serviceName),
semconv.ServiceVersionKey.String(os.Getenv("GIT_COMMIT")),
attribute.String("build.commit", os.Getenv("GIT_COMMIT")),
attribute.String("build.branch", os.Getenv("GIT_BRANCH")),
Expand All @@ -45,10 +45,10 @@ func SetupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, er

otel.SetTextMapPropagator(newPropagator())

tracerProvider, err := newTraceProvider(ctx, resource)
tracerProvider, err := newTraceProvider(ctx, otelService)
if err != nil {
handleErr(err)
return
return shutdown, err
}

shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
Expand All @@ -64,7 +64,7 @@ func newPropagator() propagation.TextMapPropagator {
)
}

func newTraceProvider(ctx context.Context, resource *resource.Resource) (*trace.TracerProvider, error) {
func newTraceProvider(ctx context.Context, service *resource.Resource) (*trace.TracerProvider, error) {
exporter, err := otlptracehttp.New(ctx)

if err != nil {
Expand All @@ -73,7 +73,8 @@ func newTraceProvider(ctx context.Context, resource *resource.Resource) (*trace.

tp := trace.NewTracerProvider(
trace.WithBatcher(exporter),
trace.WithResource(resource),
trace.WithResource(service),
)

return tp, nil
}

0 comments on commit 1529b79

Please sign in to comment.