infra: add logging wrapping

Resolves issues with gcp cloud logging quirks such as field names
This commit is contained in:
Arjun Patel
2026-05-27 15:49:27 -07:00
parent e804cee229
commit 563c91e7d5
30 changed files with 347 additions and 250 deletions
+10 -8
View File
@@ -5,15 +5,17 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"strings"
"time"
"github.com/flowy-live/llink/internal/utils/flog"
firebaseauth "firebase.google.com/go/v4/auth"
pbaero "github.com/flowy-live/llink/genproto/aero"
"github.com/flowy-live/llink/internal/utils"
"github.com/redis/go-redis/v9"
"go.jetify.com/typeid"
pbaero "github.com/flowy-live/llink/genproto/aero"
"github.com/flowy-live/llink/internal/utils"
)
const (
@@ -86,7 +88,7 @@ func (a *authServiceImpl) MintFirebaseCustomToken(ctx context.Context, humanId s
func (a *authServiceImpl) IsSystemAdmin(ctx context.Context, email string) bool {
formattedEmail, err := utils.NormalizeEmail(email)
if err != nil {
slog.Error("problem validating email", "error", err)
flog.Error("problem validating email", "error", err)
return false
}
@@ -110,7 +112,7 @@ func (a *authServiceImpl) RequestSignInCode(ctx context.Context, email string) e
err = a.redisClient.Set(ctx, formattedEmail, code, codeExpiry).Err()
if err != nil {
slog.Error("error setting code in redis", "error", err)
flog.Error("error setting code in redis", "error", err)
return fmt.Errorf("error storing sign-in code: %w", err)
}
@@ -129,7 +131,7 @@ func (a *authServiceImpl) RequestSignInCode(ctx context.Context, email string) e
return fmt.Errorf("an error occurred while sending the email: %w", err)
}
slog.Info("sent sign in code", "email", formattedEmail)
flog.Info("sent sign in code", "email", formattedEmail)
return nil
}
@@ -144,7 +146,7 @@ func (a *authServiceImpl) VerifySignInCode(ctx context.Context, email, code, hum
if errors.Is(err, redis.Nil) {
return "", ErrInvalidCode
}
slog.Error("error getting code from redis", "error", err)
flog.Error("error getting code from redis", "error", err)
return "", fmt.Errorf("error verifying code: %w", err)
}
@@ -153,7 +155,7 @@ func (a *authServiceImpl) VerifySignInCode(ctx context.Context, email, code, hum
}
if err := a.redisClient.Del(ctx, formattedEmail).Err(); err != nil {
slog.Error("error deleting code from redis", "error", err)
flog.Error("error deleting code from redis", "error", err)
}
token, err := a.createSession(ctx, formattedEmail, humanId)
+3 -2
View File
@@ -4,9 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"log/slog"
"time"
"github.com/flowy-live/llink/internal/utils/flog"
"github.com/stripe/stripe-go/v85"
"github.com/stripe/stripe-go/v85/webhook"
)
@@ -17,7 +18,7 @@ func (s *serviceImpl) HandleWebhook(ctx context.Context, payload []byte, signatu
return fmt.Errorf("verify stripe signature: %w", err)
}
slog.Info("stripe webhook", "type", event.Type, "id", event.ID)
flog.Info("stripe webhook", "type", event.Type, "id", event.ID)
switch event.Type {
case "checkout.session.completed":
+6 -5
View File
@@ -2,9 +2,10 @@ package db
import (
"context"
"log/slog"
"os"
"github.com/flowy-live/llink/internal/utils/flog"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -21,24 +22,24 @@ func Pool() *pgxpool.Pool {
func Init() {
connString := os.Getenv("LLINK_POSTGRES_CONNECTION_URL")
if connString == "" {
slog.Error("must provide LLINK_POSTGRES_CONNECTION_URL in env")
flog.Error("must provide LLINK_POSTGRES_CONNECTION_URL in env")
os.Exit(1)
}
dbpool, err := pgxpool.New(context.Background(), connString)
if err != nil {
slog.Error("unable to create connection pool", "error", err)
flog.Error("unable to create connection pool", "error", err)
os.Exit(1)
}
var greeting string
err = dbpool.QueryRow(context.Background(), "select 'Hello, world!'").Scan(&greeting)
if err != nil {
slog.Error("queryRow failed", "error", err)
flog.Error("queryRow failed", "error", err)
os.Exit(1)
}
slog.Info("successfully connected to database", "greeting", greeting)
flog.Info("successfully connected to database", "greeting", greeting)
db = dbpool
}
+12 -11
View File
@@ -5,9 +5,10 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"time"
"github.com/flowy-live/llink/internal/utils/flog"
"cloud.google.com/go/storage"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
@@ -49,7 +50,7 @@ func NewService(pool *pgxpool.Pool, storageClient *storage.Client, config Config
}
if config.GoogleServiceAccountEmail == "" {
slog.Error("GoogleServiceAccountEmail is not set in config. Signed URLs may not work if the storage client is not properly authenticated with a service account.")
flog.Error("GoogleServiceAccountEmail is not set in config. Signed URLs may not work if the storage client is not properly authenticated with a service account.")
panic("GoogleServiceAccountEmail is required for signed URL generation")
}
@@ -102,10 +103,10 @@ func (s *serviceImpl) PrepareUpload(ctx context.Context, input PrepareUploadInpu
Headers: []string{contentLengthHeader},
})
if err != nil {
slog.Error("failed to generate signed upload URL", "error", err, "bucket", s.bucketName, "object_key", objectKey)
flog.Error("failed to generate signed upload URL", "error", err, "bucket", s.bucketName, "object_key", objectKey)
// Roll back the placeholder row.
if delErr := s.repo.delete(ctx, created.ID); delErr != nil {
slog.Warn("failed to cleanup db record after signed URL failure", "error", delErr, "object_id", created.ID)
flog.Warn("failed to cleanup db record after signed URL failure", "error", delErr, "object_id", created.ID)
}
return nil, err
}
@@ -134,7 +135,7 @@ func (s *serviceImpl) ConfirmUpload(ctx context.Context, objectID string) (*Obje
if errors.Is(err, storage.ErrObjectNotExist) {
return nil, errors.Join(ErrNotFound, errors.New("object not found in storage"))
}
slog.Error("failed to get GCS object attrs", "error", err, "bucket", obj.BucketName, "object_key", obj.ObjectKey)
flog.Error("failed to get GCS object attrs", "error", err, "bucket", obj.BucketName, "object_key", obj.ObjectKey)
return nil, err
}
@@ -170,13 +171,13 @@ func (s *serviceImpl) CreateFromReader(ctx context.Context, input CreateFromRead
if _, err := io.Copy(w, body); err != nil {
// Always release the writer; surface the copy error, not Close's.
if cerr := w.Close(); cerr != nil {
slog.Warn("failed to close GCS writer after copy failure", "error", cerr, "object_key", objectKey)
flog.Warn("failed to close GCS writer after copy failure", "error", cerr, "object_key", objectKey)
}
slog.Error("failed to stream object to GCS", "error", err, "bucket", s.bucketName, "object_key", objectKey)
flog.Error("failed to stream object to GCS", "error", err, "bucket", s.bucketName, "object_key", objectKey)
return nil, err
}
if err := w.Close(); err != nil {
slog.Error("failed to close GCS writer", "error", err, "bucket", s.bucketName, "object_key", objectKey)
flog.Error("failed to close GCS writer", "error", err, "bucket", s.bucketName, "object_key", objectKey)
return nil, err
}
@@ -193,7 +194,7 @@ func (s *serviceImpl) CreateFromReader(ctx context.Context, input CreateFromRead
if err != nil {
// Best-effort: drop the now-untracked GCS object.
if delErr := s.storageClient.Bucket(s.bucketName).Object(objectKey).Delete(ctx); delErr != nil {
slog.Warn("failed to clean up GCS object after db create failure", "error", delErr, "object_key", objectKey)
flog.Warn("failed to clean up GCS object after db create failure", "error", delErr, "object_key", objectKey)
}
return nil, err
}
@@ -227,7 +228,7 @@ func (s *serviceImpl) GetDownloadURL(ctx context.Context, objectID string) (stri
Expires: time.Now().Add(s.downloadURLExpiry),
})
if err != nil {
slog.Error("failed to generate signed download URL", "error", err, "bucket", obj.BucketName, "object_key", obj.ObjectKey)
flog.Error("failed to generate signed download URL", "error", err, "bucket", obj.BucketName, "object_key", obj.ObjectKey)
return "", err
}
@@ -246,7 +247,7 @@ func (s *serviceImpl) Delete(ctx context.Context, objectID string) error {
// GCS first so we don't strand an object after the row vanishes; missing object is fine.
gcsErr := s.storageClient.Bucket(obj.BucketName).Object(obj.ObjectKey).Delete(ctx)
if gcsErr != nil && !errors.Is(gcsErr, storage.ErrObjectNotExist) {
slog.Error("failed to delete object from GCS", "error", gcsErr, "bucket", obj.BucketName, "object_key", obj.ObjectKey)
flog.Error("failed to delete object from GCS", "error", gcsErr, "bucket", obj.BucketName, "object_key", obj.ObjectKey)
return gcsErr
}
+13 -12
View File
@@ -5,9 +5,10 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"github.com/flowy-live/llink/internal/utils/flog"
"github.com/flowy-live/llink/internal/billing"
"github.com/flowy-live/llink/internal/middleware"
"github.com/flowy-live/llink/internal/network"
@@ -42,7 +43,7 @@ func (h *Handler) GetNetworkUsage(w http.ResponseWriter, r *http.Request) {
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
if err != nil {
slog.Error("failed to check network membership", "error", err, "network_id", networkID)
flog.Error("failed to check network membership", "error", err, "network_id", networkID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -53,7 +54,7 @@ func (h *Handler) GetNetworkUsage(w http.ResponseWriter, r *http.Request) {
usage, err := h.billingSvc.GetUsage(r.Context(), networkID)
if err != nil {
slog.Error("failed to get network usage", "error", err, "network_id", networkID)
flog.Error("failed to get network usage", "error", err, "network_id", networkID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -69,7 +70,7 @@ func (h *Handler) GetNetworkBilling(w http.ResponseWriter, r *http.Request) {
status, err := h.billingSvc.GetStatus(r.Context(), net.ID)
if err != nil {
slog.Error("failed to get billing status", "error", err, "network_id", net.ID)
flog.Error("failed to get billing status", "error", err, "network_id", net.ID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -97,14 +98,14 @@ func (h *Handler) CreateCheckoutSession(w http.ResponseWriter, r *http.Request)
adminHuman, err := h.humanSvc.GetByID(r.Context(), adminHumanId)
if err != nil {
slog.Error("failed to load admin human", "error", err, "human_id", adminHumanId)
flog.Error("failed to load admin human", "error", err, "human_id", adminHumanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
seats, err := h.networkSvc.CountSeats(r.Context(), net.ID)
if err != nil {
slog.Error("failed to count seats", "error", err, "network_id", net.ID)
flog.Error("failed to count seats", "error", err, "network_id", net.ID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -117,7 +118,7 @@ func (h *Handler) CreateCheckoutSession(w http.ResponseWriter, r *http.Request)
Seats: seats,
})
if err != nil {
slog.Error("failed to create checkout session", "error", err, "network_id", net.ID)
flog.Error("failed to create checkout session", "error", err, "network_id", net.ID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -137,7 +138,7 @@ func (h *Handler) CreatePortalSession(w http.ResponseWriter, r *http.Request) {
return
}
if err != nil {
slog.Error("failed to create portal session", "error", err, "network_id", net.ID)
flog.Error("failed to create portal session", "error", err, "network_id", net.ID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -150,14 +151,14 @@ const maxStripeWebhookBytes = 1 << 20 // 1 MiB
func (h *Handler) HandleStripeWebhook(w http.ResponseWriter, r *http.Request) {
payload, err := io.ReadAll(io.LimitReader(r.Body, maxStripeWebhookBytes))
if err != nil {
slog.Warn("stripe webhook: failed to read body", "error", err)
flog.Warn("stripe webhook: failed to read body", "error", err)
http.Error(w, "bad request", http.StatusBadRequest)
return
}
signature := r.Header.Get("Stripe-Signature")
if err := h.billingSvc.HandleWebhook(r.Context(), payload, signature); err != nil {
slog.Error("stripe webhook failed", "error", err)
flog.Error("stripe webhook failed", "error", err)
formattedErr := fmt.Errorf("webhook processing failed: %w", err)
http.Error(w, formattedErr.Error(), http.StatusBadRequest)
return
@@ -187,7 +188,7 @@ func (h *Handler) loadNetworkForAdmin(w http.ResponseWriter, r *http.Request) (*
return nil, "", false
}
if err != nil {
slog.Error("failed to load network", "error", err, "network_id", networkID)
flog.Error("failed to load network", "error", err, "network_id", networkID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return nil, "", false
}
@@ -203,6 +204,6 @@ func (h *Handler) loadNetworkForAdmin(w http.ResponseWriter, r *http.Request) (*
func writeJSON(w http.ResponseWriter, body any) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(body); err != nil {
slog.Error("failed to write json", "error", err)
flog.Error("failed to write json", "error", err)
}
}
+47 -45
View File
@@ -5,12 +5,15 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"github.com/flowy-live/llink/internal/utils/flog"
"cloud.google.com/go/firestore"
"github.com/livekit/protocol/webhook"
"github.com/flowy-live/llink/internal/auth"
"github.com/flowy-live/llink/internal/billing"
"github.com/flowy-live/llink/internal/depot"
@@ -22,7 +25,6 @@ import (
"github.com/flowy-live/llink/internal/particle"
"github.com/flowy-live/llink/internal/utils"
"github.com/flowy-live/llink/internal/waitlist"
"github.com/livekit/protocol/webhook"
)
type Handler struct {
@@ -187,13 +189,13 @@ func (h *Handler) RequestSignInCode(w http.ResponseWriter, r *http.Request) {
_, err := h.humanSvc.GetOrCreateByEmail(r.Context(), req.Email)
if err != nil {
slog.Error("failed to get or create human", "error", err, "email", req.Email)
flog.Error("failed to get or create human", "error", err, "email", req.Email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
if err := h.authSvc.RequestSignInCode(r.Context(), req.Email); err != nil {
slog.Error("failed to request sign-in code", "error", err, "email", req.Email)
flog.Error("failed to request sign-in code", "error", err, "email", req.Email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -220,7 +222,7 @@ func (h *Handler) SignIn(w http.ResponseWriter, r *http.Request) {
http.Error(w, "human not found", http.StatusNotFound)
return
}
slog.Error("failed to get human for sign-in", "error", err, "email", req.Email)
flog.Error("failed to get human for sign-in", "error", err, "email", req.Email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -231,7 +233,7 @@ func (h *Handler) SignIn(w http.ResponseWriter, r *http.Request) {
http.Error(w, "invalid code", http.StatusUnauthorized)
return
}
slog.Error("failed to verify sign-in code", "error", err, "email", req.Email)
flog.Error("failed to verify sign-in code", "error", err, "email", req.Email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -256,7 +258,7 @@ func (h *Handler) FirebaseToken(w http.ResponseWriter, r *http.Request) {
token, err := h.authSvc.MintFirebaseCustomToken(r.Context(), humanId)
if err != nil {
slog.Error("failed to mint Firebase custom token", "error", err, "humanId", humanId)
flog.Error("failed to mint Firebase custom token", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -273,7 +275,7 @@ func (h *Handler) SignOut(w http.ResponseWriter, r *http.Request) {
}
if err := h.authSvc.SignOut(r.Context(), token); err != nil {
slog.Error("failed to sign out", "error", err)
flog.Error("failed to sign out", "error", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -294,7 +296,7 @@ func (h *Handler) GetCurrentHuman(w http.ResponseWriter, r *http.Request) {
http.Error(w, "human not found", http.StatusNotFound)
return
}
slog.Error("failed to get current human", "error", err, "email", email)
flog.Error("failed to get current human", "error", err, "email", email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -323,7 +325,7 @@ func (h *Handler) UpdateSettings(w http.ResponseWriter, r *http.Request) {
if req.EmailNotificationsEnabled != nil {
if err := h.humanSvc.UpdateEmailNotificationsEnabled(r.Context(), humanId, *req.EmailNotificationsEnabled); err != nil {
slog.Error("failed to update email notifications setting", "error", err, "humanId", humanId)
flog.Error("failed to update email notifications setting", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -355,14 +357,14 @@ func (h *Handler) CreateNetwork(w http.ResponseWriter, r *http.Request) {
http.Error(w, "name cannot be empty", http.StatusBadRequest)
return
}
slog.Error("failed to create network", "error", err, "humanId", humanId, "name", req.Name)
flog.Error("failed to create network", "error", err, "humanId", humanId, "name", req.Name)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
resp, err := h.networkToDTO(r.Context(), net)
if err != nil {
slog.Error("failed to convert network to DTO", "error", err, "network_id", net.ID)
flog.Error("failed to convert network to DTO", "error", err, "network_id", net.ID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -381,7 +383,7 @@ func (h *Handler) ListNetworks(w http.ResponseWriter, r *http.Request) {
networks, err := h.networkSvc.ListForHuman(r.Context(), humanId)
if err != nil {
slog.Error("failed to list networks", "error", err, "humanId", humanId)
flog.Error("failed to list networks", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -390,7 +392,7 @@ func (h *Handler) ListNetworks(w http.ResponseWriter, r *http.Request) {
for _, net := range networks {
dto, err := h.networkToDTO(r.Context(), net)
if err != nil {
slog.Warn("failed to convert network to DTO in list", "error", err, "network_id", net.ID)
flog.Warn("failed to convert network to DTO in list", "error", err, "network_id", net.ID)
continue
}
resp = append(resp, dto)
@@ -415,7 +417,7 @@ func (h *Handler) GetNetwork(w http.ResponseWriter, r *http.Request) {
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
if err != nil {
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
flog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -430,14 +432,14 @@ func (h *Handler) GetNetwork(w http.ResponseWriter, r *http.Request) {
http.Error(w, "network not found", http.StatusNotFound)
return
}
slog.Error("failed to get network", "error", err, "network_id", networkID)
flog.Error("failed to get network", "error", err, "network_id", networkID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
resp, err := h.networkToDTO(r.Context(), net)
if err != nil {
slog.Error("failed to convert network to DTO", "error", err, "network_id", networkID)
flog.Error("failed to convert network to DTO", "error", err, "network_id", networkID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -463,7 +465,7 @@ func (h *Handler) AddMembersToNetwork(w http.ResponseWriter, r *http.Request) {
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
if err != nil {
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
flog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -497,7 +499,7 @@ func (h *Handler) AddMembersToNetwork(w http.ResponseWriter, r *http.Request) {
inviteEmails = append(inviteEmails, normalized)
continue
}
slog.Error("failed to look up human by email", "error", err, "email", normalized)
flog.Error("failed to look up human by email", "error", err, "email", normalized)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -506,7 +508,7 @@ func (h *Handler) AddMembersToNetwork(w http.ResponseWriter, r *http.Request) {
if len(memberHumanIds) > 0 {
if err := h.networkSvc.AddMembers(r.Context(), networkID, memberHumanIds); err != nil {
slog.Error("failed to add members to network", "error", err, "network_id", networkID)
flog.Error("failed to add members to network", "error", err, "network_id", networkID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -514,7 +516,7 @@ func (h *Handler) AddMembersToNetwork(w http.ResponseWriter, r *http.Request) {
if len(inviteEmails) > 0 {
if err := h.networkSvc.InviteByEmail(r.Context(), networkID, inviteEmails); err != nil {
slog.Error("failed to invite members to network", "error", err, "network_id", networkID)
flog.Error("failed to invite members to network", "error", err, "network_id", networkID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -522,14 +524,14 @@ func (h *Handler) AddMembersToNetwork(w http.ResponseWriter, r *http.Request) {
net, err := h.networkSvc.GetByID(r.Context(), networkID)
if err != nil {
slog.Error("failed to get network after adding members", "error", err, "network_id", networkID)
flog.Error("failed to get network after adding members", "error", err, "network_id", networkID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
resp, err := h.networkToDTO(r.Context(), net)
if err != nil {
slog.Error("failed to convert network to DTO", "error", err, "network_id", networkID)
flog.Error("failed to convert network to DTO", "error", err, "network_id", networkID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -558,7 +560,7 @@ func (h *Handler) RemoveMemberFromNetwork(w http.ResponseWriter, r *http.Request
}
if err := h.networkSvc.RemoveMember(r.Context(), net.ID, memberHumanId); err != nil {
slog.Error("failed to remove member from network", "error", err, "network_id", net.ID, "memberHumanId", memberHumanId)
flog.Error("failed to remove member from network", "error", err, "network_id", net.ID, "memberHumanId", memberHumanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -581,7 +583,7 @@ func (h *Handler) ListInvitationsForNetwork(w http.ResponseWriter, r *http.Reque
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
if err != nil {
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
flog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -592,7 +594,7 @@ func (h *Handler) ListInvitationsForNetwork(w http.ResponseWriter, r *http.Reque
invitations, err := h.networkSvc.ListInvitationsForNetwork(r.Context(), networkID)
if err != nil {
slog.Error("failed to list invitations", "error", err, "network_id", networkID)
flog.Error("failed to list invitations", "error", err, "network_id", networkID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -620,7 +622,7 @@ func (h *Handler) ListMyInvitations(w http.ResponseWriter, r *http.Request) {
invitations, err := h.networkSvc.ListInvitationsForEmail(r.Context(), email)
if err != nil {
slog.Error("failed to list invitations for email", "error", err, "email", email)
flog.Error("failed to list invitations for email", "error", err, "email", email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -663,7 +665,7 @@ func (h *Handler) AcceptInvitation(w http.ResponseWriter, r *http.Request) {
}
if err := h.networkSvc.AcceptInvitation(r.Context(), req.NetworkId, email, humanId); err != nil {
slog.Error("failed to accept invitation", "error", err, "network_id", req.NetworkId, "email", email)
flog.Error("failed to accept invitation", "error", err, "network_id", req.NetworkId, "email", email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -686,7 +688,7 @@ func (h *Handler) RevokeInvitation(w http.ResponseWriter, r *http.Request) {
isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId)
if err != nil {
slog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
flog.Error("failed to check network membership", "error", err, "network_id", networkID, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -707,7 +709,7 @@ func (h *Handler) RevokeInvitation(w http.ResponseWriter, r *http.Request) {
}
if err := h.networkSvc.RevokeInvitation(r.Context(), networkID, req.Email); err != nil {
slog.Error("failed to revoke invitation", "error", err, "network_id", networkID, "email", req.Email)
flog.Error("failed to revoke invitation", "error", err, "network_id", networkID, "email", req.Email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -729,7 +731,7 @@ func (h *Handler) DownloadParticleMedia(w http.ResponseWriter, r *http.Request)
downloadURL, err := h.depotSvc.GetDownloadURL(r.Context(), objectID)
if err != nil {
slog.Error("failed to get download URL", "error", err, "object_id", objectID)
flog.Error("failed to get download URL", "error", err, "object_id", objectID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -763,7 +765,7 @@ func (h *Handler) PrepareUpload(w http.ResponseWriter, r *http.Request) {
isMember, err := h.networkSvc.IsMember(r.Context(), req.NetworkId, humanId)
if err != nil {
slog.Error("failed to check network membership", "error", err, "network_id", req.NetworkId, "humanId", humanId)
flog.Error("failed to check network membership", "error", err, "network_id", req.NetworkId, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -785,7 +787,7 @@ func (h *Handler) PrepareUpload(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
slog.Error("failed to prepare upload", "error", err, "network_id", req.NetworkId, "name", req.Name)
flog.Error("failed to prepare upload", "error", err, "network_id", req.NetworkId, "name", req.Name)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -823,7 +825,7 @@ func (h *Handler) ConfirmUpload(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
slog.Error("failed to confirm upload", "error", err, "object_id", objectID)
flog.Error("failed to confirm upload", "error", err, "object_id", objectID)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -886,7 +888,7 @@ func (h *Handler) AddToWaitlist(w http.ResponseWriter, r *http.Request) {
http.Error(w, "already in the waitlist", http.StatusConflict)
return
}
slog.Error("failed to add to waitlist", "error", err, "email", req.Email)
flog.Error("failed to add to waitlist", "error", err, "email", req.Email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -912,7 +914,7 @@ func (h *Handler) GetWaitlist(w http.ResponseWriter, r *http.Request) {
entries, err := h.waitlistSvc.GetWaitlist(r.Context(), filter)
if err != nil {
slog.Error("failed to get waitlist", "error", err)
flog.Error("failed to get waitlist", "error", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -945,7 +947,7 @@ func (h *Handler) GetWaitlistEntry(w http.ResponseWriter, r *http.Request) {
http.Error(w, "entry not found", http.StatusNotFound)
return
}
slog.Error("failed to get waitlist entry", "error", err, "email", email)
flog.Error("failed to get waitlist entry", "error", err, "email", email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -977,7 +979,7 @@ func (h *Handler) InviteWaitlistEntrant(w http.ResponseWriter, r *http.Request)
http.Error(w, "entry not found", http.StatusNotFound)
return
}
slog.Error("failed to invite waitlist entrant", "error", err, "email", req.Email)
flog.Error("failed to invite waitlist entrant", "error", err, "email", req.Email)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -1019,7 +1021,7 @@ func (h *Handler) networkToDTO(ctx context.Context, n *network.Network) (Network
for _, memberHumanId := range n.MemberHumanIds {
hum, err := h.humanSvc.GetByID(ctx, memberHumanId)
if err != nil {
slog.Warn("failed to look up network member", "humanId", memberHumanId, "error", err)
flog.Warn("failed to look up network member", "humanId", memberHumanId, "error", err)
continue
}
humans = append(humans, humanToDTO(hum))
@@ -1070,7 +1072,7 @@ func (h *Handler) GetLivekitToken(w http.ResponseWriter, r *http.Request) {
token, err := h.livekitClient.GetJoinToken(roomName, humanId, humanEmail)
if err != nil {
slog.Error("failed to generate livekit token", "error", err, "humanId", humanId, "roomName", roomName)
flog.Error("failed to generate livekit token", "error", err, "humanId", humanId, "roomName", roomName)
http.Error(w, "failed to generate token", http.StatusInternalServerError)
return
}
@@ -1084,13 +1086,13 @@ func (h *Handler) GetLivekitToken(w http.ResponseWriter, r *http.Request) {
func (h *Handler) HandleLivekitWebhook(w http.ResponseWriter, r *http.Request) {
event, err := webhook.ReceiveWebhookEvent(r, h.livekitClient.KeyProvider())
if err != nil {
slog.Error("failed to verify livekit webhook", "error", err)
flog.Error("failed to verify livekit webhook", "error", err)
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
eventType := event.GetEvent()
slog.Info("received livekit webhook", "event", eventType, "room", event.GetRoom().GetName())
flog.Info("received livekit webhook", "event", eventType, "room", event.GetRoom().GetName())
switch eventType {
case "participant_joined", "participant_left", "room_finished":
@@ -1103,7 +1105,7 @@ func (h *Handler) HandleLivekitWebhook(w http.ResponseWriter, r *http.Request) {
roomName := event.GetRoom().GetName()
parts := strings.SplitN(roomName, "/", 2)
if len(parts) != 2 {
slog.Error("invalid room name format", "room", roomName)
flog.Error("invalid room name format", "room", roomName)
http.Error(w, "invalid room name", http.StatusBadRequest)
return
}
@@ -1120,7 +1122,7 @@ func (h *Handler) HandleLivekitWebhook(w http.ResponseWriter, r *http.Request) {
// Authoritative list avoids drift from missed/out-of-order webhooks.
participants, err := h.livekitClient.ListParticipants(ctx, roomName)
if err != nil {
slog.Error("failed to list participants", "error", err, "room", roomName)
flog.Error("failed to list participants", "error", err, "room", roomName)
// 200 to suppress LiveKit retries.
w.WriteHeader(http.StatusOK)
return
@@ -1136,7 +1138,7 @@ func (h *Handler) HandleLivekitWebhook(w http.ResponseWriter, r *http.Request) {
{Path: "huddle_active_participants", Value: participantIds},
})
if err != nil {
slog.Error("failed to update huddle participants in firestore", "error", err, "path", docPath)
flog.Error("failed to update huddle participants in firestore", "error", err, "path", docPath)
}
w.WriteHeader(http.StatusOK)
+3 -2
View File
@@ -4,7 +4,6 @@ import (
"context"
"errors"
"io"
"log/slog"
"net"
"net/http"
"net/url"
@@ -12,6 +11,8 @@ import (
"strings"
"sync"
"time"
"github.com/flowy-live/llink/internal/utils/flog"
)
// LinkMetadata mirrors the TS shape in `js/desktop/src/lib/link-metadata.ts`.
@@ -63,7 +64,7 @@ func (h *Handler) GetLinkMetadata(w http.ResponseWriter, r *http.Request) {
meta, err := fetchLinkMetadata(r.Context(), parsed)
if err != nil {
slog.Warn("link metadata fetch failed", "url", raw, "error", err)
flog.Warn("link metadata fetch failed", "url", raw, "error", err)
writeJSON(w, nil)
return
}
+4 -3
View File
@@ -3,9 +3,10 @@ package handler
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"github.com/flowy-live/llink/internal/utils/flog"
"github.com/flowy-live/llink/internal/human/pushnotify"
"github.com/flowy-live/llink/internal/middleware"
)
@@ -45,7 +46,7 @@ func (h *Handler) RegisterPushToken(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
slog.Error("failed to register push token", "error", err, "humanId", humanId)
flog.Error("failed to register push token", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
@@ -74,7 +75,7 @@ func (h *Handler) UnregisterPushToken(w http.ResponseWriter, r *http.Request) {
err := h.pushTokenSvc.Unregister(r.Context(), humanId, req.Token)
if err != nil && !errors.Is(err, pushnotify.ErrNotFound) {
slog.Error("failed to unregister push token", "error", err, "humanId", humanId)
flog.Error("failed to unregister push token", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
+9 -8
View File
@@ -4,7 +4,8 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/flowy-live/llink/internal/utils/flog"
"github.com/flowy-live/llink/internal/network"
)
@@ -55,7 +56,7 @@ func NewNotifier(networkR network.Reader, tokens Service, expo *ExpoClient) *Not
func (n *Notifier) NotifyParticleCreated(ctx context.Context, in NotifyInput) error {
if in.NetworkID == "" || in.ParticleID == "" {
slog.Info("pushnotify: skip — missing ids",
flog.Info("pushnotify: skip — missing ids",
"networkID", in.NetworkID,
"particleID", in.ParticleID,
)
@@ -71,7 +72,7 @@ func (n *Notifier) NotifyParticleCreated(ctx context.Context, in NotifyInput) er
recipientsBeforeSenderFilter := len(recipients)
recipients = filterOut(recipients, in.SenderHumanID)
if len(recipients) == 0 {
slog.Info("pushnotify: skip — no recipients",
flog.Info("pushnotify: skip — no recipients",
"networkID", in.NetworkID,
"particleID", in.ParticleID,
"senderHumanID", in.SenderHumanID,
@@ -87,7 +88,7 @@ func (n *Notifier) NotifyParticleCreated(ctx context.Context, in NotifyInput) er
return fmt.Errorf("token lookup: %w", err)
}
if len(tokens) == 0 {
slog.Info("pushnotify: skip — no tokens for recipients",
flog.Info("pushnotify: skip — no tokens for recipients",
"networkID", in.NetworkID,
"particleID", in.ParticleID,
"recipients", len(recipients),
@@ -98,7 +99,7 @@ func (n *Notifier) NotifyParticleCreated(ctx context.Context, in NotifyInput) er
msgs := buildMessages(tokens, in)
tickets, sendErr := n.expo.Send(ctx, msgs)
slog.Info("pushnotify: dispatch",
flog.Info("pushnotify: dispatch",
"networkID", in.NetworkID,
"particleID", in.ParticleID,
"recipients", len(recipients),
@@ -127,14 +128,14 @@ func (n *Notifier) cleanupDeadTokens(ctx context.Context, msgs []Message, ticket
code, _ := t.Details["error"].(string)
if code != ExpoErrorDeviceNotRegistered {
if t.Status == "error" {
slog.Warn("pushnotify: ticket error", "code", code, "message", t.Message, "to", msgs[i].To)
flog.Warn("pushnotify: ticket error", "code", code, "message", t.Message, "to", msgs[i].To)
}
continue
}
if err := n.tokens.DeleteByToken(ctx, msgs[i].To); err != nil && !errors.Is(err, ErrNotFound) {
slog.Error("pushnotify: failed to delete dead token", "error", err, "token", msgs[i].To)
flog.Error("pushnotify: failed to delete dead token", "error", err, "token", msgs[i].To)
} else {
slog.Info("pushnotify: removed unregistered token", "token", msgs[i].To)
flog.Info("pushnotify: removed unregistered token", "token", msgs[i].To)
}
}
}
+3 -2
View File
@@ -4,10 +4,11 @@ import (
"bytes"
"context"
"errors"
"log/slog"
"os"
"os/exec"
"strings"
"github.com/flowy-live/llink/internal/utils/flog"
)
func IsIOSPlayableMime(mime string) bool {
@@ -56,7 +57,7 @@ func TranscodeToMp4(ctx context.Context, input TranscodeInput) (*TranscodeOutput
tmp, err := os.CreateTemp("", "transcode-*"+outputExt)
if err != nil {
slog.Error("transcode: failed to create temp file", "error", err)
flog.Error("transcode: failed to create temp file", "error", err)
return nil, err
}
tmpPath := tmp.Name()
+3 -2
View File
@@ -2,10 +2,11 @@ package middleware
import (
"context"
"log/slog"
"net/http"
"strings"
"github.com/flowy-live/llink/internal/utils/flog"
"github.com/flowy-live/llink/internal/auth"
)
@@ -62,7 +63,7 @@ func Auth(authSvc auth.AuthService) func(http.Handler) http.Handler {
}
if err := authSvc.ExtendSession(r.Context(), token); err != nil {
slog.Warn("failed to extend session", "error", err)
flog.Warn("failed to extend session", "error", err)
}
ctx := WithEmail(r.Context(), session.Email)
+9 -7
View File
@@ -5,17 +5,19 @@ import (
"errors"
"fmt"
"html"
"log/slog"
"slices"
"strings"
"github.com/flowy-live/llink/internal/utils/flog"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
pbaero "github.com/flowy-live/llink/genproto/aero"
"github.com/flowy-live/llink/internal/billing"
"github.com/flowy-live/llink/internal/constants"
"github.com/flowy-live/llink/internal/livestore"
"github.com/flowy-live/llink/internal/utils"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var ErrNotFound = errors.New("network not found")
@@ -91,7 +93,7 @@ func (s *serviceImpl) Create(ctx context.Context, name, adminHumanId string) (*N
},
})
if err != nil {
slog.Warn("unable to send admin update email", "error", err)
flog.Warn("unable to send admin update email", "error", err)
}
return network, nil
@@ -149,13 +151,13 @@ func (s *serviceImpl) RemoveMember(ctx context.Context, networkID, humanId strin
// Postgres is the source of truth: failures are logged and the reconciler heals drift.
func (s *serviceImpl) mirrorAddMembership(ctx context.Context, humanId, networkID string) {
if err := s.pub.Add(ctx, humanId, networkID); err != nil {
slog.Error("membership publish add failed", "error", err, "humanId", humanId, "networkID", networkID)
flog.Error("membership publish add failed", "error", err, "humanId", humanId, "networkID", networkID)
}
}
func (s *serviceImpl) mirrorRemoveMembership(ctx context.Context, humanId, networkID string) {
if err := s.pub.Remove(ctx, humanId, networkID); err != nil {
slog.Error("membership publish remove failed", "error", err, "humanId", humanId, "networkID", networkID)
flog.Error("membership publish remove failed", "error", err, "humanId", humanId, "networkID", networkID)
}
}
@@ -214,7 +216,7 @@ func (s *serviceImpl) InviteByEmail(ctx context.Context, networkID string, email
},
})
if err != nil {
slog.Warn("unable to send email notification", "email", email, "network", network.Name)
flog.Warn("unable to send email notification", "email", email, "network", network.Name)
}
}
return nil
+14 -12
View File
@@ -3,12 +3,14 @@ package particle
import (
"context"
"fmt"
"log/slog"
"os"
"strings"
"time"
"github.com/flowy-live/llink/internal/utils/flog"
"cloud.google.com/go/firestore"
"github.com/flowy-live/llink/internal/depot"
"github.com/flowy-live/llink/internal/media"
)
@@ -44,17 +46,17 @@ type TranscodeResult struct {
func Transcode(ctx context.Context, depotSvc depot.Service, doc *firestore.DocumentSnapshot) TranscodeResult {
var mp FirestoreMediaParticle
if err := doc.DataTo(&mp); err != nil {
slog.Error("transcode: unable to marshal particle data", "error", err)
flog.Error("transcode: unable to marshal particle data", "error", err)
return TranscodeResult{Err: fmt.Errorf("unmarshal particle: %w", err)}
}
particleType, err := ParseParticleType(mp.Type)
if err != nil {
slog.Error("transcode: invalid particle type", "error", err)
flog.Error("transcode: invalid particle type", "error", err)
return TranscodeResult{Err: fmt.Errorf("parse type: %w", err)}
}
if particleType != TypeMedia {
slog.Info("transcode: particle is not of type media")
flog.Info("transcode: particle is not of type media")
return TranscodeResult{Skipped: true, SkipReason: SkipReasonNotMedia}
}
@@ -63,19 +65,19 @@ func Transcode(ctx context.Context, depotSvc depot.Service, doc *firestore.Docum
}
if media.IsIOSPlayableMime(mp.Properties.MimeType) {
slog.Info("transcode: skipping because already playable on ios")
flog.Info("transcode: skipping because already playable on ios")
return TranscodeResult{Skipped: true, SkipReason: SkipReasonIOSPlayable}
}
sourceURL, err := depotSvc.GetDownloadURL(ctx, mp.Properties.ObjectId)
if err != nil {
slog.Error("transcode: failed to get download URL", "error", err, "object_id", mp.Properties.ObjectId)
flog.Error("transcode: failed to get download URL", "error", err, "object_id", mp.Properties.ObjectId)
return TranscodeResult{Err: fmt.Errorf("download URL: %w", err)}
}
networkID, err := NetworkIDFromParticlePath(doc.Ref.Path)
if err != nil {
slog.Error("transcode: failed to derive network id", "error", err, "path", doc.Ref.Path)
flog.Error("transcode: failed to derive network id", "error", err, "path", doc.Ref.Path)
return TranscodeResult{Err: fmt.Errorf("network id: %w", err)}
}
@@ -86,14 +88,14 @@ func Transcode(ctx context.Context, depotSvc depot.Service, doc *firestore.Docum
MimeType: mp.Properties.MimeType,
})
if err != nil {
slog.Error("transcode: ffmpeg failed", "error", err)
flog.Error("transcode: ffmpeg failed", "error", err)
return TranscodeResult{Err: fmt.Errorf("ffmpeg: %w", err)}
}
defer os.Remove(transcodeOutput.TempLocalFilePath)
f, err := os.Open(transcodeOutput.TempLocalFilePath)
if err != nil {
slog.Error("transcode: failed to open transcoded file", "error", err, "path", transcodeOutput.TempLocalFilePath)
flog.Error("transcode: failed to open transcoded file", "error", err, "path", transcodeOutput.TempLocalFilePath)
return TranscodeResult{Err: fmt.Errorf("open temp file: %w", err)}
}
defer f.Close()
@@ -104,7 +106,7 @@ func Transcode(ctx context.Context, depotSvc depot.Service, doc *firestore.Docum
ContentType: transcodeOutput.OutputMimeType,
}, f)
if err != nil {
slog.Error("transcode: failed to upload transcoded object", "error", err, "particleID", doc.Ref.ID)
flog.Error("transcode: failed to upload transcoded object", "error", err, "particleID", doc.Ref.ID)
return TranscodeResult{Err: fmt.Errorf("upload: %w", err)}
}
@@ -114,11 +116,11 @@ func Transcode(ctx context.Context, depotSvc depot.Service, doc *firestore.Docum
"transcoded_mime_type": transcodeOutput.OutputMimeType,
},
}, firestore.MergeAll); err != nil {
slog.Error("transcode: failed to update particle in firestore", "error", err, "particleID", doc.Ref.ID)
flog.Error("transcode: failed to update particle in firestore", "error", err, "particleID", doc.Ref.ID)
return TranscodeResult{Err: fmt.Errorf("firestore update: %w", err)}
}
slog.Info("transcoded media particle", "particleID", doc.Ref.ID, "transcoded_object_id", newObj.ID, "mime", transcodeOutput.OutputMimeType)
flog.Info("transcoded media particle", "particleID", doc.Ref.ID, "transcoded_object_id", newObj.ID, "mime", transcodeOutput.OutputMimeType)
return TranscodeResult{
TranscodedObjectID: newObj.ID,
OutputMimeType: transcodeOutput.OutputMimeType,
+8 -7
View File
@@ -3,9 +3,10 @@ package pusher
import (
"context"
"encoding/json"
"log/slog"
"sync"
"github.com/flowy-live/llink/internal/utils/flog"
"nhooyr.io/websocket"
)
@@ -37,16 +38,16 @@ func (c *Conn) ReadPump(ctx context.Context, hub *Hub) {
_, data, err := c.ws.Read(ctx)
if err != nil {
if ctx.Err() != nil {
slog.Info("websocket context cancelled", "connId", c.id, "humanId", c.humanID, "error", ctx.Err())
flog.Info("websocket context cancelled", "connId", c.id, "humanId", c.humanID, "error", ctx.Err())
return
}
slog.Warn("websocket read error", "connId", c.id, "humanId", c.humanID, "error", err)
flog.Warn("websocket read error", "connId", c.id, "humanId", c.humanID, "error", err)
return
}
if string(data) == "ping" {
if err := c.ws.Write(ctx, websocket.MessageText, []byte("pong")); err != nil {
slog.Warn("websocket pong write error", "connId", c.id, "error", err)
flog.Warn("websocket pong write error", "connId", c.id, "error", err)
}
continue
}
@@ -92,7 +93,7 @@ func (c *Conn) WritePump(ctx context.Context) {
return
}
if err := c.ws.Write(ctx, websocket.MessageText, data); err != nil {
slog.Debug("websocket write error", "connId", c.id, "error", err)
flog.Debug("websocket write error", "connId", c.id, "error", err)
return
}
}
@@ -103,14 +104,14 @@ func (c *Conn) WritePump(ctx context.Context) {
func (c *Conn) Send(msg ServerMessage) {
data, err := json.Marshal(msg)
if err != nil {
slog.Error("failed to marshal server message", "error", err)
flog.Error("failed to marshal server message", "error", err)
return
}
select {
case c.send <- data:
default:
slog.Warn("slow client, closing connection", "connId", c.id, "humanId", c.humanID)
flog.Warn("slow client, closing connection", "connId", c.id, "humanId", c.humanID)
c.Close()
}
}
+5 -4
View File
@@ -3,7 +3,8 @@ package pusher
import (
"context"
"encoding/json"
"log/slog"
"github.com/flowy-live/llink/internal/utils/flog"
)
type subscribeRequest struct {
@@ -110,7 +111,7 @@ func (h *Hub) handleSubscribe(ctx context.Context, req *subscribeRequest) {
presence, err := h.bridge.Subscribe(ctx, req.channelID, req.conn.id, req.conn.humanID)
if err != nil {
slog.Error("redis subscribe failed", "channelId", req.channelID, "error", err)
flog.Error("redis subscribe failed", "channelId", req.channelID, "error", err)
// Fall back to local-only presence.
presence = ch.localHumanIDs()
}
@@ -144,7 +145,7 @@ func (h *Hub) handleUnsubscribe(ctx context.Context, req *unsubscribeRequest) {
}
if err := h.bridge.Unsubscribe(ctx, req.channelID, req.conn.id, req.conn.humanID); err != nil {
slog.Error("redis unsubscribe failed", "channelId", req.channelID, "error", err)
flog.Error("redis unsubscribe failed", "channelId", req.channelID, "error", err)
}
// Only emit leave once the humanID has no remaining tabs on this pod.
@@ -198,7 +199,7 @@ func (h *Hub) handleDisconnect(ctx context.Context, conn *Conn) {
ch.removeMember(conn)
if err := h.bridge.Unsubscribe(ctx, channelID, conn.id, conn.humanID); err != nil {
slog.Error("redis unsubscribe on disconnect failed", "channelId", channelID, "error", err)
flog.Error("redis unsubscribe on disconnect failed", "channelId", channelID, "error", err)
}
if !ch.hasHumanID(conn.humanID) {
+7 -6
View File
@@ -4,10 +4,11 @@ import (
"context"
"encoding/json"
"fmt"
"log/slog"
"strings"
"time"
"github.com/flowy-live/llink/internal/utils/flog"
"github.com/redis/go-redis/v9"
)
@@ -200,7 +201,7 @@ func (rb *RedisBridge) handlePubSubMessage(msg *redis.Message) {
var event redisEvent
if err := json.Unmarshal([]byte(msg.Payload), &event); err != nil {
slog.Error("failed to parse pub/sub event", "error", err)
flog.Error("failed to parse pub/sub event", "error", err)
return
}
@@ -257,7 +258,7 @@ func (rb *RedisBridge) cleanupStalePods(ctx context.Context) {
for {
keys, nextCursor, err := rb.client.Scan(ctx, cursor, channelConnsPrefix+"*"+channelConnsSuffix, 100).Result()
if err != nil {
slog.Error("failed to scan channel keys", "error", err)
flog.Error("failed to scan channel keys", "error", err)
return
}
@@ -292,7 +293,7 @@ func (rb *RedisBridge) cleanupStalePods(ctx context.Context) {
for podID := range knownPods {
if !alivePods[podID] {
slog.Info("cleaning up stale pod", "podId", podID)
flog.Info("cleaning up stale pod", "podId", podID)
rb.cleanupPod(ctx, podID)
}
}
@@ -344,11 +345,11 @@ func (rb *RedisBridge) connField(connID string) string {
func (rb *RedisBridge) publishEvent(ctx context.Context, channelID string, event redisEvent) {
data, err := json.Marshal(event)
if err != nil {
slog.Error("failed to marshal event", "error", err)
flog.Error("failed to marshal event", "error", err)
return
}
if err := rb.client.Publish(ctx, pubsubPrefix+channelID, data).Err(); err != nil {
slog.Error("failed to publish event", "channelId", channelID, "error", err)
flog.Error("failed to publish event", "channelId", channelID, "error", err)
}
}
+7 -6
View File
@@ -2,13 +2,14 @@ package pusher
import (
"context"
"log/slog"
"net/http"
"github.com/flowy-live/llink/genproto/llink/pusher"
"github.com/flowy-live/llink/internal/auth"
"github.com/google/uuid"
"nhooyr.io/websocket"
pbpusher "github.com/flowy-live/llink/genproto/llink/pusher"
"github.com/flowy-live/llink/internal/auth"
"github.com/flowy-live/llink/internal/utils/flog"
)
// Server handles WebSocket upgrades and gRPC presence queries.
@@ -49,14 +50,14 @@ func (s *Server) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
InsecureSkipVerify: true,
})
if err != nil {
slog.Error("websocket accept failed", "error", err)
flog.Error("websocket accept failed", "error", err)
return
}
connID := uuid.New().String()
conn := newConn(connID, session.HumanId, ws)
slog.Info("websocket connected", "connId", connID, "humanId", session.HumanId)
flog.Info("websocket connected", "connId", connID, "humanId", session.HumanId)
// Use the server context, not r.Context(): after upgrade the HTTP request
// context can be cancelled by load balancers and nhooyr/websocket would
@@ -70,7 +71,7 @@ func (s *Server) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
go conn.WritePump(ctx)
conn.ReadPump(ctx, s.hub)
slog.Info("websocket disconnected", "connId", connID, "humanId", session.HumanId)
flog.Info("websocket disconnected", "connId", connID, "humanId", session.HumanId)
}
func (s *Server) GetOnlineHumanIds(ctx context.Context, _ *pbpusher.GetOnlineHumanIdsRequest) (*pbpusher.GetOnlineHumanIdsResponse, error) {
+6 -4
View File
@@ -3,17 +3,19 @@ package internal
import (
"context"
"fmt"
"log/slog"
"os"
"github.com/flowy-live/llink/internal/utils"
"github.com/flowy-live/llink/internal/utils/flog"
"github.com/redis/go-redis/v9"
"github.com/flowy-live/llink/internal/utils"
)
func ConnectAndTestRedis(db int) *redis.Client {
redisHost := utils.MustGetEnv("REDIS_HOST")
if redisHost == "" {
slog.Error("must provide REDIS_HOST")
flog.Error("must provide REDIS_HOST")
os.Exit(1)
}
redisAddr := fmt.Sprintf("%s:%s", redisHost, "6379")
@@ -32,7 +34,7 @@ func ConnectAndTestRedis(db int) *redis.Client {
if err != nil {
panic(err)
}
slog.Debug("redis test", "key", val)
flog.Debug("redis test", "key", val)
if val != "value" {
panic("unexpected value")
}
+3 -2
View File
@@ -2,7 +2,8 @@ package speech
import (
"context"
"log/slog"
"github.com/flowy-live/llink/internal/utils/flog"
dgapi "github.com/deepgram/deepgram-go-sdk/v3/pkg/api/listen/v1/rest"
interfaces "github.com/deepgram/deepgram-go-sdk/v3/pkg/client/interfaces"
@@ -56,7 +57,7 @@ func (s *speechServiceImpl) Transcribe(ctx context.Context, mediaUrl string) (*T
response, err := s.deepgramClient.FromURL(ctx, mediaUrl, options)
if err != nil {
slog.Error("failed to transcribe prerecorded media", "error", err)
flog.Error("failed to transcribe prerecorded media", "error", err)
return nil, err
}
+9 -8
View File
@@ -3,10 +3,11 @@ package testhelper
import (
"context"
"fmt"
"log/slog"
"os"
"time"
"github.com/flowy-live/llink/internal/utils/flog"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
@@ -39,20 +40,20 @@ func SetupTestDB() *pgxpool.Pool {
),
)
if err != nil {
slog.Error("failed to start postgres container", "error", err)
flog.Error("failed to start postgres container", "error", err)
os.Exit(1)
}
// Get connection URL
host, err := container.Host(ctx)
if err != nil {
slog.Error("failed to get container host", "error", err)
flog.Error("failed to get container host", "error", err)
os.Exit(1)
}
port, err := container.MappedPort(ctx, "5432")
if err != nil {
slog.Error("failed to get container port", "error", err)
flog.Error("failed to get container port", "error", err)
os.Exit(1)
}
@@ -62,20 +63,20 @@ func SetupTestDB() *pgxpool.Pool {
// Run migrations
m, err := migrate.New("file://../../migrations", connectionURL)
if err != nil {
slog.Error("failed to create migrate instance", "error", err)
flog.Error("failed to create migrate instance", "error", err)
os.Exit(1)
}
defer m.Close()
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
slog.Error("failed to run migrations", "error", err)
flog.Error("failed to run migrations", "error", err)
os.Exit(1)
}
// Create connection pool
dbPool, err = pgxpool.New(ctx, connectionURL)
if err != nil {
slog.Error("failed to create connection pool", "error", err)
flog.Error("failed to create connection pool", "error", err)
os.Exit(1)
}
@@ -89,7 +90,7 @@ func TeardownTestDB() {
}
if container != nil {
if err := container.Terminate(ctx); err != nil {
slog.Error("failed to terminate container", "error", err)
flog.Error("failed to terminate container", "error", err)
}
}
}
+4 -3
View File
@@ -2,7 +2,8 @@ package utils
import (
"os"
"log/slog"
"github.com/flowy-live/llink/internal/utils/flog"
)
// EnvVar enumerates the env vars referenced via this package.
@@ -15,7 +16,7 @@ func MustGetEnv[T string | EnvVar](key T) string {
keyString := string(key)
value := os.Getenv(keyString)
if value == "" {
slog.Errorf("Missing required environment variable %s", key)
flog.Error("missing required environment variable", "key", key)
panic("Missing required environment variable")
}
@@ -26,7 +27,7 @@ func MustGetEnv[T string | EnvVar](key T) string {
func GetEnv(key string) string {
value := os.Getenv(key)
if value == "" {
slog.Warnf("Missing optional environment variable %s", key)
flog.Warn("missing optional environment variable", "key", key)
}
return value
}
+58
View File
@@ -0,0 +1,58 @@
// Package flog is a structured logger formatted for GCP Cloud Logging.
//
// The API mirrors log/slog: a message string followed by alternating key/value
// pairs. Each kv pair becomes a JSON field in the emitted record, which Cloud
// Logging promotes to a queryable jsonPayload field.
//
// flog.Info("particle processed", "particleID", id, "elapsed", dt)
package flog
import (
"os"
"github.com/sirupsen/logrus"
)
// Prepares global logger for GCP Cloud Logging's quirks
var log = func() *logrus.Logger {
logger := logrus.New()
logger.SetFormatter(&logrus.JSONFormatter{
FieldMap: logrus.FieldMap{
logrus.FieldKeyLevel: "severity",
logrus.FieldKeyMsg: "message",
logrus.FieldKeyTime: "timestamp",
},
})
logger.SetOutput(os.Stderr)
logger.SetLevel(logrus.DebugLevel)
return logger
}()
// fields converts alternating key/value args into a logrus.Fields map. A
// trailing odd arg or non-string key is stored under "!BADKEY" to match
// slog's behavior.
func fields(args []any) logrus.Fields {
if len(args) == 0 {
return nil
}
f := make(logrus.Fields, len(args)/2+1)
for i := 0; i < len(args); i += 2 {
if i+1 >= len(args) {
f["!BADKEY"] = args[i]
break
}
key, ok := args[i].(string)
if !ok {
f["!BADKEY"] = args[i]
continue
}
f[key] = args[i+1]
}
return f
}
func Debug(msg string, args ...any) { log.WithFields(fields(args)).Debug(msg) }
func Info(msg string, args ...any) { log.WithFields(fields(args)).Info(msg) }
func Warn(msg string, args ...any) { log.WithFields(fields(args)).Warn(msg) }
func Error(msg string, args ...any) { log.WithFields(fields(args)).Error(msg) }
func Fatal(msg string, args ...any) { log.WithFields(fields(args)).Fatal(msg) }
+5 -3
View File
@@ -4,12 +4,14 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"time"
"github.com/flowy-live/llink/internal/utils/flog"
"github.com/jackc/pgx/v5/pgxpool"
pbaero "github.com/flowy-live/llink/genproto/aero"
"github.com/flowy-live/llink/internal/utils"
"github.com/jackc/pgx/v5/pgxpool"
)
type WaitlistEntry struct {
@@ -83,7 +85,7 @@ func (s *serviceImpl) AddToWaitlist(ctx context.Context, email string, metadata
},
})
if err != nil {
slog.Error("failed to add to waitlist", "error", err, "email", email)
flog.Error("failed to add to waitlist", "error", err, "email", email)
}
return nil