diff --git a/cmd/web/assets/css/styles.css b/cmd/web/assets/css/styles.css new file mode 100644 index 0000000..0b889da --- /dev/null +++ b/cmd/web/assets/css/styles.css @@ -0,0 +1,94 @@ +/* Pure CSS styles for Music Search */ + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + line-height: 1.5; + background-color: #f3f4f6; +} + +main { + max-width: 1200px; + margin: 0 auto; + padding: 1rem; +} + +#search-container { + text-align: center; + margin-bottom: 2rem; +} + +#search_term { + width: 60vw; + max-width: 600px; + font-size: 1.5rem; + padding: 0.5rem; + border: 1px solid #9ca3af; + border-radius: 0.5rem; + background-color: #e5e7eb; + color: #000; +} + +#search_term:focus { + outline: none; + border-color: #6b7280; +} + +#clear { + font-size: 1.5rem; + padding: 0.5rem 1rem; + border: none; + border-radius: 0.5rem; + background-color: #f97316; + color: #fff; + cursor: pointer; + margin-left: 1rem; +} + +#clear:hover { + background-color: #ea580c; +} + +#games-container { + font-size: 1.5rem; +} + +/* Game result cards */ +.bg-green-100 { + background-color: #dcfce7; +} + +.p-4 { + padding: 1rem; +} + +.shadow-md { + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + #search_term { + width: 80vw; + font-size: 1.2rem; + } + + #clear { + font-size: 1.2rem; + padding: 0.4rem 0.8rem; + } +} diff --git a/internal/logging/echo_middleware.go b/internal/logging/echo_middleware.go new file mode 100644 index 0000000..22e4f19 --- /dev/null +++ b/internal/logging/echo_middleware.go @@ -0,0 +1,44 @@ +package logging + +import ( + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + "go.uber.org/zap" +) + +// RequestLogger is an Echo middleware that logs HTTP requests using Zap +func RequestLogger() echo.MiddlewareFunc { + return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogStatus: true, + LogURI: true, + LogMethod: true, + HandleError: true, + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + logger := GetLogger() + + fields := []zap.Field{ + zap.String("method", v.Method), + zap.String("uri", v.URI), + zap.Int("status", v.Status), + } + + if v.Error != nil { + fields = append(fields, zap.String("error", v.Error.Error())) + logger.Error("Request error", fields...) + } else { + logger.Info("Request completed", fields...) + } + return nil + }, + }) +} + +// ErrorHandler is a custom error handler that logs errors +func ErrorHandler(err error, c *echo.Context) { + logger := GetLogger() + logger.Error("Error occurred", + zap.String("method", c.Request().Method), + zap.String("path", c.Request().URL.Path), + zap.String("error", err.Error()), + ) +} diff --git a/internal/logging/logger.go b/internal/logging/logger.go new file mode 100644 index 0000000..c5dfb14 --- /dev/null +++ b/internal/logging/logger.go @@ -0,0 +1,104 @@ +package logging + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var ( + // Logger is the global logger instance + Logger *zap.Logger + + // SugaredLogger is the global sugared logger instance + SugaredLogger *zap.SugaredLogger +) + +// Init initializes the logger with the specified level and config +func Init(level string, jsonOutput bool) { + var config zap.Config + + // Set the log level + logLevel := zap.NewAtomicLevel() + err := logLevel.UnmarshalText([]byte(level)) + if err != nil { + logLevel.SetLevel(zap.InfoLevel) + } + + if jsonOutput { + // JSON output for Grafana Loki + config = zap.Config{ + Level: logLevel, + Development: false, + Sampling: nil, + Encoding: "json", + EncoderConfig: zapcore.EncoderConfig{ + MessageKey: "msg", + LevelKey: "level", + TimeKey: "time", + NameKey: "logger", + CallerKey: "caller", + FunctionKey: zapcore.OmitKey, + StacktraceKey: "stacktrace", + SkipLineEnding: false, + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + }, + OutputPaths: []string{"stdout"}, + ErrorOutputPaths: []string{"stderr"}, + InitialFields: map[string]interface{}{"service": "music-server"}, + } + } else { + // Human-readable output for development + config = zap.Config{ + Level: logLevel, + Development: true, + Sampling: nil, + Encoding: "console", + EncoderConfig: zapcore.EncoderConfig{ + MessageKey: "msg", + LevelKey: "level", + TimeKey: "time", + NameKey: "logger", + CallerKey: "caller", + FunctionKey: zapcore.OmitKey, + StacktraceKey: "stacktrace", + SkipLineEnding: false, + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + }, + OutputPaths: []string{"stdout"}, + ErrorOutputPaths: []string{"stderr"}, + InitialFields: map[string]interface{}{"service": "music-server"}, + } + } + + logger, err := config.Build() + if err != nil { + panic(err) + } + + Logger = logger + SugaredLogger = logger.Sugar() +} + +// GetLogger returns the global logger +func GetLogger() *zap.Logger { + if Logger == nil { + Init("info", false) + } + return Logger +} + +// GetSugaredLogger returns the global sugared logger +func GetSugaredLogger() *zap.SugaredLogger { + if SugaredLogger == nil { + Init("info", false) + } + return SugaredLogger +}