feat: implement structured logging and error handling in discord-bot
This commit is contained in:
parent
4186f864f9
commit
1d3e223dbb
@ -2,3 +2,5 @@ DISCORD_TOKEN=your-bot-token-here
|
|||||||
GUILD_ID=your-guild-id-here
|
GUILD_ID=your-guild-id-here
|
||||||
HA_GATEWAY_ADDR=localhost:50051
|
HA_GATEWAY_ADDR=localhost:50051
|
||||||
OTEL_ENDPOINT=
|
OTEL_ENDPOINT=
|
||||||
|
LOG_LEVEL=info
|
||||||
|
LOG_FORMAT=text
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
@ -15,6 +14,7 @@ import (
|
|||||||
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/adapters/secondary/gateway"
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/adapters/secondary/gateway"
|
||||||
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/app"
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/app"
|
||||||
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/config"
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/config"
|
||||||
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/logger"
|
||||||
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/telemetry"
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -25,27 +25,37 @@ func main() {
|
|||||||
|
|
||||||
cfg, err := config.Load()
|
cfg, err := config.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("config error", "err", err)
|
os.Stderr.WriteString("config error: " + err.Error() + "\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log := logger.New(cfg.LogFormat, cfg.LogLevel)
|
||||||
|
log.Info("starting discord-bot",
|
||||||
|
"version", version,
|
||||||
|
"ha_gateway_addr", cfg.HAGatewayAddr,
|
||||||
|
"discord_token", redactToken(cfg.DiscordToken),
|
||||||
|
"log_level", cfg.LogLevel,
|
||||||
|
"log_format", cfg.LogFormat,
|
||||||
|
)
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
ctx = logger.WithLogger(ctx, log)
|
||||||
|
|
||||||
shutdown, err := telemetry.Setup(ctx, cfg, version)
|
shutdown, err := telemetry.Setup(ctx, cfg, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("telemetry setup failed", "err", err)
|
log.Error("telemetry setup failed", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
haClient, err := gateway.New(ctx, cfg.HAGatewayAddr)
|
haClient, err := gateway.New(ctx, cfg.HAGatewayAddr, log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("ha-gateway client setup failed", "err", err)
|
log.Error("ha-gateway client setup failed", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := haClient.Close(); err != nil {
|
if err := haClient.Close(); err != nil {
|
||||||
slog.Error("ha-gateway client close failed", "err", err)
|
log.Error("ha-gateway client close failed", "err", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -53,7 +63,7 @@ func main() {
|
|||||||
|
|
||||||
session, err := discordgo.New("Bot " + cfg.DiscordToken)
|
session, err := discordgo.New("Bot " + cfg.DiscordToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("create discord session failed", "err", err)
|
log.Error("create discord session failed", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
session.Identify.Intents = discordgo.IntentsGuilds
|
session.Identify.Intents = discordgo.IntentsGuilds
|
||||||
@ -62,12 +72,13 @@ func main() {
|
|||||||
handler.Register(session)
|
handler.Register(session)
|
||||||
|
|
||||||
if err := session.Open(); err != nil {
|
if err := session.Open(); err != nil {
|
||||||
slog.Error("open discord session failed", "err", err)
|
log.Error("open discord session failed", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
log.Info("discord session ready", "bot_user", session.State.User.Username)
|
||||||
|
|
||||||
if err := discordadapter.RegisterCommands(session, cfg.GuildID); err != nil {
|
if err := discordadapter.RegisterCommands(session, cfg.GuildID); err != nil {
|
||||||
slog.Error("register discord commands failed", "err", err)
|
log.Error("register discord commands failed", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,18 +86,29 @@ func main() {
|
|||||||
if cfg.GuildID != "" {
|
if cfg.GuildID != "" {
|
||||||
scope = cfg.GuildID
|
scope = cfg.GuildID
|
||||||
}
|
}
|
||||||
slog.Info("discord bot started", "command_scope", scope, "ha_gateway_addr", cfg.HAGatewayAddr, "version", version)
|
log.Info("discord bot started", "command_scope", scope, "ha_gateway_addr", cfg.HAGatewayAddr, "version", version)
|
||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
slog.InfoContext(ctx, "shutting down discord bot")
|
log.Info("shutdown signal received, closing session")
|
||||||
|
|
||||||
if err := session.Close(); err != nil {
|
if err := session.Close(); err != nil {
|
||||||
slog.ErrorContext(ctx, "close discord session failed", "err", err)
|
log.Error("close discord session failed", "err", err)
|
||||||
}
|
}
|
||||||
|
log.Info("shutdown complete")
|
||||||
|
|
||||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := shutdown(shutdownCtx); err != nil {
|
if err := shutdown(shutdownCtx); err != nil {
|
||||||
slog.Error("telemetry shutdown error", "err", err)
|
log.Error("telemetry shutdown error", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func redactToken(token string) string {
|
||||||
|
if token == "" {
|
||||||
|
return "[not set]"
|
||||||
|
}
|
||||||
|
if len(token) <= 8 {
|
||||||
|
return "[set]"
|
||||||
|
}
|
||||||
|
return token[:8] + "..."
|
||||||
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
apppkg "gitea.nik4nao.com/nik/home-services/discord-bot/internal/app"
|
apppkg "gitea.nik4nao.com/nik/home-services/discord-bot/internal/app"
|
||||||
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/logger"
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ func (h *Handler) Register(s *discordgo.Session) {
|
|||||||
func (h *Handler) onInteractionCreate(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
func (h *Handler) onInteractionCreate(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
ctx = logger.WithLogger(ctx, interactionLogger(ctx, i))
|
||||||
|
|
||||||
switch i.Type {
|
switch i.Type {
|
||||||
case discordgo.InteractionApplicationCommand:
|
case discordgo.InteractionApplicationCommand:
|
||||||
@ -46,9 +48,13 @@ func (h *Handler) onInteractionCreate(s *discordgo.Session, i *discordgo.Interac
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleApplicationCommand(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
func (h *Handler) handleApplicationCommand(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
log := logger.FromContext(ctx)
|
||||||
|
start := time.Now()
|
||||||
|
log.Debug("command received")
|
||||||
|
|
||||||
data := i.ApplicationCommandData()
|
data := i.ApplicationCommandData()
|
||||||
if len(data.Options) == 0 {
|
if len(data.Options) == 0 {
|
||||||
h.respondError(ctx, s, i.Interaction, true, fmt.Errorf("missing subcommand"))
|
h.respondError(ctx, s, i.Interaction, true, start, fmt.Errorf("missing subcommand"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,44 +63,56 @@ func (h *Handler) handleApplicationCommand(ctx context.Context, s *discordgo.Ses
|
|||||||
case "light.list":
|
case "light.list":
|
||||||
msg, err := h.app.HandleLightList(ctx)
|
msg, err := h.app.HandleLightList(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.respondError(ctx, s, i.Interaction, false, err)
|
h.respondError(ctx, s, i.Interaction, false, start, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.respondMessage(ctx, s, i.Interaction, msg, false)
|
h.respondMessage(ctx, s, i.Interaction, msg, false)
|
||||||
|
log.Info("command handled", "duration_ms", time.Since(start).Milliseconds())
|
||||||
case "switch.list":
|
case "switch.list":
|
||||||
msg, err := h.app.HandleSwitchList(ctx)
|
msg, err := h.app.HandleSwitchList(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.respondError(ctx, s, i.Interaction, false, err)
|
h.respondError(ctx, s, i.Interaction, false, start, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.respondMessage(ctx, s, i.Interaction, msg, false)
|
h.respondMessage(ctx, s, i.Interaction, msg, false)
|
||||||
|
log.Info("command handled", "duration_ms", time.Since(start).Milliseconds())
|
||||||
case "light.on":
|
case "light.on":
|
||||||
if err := h.deferResponse(s, i.Interaction, true); err != nil {
|
if err := h.deferResponse(s, i.Interaction, true); err != nil {
|
||||||
slog.ErrorContext(ctx, "defer discord response failed", "err", err)
|
log.Error("discord response failed",
|
||||||
|
"duration_ms", time.Since(start).Milliseconds(),
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg, err := h.app.HandleLightOn(ctx, requiredStringOption(sub, "light"), optionalUint32Option(sub, "brightness"), optionalUint32Option(sub, "color_temp"))
|
msg, err := h.app.HandleLightOn(ctx, requiredStringOption(sub, "light"), optionalUint32Option(sub, "brightness"), optionalUint32Option(sub, "color_temp"))
|
||||||
h.followup(ctx, s, i.Interaction, msg, true, err)
|
h.followup(ctx, s, i.Interaction, msg, true, start, err)
|
||||||
case "light.off":
|
case "light.off":
|
||||||
if err := h.deferResponse(s, i.Interaction, true); err != nil {
|
if err := h.deferResponse(s, i.Interaction, true); err != nil {
|
||||||
slog.ErrorContext(ctx, "defer discord response failed", "err", err)
|
log.Error("discord response failed",
|
||||||
|
"duration_ms", time.Since(start).Milliseconds(),
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg, err := h.app.HandleLightOff(ctx, requiredStringOption(sub, "light"), optionalUint32Option(sub, "transition"))
|
msg, err := h.app.HandleLightOff(ctx, requiredStringOption(sub, "light"), optionalUint32Option(sub, "transition"))
|
||||||
h.followup(ctx, s, i.Interaction, msg, true, err)
|
h.followup(ctx, s, i.Interaction, msg, true, start, err)
|
||||||
case "light.toggle":
|
case "light.toggle":
|
||||||
if err := h.deferResponse(s, i.Interaction, true); err != nil {
|
if err := h.deferResponse(s, i.Interaction, true); err != nil {
|
||||||
slog.ErrorContext(ctx, "defer discord response failed", "err", err)
|
log.Error("discord response failed",
|
||||||
|
"duration_ms", time.Since(start).Milliseconds(),
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg, err := h.app.HandleLightToggle(ctx, requiredStringOption(sub, "light"))
|
msg, err := h.app.HandleLightToggle(ctx, requiredStringOption(sub, "light"))
|
||||||
h.followup(ctx, s, i.Interaction, msg, true, err)
|
h.followup(ctx, s, i.Interaction, msg, true, start, err)
|
||||||
default:
|
default:
|
||||||
h.respondError(ctx, s, i.Interaction, true, fmt.Errorf("unsupported command: %s.%s", data.Name, sub.Name))
|
h.respondError(ctx, s, i.Interaction, true, start, fmt.Errorf("unsupported command: %s.%s", data.Name, sub.Name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleAutocomplete(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
func (h *Handler) handleAutocomplete(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
log := logger.FromContext(ctx)
|
||||||
data := i.ApplicationCommandData()
|
data := i.ApplicationCommandData()
|
||||||
var (
|
var (
|
||||||
choices []apppkg.Choice
|
choices []apppkg.Choice
|
||||||
@ -110,7 +128,7 @@ func (h *Handler) handleAutocomplete(ctx context.Context, s *discordgo.Session,
|
|||||||
choices = nil
|
choices = nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "autocomplete failed", "command", data.Name, "err", err)
|
log.Error("command failed", "error", err.Error())
|
||||||
choices = nil
|
choices = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +153,7 @@ func (h *Handler) handleAutocomplete(ctx context.Context, s *discordgo.Session,
|
|||||||
Choices: respChoices,
|
Choices: respChoices,
|
||||||
},
|
},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
slog.ErrorContext(ctx, "discord autocomplete response failed", "err", err)
|
log.Error("discord response failed", "error", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,6 +172,7 @@ func (h *Handler) deferResponse(s *discordgo.Session, interaction *discordgo.Int
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) respondMessage(ctx context.Context, s *discordgo.Session, interaction *discordgo.Interaction, content string, ephemeral bool) {
|
func (h *Handler) respondMessage(ctx context.Context, s *discordgo.Session, interaction *discordgo.Interaction, content string, ephemeral bool) {
|
||||||
|
log := logger.FromContext(ctx)
|
||||||
data := &discordgo.InteractionResponseData{Content: content}
|
data := &discordgo.InteractionResponseData{Content: content}
|
||||||
if ephemeral {
|
if ephemeral {
|
||||||
data.Flags = discordgo.MessageFlagsEphemeral
|
data.Flags = discordgo.MessageFlagsEphemeral
|
||||||
@ -162,29 +181,55 @@ func (h *Handler) respondMessage(ctx context.Context, s *discordgo.Session, inte
|
|||||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
Data: data,
|
Data: data,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
slog.ErrorContext(ctx, "discord response failed", "err", err)
|
log.Error("discord response failed", "error", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) respondError(ctx context.Context, s *discordgo.Session, interaction *discordgo.Interaction, ephemeral bool, err error) {
|
func (h *Handler) respondError(ctx context.Context, s *discordgo.Session, interaction *discordgo.Interaction, ephemeral bool, start time.Time, err error) {
|
||||||
slog.ErrorContext(ctx, "discord command failed", "err", err)
|
logger.FromContext(ctx).Error("command failed",
|
||||||
|
"duration_ms", time.Since(start).Milliseconds(),
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
h.respondMessage(ctx, s, interaction, fmt.Sprintf("Error: %v", err), ephemeral)
|
h.respondMessage(ctx, s, interaction, fmt.Sprintf("Error: %v", err), ephemeral)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) followup(ctx context.Context, s *discordgo.Session, interaction *discordgo.Interaction, content string, ephemeral bool, err error) {
|
func (h *Handler) followup(ctx context.Context, s *discordgo.Session, interaction *discordgo.Interaction, content string, ephemeral bool, start time.Time, err error) {
|
||||||
|
log := logger.FromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "discord action failed", "err", err)
|
log.Error("command failed",
|
||||||
|
"duration_ms", time.Since(start).Milliseconds(),
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
content = fmt.Sprintf("Error: %v", err)
|
content = fmt.Sprintf("Error: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Info("command handled", "duration_ms", time.Since(start).Milliseconds())
|
||||||
}
|
}
|
||||||
params := &discordgo.WebhookParams{Content: content}
|
params := &discordgo.WebhookParams{Content: content}
|
||||||
if ephemeral {
|
if ephemeral {
|
||||||
params.Flags = discordgo.MessageFlagsEphemeral
|
params.Flags = discordgo.MessageFlagsEphemeral
|
||||||
}
|
}
|
||||||
if _, followErr := s.FollowupMessageCreate(interaction, true, params); followErr != nil {
|
if _, followErr := s.FollowupMessageCreate(interaction, true, params); followErr != nil {
|
||||||
slog.ErrorContext(ctx, "discord followup failed", "err", followErr)
|
log.Error("discord response failed", "error", followErr.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func interactionLogger(ctx context.Context, i *discordgo.InteractionCreate) *slog.Logger {
|
||||||
|
log := logger.FromContext(ctx)
|
||||||
|
data := i.ApplicationCommandData()
|
||||||
|
command := data.Name
|
||||||
|
user := ""
|
||||||
|
if i.Member != nil && i.Member.User != nil {
|
||||||
|
user = i.Member.User.Username
|
||||||
|
} else if i.User != nil {
|
||||||
|
user = i.User.Username
|
||||||
|
}
|
||||||
|
return log.With(
|
||||||
|
"command", command,
|
||||||
|
"user", user,
|
||||||
|
"guild", i.GuildID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func requiredStringOption(sub *discordgo.ApplicationCommandInteractionDataOption, name string) string {
|
func requiredStringOption(sub *discordgo.ApplicationCommandInteractionDataOption, name string) string {
|
||||||
for _, opt := range sub.Options {
|
for _, opt := range sub.Options {
|
||||||
if opt.Name == name {
|
if opt.Name == name {
|
||||||
|
|||||||
@ -3,8 +3,11 @@ package gateway
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/core/ports/driven"
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/core/ports/driven"
|
||||||
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/logger"
|
||||||
hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1"
|
hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1"
|
||||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@ -15,9 +18,10 @@ type Client struct {
|
|||||||
conn *grpc.ClientConn
|
conn *grpc.ClientConn
|
||||||
lightClient hav1.LightServiceClient
|
lightClient hav1.LightServiceClient
|
||||||
switchClient hav1.SwitchServiceClient
|
switchClient hav1.SwitchServiceClient
|
||||||
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, addr string) (*Client, error) {
|
func New(ctx context.Context, addr string, log *slog.Logger) (*Client, error) {
|
||||||
conn, err := grpc.NewClient(
|
conn, err := grpc.NewClient(
|
||||||
addr,
|
addr,
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
@ -31,6 +35,7 @@ func New(ctx context.Context, addr string) (*Client, error) {
|
|||||||
conn: conn,
|
conn: conn,
|
||||||
lightClient: hav1.NewLightServiceClient(conn),
|
lightClient: hav1.NewLightServiceClient(conn),
|
||||||
switchClient: hav1.NewSwitchServiceClient(conn),
|
switchClient: hav1.NewSwitchServiceClient(conn),
|
||||||
|
log: log,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,10 +47,17 @@ func (c *Client) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ListLights(ctx context.Context) ([]driven.Light, error) {
|
func (c *Client) ListLights(ctx context.Context) ([]driven.Light, error) {
|
||||||
|
start := time.Now()
|
||||||
|
log := logger.FromContext(ctx).With("grpc.method", "LightService/ListLights")
|
||||||
resp, err := c.lightClient.ListLights(ctx, &hav1.ListLightsRequest{})
|
resp, err := c.lightClient.ListLights(ctx, &hav1.ListLightsRequest{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error("grpc call failed",
|
||||||
|
"duration_ms", time.Since(start).Milliseconds(),
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
return nil, fmt.Errorf("list lights: %w", err)
|
return nil, fmt.Errorf("list lights: %w", err)
|
||||||
}
|
}
|
||||||
|
log.Debug("grpc call completed", "duration_ms", time.Since(start).Milliseconds())
|
||||||
|
|
||||||
lights := make([]driven.Light, 0, len(resp.GetLights()))
|
lights := make([]driven.Light, 0, len(resp.GetLights()))
|
||||||
for _, light := range resp.GetLights() {
|
for _, light := range resp.GetLights() {
|
||||||
@ -64,10 +76,17 @@ func (c *Client) ListLights(ctx context.Context) ([]driven.Light, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ListSwitches(ctx context.Context) ([]driven.Switch, error) {
|
func (c *Client) ListSwitches(ctx context.Context) ([]driven.Switch, error) {
|
||||||
|
start := time.Now()
|
||||||
|
log := logger.FromContext(ctx).With("grpc.method", "SwitchService/ListSwitches")
|
||||||
resp, err := c.switchClient.ListSwitches(ctx, &hav1.ListSwitchesRequest{})
|
resp, err := c.switchClient.ListSwitches(ctx, &hav1.ListSwitchesRequest{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error("grpc call failed",
|
||||||
|
"duration_ms", time.Since(start).Milliseconds(),
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
return nil, fmt.Errorf("list switches: %w", err)
|
return nil, fmt.Errorf("list switches: %w", err)
|
||||||
}
|
}
|
||||||
|
log.Debug("grpc call completed", "duration_ms", time.Since(start).Milliseconds())
|
||||||
|
|
||||||
switches := make([]driven.Switch, 0, len(resp.GetSwitches()))
|
switches := make([]driven.Switch, 0, len(resp.GetSwitches()))
|
||||||
for _, sw := range resp.GetSwitches() {
|
for _, sw := range resp.GetSwitches() {
|
||||||
@ -82,6 +101,8 @@ func (c *Client) ListSwitches(ctx context.Context) ([]driven.Switch, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) TurnOnLight(ctx context.Context, entityID string, brightnessPct *uint32, colorTempKelvin *uint32) error {
|
func (c *Client) TurnOnLight(ctx context.Context, entityID string, brightnessPct *uint32, colorTempKelvin *uint32) error {
|
||||||
|
start := time.Now()
|
||||||
|
log := logger.FromContext(ctx).With("grpc.method", "LightService/TurnOn")
|
||||||
req := &hav1.TurnOnRequest{EntityId: entityID}
|
req := &hav1.TurnOnRequest{EntityId: entityID}
|
||||||
if brightnessPct != nil {
|
if brightnessPct != nil {
|
||||||
req.BrightnessPct = brightnessPct
|
req.BrightnessPct = brightnessPct
|
||||||
@ -91,26 +112,45 @@ func (c *Client) TurnOnLight(ctx context.Context, entityID string, brightnessPct
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := c.lightClient.TurnOn(ctx, req); err != nil {
|
if _, err := c.lightClient.TurnOn(ctx, req); err != nil {
|
||||||
|
log.Error("grpc call failed",
|
||||||
|
"duration_ms", time.Since(start).Milliseconds(),
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
return fmt.Errorf("turn on light %s: %w", entityID, err)
|
return fmt.Errorf("turn on light %s: %w", entityID, err)
|
||||||
}
|
}
|
||||||
|
log.Debug("grpc call completed", "duration_ms", time.Since(start).Milliseconds())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) TurnOffLight(ctx context.Context, entityID string, transition *uint32) error {
|
func (c *Client) TurnOffLight(ctx context.Context, entityID string, transition *uint32) error {
|
||||||
|
start := time.Now()
|
||||||
|
log := logger.FromContext(ctx).With("grpc.method", "LightService/TurnOff")
|
||||||
req := &hav1.TurnOffRequest{EntityId: entityID}
|
req := &hav1.TurnOffRequest{EntityId: entityID}
|
||||||
if transition != nil {
|
if transition != nil {
|
||||||
req.Transition = transition
|
req.Transition = transition
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := c.lightClient.TurnOff(ctx, req); err != nil {
|
if _, err := c.lightClient.TurnOff(ctx, req); err != nil {
|
||||||
|
log.Error("grpc call failed",
|
||||||
|
"duration_ms", time.Since(start).Milliseconds(),
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
return fmt.Errorf("turn off light %s: %w", entityID, err)
|
return fmt.Errorf("turn off light %s: %w", entityID, err)
|
||||||
}
|
}
|
||||||
|
log.Debug("grpc call completed", "duration_ms", time.Since(start).Milliseconds())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ToggleLight(ctx context.Context, entityID string) error {
|
func (c *Client) ToggleLight(ctx context.Context, entityID string) error {
|
||||||
|
start := time.Now()
|
||||||
|
log := logger.FromContext(ctx).With("grpc.method", "LightService/Toggle")
|
||||||
if _, err := c.lightClient.Toggle(ctx, &hav1.ToggleRequest{EntityId: entityID}); err != nil {
|
if _, err := c.lightClient.Toggle(ctx, &hav1.ToggleRequest{EntityId: entityID}); err != nil {
|
||||||
|
log.Error("grpc call failed",
|
||||||
|
"duration_ms", time.Since(start).Milliseconds(),
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
return fmt.Errorf("toggle light %s: %w", entityID, err)
|
return fmt.Errorf("toggle light %s: %w", entityID, err)
|
||||||
}
|
}
|
||||||
|
log.Debug("grpc call completed", "duration_ms", time.Since(start).Milliseconds())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,8 @@ type Config struct {
|
|||||||
GuildID string
|
GuildID string
|
||||||
HAGatewayAddr string
|
HAGatewayAddr string
|
||||||
OTELEndpoint string
|
OTELEndpoint string
|
||||||
|
LogLevel string
|
||||||
|
LogFormat string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
@ -28,5 +30,14 @@ func Load() (*Config, error) {
|
|||||||
GuildID: os.Getenv("GUILD_ID"),
|
GuildID: os.Getenv("GUILD_ID"),
|
||||||
HAGatewayAddr: addr,
|
HAGatewayAddr: addr,
|
||||||
OTELEndpoint: os.Getenv("OTEL_ENDPOINT"),
|
OTELEndpoint: os.Getenv("OTEL_ENDPOINT"),
|
||||||
|
LogLevel: getenvDefault("LOG_LEVEL", "info"),
|
||||||
|
LogFormat: getenvDefault("LOG_FORMAT", "json"),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getenvDefault(key, fallback string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|||||||
35
discord-bot/internal/logger/logger.go
Normal file
35
discord-bot/internal/logger/logger.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey struct{}
|
||||||
|
|
||||||
|
func New(format, level string) *slog.Logger {
|
||||||
|
var parsed slog.Level
|
||||||
|
if err := parsed.UnmarshalText([]byte(level)); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "invalid log level %q, falling back to info\n", level)
|
||||||
|
parsed = slog.LevelInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &slog.HandlerOptions{Level: parsed}
|
||||||
|
if format == "json" {
|
||||||
|
return slog.New(slog.NewJSONHandler(os.Stdout, opts))
|
||||||
|
}
|
||||||
|
return slog.New(slog.NewTextHandler(os.Stdout, opts))
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLogger(ctx context.Context, l *slog.Logger) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKey{}, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromContext(ctx context.Context) *slog.Logger {
|
||||||
|
if l, ok := ctx.Value(contextKey{}).(*slog.Logger); ok && l != nil {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
return slog.Default()
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user