Skip to content

Commit 3f53a55

Browse files
authored
Merge pull request #11 from abiddiscombe/dev
Merge Dev (v0.4.0) to Main
2 parents 589e7cb + 0ece366 commit 3f53a55

File tree

10 files changed

+146
-50
lines changed

10 files changed

+146
-50
lines changed

README.md

+13-8
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,18 @@ If the `alias` is vacant, a new unique record will be created for the provided `
2929

3030
## Deployment Instructions
3131

32-
Concierge can be run as a Docker container and uses internal port `:3000`. \
33-
The latest release image of [abiddiscombe/concierge](https://hub.docker.com/repository/docker/abiddiscombe/concierge/general) can be pulled from Docker Hub.
32+
### Setup
3433

35-
The API server is backed by PostgreSQL; the following environment variables are required:
34+
Concierge can be run as a Docker container and uses port `3000`. The latest release image of [abiddiscombe/concierge](https://hub.docker.com/repository/docker/abiddiscombe/concierge/general) can be pulled from Docker Hub.
3635

37-
- `CONCIERGE_PG_HOST` - PostgreSQL Server URL
38-
- `CONCIERGE_PG_PORT` - PostgreSQL Server Port
39-
- `CONCIERGE_PG_NAME` - PostgreSQL Database Name
40-
- `CONCIERGE_PG_USER` - PostgreSQL Connection User
41-
- `CONCIERGE_PG_PASS` - PostgreSQL Connection Password
36+
Concierge uses PostgreSQL to persist data. The following environment variables are required to connect to a PostgreSQL server:
37+
38+
- `CONCIERGE_PG_HOST` - DB URL
39+
- `CONCIERGE_PG_PORT` - DB Port
40+
- `CONCIERGE_PG_NAME` - DB Name
41+
- `CONCIERGE_PG_USER` - DB User
42+
- `CONCIERGE_PG_PASS` - DB Password
43+
44+
### Logging
45+
46+
Concierge uses the `log/slog` package to print structured logs. These can be captured and ingested by a supported third-party `syslog` service.

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/abiddiscombe/concierge
33
go 1.22.1
44

55
require (
6+
github.com/alfonmga/slog-gorm v0.0.0-20230918104600-53fa2b611c42
67
github.com/labstack/echo/v4 v4.11.4
78
gorm.io/driver/postgres v1.5.7
89
gorm.io/gorm v1.25.9

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/alfonmga/slog-gorm v0.0.0-20230918104600-53fa2b611c42 h1:ydAr4vN7OYdmpWX7ca0dEGQf1Nl5lfZvIRTg63KJ+4Q=
2+
github.com/alfonmga/slog-gorm v0.0.0-20230918104600-53fa2b611c42/go.mod h1:2uDbFxQZPyIWIpz6C+nwmotBfkREmuVPRWEoH4cbs2s=
13
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
35
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

internal/controllers/link.go

+32-16
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,32 @@ func LinkGet(c echo.Context) error {
2626
alias := c.Param("alias")
2727

2828
if alias == "" {
29-
return c.JSON(http.StatusBadRequest, LinkResponse{
29+
return echo.NewHTTPError(http.StatusBadRequest, LinkResponse{
3030
Title: "[Concierge] Alias Lookup",
31-
Message: "Error. An 'alias' URL parameter must be provided.",
31+
Message: "An 'alias' URL parameter must be provided.",
3232
})
3333
}
3434

3535
url, createdAt, err := database.LinkRead(alias)
3636

37-
if url == "" || err != nil {
38-
errorMessage := fmt.Sprintf("Error. The provided 'alias' of '%s' does not exist.", alias)
39-
return c.JSON(http.StatusNotFound, LinkResponse{
37+
if err != nil {
38+
return echo.NewHTTPError(http.StatusInternalServerError, LinkResponse{
39+
Title: "[Concierge] Alias Lookup",
40+
Message: "Internal Server Error",
41+
})
42+
}
43+
44+
if url == "" {
45+
errorMessage := fmt.Sprintf("The provided alias of '%s' does not exist.", alias)
46+
return echo.NewHTTPError(http.StatusNotFound, LinkResponse{
4047
Title: "[Concierge] Alias Lookup",
4148
Message: errorMessage,
4249
})
4350
}
4451

4552
return c.JSON(http.StatusOK, LinkResponse{
4653
Title: "[Concierge] Alias Lookup",
47-
Message: "Success. Returned information for the alias entry.",
54+
Message: "Returned information for the alias entry.",
4855
Metadata: &LinkResponseMetadata{
4956
URL: fmt.Sprintf("https://%s", url),
5057
Link: fmt.Sprintf("https://%s/to/%s", c.Request().Host, alias),
@@ -59,9 +66,9 @@ func LinkPost(c echo.Context) error {
5966
alias := c.Param("alias")
6067

6168
if url == "" || alias == "" {
62-
return c.JSON(http.StatusBadRequest, LinkResponse{
69+
return echo.NewHTTPError(http.StatusBadRequest, LinkResponse{
6370
Title: "[Concierge] Alias Creation",
64-
Message: "Error. Both 'url' and 'alias' parameters must be provided.",
71+
Message: "Both 'url' and 'alias' parameters must be provided.",
6572
})
6673
}
6774

@@ -70,32 +77,41 @@ func LinkPost(c echo.Context) error {
7077
for _, value := range PROTOCOLS {
7178
index := strings.Contains(url, value)
7279
if index {
73-
return c.JSON(http.StatusBadRequest, LinkResponse{
80+
return echo.NewHTTPError(http.StatusBadRequest, LinkResponse{
7481
Title: "[Concierge] Alias Creation",
75-
Message: "Error. The 'url' must not include a protocol (e.g. 'https://').",
82+
Message: "The 'url' must not include a protocol (e.g. 'https://').",
7683
})
7784
}
7885
}
7986

8087
if url[0:1] == "/" {
81-
return c.JSON(http.StatusBadRequest, LinkResponse{
88+
return echo.NewHTTPError(http.StatusBadRequest, LinkResponse{
8289
Title: "[Concierge] Alias Creation",
83-
Message: "Error. The 'url' must start with a fully-qualified domain name.",
90+
Message: "The 'url' must start with a fully-qualified domain name.",
8491
})
8592
}
8693

87-
_, createdAt, err := database.LinkWrite(url, alias)
94+
url, createdAt, err := database.LinkWrite(url, alias)
8895

8996
if err != nil {
90-
return c.JSON(http.StatusBadRequest, LinkResponse{
97+
// This approach of determining if an HTTP-500 error
98+
// has occurred is rather hacky. To be revisted later.
99+
errorStartingText := err.Error()[0:26]
100+
if errorStartingText == "ERROR: duplicate key value" {
101+
return echo.NewHTTPError(http.StatusBadRequest, LinkResponse{
102+
Title: "[Concierge] Alias Creation",
103+
Message: "The specified alias already exists.",
104+
})
105+
}
106+
return echo.NewHTTPError(http.StatusInternalServerError, LinkResponse{
91107
Title: "[Concierge] Alias Creation",
92-
Message: "Error. The specified 'alias' already exists.",
108+
Message: "Internal Server Error",
93109
})
94110
}
95111

96112
return c.JSON(http.StatusCreated, LinkResponse{
97113
Title: "[Concierge] Alias Creation",
98-
Message: "Success. A new alias entry has been created.",
114+
Message: "A new alias entry has been created.",
99115
Metadata: &LinkResponseMetadata{
100116
URL: fmt.Sprintf("https://%s", url),
101117
Link: fmt.Sprintf("https://%s/to/%s", c.Request().Host, alias),

internal/controllers/root.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ func RootGet(c echo.Context) error {
2626
Title: "Root (Self)",
2727
Summary: "[GET] Returns information about this API.",
2828
},
29-
{
30-
Href: "/link",
31-
Title: "Link & Alias Management",
32-
Summary: "[GET, POST] Lookup or create new aliases.",
33-
},
3429
{
3530
Href: "/to/:alias",
36-
Title: "Link Activation & Redirection",
31+
Title: "Link Redirection",
3732
Summary: "[GET] Accepts a valid alias and redirects to target URL",
3833
},
34+
{
35+
Href: "/link/:alias",
36+
Title: "Link & Alias Management",
37+
Summary: "[GET, POST] Lookup existing or create new aliases.",
38+
},
3939
},
4040
})
4141
}

internal/controllers/to.go

+13-6
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,25 @@ func ToGet(c echo.Context) error {
1717
alias := c.Param("alias")
1818

1919
if alias == "" {
20-
return c.JSON(http.StatusNotFound, ToGetResponse{
21-
Title: "[Concierge] Alias Missing.",
20+
return echo.NewHTTPError(http.StatusNotFound, ToGetResponse{
21+
Title: "[Concierge] Alias Redirection.",
2222
Message: "An '/alias' value must be provided.",
2323
})
2424
}
2525

2626
url, _, err := database.LinkRead(alias)
2727

28-
if url == "" || err != nil {
29-
return c.JSON(http.StatusNotFound, ToGetResponse{
30-
Title: "[Concierge] Alias Invalid.",
31-
Message: "The '/alias' provided is not valid.",
28+
if err != nil {
29+
return echo.NewHTTPError(http.StatusInternalServerError, ToGetResponse{
30+
Title: "[Concierge] Alias Redirection.",
31+
Message: "Internal Server Error",
32+
})
33+
}
34+
35+
if url == "" {
36+
return echo.NewHTTPError(http.StatusNotFound, ToGetResponse{
37+
Title: "[Concierge] Alias Redirection.",
38+
Message: "The provided 'alias' is not valid.",
3239
})
3340
}
3441

internal/database/database.go

+21-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"os"
66

7+
"github.com/abiddiscombe/concierge/internal/log"
8+
slogGorm "github.com/alfonmga/slog-gorm"
79
"gorm.io/driver/postgres"
810
"gorm.io/gorm"
911
)
@@ -28,23 +30,37 @@ func parseEnv(key string) string {
2830
}
2931

3032
func Init() {
31-
3233
dbHost := parseEnv("CONCIERGE_PG_HOST")
3334
dbUser := parseEnv("CONCIERGE_PG_USER")
3435
dbPass := parseEnv("CONCIERGE_PG_PASS")
3536
dbName := parseEnv("CONCIERGE_PG_NAME")
3637
dbPort := parseEnv("CONCIERGE_PG_PORT")
3738

39+
logger := log.NewLogger("database")
40+
41+
DBLogger := slogGorm.New(
42+
slogGorm.WithLogger(logger),
43+
)
44+
3845
connString := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=prefer TimeZone=Europe/London", dbHost, dbUser, dbPass, dbName, dbPort)
39-
db, err := gorm.Open(postgres.Open(connString), &gorm.Config{})
46+
db, err := gorm.Open(postgres.Open(connString), &gorm.Config{
47+
Logger: DBLogger,
48+
})
4049

4150
if err != nil {
42-
panic("Failed to connect to PostgreSQL (via GORM).")
51+
msg := "Failed to connect to PostgreSQL"
52+
logger.Error(msg)
53+
panic(msg)
4354
}
4455

45-
db.AutoMigrate(&UriLinkEntry{})
56+
err = db.AutoMigrate(&UriLinkEntry{})
4657

47-
fmt.Println("[Concierge] Connected to PostgreSQL.")
58+
if err != nil {
59+
msg := "Failed to sync models with PostgreSQL"
60+
logger.Error(msg)
61+
panic(msg)
62+
}
4863

64+
logger.Info("Connected to PostgreSQL")
4965
DB = db
5066
}

internal/database/link.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package database
22

33
import (
4+
"errors"
45
"time"
56
)
67

@@ -12,10 +13,10 @@ func parseTimestamp(unixTimestamp int64) string {
1213
func LinkRead(alias string) (string, string, error) {
1314

1415
var result UriLinkEntry
15-
err := DB.Find(&result, UriLinkEntry{Alias: alias})
16+
dbResponse := DB.Find(&result, UriLinkEntry{Alias: alias})
1617

17-
if err == nil {
18-
return "", "", DB.Error
18+
if dbResponse.Error != nil {
19+
return "", "", errors.New(dbResponse.Error.Error())
1920
}
2021

2122
createdAtStr := parseTimestamp(result.CreatedAt)
@@ -29,10 +30,10 @@ func LinkWrite(url string, alias string) (string, string, error) {
2930
CreatedAt: 0,
3031
}
3132

32-
result := DB.Create(&link)
33+
dbResponse := DB.Create(&link)
3334

34-
if result.Error != nil {
35-
return "", "", result.Error
35+
if dbResponse.Error != nil {
36+
return "", "", errors.New(dbResponse.Error.Error())
3637
}
3738

3839
createdAtStr := parseTimestamp(link.CreatedAt)

internal/log/log.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package log
2+
3+
import (
4+
"log/slog"
5+
"os"
6+
"strings"
7+
)
8+
9+
var logHandler = slog.NewTextHandler(os.Stdout, nil)
10+
11+
func NewLogger(service string) *slog.Logger {
12+
baseLogger := slog.New(logHandler)
13+
return baseLogger.With("zone", strings.ToUpper(service))
14+
}

internal/server/server.go

+37-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,50 @@
11
package server
22

33
import (
4-
"fmt"
4+
"context"
5+
"log/slog"
6+
"net/http"
57

68
"github.com/abiddiscombe/concierge/internal/controllers"
9+
"github.com/abiddiscombe/concierge/internal/log"
710
"github.com/labstack/echo/v4"
811
"github.com/labstack/echo/v4/middleware"
912
)
1013

1114
func Init() {
15+
logger := log.NewLogger("server")
16+
1217
server := echo.New()
18+
server.HidePort = true
1319
server.HideBanner = true
20+
1421
server.Pre(middleware.RemoveTrailingSlash())
22+
server.Use(middleware.Recover())
23+
server.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
24+
LogStatus: true,
25+
LogMethod: true,
26+
LogURI: true,
27+
LogError: true,
28+
HandleError: true,
29+
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
30+
var logLevel slog.Level
31+
if v.Error == nil {
32+
logLevel = slog.LevelInfo
33+
} else if v.Status >= 400 && v.Status <= 499 {
34+
logLevel = slog.LevelWarn
35+
} else {
36+
logLevel = slog.LevelError
37+
}
38+
39+
logger.LogAttrs(context.Background(), logLevel, "HTTP Event",
40+
slog.Int("status", v.Status),
41+
slog.String("method", v.Method),
42+
slog.String("uri", v.URI),
43+
)
44+
45+
return nil
46+
},
47+
}))
1548

1649
server.GET("/", controllers.RootGet)
1750

@@ -23,6 +56,7 @@ func Init() {
2356
server.GET("/link/:alias", controllers.LinkGet)
2457
server.POST("/link/:alias", controllers.LinkPost)
2558

26-
fmt.Println("[Concierge] Server Starting.")
27-
server.Start(":3000")
59+
if err := server.Start(":3000"); err != http.ErrServerClosed {
60+
logger.Error("Server closed unexpectedly", "err", err)
61+
}
2862
}

0 commit comments

Comments
 (0)