Skip to content

Commit 83d1c91

Browse files
committed
Create heartbeat listner
1 parent f6e4126 commit 83d1c91

12 files changed

Lines changed: 418 additions & 3 deletions

api/pkg/entities/heartbeat.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package entities
2+
3+
import (
4+
"time"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
// Heartbeat represents a message sent between 2 phone numbers
10+
type Heartbeat struct {
11+
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
12+
Owner string `json:"owner" gorm:"index:idx_heartbeats_owner_timestamp" example:"+18005550199"`
13+
Timestamp time.Time `json:"timestamp" gorm:"index:idx_heartbeats_owner_timestamp" example:"2022-06-05T14:26:01.520828+03:00"`
14+
Quantity int `json:"quantity" example:"2"`
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package events
2+
3+
import (
4+
"time"
5+
)
6+
7+
// EventTypeHeartbeatPhoneOutstanding is emitted when the phone requests for outstanding messages
8+
const EventTypeHeartbeatPhoneOutstanding = "heartbeat.phone.outstanding"
9+
10+
// HeartbeatPhoneOutstandingPayload is the payload of the EventTypeHeartbeatPhoneOutstanding event
11+
type HeartbeatPhoneOutstandingPayload struct {
12+
Owner string `json:"owner"`
13+
Timestamp time.Time `json:"timestamp"`
14+
Quantity int `json:"quantity"`
15+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package handlers
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/NdoleStudio/http-sms-manager/pkg/requests"
7+
"github.com/NdoleStudio/http-sms-manager/pkg/services"
8+
"github.com/NdoleStudio/http-sms-manager/pkg/telemetry"
9+
"github.com/NdoleStudio/http-sms-manager/pkg/validators"
10+
"github.com/davecgh/go-spew/spew"
11+
"github.com/gofiber/fiber/v2"
12+
"github.com/palantir/stacktrace"
13+
)
14+
15+
// HeartbeatHandler handles heartbeat http requests.
16+
type HeartbeatHandler struct {
17+
handler
18+
logger telemetry.Logger
19+
tracer telemetry.Tracer
20+
validator *validators.HeartbeatHandlerValidator
21+
service *services.HeartbeatService
22+
}
23+
24+
// NewHeartbeatHandler creates a new HeartbeatHandler
25+
func NewHeartbeatHandler(
26+
logger telemetry.Logger,
27+
tracer telemetry.Tracer,
28+
validator *validators.HeartbeatHandlerValidator,
29+
service *services.HeartbeatService,
30+
) (h *HeartbeatHandler) {
31+
return &HeartbeatHandler{
32+
logger: logger.WithService(fmt.Sprintf("%T", h)),
33+
tracer: tracer,
34+
validator: validator,
35+
service: service,
36+
}
37+
}
38+
39+
// RegisterRoutes registers the routes for the MessageHandler
40+
func (h *HeartbeatHandler) RegisterRoutes(router fiber.Router) {
41+
router.Get("/heartbeat", h.Index)
42+
}
43+
44+
// Index returns the heartbeats of a phone number
45+
// @Summary Get heartbeats of an owner phone number
46+
// @Description Get the last time a phone number requested for outstanding messages. It will be sorted by timestamp in descending order.
47+
// @Tags Messages
48+
// @Accept json
49+
// @Produce json
50+
// @Param owner query string true "the owner's phone number" default(+18005550199)
51+
// @Param skip query int false "number of heartbeats to skip" minimum(0)
52+
// @Param query query string false "filter containing query"
53+
// @Param limit query int false "number of heartbeats to return" minimum(1) maximum(20)
54+
// @Success 200 {object} responses.HeartbeatsResponse
55+
// @Failure 400 {object} responses.BadRequest
56+
// @Failure 422 {object} responses.UnprocessableEntity
57+
// @Failure 500 {object} responses.InternalServerError
58+
// @Router /heartbeats [get]
59+
func (h *HeartbeatHandler) Index(c *fiber.Ctx) error {
60+
ctx, span := h.tracer.StartFromFiberCtx(c)
61+
defer span.End()
62+
63+
ctxLogger := h.tracer.CtxLogger(h.logger, span)
64+
65+
var request requests.HeartbeatIndex
66+
if err := c.QueryParser(&request); err != nil {
67+
msg := fmt.Sprintf("cannot marshall params [%s] into %T", c.OriginalURL(), request)
68+
ctxLogger.Warn(stacktrace.Propagate(err, msg))
69+
return h.responseBadRequest(c, err)
70+
}
71+
72+
if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 {
73+
msg := fmt.Sprintf("validation errors [%s], while fetching heartbeats [%+#v]", spew.Sdump(errors), request)
74+
ctxLogger.Warn(stacktrace.NewError(msg))
75+
return h.responseUnprocessableEntity(c, errors, "validation errors while fetching heartbeats")
76+
}
77+
78+
heartbeats, err := h.service.Index(ctx, request.Owner, request.ToIndexParams())
79+
if err != nil {
80+
msg := fmt.Sprintf("cannot get messgaes with params [%+#v]", request)
81+
ctxLogger.Error(stacktrace.Propagate(err, msg))
82+
return h.responseInternalServerError(c)
83+
}
84+
85+
return h.responseOK(c, fmt.Sprintf("fetched %d %s", len(*heartbeats), h.pluralize("heartbeat", len(*heartbeats))), heartbeats)
86+
}

api/pkg/listeners/contact_listener.go

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package listeners
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/NdoleStudio/http-sms-manager/pkg/events"
8+
"github.com/NdoleStudio/http-sms-manager/pkg/services"
9+
"github.com/NdoleStudio/http-sms-manager/pkg/telemetry"
10+
cloudevents "github.com/cloudevents/sdk-go/v2"
11+
"github.com/palantir/stacktrace"
12+
)
13+
14+
// HeartbeatListener handles cloud events which need to register entities.Heartbeat
15+
type HeartbeatListener struct {
16+
logger telemetry.Logger
17+
tracer telemetry.Tracer
18+
service *services.HeartbeatService
19+
}
20+
21+
// NewHeartbeatListener creates a new instance of HeartbeatListener
22+
func NewHeartbeatListener(
23+
logger telemetry.Logger,
24+
tracer telemetry.Tracer,
25+
service *services.HeartbeatService,
26+
) (l *HeartbeatListener, routes map[string]events.EventListener) {
27+
l = &HeartbeatListener{
28+
logger: logger.WithService(fmt.Sprintf("%T", l)),
29+
tracer: tracer,
30+
service: service,
31+
}
32+
33+
return l, map[string]events.EventListener{
34+
events.EventTypeHeartbeatPhoneOutstanding: l.onHeartbeatPhoneOutstanding,
35+
}
36+
}
37+
38+
// onHeartbeatPhoneOutstanding handles the events.EventTypeHeartbeatPhoneOutstanding event
39+
func (listener *HeartbeatListener) onHeartbeatPhoneOutstanding(ctx context.Context, event cloudevents.Event) error {
40+
ctx, span := listener.tracer.Start(ctx)
41+
defer span.End()
42+
43+
var payload events.HeartbeatPhoneOutstandingPayload
44+
if err := event.DataAs(&payload); err != nil {
45+
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
46+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
47+
}
48+
49+
storeParams := services.HeartbeatStoreParams{
50+
Owner: payload.Owner,
51+
Timestamp: payload.Timestamp,
52+
Quantity: payload.Quantity,
53+
}
54+
55+
if _, err := listener.service.Store(ctx, storeParams); err != nil {
56+
msg := fmt.Sprintf("cannot store heartbeat with params [%+#v] for event with ID [%s]", storeParams, event.ID())
57+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
58+
}
59+
60+
return nil
61+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package repositories
2+
3+
import (
4+
"context"
5+
6+
"github.com/NdoleStudio/http-sms-manager/pkg/entities"
7+
)
8+
9+
// HeartbeatRepository loads and persists an entities.Heartbeat
10+
type HeartbeatRepository interface {
11+
// Store a new entities.Heartbeat
12+
Store(ctx context.Context, heartbeat *entities.Heartbeat) error
13+
14+
// Index entities.Heartbeat of an owner
15+
Index(ctx context.Context, owner string, params IndexParams) (*[]entities.Heartbeat, error)
16+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package requests
2+
3+
import (
4+
"strings"
5+
6+
"github.com/NdoleStudio/http-sms-manager/pkg/repositories"
7+
)
8+
9+
// HeartbeatIndex is the payload fetching entities.Message sent between 2 numbers
10+
type HeartbeatIndex struct {
11+
request
12+
Skip string `json:"skip" query:"skip"`
13+
Owner string `json:"owner" query:"owner"`
14+
Query string `json:"query" query:"query"`
15+
Limit string `json:"limit" query:"limit"`
16+
}
17+
18+
// Sanitize sets defaults to MessageOutstanding
19+
func (input *HeartbeatIndex) Sanitize() HeartbeatIndex {
20+
if strings.TrimSpace(input.Limit) == "" {
21+
input.Limit = "1"
22+
}
23+
input.Query = strings.TrimSpace(input.Query)
24+
input.Owner = input.sanitizeAddress(input.Owner)
25+
input.Skip = strings.TrimSpace(input.Skip)
26+
if input.Skip == "" {
27+
input.Skip = "0"
28+
}
29+
return *input
30+
}
31+
32+
// ToIndexParams converts HeartbeatIndex to repositories.IndexParams
33+
func (input *HeartbeatIndex) ToIndexParams() repositories.IndexParams {
34+
return repositories.IndexParams{
35+
Skip: input.getInt(input.Skip),
36+
Query: input.Query,
37+
Limit: input.getInt(input.Limit),
38+
}
39+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
type request struct{}
99

10+
// getLimit gets the take as a string
1011
func (input *request) sanitizeAddress(value string) string {
1112
value = strings.TrimRight(value, " ")
1213
if len(value) > 0 && value[0] == ' ' {
@@ -15,6 +16,7 @@ func (input *request) sanitizeAddress(value string) string {
1516
return value
1617
}
1718

19+
// getLimit gets the take as a string
1820
func (input *request) getInt(value string) int {
1921
val, _ := strconv.Atoi(value)
2022
return val
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package responses
2+
3+
import "github.com/NdoleStudio/http-sms-manager/pkg/entities"
4+
5+
// HeartbeatsResponse is the payload containing []entities.Heartbeat
6+
type HeartbeatsResponse struct {
7+
response
8+
Data []entities.Heartbeat `json:"data"`
9+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package services
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/google/uuid"
9+
10+
"github.com/NdoleStudio/http-sms-manager/pkg/repositories"
11+
"github.com/palantir/stacktrace"
12+
13+
"github.com/NdoleStudio/http-sms-manager/pkg/entities"
14+
"github.com/NdoleStudio/http-sms-manager/pkg/telemetry"
15+
)
16+
17+
// HeartbeatService is handles heartbeat requests
18+
type HeartbeatService struct {
19+
logger telemetry.Logger
20+
tracer telemetry.Tracer
21+
repository repositories.HeartbeatRepository
22+
}
23+
24+
// NewHeartbeatService creates a new HeartbeatService
25+
func NewHeartbeatService(
26+
logger telemetry.Logger,
27+
tracer telemetry.Tracer,
28+
repository repositories.HeartbeatRepository,
29+
) (s *HeartbeatService) {
30+
return &HeartbeatService{
31+
logger: logger.WithService(fmt.Sprintf("%T", s)),
32+
tracer: tracer,
33+
repository: repository,
34+
}
35+
}
36+
37+
// Index fetches the heartbeats for a phone number
38+
func (service *HeartbeatService) Index(ctx context.Context, owner string, params repositories.IndexParams) (*[]entities.Heartbeat, error) {
39+
ctx, span := service.tracer.Start(ctx)
40+
defer span.End()
41+
42+
ctxLogger := service.tracer.CtxLogger(service.logger, span)
43+
44+
heartbeats, err := service.repository.Index(ctx, owner, params)
45+
if err != nil {
46+
msg := fmt.Sprintf("could not fetch heartbeats with parms [%+#v]", params)
47+
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
48+
}
49+
50+
ctxLogger.Info(fmt.Sprintf("fetched [%d] messages with prams [%+#v]", len(*heartbeats), params))
51+
return heartbeats, nil
52+
}
53+
54+
// HeartbeatStoreParams are parameters for creating a new entities.Heartbeat
55+
type HeartbeatStoreParams struct {
56+
Owner string
57+
Timestamp time.Time
58+
Quantity int
59+
}
60+
61+
// Store a new entities.Heartbeat
62+
func (service *HeartbeatService) Store(ctx context.Context, params HeartbeatStoreParams) (*entities.Heartbeat, error) {
63+
ctx, span := service.tracer.Start(ctx)
64+
defer span.End()
65+
66+
ctxLogger := service.tracer.CtxLogger(service.logger, span)
67+
68+
heartbeat := &entities.Heartbeat{
69+
ID: uuid.New(),
70+
Owner: params.Owner,
71+
Timestamp: params.Timestamp,
72+
Quantity: params.Quantity,
73+
}
74+
75+
if err := service.repository.Store(ctx, heartbeat); err != nil {
76+
msg := fmt.Sprintf("cannot save heartbeat with id [%s]", heartbeat.ID)
77+
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
78+
}
79+
80+
ctxLogger.Info(fmt.Sprintf("heartbeat saved with id [%s] in the repository", heartbeat.ID))
81+
return heartbeat, nil
82+
}

0 commit comments

Comments
 (0)