From d201c81fee444d7aa742cd28b7b675a35ca0a6e0 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 28 Aug 2024 00:26:43 -0400 Subject: [PATCH 01/16] Update Logging from log to slog --- core/archtype.go | 3 +- core/character.go | 27 +++++++-------- core/go.mod | 13 ++++++- core/logging.go | 88 +++++++++++++++++++++++++++++++++++++++++++++++ core/player.go | 32 ++++++++++++++++- 5 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 core/logging.go diff --git a/core/archtype.go b/core/archtype.go index 670a946..825df61 100644 --- a/core/archtype.go +++ b/core/archtype.go @@ -3,7 +3,6 @@ package core import ( "encoding/json" "fmt" - "log" "os" "github.com/aws/aws-sdk-go/aws" @@ -50,7 +49,7 @@ func (kp *KeyPair) StoreArchetypes(archetypes *ArchetypesData) error { return fmt.Errorf("error storing archetype %s: %w", archetype.Name, err) } - log.Printf("Stored archetype: %s", archetype.Name) + Logger.Info("Loaded archetype", "name", archetype.Name) } return nil diff --git a/core/character.go b/core/character.go index 7bf587a..192e6f1 100644 --- a/core/character.go +++ b/core/character.go @@ -2,7 +2,6 @@ package core import ( "fmt" - "log" "strings" "github.com/aws/aws-sdk-go/aws" @@ -62,7 +61,7 @@ func (c *Character) FromData(cd *CharacterData) error { room, exists := c.Server.Rooms[cd.RoomID] if !exists { - log.Printf("room with ID %d not found", cd.RoomID) + Logger.Warn("Room not found", "roomID", cd.RoomID) room = c.Server.Rooms[0] } c.Room = room @@ -71,7 +70,7 @@ func (c *Character) FromData(cd *CharacterData) error { for key, objID := range cd.Inventory { obj, err := c.Server.Database.LoadItem(objID, false) if err != nil { - log.Printf("Error loading object %s for character %s: %v", objID, c.Name, err) + Logger.Error("Error loading object for character", "objectID", objID, "characterName", c.Name, "error", err) continue } c.Inventory[key] = obj @@ -128,7 +127,7 @@ func (kp *KeyPair) WriteCharacter(character *Character) error { return fmt.Errorf("error writing character data: %w", err) } - log.Printf("Successfully wrote character %s with ID %s to database", character.Name, character.ID) + Logger.Info("Successfully wrote character to database", "characterName", character.Name, "characterID", character.ID) return nil } @@ -160,12 +159,12 @@ func (kp *KeyPair) LoadCharacter(characterID uuid.UUID, player *Player, server * } character.Room.Characters[character.ID] = character character.Room.Mutex.Unlock() - log.Printf("Added character %s (ID: %s) to room %d", character.Name, character.ID, character.Room.RoomID) + Logger.Info("Added character to room", "characterName", character.Name, "characterID", character.ID, "roomID", character.Room.RoomID) } else { - log.Printf("Warning: Character %s (ID: %s) loaded without a valid room", character.Name, character.ID) + Logger.Warn("Character loaded without a valid room", "characterName", character.Name, "characterID", character.ID) } - log.Printf("Loaded character %s (ID: %s) in Room %d", character.Name, character.ID, character.Room.RoomID) + Logger.Info("Loaded character in room", "characterName", character.Name, "characterID", character.ID, "roomID", character.Room.RoomID) return character, nil } @@ -197,7 +196,7 @@ func SaveActiveCharacters(s *Server) error { s.Mutex.Lock() defer s.Mutex.Unlock() - log.Println("Saving active characters...") + Logger.Info("Saving active characters...") for _, character := range s.Characters { err := s.Database.WriteCharacter(character) @@ -206,7 +205,7 @@ func SaveActiveCharacters(s *Server) error { } } - log.Println("Active characters saved successfully.") + Logger.Info("Active characters saved successfully.") return nil } @@ -255,7 +254,7 @@ func WearItem(c *Character, item *Item) error { func ListInventory(c *Character) string { - log.Printf("Character %s is listing inventory", c.Name) + Logger.Debug("Character is listing inventory", "characterName", c.Name) c.Mutex.Lock() defer c.Mutex.Unlock() @@ -290,7 +289,7 @@ func ListInventory(c *Character) string { func AddToInventory(c *Character, item *Item) { - log.Printf("Character %s is adding item %s to inventory", c.Name, item.Name) + Logger.Debug("Character is adding item to inventory", "characterName", c.Name, "itemName", item.Name) c.Mutex.Lock() defer c.Mutex.Unlock() @@ -307,7 +306,7 @@ func AddToInventory(c *Character, item *Item) { func FindInInventory(c *Character, itemName string) *Item { - log.Printf("Character %s is searching inventory for item %s", c.Name, itemName) + Logger.Debug("Character is searching inventory for item", "characterName", c.Name, "itemName", itemName) c.Mutex.Lock() defer c.Mutex.Unlock() @@ -325,7 +324,7 @@ func FindInInventory(c *Character, itemName string) *Item { func RemoveFromInventory(c *Character, item *Item) { - log.Printf("Character %s is removing item %s from inventory", c.Name, item.Name) + Logger.Debug("Character is removing item from inventory", "characterName", c.Name, "itemName", item.Name) c.Mutex.Lock() defer c.Mutex.Unlock() @@ -342,7 +341,7 @@ func RemoveFromInventory(c *Character, item *Item) { func CanCarryItem(c *Character, item *Item) bool { - log.Printf("Character %s is checking if they can carry item %s", c.Name, item.Name) + Logger.Info("Character is checking if they can carry item", "characterName", c.Name, "itemName", item.Name) // Placeholder implementation return true diff --git a/core/go.mod b/core/go.mod index b8a0b40..ab91373 100644 --- a/core/go.mod +++ b/core/go.mod @@ -4,12 +4,23 @@ go 1.22 require ( github.com/aws/aws-sdk-go v1.54.15 + github.com/aws/aws-xray-sdk-go v1.8.4 github.com/google/uuid v1.6.0 golang.org/x/crypto v0.24.0 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/andybalholm/brotli v1.0.6 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/klauspost/compress v1.17.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.50.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect ) diff --git a/core/logging.go b/core/logging.go new file mode 100644 index 0000000..afbf2d6 --- /dev/null +++ b/core/logging.go @@ -0,0 +1,88 @@ +package core + +import ( + "fmt" + "log/slog" + "os" + "strconv" + + "github.com/aws/aws-xray-sdk-go/xray" +) + +// Global variables +var ( + Logger *slog.Logger +) + +// APPLICATION_NAME is the name of the application, defaulting to "dark_relics" +var APPLICATION_NAME = GetEnv("APPLICATION_NAME", "dark_relics") + +// REGION is the primary AWS region for Observatory resources +var REGION = GetEnv("AWS_REGION", "us-east-1") + +// LOG_LEVEL is the logging level (default: 20) +var LOG_LEVEL, _ = strconv.Atoi(GetEnv("LOGGING", "20")) + +func init() { + + // Determine the log level + var level slog.Level + switch LOG_LEVEL { + case 10: + level = slog.LevelDebug + case 20: + level = slog.LevelInfo + case 30: + level = slog.LevelWarn + case 40: + level = slog.LevelError + default: + level = slog.LevelInfo + } + + // Initialize the Logger + Logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})) + slog.SetDefault(Logger) + +} + +// GetEnv retrieves environment variables or returns a default value if not set +func GetEnv(key, defaultValue string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return defaultValue +} + +func EnableXRay() error { + // Determine the log level + var xrayLogLevel string + switch LOG_LEVEL { + case 10: + xrayLogLevel = "debug" + case 20: + xrayLogLevel = "info" + case 30: + xrayLogLevel = "warn" + case 40: + xrayLogLevel = "error" + default: + xrayLogLevel = "info" + } + + Logger.Info("Configuring AWS X-Ray", "logLevel", xrayLogLevel) + + err := xray.Configure(xray.Config{ + LogLevel: xrayLogLevel, + ServiceVersion: APPLICATION_NAME, // Assuming APPLICATION_NAME is your service version + }) + + if err != nil { + Logger.Error("Failed to configure AWS X-Ray", "error", err) + return fmt.Errorf("failed to configure AWS X-Ray: %w", err) + } + + Logger.Info("AWS X-Ray successfully configured") + + return nil +} diff --git a/core/player.go b/core/player.go index ae0f58e..8045049 100644 --- a/core/player.go +++ b/core/player.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "log" + "sort" "strconv" "strings" "time" @@ -311,8 +312,37 @@ func CreateCharacter(player *Player, server *Server) (*Character, error) { } var selectedArchetype string + if server.Archetypes != nil && len(server.Archetypes.Archetypes) > 0 { - // ... (keep existing archetype selection logic) + for { + selectionMsg := "\n\rSelect a character archetype.\n\r" + archetypeOptions := make([]string, 0, len(server.Archetypes.Archetypes)) + for name, archetype := range server.Archetypes.Archetypes { + archetypeOptions = append(archetypeOptions, name+" - "+archetype.Description) + } + sort.Strings(archetypeOptions) + + for i, option := range archetypeOptions { + selectionMsg += fmt.Sprintf("%d: %s\n\r", i+1, option) + } + + selectionMsg += "Enter the number of your choice: " + player.ToPlayer <- selectionMsg + + selection, ok := <-player.FromPlayer + if !ok { + return nil, fmt.Errorf("failed to receive archetype selection") + } + + selectionNum, err := strconv.Atoi(strings.TrimSpace(selection)) + if err == nil && selectionNum >= 1 && selectionNum <= len(archetypeOptions) { + selectedOption := archetypeOptions[selectionNum-1] + selectedArchetype = strings.Split(selectedOption, " - ")[0] + break + } else { + player.ToPlayer <- "Invalid selection. Please select a valid archetype number." + } + } } log.Printf("Creating character with name: %s", charName) From bd47e207fea63d00043c9e09c5a7aa9d03d21772 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 28 Aug 2024 00:28:10 -0400 Subject: [PATCH 02/16] Update Logging from log to slog --- core/colors.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/colors.go b/core/colors.go index dac0d4e..b4c248c 100644 --- a/core/colors.go +++ b/core/colors.go @@ -2,7 +2,6 @@ package core import ( "fmt" - "log" ) // ColorMap maps color names to ANSI color codes. @@ -20,7 +19,7 @@ var ColorMap = map[string]string{ // ApplyColor applies the specified color to the text if the color exists in ColorMap. func ApplyColor(colorName, text string) string { - log.Printf("Applying color %s to text: %s", colorName, text) + Logger.Debug("Applying color to text", "colorName", colorName, "text", text) if colorCode, exists := ColorMap[colorName]; exists { return fmt.Sprintf("\033[%sm%s\033[0m", colorCode, text) From 3148b2ce095943a1c0d5f9bb8304afaf6ea67e2a Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 28 Aug 2024 01:23:25 -0400 Subject: [PATCH 03/16] Update Logging from log to slog --- core/cognito.go | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/core/cognito.go b/core/cognito.go index 191e9d7..f60b507 100644 --- a/core/cognito.go +++ b/core/cognito.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "encoding/base64" "fmt" - "log" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" @@ -74,7 +73,7 @@ func SignInUser(email, password string, config Configuration) (*cognitoidentityp func SignUpUser(email, password string, config Configuration) (*cognitoidentityprovider.SignUpOutput, error) { sess, err := session.NewSession(&aws.Config{Region: aws.String(config.UserPoolRegion)}) if err != nil { - log.Printf("Error creating AWS session for sign-up: %v", err) + Logger.Error("Error creating AWS session for sign-up", "error", err) return nil, fmt.Errorf("an internal error occurred while creating AWS session") } @@ -93,7 +92,7 @@ func SignUpUser(email, password string, config Configuration) (*cognitoidentityp signUpOutput, err := cognitoClient.SignUp(signUpInput) if err != nil { - log.Printf("Error signing up user %s with Cognito: %v", email, err) + Logger.Error("Error signing up user with Cognito", "email", email, "error", err) return nil, fmt.Errorf("error signing up, please try again") } @@ -103,7 +102,7 @@ func SignUpUser(email, password string, config Configuration) (*cognitoidentityp func ConfirmUser(email, confirmationCode string, config Configuration) (*cognitoidentityprovider.ConfirmSignUpOutput, error) { sess, err := session.NewSession(&aws.Config{Region: aws.String(config.UserPoolRegion)}) if err != nil { - log.Printf("Error creating AWS session for user confirmation: %v", err) + Logger.Error("Error creating AWS session for user confirmation", "error", err) return nil, fmt.Errorf("an internal error occurred while creating AWS session") } @@ -119,7 +118,7 @@ func ConfirmUser(email, confirmationCode string, config Configuration) (*cognito confirmSignUpOutput, err := cognitoClient.ConfirmSignUp(confirmSignUpInput) if err != nil { - log.Printf("Error confirming sign up for user %s: %v", email, err) + Logger.Error("Error confirming sign-up for user", "email", email, "error", err) return nil, fmt.Errorf("error confirming sign up, please check your code and try again") } @@ -129,7 +128,7 @@ func ConfirmUser(email, confirmationCode string, config Configuration) (*cognito func GetUserData(accessToken string, config Configuration) (*cognitoidentityprovider.GetUserOutput, error) { sess, err := session.NewSession(&aws.Config{Region: aws.String(config.UserPoolRegion)}) if err != nil { - log.Printf("Error creating AWS session for getting user data: %v", err) + Logger.Error("Error creating AWS session for getting user data", "error", err) return nil, fmt.Errorf("an internal error occurred while creating AWS session") } @@ -138,7 +137,7 @@ func GetUserData(accessToken string, config Configuration) (*cognitoidentityprov getUserInput := &cognitoidentityprovider.GetUserInput{AccessToken: aws.String(accessToken)} userOutput, err := cognitoClient.GetUser(getUserInput) if err != nil { - log.Printf("Error getting user data with access token: %v", err) + Logger.Error("Error getting user data with access token", "error", err) return nil, fmt.Errorf("error retrieving user data, please try again") } @@ -146,28 +145,28 @@ func GetUserData(accessToken string, config Configuration) (*cognitoidentityprov } func ChangePassword(server *Server, username, oldPassword, newPassword string) error { - log.Printf("Attempting to change password for user: %s", username) + Logger.Info("Attempting to change password for user", "username", username) // Step 1: Authenticate the user - log.Printf("Step 1: Authenticating user %s", username) + Logger.Info("Step 1: Authenticating user", "username", username) signInOutput, err := SignInUser(username, oldPassword, server.Config) if err != nil { - log.Printf("Authentication failed for user %s: %v", username, err) + Logger.Error("Authentication failed for user", "username", username, "error", err) return fmt.Errorf("authentication failed: %v", err) } - log.Printf("Authentication successful for user %s", username) - log.Printf("SignInOutput for user %s: %+v", username, signInOutput) + Logger.Info("Authentication successful for user", "username", username) + Logger.Info("SignInOutput for user", "username", username, "signInOutput", signInOutput) // Step 2: Handle NEW_PASSWORD_REQUIRED challenge if present if signInOutput.ChallengeName != nil && *signInOutput.ChallengeName == cognitoidentityprovider.ChallengeNameTypeNewPasswordRequired { - log.Printf("NEW_PASSWORD_REQUIRED challenge detected for user %s", username) + Logger.Info("NEW_PASSWORD_REQUIRED challenge detected for user", "username", username) // Create a new AWS session sess, err := session.NewSession(&aws.Config{ Region: aws.String(server.Config.UserPoolRegion), }) if err != nil { - log.Printf("Failed to create AWS session for user %s: %v", username, err) + Logger.Error("Failed to create AWS session for user", "username", username, "error", err) return fmt.Errorf("failed to create AWS session: %v", err) } @@ -189,15 +188,15 @@ func ChangePassword(server *Server, username, oldPassword, newPassword string) e Session: signInOutput.Session, } - log.Printf("Sending challenge response for user %s", username) + Logger.Info("Sending challenge response for user", "username", username) challengeResponse, err := cognitoClient.RespondToAuthChallenge(challengeResponseInput) if err != nil { - log.Printf("Failed to respond to NEW_PASSWORD_REQUIRED challenge for user %s: %v", username, err) + Logger.Error("Failed to respond to NEW_PASSWORD_REQUIRED challenge for user", "username", username, "error", err) return fmt.Errorf("failed to set new password: %v", err) } - log.Printf("Successfully responded to NEW_PASSWORD_REQUIRED challenge for user %s", username) - log.Printf("Challenge response: %+v", challengeResponse) + Logger.Info("Successfully responded to NEW_PASSWORD_REQUIRED challenge for user", "username", username) + Logger.Debug("Challenge response", "challengeResponse", challengeResponse) // Password has been changed successfully return nil @@ -207,7 +206,7 @@ func ChangePassword(server *Server, username, oldPassword, newPassword string) e // In this case, we need to use the ChangePassword API as before if signInOutput.AuthenticationResult == nil || signInOutput.AuthenticationResult.AccessToken == nil { - log.Printf("No valid access token for user %s", username) + Logger.Warn("No valid access token for user", "username", username) return fmt.Errorf("no valid access token available") } @@ -216,7 +215,7 @@ func ChangePassword(server *Server, username, oldPassword, newPassword string) e Region: aws.String(server.Config.UserPoolRegion), }) if err != nil { - log.Printf("Failed to create AWS session for user %s: %v", username, err) + Logger.Error("Failed to create AWS session for user", "username", username, "error", err) return fmt.Errorf("failed to create AWS session: %v", err) } @@ -232,10 +231,10 @@ func ChangePassword(server *Server, username, oldPassword, newPassword string) e _, err = cognitoClient.ChangePassword(input) if err != nil { - log.Printf("Failed to change password for user %s: %v", username, err) + Logger.Error("Failed to change password for user", "username", username, "error", err) return fmt.Errorf("failed to change password: %v", err) } - log.Printf("Password successfully changed for user %s", username) + Logger.Info("Password successfully changed for user", "username", username) return nil } From cc231a9ce66b6fc709389c74f9beb49585518180 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 28 Aug 2024 01:39:04 -0400 Subject: [PATCH 04/16] Update Logging from log to slog --- core/commands.go | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/core/commands.go b/core/commands.go index 7259df1..328a46d 100644 --- a/core/commands.go +++ b/core/commands.go @@ -3,7 +3,6 @@ package core import ( "errors" "fmt" - "log" "sort" "strconv" "strings" @@ -37,7 +36,7 @@ var CommandHandlers = map[string]CommandHandler{ func ValidateCommand(command string) (string, []string, error) { - log.Printf("Received command: %s", command) + Logger.Debug("Received command", "command", command) trimmedCommand := strings.TrimSpace(command) tokens := strings.Fields(trimmedCommand) @@ -56,7 +55,7 @@ func ValidateCommand(command string) (string, []string, error) { func ExecuteCommand(character *Character, verb string, tokens []string) bool { - log.Printf("Executing command: %s", verb) + Logger.Debug("Executing command", "verb", verb) handler, ok := CommandHandlers[verb] if !ok { @@ -67,7 +66,7 @@ func ExecuteCommand(character *Character, verb string, tokens []string) bool { } func ExecuteQuitCommand(character *Character, tokens []string) bool { - log.Printf("Player %s is quitting", character.Player.Name) + Logger.Info("Player is quitting", "playerName", character.Player.Name) // Send goodbye message character.Player.ToPlayer <- "\n\rGoodbye!" @@ -88,17 +87,17 @@ func ExecuteQuitCommand(character *Character, tokens []string) bool { // Save character state to database err := character.Server.Database.WriteCharacter(character) if err != nil { - log.Printf("Error saving character %s state on quit: %v", character.Name, err) + Logger.Error("Error saving character state on quit", "characterName", character.Name, "error", err) } - log.Printf("Player %s has successfully quit", character.Player.Name) + Logger.Info("Player has successfully quit", "playerName", character.Player.Name) return true // Indicate that the loop should be exited } func ExecuteSayCommand(character *Character, tokens []string) bool { - log.Printf("Player %s is saying something", character.Player.Name) + Logger.Info("Player is saying something", "playerName", character.Player.Name) if len(tokens) < 2 { character.Player.ToPlayer <- "\n\rWhat do you want to say?\n\r" @@ -126,7 +125,7 @@ func ExecuteSayCommand(character *Character, tokens []string) bool { func ExecuteLookCommand(character *Character, tokens []string) bool { - log.Printf("Player %s is looking around", character.Player.Name) + Logger.Info("Player is looking around", "playerName", character.Player.Name) room := character.Room character.Player.ToPlayer <- RoomInfo(room, character) @@ -135,7 +134,7 @@ func ExecuteLookCommand(character *Character, tokens []string) bool { func ExecuteGoCommand(character *Character, tokens []string) bool { - log.Printf("Player %s is attempting to move", character.Player.Name) + Logger.Info("Player is attempting to move", "playerName", character.Player.Name) if len(tokens) < 2 { character.Player.ToPlayer <- "\n\rWhich direction do you want to go?\n\r" @@ -149,7 +148,7 @@ func ExecuteGoCommand(character *Character, tokens []string) bool { func ExecuteChallengeCommand(character *Character, tokens []string) bool { - log.Printf("Player %s is attempting a challenge", character.Player.Name) + Logger.Info("Player is attempting a challenge", "playerName", character.Player.Name) // Ensure the correct number of arguments are provided if len(tokens) < 3 { @@ -181,7 +180,7 @@ func ExecuteChallengeCommand(character *Character, tokens []string) bool { } func ExecuteWhoCommand(character *Character, tokens []string) bool { - log.Printf("Player %s is listing all characters online", character.Player.Name) + Logger.Info("Player is listing all characters online", "playerName", character.Player.Name) // Retrieve the server instance from the character server := character.Server @@ -229,7 +228,7 @@ func ExecuteWhoCommand(character *Character, tokens []string) bool { func ExecutePasswordCommand(character *Character, tokens []string) bool { - log.Printf("Player %s is attempting to change their password", character.Player.Name) + Logger.Info("Player is attempting to change their password", "playerName", character.Player.Name) if len(tokens) != 3 { character.Player.ToPlayer <- "\n\rUsage: password \n\r" @@ -241,7 +240,7 @@ func ExecutePasswordCommand(character *Character, tokens []string) bool { err := ChangePassword(character.Server, character.Player.Name, oldPassword, newPassword) if err != nil { - log.Printf("Failed to change password for user %s: %v", character.Player.Name, err) + Logger.Error("Failed to change password for user", "playerName", character.Player.Name, "error", err) character.Player.ToPlayer <- "\n\rFailed to change password. Please try again.\n\r" return false } @@ -252,7 +251,7 @@ func ExecutePasswordCommand(character *Character, tokens []string) bool { func ExecuteShowCommand(character *Character, tokens []string) bool { - log.Printf("Player %s is displaying character information", character.Player.Name) + Logger.Info("Player is displaying character information", "playerName", character.Player.Name) player := character.Player var output strings.Builder @@ -332,7 +331,7 @@ func ExecuteTakeCommand(character *Character, tokens []string) bool { func ExecuteInventoryCommand(character *Character, tokens []string) bool { - log.Printf("Player %s is checking their inventory", character.Player.Name) + Logger.Info("Player is checking their inventory", "playerName", character.Player.Name) inventoryList := ListInventory(character) character.Player.ToPlayer <- inventoryList @@ -373,7 +372,7 @@ func ExecuteDropCommand(character *Character, tokens []string) bool { func ExecuteWearCommand(character *Character, tokens []string) bool { - log.Printf("Player %s is attempting to wear an item", character.Player.Name) + Logger.Info("Player is attempting to wear an item", "playerName", character.Player.Name) if len(tokens) < 2 { character.Player.ToPlayer <- "\n\rUsage: wear \n\r" @@ -442,7 +441,7 @@ func ExecuteRemoveCommand(character *Character, tokens []string) bool { func ExecuteExamineCommand(character *Character, tokens []string) bool { - log.Printf("Player %s is examining an item", character.Player.Name) + Logger.Info("Player is examining an item", "playerName", character.Player.Name) if len(tokens) < 2 { character.Player.ToPlayer <- "\n\rUsage: examine \n\r" @@ -524,7 +523,7 @@ func ExecuteExamineCommand(character *Character, tokens []string) bool { func ExecuteHelpCommand(character *Character, tokens []string) bool { - log.Printf("Player %s is requesting help", character.Player.Name) + Logger.Info("Player is requesting help", "playerName", character.Player.Name) helpMessage := "\n\rAvailable Commands:" + "\n\rhelp - Display available commands" + From f560dd9a37b8b76406f00a1f06c13ef3394c21ae Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 28 Aug 2024 02:01:27 -0400 Subject: [PATCH 05/16] Update Logging from log to slog --- core/database.go | 5 ++--- core/item.go | 38 +++++++++++++++++++------------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/core/database.go b/core/database.go index b52ff78..0393480 100644 --- a/core/database.go +++ b/core/database.go @@ -2,7 +2,6 @@ package core import ( "fmt" - "log" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" @@ -11,7 +10,7 @@ import ( ) func NewKeyPair(region string) (*KeyPair, error) { - log.Printf("Initializing DynamoDB client for region %s", region) + Logger.Info("Initializing DynamoDB client", "region", region) sess, err := session.NewSession(&aws.Config{ Region: aws.String(region), @@ -66,7 +65,7 @@ func (k *KeyPair) Put(tableName string, key map[string]*dynamodb.AttributeValue, return fmt.Errorf("error updating item in table %s: %w", tableName, err) } - log.Printf("Successfully updated item in table %s", tableName) + Logger.Info("Successfully updated item in table", "tableName", tableName) return nil } diff --git a/core/item.go b/core/item.go index 07a1566..8f1b473 100644 --- a/core/item.go +++ b/core/item.go @@ -3,7 +3,6 @@ package core import ( "encoding/json" "fmt" - "log" "os" "github.com/aws/aws-sdk-go/aws" @@ -192,7 +191,7 @@ func (s *Server) SaveActiveItems() error { return fmt.Errorf("server is nil") } - log.Println("Starting to save active items...") + Logger.Info("Starting to save active items...") // Collect all items from rooms and characters itemsToSave := make(map[uuid.UUID]*Item) @@ -201,13 +200,13 @@ func (s *Server) SaveActiveItems() error { if s.Rooms != nil { for roomID, room := range s.Rooms { if room == nil { - log.Printf("Warning: Nil room found with ID %d", roomID) + Logger.Warn("Nil room found", "roomID", roomID) continue } room.Mutex.Lock() for itemID, item := range room.Items { if item == nil { - log.Printf("Warning: Nil item found in room %d with ID %s", roomID, itemID) + Logger.Warn("Nil item found in room", "roomID", roomID, "itemID", itemID) continue } itemsToSave[item.ID] = item @@ -215,20 +214,20 @@ func (s *Server) SaveActiveItems() error { room.Mutex.Unlock() } } else { - log.Println("Warning: Server Rooms map is nil") + Logger.Warn("Server Rooms map is nil") } // Items in character inventories if s.Characters != nil { for charName, character := range s.Characters { if character == nil { - log.Printf("Warning: Nil character found with name %s", charName) + Logger.Warn("Nil character found", "characterName", charName) continue } character.Mutex.Lock() for itemName, item := range character.Inventory { if item == nil { - log.Printf("Warning: Nil item found in inventory of character %s with name %s", charName, itemName) + Logger.Warn("Nil item found in inventory", "characterName", charName, "itemName", itemName) continue } itemsToSave[item.ID] = item @@ -236,7 +235,7 @@ func (s *Server) SaveActiveItems() error { character.Mutex.Unlock() } } else { - log.Println("Warning: Server Characters map is nil") + Logger.Warn("Server Characters map is nil") } // Save all collected items @@ -246,18 +245,18 @@ func (s *Server) SaveActiveItems() error { for _, item := range itemsToSave { if item == nil { - log.Println("Warning: Attempting to save a nil item, skipping") + Logger.Warn("Attempting to save a nil item, skipping") continue } if err := s.Database.WriteItem(item); err != nil { - log.Printf("Error saving item %s (ID: %s): %v", item.Name, item.ID, err) + Logger.Error("Error saving item", "itemName", item.Name, "itemID", item.ID, "error", err) // Continue saving other items even if one fails } else { - log.Printf("Successfully saved item %s (ID: %s)", item.Name, item.ID) + Logger.Info("Successfully saved item", "itemName", item.Name, "itemID", item.ID) } } - log.Println("Finished saving active items") + Logger.Info("Finished saving active items") return nil } @@ -305,7 +304,7 @@ func (s *Server) CreateItemFromPrototype(prototypeID string) (*Item, error) { for _, contentItem := range prototype.Contents { newContentItem, err := s.CreateItemFromPrototype(contentItem.ID.String()) if err != nil { - log.Printf("Error creating content item from prototype %s: %v", contentItem.ID, err) + Logger.Error("Error creating content item from prototype", "prototypeID", contentItem.ID, "error", err) continue } newItem.Contents = append(newItem.Contents, newContentItem) @@ -316,7 +315,8 @@ func (s *Server) CreateItemFromPrototype(prototypeID string) (*Item, error) { return nil, fmt.Errorf("failed to write new item to database: %w", err) } - log.Printf("Created new item %s (ID: %s) from prototype %s", newItem.Name, newItem.ID, prototypeID) + Logger.Info("Created new item from prototype", "itemName", newItem.Name, "itemID", newItem.ID, "prototypeID", prototypeID) + return newItem, nil } @@ -325,7 +325,7 @@ func (r *Room) AddItem(item *Item) { defer r.Mutex.Unlock() if item == nil { - log.Printf("Warning: Attempted to add nil item to room %d", r.RoomID) + Logger.Warn("Attempted to add nil item to room", "roomID", r.RoomID) return } @@ -334,7 +334,7 @@ func (r *Room) AddItem(item *Item) { } r.Items[item.ID.String()] = item - log.Printf("Added item %s (ID: %s) to room %d", item.Name, item.ID, r.RoomID) + Logger.Info("Added item to room", "itemName", item.Name, "itemID", item.ID, "roomID", r.RoomID) } func (r *Room) RemoveItem(item *Item) { @@ -342,12 +342,12 @@ func (r *Room) RemoveItem(item *Item) { defer r.Mutex.Unlock() if item == nil { - log.Printf("Warning: Attempted to remove nil item from room %d", r.RoomID) + Logger.Warn("Attempted to remove nil item from room", "roomID", r.RoomID) return } delete(r.Items, item.ID.String()) - log.Printf("Removed item %s (ID: %s) from room %d", item.Name, item.ID, r.RoomID) + Logger.Info("Removed item from room", "itemName", item.Name, "itemID", item.ID, "roomID", r.RoomID) } // Add a new method to clean up nil items @@ -358,7 +358,7 @@ func (r *Room) CleanupNilItems() { for id, item := range r.Items { if item == nil { delete(r.Items, id) - log.Printf("Removed nil item with ID %s from room %d", id, r.RoomID) + Logger.Info("Removed nil item from room", "itemID", id, "roomID", r.RoomID) } } } From 7973a1435c863eaa587f70e9aa252e0a1be6dc53 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 28 Aug 2024 02:19:44 -0400 Subject: [PATCH 06/16] Update Logging from log to slog --- core/player.go | 53 +++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/core/player.go b/core/player.go index 8045049..b45a6d1 100644 --- a/core/player.go +++ b/core/player.go @@ -5,7 +5,6 @@ import ( "bytes" "fmt" "io" - "log" "sort" "strconv" "strings" @@ -42,7 +41,7 @@ func (k *KeyPair) WritePlayer(player *Player) error { return fmt.Errorf("error storing player data: %w", err) } - log.Printf("Successfully wrote player data for %s with %d characters", player.Name, len(player.CharacterList)) + Logger.Info("Successfully wrote player data", "playerName", player.Name, "characterCount", len(player.CharacterList)) return nil } @@ -54,7 +53,7 @@ func (k *KeyPair) ReadPlayer(playerName string) (string, map[string]uuid.UUID, e var pd PlayerData err := k.Get("players", key, &pd) if err != nil { - log.Printf("Error reading player data: %v", err) + Logger.Error("Error reading player data", "error", err) return "", nil, fmt.Errorf("player not found") } @@ -66,18 +65,18 @@ func (k *KeyPair) ReadPlayer(playerName string) (string, map[string]uuid.UUID, e for name, idString := range pd.CharacterList { id, err := uuid.Parse(idString) if err != nil { - log.Printf("Error parsing UUID for character %s: %v", name, err) + Logger.Error("Error parsing UUID for character", "characterName", name, "error", err) continue } characterList[name] = id } - log.Printf("Successfully read player data for %s with %d characters", pd.Name, len(characterList)) + Logger.Info("Successfully read player data", "playerName", pd.Name, "characterCount", len(characterList)) return pd.Name, characterList, nil } func PlayerInput(p *Player) { - log.Printf("Player %s input goroutine started", p.Name) + Logger.Info("Player input goroutine started", "playerName", p.Name) var inputBuffer bytes.Buffer const maxBufferSize = 1024 // Maximum input size in bytes @@ -88,11 +87,11 @@ func PlayerInput(p *Player) { char, _, err := reader.ReadRune() if err != nil { if err == io.EOF { - log.Printf("Player %s disconnected: %v", p.Name, err) + Logger.Info("Player disconnected", "playerName", p.Name, "error", err) p.PlayerError <- err break } else { - log.Printf("Error reading from player %s: %v", p.Name, err) + Logger.Error("Error reading from player", "playerName", p.Name, "error", err) p.PlayerError <- err continue } @@ -100,7 +99,7 @@ func PlayerInput(p *Player) { if p.Echo && char != '\n' && char != '\r' { if _, err := p.Connection.Write([]byte(string(char))); err != nil { - log.Printf("Failed to echo character to player %s: %v", p.Name, err) + Logger.Error("Failed to echo character to player", "playerName", p.Name, "error", err) } } @@ -113,7 +112,7 @@ func PlayerInput(p *Player) { } if inputBuffer.Len() >= maxBufferSize { - log.Printf("Input buffer overflow for player %s, discarding input", p.Name) + Logger.Warn("Input buffer overflow, discarding input", "playerName", p.Name) p.ToPlayer <- "\n\rInput too long, discarded.\n\r" inputBuffer.Reset() continue @@ -127,24 +126,24 @@ func PlayerInput(p *Player) { func PlayerOutput(p *Player) { - log.Printf("Player %s output goroutine started", p.Name) + Logger.Info("Player output goroutine started", "playerName", p.Name) for message := range p.ToPlayer { // Append carriage return and newline for SSH protocol compatibility messageToSend := message if _, err := p.Connection.Write([]byte(messageToSend)); err != nil { - log.Printf("Failed to send message to player %s: %v", p.Name, err) + Logger.Error("Failed to send message to player", "playerName", p.Name, "error", err) // Consider whether to continue or break based on your error handling policy continue } } // Optionally, perform any cleanup here after the channel is closed and loop exits - log.Printf("Message channel closed for player %s", p.Name) + Logger.Info("Message channel closed for player", "playerName", p.Name) } func InputLoop(c *Character) { - log.Printf("Starting input loop for character %s", c.Name) + Logger.Info("Starting input loop for character", "characterName", c.Name) // Initially execute the look command with no additional tokens ExecuteLookCommand(c, []string{}) @@ -169,7 +168,7 @@ func InputLoop(c *Character) { } else { // Execute the command shouldQuit = ExecuteCommand(c, verb, tokens) - log.Printf("Player %s issued command: %s", c.Player.Name, strings.Join(tokens, " ")) + Logger.Info("Player issued command", "playerName", c.Player.Name, "command", strings.Join(tokens, " ")) } lastCommand = "" if !shouldQuit { @@ -179,7 +178,7 @@ func InputLoop(c *Character) { case inputLine, more := <-c.Player.FromPlayer: if !more { - log.Printf("Input channel closed for player %s.", c.Player.Name) + Logger.Info("Input channel closed for player", "playerName", c.Player.Name) shouldQuit = true break } @@ -200,14 +199,14 @@ func InputLoop(c *Character) { err := c.Server.Database.WriteCharacter(c) if err != nil { - log.Printf("Error saving character %s: %v", c.Name, err) + Logger.Error("Error saving character", "characterName", c.Name, "error", err) } - log.Printf("Input loop ended for character %s", c.Name) + Logger.Info("Input loop ended for character", "characterName", c.Name) } func SelectCharacter(player *Player, server *Server) (*Character, error) { - log.Printf("Player %s is selecting a character", player.Name) + Logger.Info("Player is selecting a character", "playerName", player.Name) var options []string @@ -255,7 +254,7 @@ func SelectCharacter(player *Player, server *Server) (*Character, error) { characterID := player.CharacterList[characterName] character, err = server.Database.LoadCharacter(characterID, player, server) if err != nil { - log.Printf("Error loading character %s for player %s: %v", characterName, player.Name, err) + Logger.Error("Error loading character for player", "characterName", characterName, "playerName", player.Name, "error", err) player.ToPlayer <- fmt.Sprintf("Error loading character: %v\n\r", err) continue } @@ -281,14 +280,14 @@ func SelectCharacter(player *Player, server *Server) (*Character, error) { SendRoomMessage(character.Room, fmt.Sprintf("\n\r%s has arrived.\n\r", character.Name)) } - log.Printf("Character %s (ID: %s) selected and added to server character list and room", character.Name, character.ID) + Logger.Info("Character selected and added to server", "characterName", character.Name, "characterID", character.ID) return character, nil } } func CreateCharacter(player *Player, server *Server) (*Character, error) { - log.Printf("Player %s is creating a new character", player.Name) + Logger.Info("Player is creating a new character", "playerName", player.Name) player.ToPlayer <- "\n\rEnter your character name: " @@ -345,7 +344,7 @@ func CreateCharacter(player *Player, server *Server) (*Character, error) { } } - log.Printf("Creating character with name: %s", charName) + Logger.Info("Creating character", "characterName", charName) room, ok := server.Rooms[1] if !ok { @@ -364,23 +363,23 @@ func CreateCharacter(player *Player, server *Server) (*Character, error) { player.CharacterList[charName] = character.ID player.Mutex.Unlock() - log.Printf("Added character %s (ID: %s) to player %s's character list", charName, character.ID, player.Name) + Logger.Info("Added character to player's character list", "characterName", charName, "characterID", character.ID, "playerName", player.Name) // Save the character to the database err := server.Database.WriteCharacter(character) if err != nil { - log.Printf("Error saving character %s to database: %v", charName, err) + Logger.Error("Error saving character to database", "characterName", charName, "error", err) return nil, fmt.Errorf("failed to save character to database") } // Save the updated player data err = server.Database.WritePlayer(player) if err != nil { - log.Printf("Error saving player data for %s: %v", player.Name, err) + Logger.Error("Error saving player data", "playerName", player.Name, "error", err) return nil, fmt.Errorf("failed to save player data") } - log.Printf("Successfully created and saved character %s (ID: %s) for player %s", charName, character.ID, player.Name) + Logger.Info("Successfully created and saved character for player", "characterName", charName, "characterID", character.ID, "playerName", player.Name) return character, nil } From 258af24f37fceee6a97e62ae492444728afcb67f Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 28 Aug 2024 21:58:57 -0400 Subject: [PATCH 07/16] Update Logging from log to slog --- core/commands.go | 2 +- core/room.go | 33 ++++++++++++++++----------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/core/commands.go b/core/commands.go index 328a46d..7616faf 100644 --- a/core/commands.go +++ b/core/commands.go @@ -20,7 +20,7 @@ var CommandHandlers = map[string]CommandHandler{ "who": ExecuteWhoCommand, "password": ExecutePasswordCommand, "challenge": ExecuteChallengeCommand, - "take": ExecuteTakeCommand, // Add the new take command + "take": ExecuteTakeCommand, "get": ExecuteTakeCommand, // Alias for take command "drop": ExecuteDropCommand, "inventory": ExecuteInventoryCommand, diff --git a/core/room.go b/core/room.go index 12abbdc..2474481 100644 --- a/core/room.go +++ b/core/room.go @@ -3,7 +3,6 @@ package core import ( "encoding/json" "fmt" - "log" "os" "sort" "strconv" @@ -108,7 +107,7 @@ func (kp *KeyPair) LoadRooms() (map[int64]*Room, error) { // Load exits for this room exits, err := kp.LoadExits(roomData.RoomID) if err != nil { - log.Printf("Warning: Failed to load exits for room %d: %v", roomData.RoomID, err) + Logger.Warn("Failed to load exits for room", "room_id", roomData.RoomID, "error", err) } room.Exits = exits @@ -116,7 +115,7 @@ func (kp *KeyPair) LoadRooms() (map[int64]*Room, error) { for _, itemID := range roomData.ItemIDs { item, err := kp.LoadItem(itemID, false) if err != nil { - log.Printf("Warning: Failed to load item %s for room %d: %v", itemID, roomData.RoomID, err) + Logger.Warn("Failed to load item for room", "item_id", itemID, "room_id", roomData.RoomID, "error", err) continue } room.AddItem(item) @@ -231,7 +230,7 @@ func (kp *KeyPair) WriteRoom(room *Room) error { } } - log.Printf("Successfully wrote room %d to database", room.RoomID) + Logger.Info("Successfully wrote room to database", "room_id", room.RoomID) return nil } @@ -260,7 +259,7 @@ func NewRoom(RoomID int64, Area string, Title string, Description string) *Room Mutex: sync.Mutex{}, } - log.Printf("Created room %s with ID %d", room.Title, room.RoomID) + Logger.Info("Created room", "room_title", room.Title, "room_id", room.RoomID) return room } @@ -270,7 +269,7 @@ func (r *Room) AddExit(exit *Exit) { } func Move(c *Character, direction string) { - log.Printf("Player %s is attempting to move %s", c.Name, direction) + Logger.Info("Player is attempting to move", "player_name", c.Name, "direction", direction) c.Mutex.Lock() defer c.Mutex.Unlock() @@ -317,7 +316,7 @@ func Move(c *Character, direction string) { func SendRoomMessage(r *Room, message string) { - log.Printf("Sending message to room %d: %s", r.RoomID, message) + Logger.Info("Sending message to room", "room_id", r.RoomID, "message", message) for _, character := range r.Characters { character.Player.ToPlayer <- message @@ -330,11 +329,11 @@ func SendRoomMessage(r *Room, message string) { func RoomInfo(r *Room, character *Character) string { if r == nil { - log.Printf("Error: Attempted to get room info for nil room (Character: %s)", character.Name) + Logger.Error("Attempted to get room info for nil room", "character_name", character.Name) return "\n\rError: You are not in a valid room.\n\r" } if character == nil { - log.Printf("Error: Attempted to get room info for nil character (Room ID: %d)", r.RoomID) + Logger.Error("Attempted to get room info for nil character", "room_id", r.RoomID) return "\n\rError: Invalid character.\n\r" } @@ -379,7 +378,7 @@ func RoomInfo(r *Room, character *Character) string { func sortedExits(r *Room) []string { - log.Printf("Sorting exits for room %d", r.RoomID) + Logger.Info("Sorting exits for room", "room_id", r.RoomID) if r.Exits == nil { return []string{} @@ -396,7 +395,7 @@ func sortedExits(r *Room) []string { func getOtherCharacters(r *Room, currentCharacter *Character) []string { if r == nil || r.Characters == nil { - log.Printf("Warning: Room or Characters map is nil in getOtherCharacters") + Logger.Warn("Room or Characters map is nil in getOtherCharacters") return []string{} } @@ -407,31 +406,31 @@ func getOtherCharacters(r *Room, currentCharacter *Character) []string { } } - log.Printf("Found %d other characters in room %d", len(otherCharacters), r.RoomID) + Logger.Info("Found other characters in room", "count", len(otherCharacters), "room_id", r.RoomID) return otherCharacters } func getVisibleItems(r *Room) []string { - log.Printf("Getting visible items in room %d", r.RoomID) + Logger.Info("Getting visible items in room", "room_id", r.RoomID) r.Mutex.Lock() defer r.Mutex.Unlock() if r.Items == nil { - log.Printf("Warning: Items map is nil for room %d", r.RoomID) + Logger.Warn("Items map is nil for room", "room_id", r.RoomID) return []string{} } visibleItems := make([]string, 0, len(r.Items)) for itemID, item := range r.Items { if item == nil { - log.Printf("Warning: Nil item found with ID %s in room %d", itemID, r.RoomID) + Logger.Warn("Nil item found with ID in room", "item_id", itemID, "room_id", r.RoomID) continue } visibleItems = append(visibleItems, item.Name) - log.Printf("Found item %s (ID: %s) in room %d", item.Name, itemID, r.RoomID) + Logger.Info("Found item", "item_name", item.Name, "item_id", itemID) } - log.Printf("Total visible items in room %d: %d", r.RoomID, len(visibleItems)) + Logger.Info("Total visible items in room", "room_id", r.RoomID, "count", len(visibleItems)) return visibleItems } From 7986bfd9c63ac547fdb3a0a483a7fd38e308bb21 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 28 Aug 2024 22:34:38 -0400 Subject: [PATCH 08/16] Update Logging from log to slog --- core/utils.go | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/core/utils.go b/core/utils.go index ddf2477..2245889 100644 --- a/core/utils.go +++ b/core/utils.go @@ -1,7 +1,6 @@ package core import ( - "log" "math" "math/rand" "time" @@ -24,33 +23,33 @@ func Challenge(attacker, defender, balance float64) float64 { } func AutoSave(server *Server) { - log.Printf("Starting auto-save routine...") + Logger.Info("Starting auto-save routine...") for { // Sleep for the configured duration time.Sleep(time.Duration(server.AutoSave) * time.Minute) - log.Println("Starting auto-save process...") + Logger.Info("Starting auto-save process...") // Save active characters if err := SaveActiveCharacters(server); err != nil { - log.Printf("Failed to save characters: %v", err) + Logger.Error("Failed to save characters", "error", err) } else { - log.Println("Active characters saved successfully") + Logger.Info("Active characters saved successfully") } // Save active rooms if err := SaveActiveRooms(server); err != nil { - log.Printf("Failed to save rooms: %v", err) + Logger.Error("Failed to save rooms", "error", err) } else { - log.Println("Active rooms saved successfully") + Logger.Info("Active rooms saved successfully") } // Save active items if err := server.SaveActiveItems(); err != nil { - log.Printf("Failed to save items: %v", err) + Logger.Error("Failed to save items", "error", err) } else { - log.Println("Active items saved successfully") + Logger.Info("Active items saved successfully") } // Save active player records @@ -60,15 +59,15 @@ func AutoSave(server *Server) { if character.Player != nil { err := server.Database.WritePlayer(character.Player) if err != nil { - log.Printf("Failed to save player data for %s: %v", character.Player.Name, err) + Logger.Error("Failed to save player data", "player_name", character.Player.Name, "error", err) } else { savedPlayers++ } } } server.Mutex.Unlock() - log.Printf("Saved %d active player records", savedPlayers) + Logger.Info("Saved active player records", "count", savedPlayers) - log.Println("Auto-save process completed") + Logger.Info("Auto-save process completed") } } From 8f68e8bf573e6c54adde5f2f4ac6d602b5763a14 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 28 Aug 2024 22:50:09 -0400 Subject: [PATCH 09/16] Update Logging from log to slog --- ssh_server/server.go | 55 ++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/ssh_server/server.go b/ssh_server/server.go index b2d0604..970fc62 100644 --- a/ssh_server/server.go +++ b/ssh_server/server.go @@ -3,7 +3,6 @@ package main import ( "flag" "fmt" - "log" "net" "os" "time" @@ -16,7 +15,7 @@ import ( func NewServer(config core.Configuration) (*core.Server, error) { - log.Printf("Initializing server...") + core.Logger.Info("Initializing server...") // Initialize the server with the configuration server := &core.Server{ @@ -32,7 +31,7 @@ func NewServer(config core.Configuration) (*core.Server, error) { Essence: config.Essence, } - log.Printf("Initializing database...") + core.Logger.Info("Initializing database...") // Initialize the database var err error @@ -46,16 +45,16 @@ func NewServer(config core.Configuration) (*core.Server, error) { // Load the character names from the database - log.Printf("Loading character names from database...") + core.Logger.Info("Loading character names from database...") server.CharacterExists, err = server.Database.LoadCharacterNames() if err != nil { - log.Printf("Error loading character names from database: %v", err) + core.Logger.Error("Error loading character names from database", "error", err) } server.Archetypes, err = server.Database.LoadArchetypes() if err != nil { - log.Printf("Error loading archetypes from database: %v", err) + core.Logger.Error("Error loading archetypes from database", "error", err) } // Add a default room @@ -64,7 +63,7 @@ func NewServer(config core.Configuration) (*core.Server, error) { // Load rooms into the server - log.Printf("Loading rooms from database...") + core.Logger.Info("Loading rooms from database...") server.Rooms, err = server.Database.LoadRooms() if err != nil { @@ -75,7 +74,7 @@ func NewServer(config core.Configuration) (*core.Server, error) { } func loadConfiguration(configFile string) (core.Configuration, error) { - log.Printf("Loading configuration from %s", configFile) + core.Logger.Info("Loading configuration", "config_file", configFile) var config core.Configuration @@ -94,7 +93,7 @@ func loadConfiguration(configFile string) (core.Configuration, error) { func main() { - log.Printf("Starting server...") + core.Logger.Info("Starting server...") // Read configuration file configFile := flag.String("config", "config.yml", "Configuration file") @@ -102,13 +101,13 @@ func main() { config, err := loadConfiguration(*configFile) if err != nil { - log.Printf("Error loading configuration: %v", err) + core.Logger.Error("Error loading configuration", "error", err) return } server, err := NewServer(config) if err != nil { - log.Printf("Failed to create server: %v", err) + core.Logger.Error("Failed to create server", "error", err) return } @@ -117,18 +116,18 @@ func main() { // Start the server if err := StartSSHServer(server); err != nil { - log.Printf("Failed to start server: %v", err) + core.Logger.Error("Failed to start server", "error", err) return } } func Authenticate(username, password string, config core.Configuration) bool { - log.Printf("Authenticating user %s", username) + core.Logger.Info("Authenticating user", "username", username) _, err := core.SignInUser(username, password, config) if err != nil { - log.Printf("Authentication attempt failed for user %s: %v", username, err) + core.Logger.Error("Authentication attempt failed for user", "username", username, "error", err) return false } return true @@ -136,7 +135,7 @@ func Authenticate(username, password string, config core.Configuration) bool { func StartSSHServer(server *core.Server) error { - log.Printf("Starting SSH server on port %d", server.Port) + core.Logger.Info("Starting SSH server", "port", server.Port) // Read the private key from disk privateBytes, err := os.ReadFile("./server.key") @@ -153,10 +152,10 @@ func StartSSHServer(server *core.Server) error { // Authenticate the player authenticated := Authenticate(conn.User(), string(password), server.Config) if authenticated { - log.Printf("Player %s authenticated", conn.User()) + core.Logger.Info("Player authenticated", "player_name", conn.User()) return nil, nil } - log.Printf("Player %s failed authentication", conn.User()) + core.Logger.Warn("Player failed authentication", "player_name", conn.User()) return nil, fmt.Errorf("password rejected for %q", conn.User()) }, } @@ -170,18 +169,18 @@ func StartSSHServer(server *core.Server) error { server.Listener = listener - log.Printf("SSH server listening on port %d", server.Port) + core.Logger.Info("SSH server listening", "port", server.Port) for { conn, err := server.Listener.Accept() if err != nil { - log.Printf("Error accepting connection: %v", err) + core.Logger.Error("Error accepting connection", "error", err) continue } sshConn, chans, reqs, err := ssh.NewServerConn(conn, server.SSHConfig) if err != nil { - log.Printf("Failed to handshake: %v", err) + core.Logger.Error("Failed to handshake", "error", err) continue } @@ -193,12 +192,12 @@ func StartSSHServer(server *core.Server) error { func handleChannels(server *core.Server, sshConn *ssh.ServerConn, channels <-chan ssh.NewChannel) { - log.Printf("New connection from %s (%s)", sshConn.User(), sshConn.RemoteAddr()) + core.Logger.Info("New connection", "address", sshConn.RemoteAddr().String(), "user", sshConn.User()) for newChannel := range channels { channel, requests, err := newChannel.Accept() if err != nil { - log.Printf("Could not accept channel: %v", err) + core.Logger.Error("Could not accept channel", "error", err) continue } @@ -210,18 +209,18 @@ func handleChannels(server *core.Server, sshConn *ssh.ServerConn, channels <-cha if err != nil { // If the player does not exist, create a new record if err.Error() == "player not found" { - log.Printf("Creating new player record for %s", playerName) + core.Logger.Info("Creating new player record", "player_name", playerName) characterList = make(map[string]uuid.UUID) // Initialize an empty character list for new players err = server.Database.WritePlayer(&core.Player{ Name: playerName, CharacterList: characterList, }) if err != nil { - log.Printf("Error creating player record: %v", err) + core.Logger.Error("Error creating player record", "error", err) continue } } else { - log.Printf("Error reading player from database: %v", err) + core.Logger.Error("Error reading player from database", "error", err) continue } } @@ -251,7 +250,7 @@ func handleChannels(server *core.Server, sshConn *ssh.ServerConn, channels <-cha go func(p *core.Player) { defer p.Connection.Close() - log.Printf("Player %s connected", p.Name) + core.Logger.Info("Player connected", "player_name", p.Name) // Send welcome message p.ToPlayer <- fmt.Sprintf("Welcome to the game, %s!\n\r", p.Name) @@ -267,7 +266,7 @@ func handleChannels(server *core.Server, sshConn *ssh.ServerConn, channels <-cha server.Database.WritePlayer(player) - log.Printf("Player %s disconnected", p.Name) + core.Logger.Info("Player disconnected", "player_name", p.Name) player = nil }(player) @@ -285,7 +284,7 @@ func parseDims(b []byte) (width, height int) { // HandleSSHRequests handles SSH requests from the client func HandleSSHRequests(player *core.Player, requests <-chan *ssh.Request) { - log.Printf("Handling SSH requests for player %s", player.Name) + core.Logger.Info("Handling SSH requests for player", "player_name", player.Name) for req := range requests { switch req.Type { From e450577af946c7a010e2f79be9c0ca793fb8c0fb Mon Sep 17 00:00:00 2001 From: "Jason E. Robinson" Date: Thu, 29 Aug 2024 01:09:00 -0400 Subject: [PATCH 10/16] Add Cloudwatch Logging --- core/go.mod | 15 ++++++ core/logging.go | 120 +++++++++++++++++++++++++++++++++++++++++++++++- core/types.go | 13 ++++++ 3 files changed, 146 insertions(+), 2 deletions(-) diff --git a/core/go.mod b/core/go.mod index ab91373..4a412cc 100644 --- a/core/go.mod +++ b/core/go.mod @@ -4,6 +4,9 @@ go 1.22 require ( github.com/aws/aws-sdk-go v1.54.15 + github.com/aws/aws-sdk-go-v2 v1.30.4 + github.com/aws/aws-sdk-go-v2/config v1.27.31 + github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.37.5 github.com/aws/aws-xray-sdk-go v1.8.4 github.com/google/uuid v1.6.0 golang.org/x/crypto v0.24.0 @@ -11,6 +14,18 @@ require ( require ( github.com/andybalholm/brotli v1.0.6 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.30 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 // indirect + github.com/aws/smithy-go v1.20.4 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/compress v1.17.2 // indirect diff --git a/core/logging.go b/core/logging.go index afbf2d6..dca11c1 100644 --- a/core/logging.go +++ b/core/logging.go @@ -1,11 +1,17 @@ package core import ( + "context" "fmt" "log/slog" "os" "strconv" + "time" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" "github.com/aws/aws-xray-sdk-go/xray" ) @@ -23,6 +29,12 @@ var REGION = GetEnv("AWS_REGION", "us-east-1") // LOG_LEVEL is the logging level (default: 20) var LOG_LEVEL, _ = strconv.Atoi(GetEnv("LOGGING", "20")) +// LOG_GROUP is the CloudWatch log group name +var LOG_GROUP = GetEnv("LOG_GROUP", "/"+APPLICATION_NAME) + +// LOG_STREAM is the CloudWatch log stream name +var LOG_STREAM = GetEnv("LOG_STREAM", "application") + func init() { // Determine the log level @@ -40,8 +52,26 @@ func init() { level = slog.LevelInfo } - // Initialize the Logger - Logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})) + // Initialize AWS SDK configuration + cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(REGION)) + if err != nil { + panic("unable to load SDK config, " + err.Error()) + } + + // Create CloudWatch Logs client + client := cloudwatchlogs.NewFromConfig(cfg) + + // Create CloudWatch handler + cwHandler := NewCloudWatchHandler(client, LOG_GROUP, LOG_STREAM) + + // Create a multi-writer handler that writes to both CloudWatch and stdout + multiHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}).WithAttrs([]slog.Attr{ + slog.String("application", APPLICATION_NAME), + slog.String("region", REGION), + }) + + // Initialize the Logger with both handlers + Logger = slog.New(NewMultiHandler(multiHandler, cwHandler)) slog.SetDefault(Logger) } @@ -86,3 +116,89 @@ func EnableXRay() error { return nil } + +func NewCloudWatchHandler(client *cloudwatchlogs.Client, logGroup, logStream string) *CloudWatchHandler { + return &CloudWatchHandler{ + client: client, + logGroup: logGroup, + logStream: logStream, + } +} + +func (h *CloudWatchHandler) Enabled(ctx context.Context, level slog.Level) bool { + return true +} + +func (h *CloudWatchHandler) Handle(ctx context.Context, r slog.Record) error { + message := r.Message + for _, attr := range h.attrs { + message += fmt.Sprintf(" %s=%v", attr.Key, attr.Value) + } + r.Attrs(func(a slog.Attr) bool { + message += fmt.Sprintf(" %s=%v", a.Key, a.Value) + return true + }) + + _, err := h.client.PutLogEvents(ctx, &cloudwatchlogs.PutLogEventsInput{ + LogGroupName: aws.String(h.logGroup), + LogStreamName: aws.String(h.logStream), + LogEvents: []types.InputLogEvent{ + { + Message: aws.String(message), + Timestamp: aws.Int64(time.Now().UnixNano() / int64(time.Millisecond)), + }, + }, + }) + return err +} + +func (h *CloudWatchHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &CloudWatchHandler{ + client: h.client, + logGroup: h.logGroup, + logStream: h.logStream, + attrs: append(h.attrs, attrs...), + } +} + +func (h *CloudWatchHandler) WithGroup(name string) slog.Handler { + return h +} + +func NewMultiHandler(handlers ...slog.Handler) *MultiHandler { + return &MultiHandler{handlers: handlers} +} + +func (h *MultiHandler) Enabled(ctx context.Context, level slog.Level) bool { + for _, handler := range h.handlers { + if handler.Enabled(ctx, level) { + return true + } + } + return false +} + +func (h *MultiHandler) Handle(ctx context.Context, r slog.Record) error { + for _, handler := range h.handlers { + if err := handler.Handle(ctx, r); err != nil { + return err + } + } + return nil +} + +func (h *MultiHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + newHandlers := make([]slog.Handler, len(h.handlers)) + for i, handler := range h.handlers { + newHandlers[i] = handler.WithAttrs(attrs) + } + return NewMultiHandler(newHandlers...) +} + +func (h *MultiHandler) WithGroup(name string) slog.Handler { + newHandlers := make([]slog.Handler, len(h.handlers)) + for i, handler := range h.handlers { + newHandlers[i] = handler.WithGroup(name) + } + return NewMultiHandler(newHandlers...) +} diff --git a/core/types.go b/core/types.go index 2dc7078..c94cf42 100644 --- a/core/types.go +++ b/core/types.go @@ -1,10 +1,12 @@ package core import ( + "log/slog" "net" "sync" "time" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/google/uuid" "golang.org/x/crypto/ssh" @@ -184,3 +186,14 @@ type ItemData struct { type PrototypesData struct { ItemPrototypes []ItemData `json:"itemPrototypes"` } + +type CloudWatchHandler struct { + client *cloudwatchlogs.Client + logGroup string + logStream string + attrs []slog.Attr +} + +type MultiHandler struct { + handlers []slog.Handler +} From ff0c16bb9708c69671021f6a04764162c9753728 Mon Sep 17 00:00:00 2001 From: "Jason E. Robinson" Date: Thu, 29 Aug 2024 01:14:04 -0400 Subject: [PATCH 11/16] Add Cloudwatch Logging --- ssh_server/go.mod | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ssh_server/go.mod b/ssh_server/go.mod index 21c5564..cc98ba9 100644 --- a/ssh_server/go.mod +++ b/ssh_server/go.mod @@ -12,7 +12,34 @@ require ( ) require ( + github.com/andybalholm/brotli v1.0.6 // indirect github.com/aws/aws-sdk-go v1.54.15 // indirect + github.com/aws/aws-sdk-go-v2 v1.30.4 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect + github.com/aws/aws-sdk-go-v2/config v1.27.31 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.30 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.37.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 // indirect + github.com/aws/aws-xray-sdk-go v1.8.4 // indirect + github.com/aws/smithy-go v1.20.4 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/klauspost/compress v1.17.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.50.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect ) From a7e9005cb5fca997829989534a744b9d9474eee0 Mon Sep 17 00:00:00 2001 From: "Jason E. Robinson" Date: Thu, 29 Aug 2024 01:49:40 -0400 Subject: [PATCH 12/16] Centralize Configuration --- core/cognito.go | 28 +++++++++++------------ core/logging.go | 41 ++++++++++------------------------ core/types.go | 35 ++++++++++++++++++++--------- ssh_server/config.template.yml | 22 +++++++++++++----- ssh_server/server.go | 31 ++++++++++++------------- 5 files changed, 84 insertions(+), 73 deletions(-) diff --git a/core/cognito.go b/core/cognito.go index f60b507..9419ade 100644 --- a/core/cognito.go +++ b/core/cognito.go @@ -40,13 +40,13 @@ func handleCognitoError(err error, email string) error { // SignInUser attempts to sign in a user with the provided credentials func SignInUser(email, password string, config Configuration) (*cognitoidentityprovider.InitiateAuthOutput, error) { - sess, err := session.NewSession(&aws.Config{Region: aws.String(config.UserPoolRegion)}) + sess, err := session.NewSession(&aws.Config{Region: aws.String(config.Aws.Region)}) if err != nil { return nil, fmt.Errorf("create AWS session: %w", err) } cognitoClient := cognitoidentityprovider.New(sess) - secretHash := calculateSecretHash(config.ClientID, config.ClientSecret, email) + secretHash := calculateSecretHash(config.Cognito.ClientID, config.Cognito.ClientSecret, email) authInput := &cognitoidentityprovider.InitiateAuthInput{ AuthFlow: aws.String(cognitoidentityprovider.AuthFlowTypeUserPasswordAuth), @@ -55,7 +55,7 @@ func SignInUser(email, password string, config Configuration) (*cognitoidentityp "PASSWORD": aws.String(password), "SECRET_HASH": aws.String(secretHash), }, - ClientId: aws.String(config.ClientID), + ClientId: aws.String(config.Cognito.ClientID), } authOutput, err := cognitoClient.InitiateAuth(authInput) @@ -71,17 +71,17 @@ func SignInUser(email, password string, config Configuration) (*cognitoidentityp } func SignUpUser(email, password string, config Configuration) (*cognitoidentityprovider.SignUpOutput, error) { - sess, err := session.NewSession(&aws.Config{Region: aws.String(config.UserPoolRegion)}) + sess, err := session.NewSession(&aws.Config{Region: aws.String(config.Aws.Region)}) if err != nil { Logger.Error("Error creating AWS session for sign-up", "error", err) return nil, fmt.Errorf("an internal error occurred while creating AWS session") } cognitoClient := cognitoidentityprovider.New(sess) - secretHash := calculateSecretHash(config.ClientID, config.ClientSecret, email) + secretHash := calculateSecretHash(config.Cognito.ClientID, config.Cognito.ClientSecret, email) signUpInput := &cognitoidentityprovider.SignUpInput{ - ClientId: aws.String(config.ClientID), + ClientId: aws.String(config.Cognito.ClientID), Username: aws.String(email), Password: aws.String(password), SecretHash: aws.String(secretHash), @@ -100,17 +100,17 @@ func SignUpUser(email, password string, config Configuration) (*cognitoidentityp } func ConfirmUser(email, confirmationCode string, config Configuration) (*cognitoidentityprovider.ConfirmSignUpOutput, error) { - sess, err := session.NewSession(&aws.Config{Region: aws.String(config.UserPoolRegion)}) + sess, err := session.NewSession(&aws.Config{Region: aws.String(config.Aws.Region)}) if err != nil { Logger.Error("Error creating AWS session for user confirmation", "error", err) return nil, fmt.Errorf("an internal error occurred while creating AWS session") } cognitoClient := cognitoidentityprovider.New(sess) - secretHash := calculateSecretHash(config.ClientID, config.ClientSecret, email) + secretHash := calculateSecretHash(config.Cognito.ClientID, config.Cognito.ClientSecret, email) confirmSignUpInput := &cognitoidentityprovider.ConfirmSignUpInput{ - ClientId: aws.String(config.ClientID), + ClientId: aws.String(config.Cognito.ClientID), Username: aws.String(email), ConfirmationCode: aws.String(confirmationCode), SecretHash: aws.String(secretHash), @@ -126,7 +126,7 @@ func ConfirmUser(email, confirmationCode string, config Configuration) (*cognito } func GetUserData(accessToken string, config Configuration) (*cognitoidentityprovider.GetUserOutput, error) { - sess, err := session.NewSession(&aws.Config{Region: aws.String(config.UserPoolRegion)}) + sess, err := session.NewSession(&aws.Config{Region: aws.String(config.Aws.Region)}) if err != nil { Logger.Error("Error creating AWS session for getting user data", "error", err) return nil, fmt.Errorf("an internal error occurred while creating AWS session") @@ -163,7 +163,7 @@ func ChangePassword(server *Server, username, oldPassword, newPassword string) e // Create a new AWS session sess, err := session.NewSession(&aws.Config{ - Region: aws.String(server.Config.UserPoolRegion), + Region: aws.String(server.Config.Aws.Region), }) if err != nil { Logger.Error("Failed to create AWS session for user", "username", username, "error", err) @@ -174,12 +174,12 @@ func ChangePassword(server *Server, username, oldPassword, newPassword string) e cognitoClient := cognitoidentityprovider.New(sess) // Calculate SECRET_HASH - secretHash := calculateSecretHash(server.Config.ClientID, server.Config.ClientSecret, username) + secretHash := calculateSecretHash(server.Config.Cognito.ClientID, server.Config.Cognito.ClientSecret, username) // Respond to the NEW_PASSWORD_REQUIRED challenge challengeResponseInput := &cognitoidentityprovider.RespondToAuthChallengeInput{ ChallengeName: aws.String(cognitoidentityprovider.ChallengeNameTypeNewPasswordRequired), - ClientId: aws.String(server.Config.ClientID), + ClientId: aws.String(server.Config.Cognito.ClientID), ChallengeResponses: map[string]*string{ "USERNAME": aws.String(username), "NEW_PASSWORD": aws.String(newPassword), @@ -212,7 +212,7 @@ func ChangePassword(server *Server, username, oldPassword, newPassword string) e // Create a new AWS session sess, err := session.NewSession(&aws.Config{ - Region: aws.String(server.Config.UserPoolRegion), + Region: aws.String(server.Config.Aws.Region), }) if err != nil { Logger.Error("Failed to create AWS session for user", "username", username, "error", err) diff --git a/core/logging.go b/core/logging.go index dca11c1..b116467 100644 --- a/core/logging.go +++ b/core/logging.go @@ -5,7 +5,6 @@ import ( "fmt" "log/slog" "os" - "strconv" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -20,26 +19,10 @@ var ( Logger *slog.Logger ) -// APPLICATION_NAME is the name of the application, defaulting to "dark_relics" -var APPLICATION_NAME = GetEnv("APPLICATION_NAME", "dark_relics") - -// REGION is the primary AWS region for Observatory resources -var REGION = GetEnv("AWS_REGION", "us-east-1") - -// LOG_LEVEL is the logging level (default: 20) -var LOG_LEVEL, _ = strconv.Atoi(GetEnv("LOGGING", "20")) - -// LOG_GROUP is the CloudWatch log group name -var LOG_GROUP = GetEnv("LOG_GROUP", "/"+APPLICATION_NAME) - -// LOG_STREAM is the CloudWatch log stream name -var LOG_STREAM = GetEnv("LOG_STREAM", "application") - -func init() { - +func InitializeLogging(cfg *Configuration) error { // Determine the log level var level slog.Level - switch LOG_LEVEL { + switch cfg.Logging.LogLevel { case 10: level = slog.LevelDebug case 20: @@ -53,27 +36,28 @@ func init() { } // Initialize AWS SDK configuration - cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(REGION)) + awsCfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(cfg.Aws.Region)) if err != nil { - panic("unable to load SDK config, " + err.Error()) + return fmt.Errorf("unable to load SDK config: %w", err) } // Create CloudWatch Logs client - client := cloudwatchlogs.NewFromConfig(cfg) + client := cloudwatchlogs.NewFromConfig(awsCfg) // Create CloudWatch handler - cwHandler := NewCloudWatchHandler(client, LOG_GROUP, LOG_STREAM) + cwHandler := NewCloudWatchHandler(client, cfg.Logging.LogGroup, cfg.Logging.LogStream) // Create a multi-writer handler that writes to both CloudWatch and stdout multiHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}).WithAttrs([]slog.Attr{ - slog.String("application", APPLICATION_NAME), - slog.String("region", REGION), + slog.String("application", cfg.Logging.ApplicationName), + slog.String("region", cfg.Aws.Region), }) // Initialize the Logger with both handlers Logger = slog.New(NewMultiHandler(multiHandler, cwHandler)) slog.SetDefault(Logger) + return nil } // GetEnv retrieves environment variables or returns a default value if not set @@ -84,10 +68,10 @@ func GetEnv(key, defaultValue string) string { return defaultValue } -func EnableXRay() error { +func EnableXRay(cfg *Configuration) error { // Determine the log level var xrayLogLevel string - switch LOG_LEVEL { + switch cfg.Logging.LogLevel { case 10: xrayLogLevel = "debug" case 20: @@ -103,8 +87,7 @@ func EnableXRay() error { Logger.Info("Configuring AWS X-Ray", "logLevel", xrayLogLevel) err := xray.Configure(xray.Config{ - LogLevel: xrayLogLevel, - ServiceVersion: APPLICATION_NAME, // Assuming APPLICATION_NAME is your service version + LogLevel: xrayLogLevel, }) if err != nil { diff --git a/core/types.go b/core/types.go index c94cf42..4c03fc9 100644 --- a/core/types.go +++ b/core/types.go @@ -19,16 +19,31 @@ type Index struct { } type Configuration struct { - Port uint16 `json:"Port"` - UserPoolID string `json:"UserPoolId"` - ClientSecret string `json:"UserPoolClientSecret"` - UserPoolRegion string `json:"UserPoolRegion"` - ClientID string `json:"UserPoolClientId"` - DataFile string `json:"DataFile"` - Balance float64 `json:"Balance"` - AutoSave uint16 `json:"AutoSave"` - Essence uint16 `json:"StartingEssence"` - Health uint16 `json:"StartingHealth"` + Server struct { + Port uint16 `yaml:"Port"` + } `yaml:"Server"` + Aws struct { + Region string `yaml:"Region"` + } `yaml:"Aws"` + Cognito struct { + UserPoolID string `yaml:"UserPoolId"` + ClientSecret string `yaml:"UserPoolClientSecret"` + ClientID string `yaml:"UserPoolClientId"` + UserPoolDomain string `yaml:"UserPoolDomain"` + UserPoolArn string `yaml:"UserPoolArn"` + } `yaml:"Cognito"` + Game struct { + Balance float64 `yaml:"Balance"` + AutoSave uint16 `yaml:"AutoSave"` + StartingEssence uint16 `yaml:"StartingEssence"` + StartingHealth uint16 `yaml:"StartingHealth"` + } `yaml:"Game"` + Logging struct { + ApplicationName string `yaml:"ApplicationName"` + LogLevel int `yaml:"LogLevel"` + LogGroup string `yaml:"LogGroup"` + LogStream string `yaml:"LogStream"` + } `yaml:"Logging"` } type KeyPair struct { diff --git a/ssh_server/config.template.yml b/ssh_server/config.template.yml index 799d1b1..fd19592 100644 --- a/ssh_server/config.template.yml +++ b/ssh_server/config.template.yml @@ -1,12 +1,24 @@ +Server: Port: 9050 -UserPoolRegion: "us-east-1" + +Aws: +Region: "us-east-1" + +Cognito: +UserPoolId: "us-east-1_xxxxxxxxx" +UserPoolClientSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" UserPoolClientId: "xxxxxxxxxxxxxxxxxxxxxxxxxx" UserPoolDomain: "mud-user-pool" -UserPoolId: "us-east-1_xxxxxxxxx" UserPoolArn: "arn:aws:cognito-idp:us-east-1:999999999999:userpool/us-east-1_xxxxxxxxx" -UserPoolClientSecret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -DataFile: "./data.bolt" + +Game: Balance: 0.25 AutoSave: 5 StartingHealth: 10 -StartingEssence: 3 \ No newline at end of file +StartingEssence: 3 + +Logging: +ApplicationName: "dark_relics" +LogLevel: 20 +LogGroup: "/dark_relics" +LogStream: "application" \ No newline at end of file diff --git a/ssh_server/server.go b/ssh_server/server.go index 970fc62..faeb0c5 100644 --- a/ssh_server/server.go +++ b/ssh_server/server.go @@ -19,23 +19,23 @@ func NewServer(config core.Configuration) (*core.Server, error) { // Initialize the server with the configuration server := &core.Server{ - Port: config.Port, + Port: config.Server.Port, PlayerIndex: &core.Index{}, Config: config, StartTime: time.Now(), Rooms: make(map[int64]*core.Room), Characters: make(map[uuid.UUID]*core.Character), - Balance: config.Balance, - AutoSave: config.AutoSave, - Health: config.Health, - Essence: config.Essence, + Balance: config.Game.Balance, + AutoSave: config.Game.AutoSave, + Health: config.Game.StartingHealth, + Essence: config.Game.StartingEssence, } core.Logger.Info("Initializing database...") // Initialize the database var err error - server.Database, err = core.NewKeyPair(config.DataFile) + server.Database, err = core.NewKeyPair(config.Aws.Region) if err != nil { return nil, fmt.Errorf("failed to initialize database: %v", err) } @@ -74,34 +74,35 @@ func NewServer(config core.Configuration) (*core.Server, error) { } func loadConfiguration(configFile string) (core.Configuration, error) { - core.Logger.Info("Loading configuration", "config_file", configFile) - var config core.Configuration data, err := os.ReadFile(configFile) if err != nil { - return config, err + return config, fmt.Errorf("error reading config file: %w", err) } err = yaml.Unmarshal(data, &config) if err != nil { - return config, err + return config, fmt.Errorf("error unmarshalling config: %w", err) } return config, nil } func main() { - - core.Logger.Info("Starting server...") - - // Read configuration file configFile := flag.String("config", "config.yml", "Configuration file") flag.Parse() config, err := loadConfiguration(*configFile) if err != nil { - core.Logger.Error("Error loading configuration", "error", err) + fmt.Printf("Error loading configuration: %v\n", err) + return + } + + // Initialize logging + err = core.InitializeLogging(&config) + if err != nil { + fmt.Printf("Error initializing logging: %v\n", err) return } From 66457f4fc4092827e6a060e2d8ada0bc4b764394 Mon Sep 17 00:00:00 2001 From: "Jason E. Robinson" Date: Thu, 29 Aug 2024 02:13:35 -0400 Subject: [PATCH 13/16] Update Deployment --- cloudformation/cloudwatch.yml | 53 ++++++++++ scripts/deploy.py | 180 +++++++++++++++------------------ ssh_server/config.template.yml | 4 +- 3 files changed, 138 insertions(+), 99 deletions(-) create mode 100644 cloudformation/cloudwatch.yml diff --git a/cloudformation/cloudwatch.yml b/cloudformation/cloudwatch.yml new file mode 100644 index 0000000..b110f9d --- /dev/null +++ b/cloudformation/cloudwatch.yml @@ -0,0 +1,53 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'CloudFormation template for CloudWatch Log Group, Metrics, and IAM Policy' + +Parameters: + LogGroupName: + Type: String + Description: Name for the CloudWatch Log Group + Default: '/mud/application' + + MetricNamespace: + Type: String + Description: Namespace for CloudWatch Metrics + Default: 'MUD/Application' + +Resources: + MUDLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Ref LogGroupName + RetentionInDays: 30 + + MUDCloudWatchPolicy: + Type: AWS::IAM::ManagedPolicy + Properties: + Description: Allows writing to CloudWatch Logs and Metrics + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:PutLogEvents + Resource: !GetAtt MUDLogGroup.Arn + - Effect: Allow + Action: + - cloudwatch:PutMetricData + Resource: '*' + Condition: + StringEquals: + 'cloudwatch:namespace': !Ref MetricNamespace + +Outputs: + LogGroupName: + Description: Name of the created CloudWatch Log Group + Value: !Ref MUDLogGroup + + MetricNamespace: + Description: Namespace for CloudWatch Metrics + Value: !Ref MetricNamespace + + CloudWatchPolicyArn: + Description: ARN of the IAM Managed Policy for CloudWatch access + Value: !Ref MUDCloudWatchPolicy \ No newline at end of file diff --git a/scripts/deploy.py b/scripts/deploy.py index 2a581f0..9559617 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -1,67 +1,53 @@ -""" -Script for Deploying the AWS Components. - -This project is licensed under the Apache 2.0 License. See the LICENSE file for more details. -""" - import yaml - import boto3 +from botocore.exceptions import ClientError # Constants for stack names COGNITO_STACK_NAME = "MUD-Cognito-Stack" DYNAMO_STACK_NAME = "MUD-DynamoDB-Stack" CODEBUILD_STACK_NAME = "MUD-CodeBuild-Stack" +CLOUDWATCH_STACK_NAME = "MUD-CloudWatch-Stack" # Paths to the CloudFormation templates COGNITO_TEMPLATE_PATH = "../cloudformation/cognito.yml" DYNAMO_TEMPLATE_PATH = "../cloudformation/dynamo.yml" CODEBUILD_TEMPLATE_PATH = "../cloudformation/codebuild.yml" +CLOUDWATCH_TEMPLATE_PATH = "../cloudformation/cloudwatch.yml" # Configuration file path -CONFIG_PATH = "../mud/config.yml" +CONFIG_PATH = "../ssh_server/config.yml" - -def prompt_for_parameters(template_name) -> dict: - """ - Prompts the user for parameters based on the specified template name. - """ +def prompt_for_parameters(template_name): if template_name == "cognito": - parameters: dict = { + return { "UserPoolName": input("Enter the Name of the user pool [default: mud-user-pool]: ") or "mud-user-pool", "AppClientName": input("Enter the Name of the app client [default: mud-app-client]: ") or "mud-app-client", "CallbackURL": input("Enter the URL of the callback for the app client [default: https://localhost:3000/callback]: ") or "https://localhost:3000/callback", - "SignOutURL": input( - "Enter the URL of the sign-out page for the app client [default: https://localhost:3000/sign-out]: " - ) + "SignOutURL": input("Enter the URL of the sign-out page for the app client [default: https://localhost:3000/sign-out]: ") or "https://localhost:3000/sign-out", "ReplyEmailAddress": input("Enter the email address to send from: "), } elif template_name == "dynamo": - # DynamoDB template doesn't require any parameters - parameters = {} + return {} elif template_name == "codebuild": - parameters = { + return { "GitHubSourceRepo": input("Enter the GitHub repository URL for the source code: "), "S3BucketName": input("Enter the name of the S3 bucket where build artifacts will be stored: "), } - return parameters - + elif template_name == "cloudwatch": + return { + "LogGroupName": input("Enter the name for the CloudWatch Log Group [default: /mud/game-logs]: ") or "/mud/game-logs", + "RetentionInDays": input("Enter the number of days to retain logs [default: 30]: ") or "30", + } + return {} -def load_template(template_path) -> str: - """ - Loads a CloudFormation template from the specified path. - """ +def load_template(template_path): with open(template_path, "r", encoding="utf-8") as file: return file.read() - -def deploy_stack(client, stack_name, template_body, parameters) -> None: - """ - Deploy or update a CloudFormation stack with the given parameters. - """ - cf_parameters: list = [{"ParameterKey": k, "ParameterValue": v} for k, v in parameters.items()] +def deploy_stack(client, stack_name, template_body, parameters): + cf_parameters = [{"ParameterKey": k, "ParameterValue": v} for k, v in parameters.items()] try: if stack_exists(client, stack_name): print(f"Updating existing stack: {stack_name}") @@ -80,69 +66,67 @@ def deploy_stack(client, stack_name, template_body, parameters) -> None: Capabilities=["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"], ) wait_for_stack_completion(client, stack_name) - except client.exceptions.ClientError as err: + except ClientError as err: print(f"Error in stack operation: {err}") - -def stack_exists(client, stack_name) -> bool: - """ - Check if a CloudFormation stack exists. - """ +def stack_exists(client, stack_name): try: client.describe_stacks(StackName=stack_name) return True except client.exceptions.ClientError: return False - -def wait_for_stack_completion(client, stack_name) -> None: - """ - Wait for the CloudFormation stack to complete its operation. - """ - waiter = client.get_waiter("stack_create_complete") +def wait_for_stack_completion(client, stack_name): print(f"Waiting for stack {stack_name} to complete...") + waiter = client.get_waiter("stack_create_complete") waiter.wait(StackName=stack_name) print("Stack operation completed.") - -def get_stack_outputs(client, stack_name) -> dict: - """ - Retrieve the outputs of a CloudFormation stack. - """ +def get_stack_outputs(client, stack_name): stack = client.describe_stacks(StackName=stack_name) outputs = stack["Stacks"][0]["Outputs"] return {output["OutputKey"]: output["OutputValue"] for output in outputs} - -def start_codebuild_project(codebuild_client, project_name): - """ - Starts an AWS CodeBuild project. - """ - response = codebuild_client.start_build(projectName=project_name) - build_id = response["build"]["id"] - print(f"Started CodeBuild project: {project_name}, Build ID: {build_id}") - return build_id - - -def wait_for_codebuild_completion(codebuild_client, build_id): - """ - Waits for the specified CodeBuild project build to complete. - """ - print(f"Waiting for CodeBuild build {build_id} to complete...") - waiter = codebuild_client.get_waiter("build_completed") - waiter.wait(ids=[build_id]) - print("CodeBuild build completed.") - - -def update_configuration_file(config_updates) -> None: - """ - Updates the config.yml file based on the provided parameters. - """ +def update_configuration_file(config_updates): try: with open(CONFIG_PATH, "r", encoding="utf-8") as file: config = yaml.safe_load(file) or {} - config.update(config_updates) + # Update the configuration + if "Server" not in config: + config["Server"] = {"Port": 9050} + if "Aws" not in config: + config["Aws"] = {"Region": "us-east-1"} + if "Game" not in config: + config["Game"] = {"Balance": 0.25, "AutoSave": 5, "StartingHealth": 10, "StartingEssence": 3} + if "Logging" not in config: + config["Logging"] = {"ApplicationName": "mud", "LogLevel": 20} + if "Cognito" not in config: + config["Cognito"] = {} + + # Update the configuration structure + config.setdefault("Server", {})["Port"] = config.get("Port", 9050) + config.setdefault("Aws", {})["Region"] = config.get("Region", "us-east-1") + config.setdefault("Cognito", {}).update(config_updates.get("Cognito", {})) + config.setdefault("Game", {}).update({ + "Balance": config.get("Balance", 0.25), + "AutoSave": config.get("AutoSave", 5), + "StartingHealth": config.get("StartingHealth", 10), + "StartingEssence": config.get("StartingEssence", 3), + }) + config.setdefault("Logging", {}).update({ + "ApplicationName": "mud", + "LogLevel": 20, + "LogGroup": config_updates.get("LogGroupName", "/mud"), + "LogStream": "application", + }) + config["Cognito"].update({ + "UserPoolId": config_updates.get("UserPoolId", ""), + "UserPoolClientSecret": config_updates.get("UserPoolClientSecret", ""), + "UserPoolClientId": config_updates.get("UserPoolClientId", ""), + "UserPoolDomain": config_updates.get("UserPoolDomain", ""), + "UserPoolArn": config_updates.get("UserPoolArn", ""), + }) with open(CONFIG_PATH, "w", encoding="utf-8") as file: yaml.dump(config, file, default_flow_style=False) @@ -151,41 +135,43 @@ def update_configuration_file(config_updates) -> None: except Exception as err: print(f"An error occurred while updating configuration file: {err}") - -def main() -> None: - # Initialize Boto3 clients +def main(): cloudformation_client = boto3.client("cloudformation") - codebuild_client = boto3.client("codebuild") # Deploy Cognito stack - cognito_parameters: dict = prompt_for_parameters("cognito") - cognito_template: str = load_template(COGNITO_TEMPLATE_PATH) + cognito_parameters = prompt_for_parameters("cognito") + cognito_template = load_template(COGNITO_TEMPLATE_PATH) deploy_stack(cloudformation_client, COGNITO_STACK_NAME, cognito_template, cognito_parameters) - cognito_outputs: dict = get_stack_outputs(cloudformation_client, COGNITO_STACK_NAME) + cognito_outputs = get_stack_outputs(cloudformation_client, COGNITO_STACK_NAME) # Deploy DynamoDB stack - dynamo_parameters: dict = prompt_for_parameters("dynamo") - dynamo_template: str = load_template(DYNAMO_TEMPLATE_PATH) + dynamo_parameters = prompt_for_parameters("dynamo") + dynamo_template = load_template(DYNAMO_TEMPLATE_PATH) deploy_stack(cloudformation_client, DYNAMO_STACK_NAME, dynamo_template, dynamo_parameters) - dynamo_outputs: dict = get_stack_outputs(cloudformation_client, DYNAMO_STACK_NAME) + dynamo_outputs = get_stack_outputs(cloudformation_client, DYNAMO_STACK_NAME) # Deploy CodeBuild stack - codebuild_parameters: dict = prompt_for_parameters("codebuild") - codebuild_parameters.update(cognito_outputs) # Add Cognito outputs as parameters for CodeBuild stack - codebuild_parameters.update(dynamo_outputs) # Add DynamoDB outputs as parameters for CodeBuild stack - codebuild_template: str = load_template(CODEBUILD_TEMPLATE_PATH) + codebuild_parameters = prompt_for_parameters("codebuild") + codebuild_parameters.update(cognito_outputs) + codebuild_parameters.update(dynamo_outputs) + codebuild_template = load_template(CODEBUILD_TEMPLATE_PATH) deploy_stack(cloudformation_client, CODEBUILD_STACK_NAME, codebuild_template, codebuild_parameters) - codebuild_outputs: dict = get_stack_outputs(cloudformation_client, CODEBUILD_STACK_NAME) + codebuild_outputs = get_stack_outputs(cloudformation_client, CODEBUILD_STACK_NAME) - # Start CodeBuild project - codebuild_project_name = codebuild_outputs["CodeBuildProjectName"] - build_id = start_codebuild_project(codebuild_client, codebuild_project_name) - wait_for_codebuild_completion(codebuild_client, build_id) + # Deploy CloudWatch stack + cloudwatch_parameters = prompt_for_parameters("cloudwatch") + cloudwatch_template = load_template(CLOUDWATCH_TEMPLATE_PATH) + deploy_stack(cloudformation_client, CLOUDWATCH_STACK_NAME, cloudwatch_template, cloudwatch_parameters) + cloudwatch_outputs = get_stack_outputs(cloudformation_client, CLOUDWATCH_STACK_NAME) # Update configuration file with outputs from all stacks - config_updates: dict = {**cognito_outputs, **dynamo_outputs, **codebuild_outputs} + config_updates = { + "Cognito": cognito_outputs, + "Dynamo": dynamo_outputs, + "CodeBuild": codebuild_outputs, + "CloudWatch": cloudwatch_outputs, + } update_configuration_file(config_updates) - if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/ssh_server/config.template.yml b/ssh_server/config.template.yml index fd19592..8a1323c 100644 --- a/ssh_server/config.template.yml +++ b/ssh_server/config.template.yml @@ -18,7 +18,7 @@ StartingHealth: 10 StartingEssence: 3 Logging: -ApplicationName: "dark_relics" +ApplicationName: "mud" LogLevel: 20 -LogGroup: "/dark_relics" +LogGroup: "/mud" LogStream: "application" \ No newline at end of file From 796da30dd990a577009136b36da3bb255993c3a9 Mon Sep 17 00:00:00 2001 From: "Jason E. Robinson" Date: Thu, 29 Aug 2024 02:19:06 -0400 Subject: [PATCH 14/16] Add Cloudwatch Metrics --- core/types.go | 11 +++++++---- scripts/deploy.py | 7 +++++-- ssh_server/config.template.yml | 3 ++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/core/types.go b/core/types.go index 4c03fc9..04eee9c 100644 --- a/core/types.go +++ b/core/types.go @@ -39,10 +39,13 @@ type Configuration struct { StartingHealth uint16 `yaml:"StartingHealth"` } `yaml:"Game"` Logging struct { - ApplicationName string `yaml:"ApplicationName"` - LogLevel int `yaml:"LogLevel"` - LogGroup string `yaml:"LogGroup"` - LogStream string `yaml:"LogStream"` + Logging struct { + ApplicationName string `yaml:"ApplicationName"` + LogLevel int `yaml:"LogLevel"` + LogGroup string `yaml:"LogGroup"` + LogStream string `yaml:"LogStream"` + MetricNamespace string `yaml:"MetricNamespace"` // Add this line + } `yaml:"Logging"` } `yaml:"Logging"` } diff --git a/scripts/deploy.py b/scripts/deploy.py index 9559617..0f99430 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -39,6 +39,7 @@ def prompt_for_parameters(template_name): return { "LogGroupName": input("Enter the name for the CloudWatch Log Group [default: /mud/game-logs]: ") or "/mud/game-logs", "RetentionInDays": input("Enter the number of days to retain logs [default: 30]: ") or "30", + "MetricNamespace": input("Enter the namespace for CloudWatch Metrics [default: MUD/Application]: ") or "MUD/Application", } return {} @@ -117,8 +118,9 @@ def update_configuration_file(config_updates): config.setdefault("Logging", {}).update({ "ApplicationName": "mud", "LogLevel": 20, - "LogGroup": config_updates.get("LogGroupName", "/mud"), + "LogGroup": config_updates.get("CloudWatch", {}).get("LogGroupName", "/mud"), "LogStream": "application", + "MetricNamespace": config_updates.get("CloudWatch", {}).get("MetricNamespace", "MUD/Application"), }) config["Cognito"].update({ "UserPoolId": config_updates.get("UserPoolId", ""), @@ -158,12 +160,13 @@ def main(): deploy_stack(cloudformation_client, CODEBUILD_STACK_NAME, codebuild_template, codebuild_parameters) codebuild_outputs = get_stack_outputs(cloudformation_client, CODEBUILD_STACK_NAME) - # Deploy CloudWatch stack + # Deploy CloudWatch stack cloudwatch_parameters = prompt_for_parameters("cloudwatch") cloudwatch_template = load_template(CLOUDWATCH_TEMPLATE_PATH) deploy_stack(cloudformation_client, CLOUDWATCH_STACK_NAME, cloudwatch_template, cloudwatch_parameters) cloudwatch_outputs = get_stack_outputs(cloudformation_client, CLOUDWATCH_STACK_NAME) + # Update configuration file with outputs from all stacks config_updates = { "Cognito": cognito_outputs, diff --git a/ssh_server/config.template.yml b/ssh_server/config.template.yml index 8a1323c..9eaf8fb 100644 --- a/ssh_server/config.template.yml +++ b/ssh_server/config.template.yml @@ -21,4 +21,5 @@ Logging: ApplicationName: "mud" LogLevel: 20 LogGroup: "/mud" -LogStream: "application" \ No newline at end of file +LogStream: "application" +MetricNamespace: "MUD/Application" \ No newline at end of file From 6458de8619ac400bfa0aa51e81f54752aa347409 Mon Sep 17 00:00:00 2001 From: "Jason E. Robinson" Date: Thu, 29 Aug 2024 02:39:42 -0400 Subject: [PATCH 15/16] Collect Metrics --- core/go.mod | 1 + core/logging.go | 56 ++++++++++++++++++++++++++++++++++++++++++-- core/types.go | 14 +++++------ ssh_server/go.mod | 1 + ssh_server/server.go | 9 +++++++ 5 files changed, 72 insertions(+), 9 deletions(-) diff --git a/core/go.mod b/core/go.mod index 4a412cc..3379b4e 100644 --- a/core/go.mod +++ b/core/go.mod @@ -6,6 +6,7 @@ require ( github.com/aws/aws-sdk-go v1.54.15 github.com/aws/aws-sdk-go-v2 v1.30.4 github.com/aws/aws-sdk-go-v2/config v1.27.31 + github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.40.5 github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.37.5 github.com/aws/aws-xray-sdk-go v1.8.4 github.com/google/uuid v1.6.0 diff --git a/core/logging.go b/core/logging.go index b116467..78de3ce 100644 --- a/core/logging.go +++ b/core/logging.go @@ -5,12 +5,15 @@ import ( "fmt" "log/slog" "os" + "runtime" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types" "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" - "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + cwlogtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" "github.com/aws/aws-xray-sdk-go/xray" ) @@ -125,7 +128,7 @@ func (h *CloudWatchHandler) Handle(ctx context.Context, r slog.Record) error { _, err := h.client.PutLogEvents(ctx, &cloudwatchlogs.PutLogEventsInput{ LogGroupName: aws.String(h.logGroup), LogStreamName: aws.String(h.logStream), - LogEvents: []types.InputLogEvent{ + LogEvents: []cwlogtypes.InputLogEvent{ { Message: aws.String(message), Timestamp: aws.Int64(time.Now().UnixNano() / int64(time.Millisecond)), @@ -185,3 +188,52 @@ func (h *MultiHandler) WithGroup(name string) slog.Handler { } return NewMultiHandler(newHandlers...) } + +func SendMetrics(s *Server, interval time.Duration) error { + cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(s.Config.Aws.Region)) + if err != nil { + return fmt.Errorf("failed to load AWS SDK config: %w", err) + } + + client := cloudwatch.NewFromConfig(cfg) + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + var m runtime.MemStats + runtime.ReadMemStats(&m) + + playerCount := float64(len(s.Characters)) + memoryUsageMB := float64(m.Alloc) / 1024 / 1024 + + _, err := client.PutMetricData(context.Background(), &cloudwatch.PutMetricDataInput{ + Namespace: aws.String(s.Config.Logging.MetricNamespace), + MetricData: []types.MetricDatum{ + { + MetricName: aws.String("PlayerCount"), + Unit: types.StandardUnitCount, + Value: aws.Float64(playerCount), + }, + { + MetricName: aws.String("MemoryUsage"), + Unit: types.StandardUnitMegabytes, + Value: aws.Float64(memoryUsageMB), + }, + }, + }) + + if err != nil { + Logger.Error("Failed to send metrics to CloudWatch", "error", err) + } else { + Logger.Info("Sent metrics to CloudWatch", "playerCount", playerCount, "memoryUsageMB", memoryUsageMB) + } + + case <-s.Context.Done(): + Logger.Info("Stopping metrics collection due to context cancellation") + return nil + } + } +} diff --git a/core/types.go b/core/types.go index 04eee9c..fd487db 100644 --- a/core/types.go +++ b/core/types.go @@ -1,6 +1,7 @@ package core import ( + "context" "log/slog" "net" "sync" @@ -39,13 +40,11 @@ type Configuration struct { StartingHealth uint16 `yaml:"StartingHealth"` } `yaml:"Game"` Logging struct { - Logging struct { - ApplicationName string `yaml:"ApplicationName"` - LogLevel int `yaml:"LogLevel"` - LogGroup string `yaml:"LogGroup"` - LogStream string `yaml:"LogStream"` - MetricNamespace string `yaml:"MetricNamespace"` // Add this line - } `yaml:"Logging"` + ApplicationName string `yaml:"ApplicationName"` + LogLevel int `yaml:"LogLevel"` + LogGroup string `yaml:"LogGroup"` + LogStream string `yaml:"LogStream"` + MetricNamespace string `yaml:"MetricNamespace"` } `yaml:"Logging"` } @@ -73,6 +72,7 @@ type Server struct { Essence uint16 Items map[uint64]*Item ItemPrototypes map[uint64]*Item + Context context.Context Mutex sync.Mutex } diff --git a/ssh_server/go.mod b/ssh_server/go.mod index cc98ba9..92f2e17 100644 --- a/ssh_server/go.mod +++ b/ssh_server/go.mod @@ -22,6 +22,7 @@ require ( github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.40.5 // indirect github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.37.5 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect diff --git a/ssh_server/server.go b/ssh_server/server.go index faeb0c5..328780b 100644 --- a/ssh_server/server.go +++ b/ssh_server/server.go @@ -1,6 +1,7 @@ package main import ( + "context" "flag" "fmt" "net" @@ -22,6 +23,7 @@ func NewServer(config core.Configuration) (*core.Server, error) { Port: config.Server.Port, PlayerIndex: &core.Index{}, Config: config, + Context: context.Background(), StartTime: time.Now(), Rooms: make(map[int64]*core.Room), Characters: make(map[uuid.UUID]*core.Character), @@ -112,6 +114,13 @@ func main() { return } + // Start sending metrics + go func() { + if err := core.SendMetrics(server, 1*time.Minute); err != nil { + core.Logger.Error("Error in SendMetrics", "error", err) + } + }() + // Start the auto-save routine go core.AutoSave(server) From 2816485f56b45fc4f116a3dfdf876adf08970360 Mon Sep 17 00:00:00 2001 From: "Jason E. Robinson" Date: Thu, 29 Aug 2024 02:47:51 -0400 Subject: [PATCH 16/16] Update Readme --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index af28b8f..c70f847 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ The current implementation includes an SSH server for secure authentication and - [x] Implement a database for the game. - [x] Implement a character creation system. - [x] Implement a text colorization system. -- [ ] Add Cloudwatch Logs and Metrics. +- [x] Add Cloudwatch Logs and Metrics. +- [x] Build an interactive password change system. - [ ] Construct the item system. - [ ] Develop game mechanics. - [ ] Design an ecenomic framework. @@ -22,10 +23,12 @@ The current implementation includes an SSH server for secure authentication and - [ ] Build a direct messaging system. - [ ] Develop more complex Non-Player Characters (NPCs) with basic AI. - [ ] Implement a dynamic content updating system. -- [ ] Build an interactive password change system. - [ ] Implement a player-to-player trading system. -- [ ] Develop more complex Non-Player Characters (NPCs) with basic AI. -- [ ] Build an interactive password change system. +- [ ] Create a crafting system for items. +- [ ] Develop a weather and time system. +- [ ] Implement a party system for cooperative gameplay. +- [ ] Implement a magic system. + ## TODO @@ -42,18 +45,19 @@ The current implementation includes an SSH server for secure authentication and - [x] Add wear item command. - [x] Add remove item command. - [x] Add examine item command. +- [x] Implement Persistent Logging. +- [x] Load item prototypes at start. +- [x] Create function for creating items from prototypes. +- [x] Ensure that a message is passed when a characters is added to the game. - [ ] Add a Message of the Day (MOTD) command. -- [ ] Implement Persistent Logging. - [ ] Add the ability to delete characters. - [ ] Add the ability to delete accounts. - [ ] Implement an obscenity filter. - [ ] Validate graph of loaded rooms and exits. -- [ ] Load item prototypes at start. -- [ ] Create function for creating items from prototypes. - [ ] Add look at item command. - [ ] Improve the say commands. - [ ] Improve the input filters -- [ ] Ensure that a message is passed when a characters is added to the game. + ## Project Overview