Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Logging] Implement Player Event Logging system #2833

Merged
merged 117 commits into from
Feb 13, 2023

Conversation

Akkadius
Copy link
Member

@Akkadius Akkadius commented Feb 5, 2023

Player Event Logging

For the longest time, player audit logging or event logging of any type has been sub-par. It is difficult to triage when a player lost an item because we have no adequate out of the box logging. It is difficult to know get visibility into player activity to understand how things are being used or misused

Goals

  • As a server operator, be able to see a lineage of events of a player
  • As a server operator, be able to see events by type
  • As a server operator, be able to see events by zone
  • As a server operator, be able to see record event specific data in a non-structured way but be able to query it in a flexible semi-structured way
  • Have player events be recorded in a highly robust, high performing, low overhead way
  • Have player events be turned on and off by type
  • Have player events automatically truncate by type so data retention can be tweaked and tuned to the server operators unique needs
  • Have the player event creation pipeline be easy to extend and adapt from a server operator perspective
  • Be able to process events within World or within Queryserv as a dedicated events processor on a dedicated server if the operator so chooses

Features

  • Roughly 40 implemented player events
  • One single event table, which heavily simplifies creating events and querying events in a lineage along with other events. Querying the JSON event data can get interesting when running reporting but it is all flexible enough.
  • Highly robust event pipeline where World (Default) or QueryServ can process events in batch. This allows zone processes to very minimally and lightly process event creation and the event processor can flush events at a configurable interval (Default 5s) Rule "Logging:BatchPlayerEventProcessIntervalSeconds". The event processor runs in its own dedicated thread and uses a mutex lock to interact with the queue that gets mutated between the main thread and the processing thread
  • Highly performant event creation. Events do not incur overhead at the Zone level if they are not enabled
  • Highly readable code. All player events are represented as structs which are serialized into JSON when the event reaches the database
  • Discord Webhook messages unique to each event are supported and can be turned on per-event and routed to Channels of your choosing, player facing or admin facing. Consider that some events could be too noisy. Discord webhook messages are routed by Universal Chat Service (Server)
  • New player events are automatically injected when they are added by server developers
  • Deprecated player events are automatically removed when marked as deprecated or unimplemented
  • Data retention policies. Every event is trimmed hourly at given data retention intervals based on the age of the event. Most events defaults are to be stored for a week. Configurable by the server operator.

image

image

image

Deprecates

  • The old hackers table is now deprecated. These are now their own POSSIBLE_HACK events since all of this was player contextual logging to begin with
  • The old eventlog table is now deprecated. All events covered in here are also covered in the player event logging system and far more

Table Schema(s)

player_event_logs

+-----------------+--------------+------+-----+---------+----------------+
| Field           | Type         | Null | Key | Default | Extra          |
+-----------------+--------------+------+-----+---------+----------------+
| id              | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| account_id      | bigint(20)   | YES  |     | NULL    |                |
| character_id    | bigint(20)   | YES  | MUL | NULL    |                |
| zone_id         | int(11)      | YES  | MUL | NULL    |                |
| instance_id     | int(11)      | YES  |     | NULL    |                |
| x               | float        | YES  |     | NULL    |                |
| y               | float        | YES  |     | NULL    |                |
| z               | float        | YES  |     | NULL    |                |
| heading         | float        | YES  |     | NULL    |                |
| event_type_id   | int(11)      | YES  | MUL | NULL    |                |
| event_type_name | varchar(255) | YES  |     | NULL    |                |
| event_data      | longtext     | YES  |     | NULL    |                |
| created_at      | datetime     | YES  | MUL | NULL    |                |
+-----------------+--------------+------+-----+---------+----------------+

Indexes

KEY `event_created_at` (`event_type_id`,`created_at`),
KEY `zone_id` (`zone_id`),
KEY `character_id` (`character_id`,`zone_id`) USING BTREE,
KEY `created_at` (`created_at`)

player_event_log_settings

+--------------------+--------------+------+-----+---------+-------+
| Field              | Type         | Null | Key | Default | Extra |
+--------------------+--------------+------+-----+---------+-------+
| id                 | bigint(20)   | NO   | PRI | NULL    |       |
| event_name         | varchar(100) | YES  |     | NULL    |       |
| event_enabled      | tinyint(1)   | YES  |     | NULL    |       |
| retention_days     | int(11)      | YES  |     | 0       |       |
| discord_webhook_id | int(11)      | YES  |     | 0       |       |
+--------------------+--------------+------+-----+---------+-------+

Currently Handled Events

These are the events currently supported at the time of this writing. It's very likely more will be added later on and you can find the most recent events in ./common/player_events.h

1	GM Command
2	Zoning
3	AA Gain
4	AA Purchase
5	Forage Success
6	Forage Failure
7	Fish Success
8	Fish Failure
9	Item Destroy
10	Went Online
11	Went Offline
12	Level Gain
13	Level Loss
14	Loot Item
15	Merchant Purchase
16	Merchant Sell
21	Groundspawn Pickup
22	NPC Handin
23	Skill Up
24	Task Accept
25	Task Update
26	Task Complete
27	Trade
29	Say
30	Rez Accepted
31	Death
32	Combine Failure
33	Combine Success
34	Dropped Item
35	Split Money
38	Trader Purchase
39	Trader Sell
42	Discover Item
43	Possible Hack
44	Killed NPC
45	Killed Named NPC
46	Killed Raid NPC

Credits

The system was designed by Akkadius and co-designed by Kinglykrab. Aead and Kinglykrab also helped with the implementation of several events and many Discord formatters. Thank you both for your help <3

Quest Hand-In Events

In order to capture player hand-ins, you will need special plugin capture code that is included in PEQ Quest PR https://github.com/ProjectEQ/projecteqquests/pull/1372/files

Hand-ins may need further work to capture other code paths

Player Event Log Explorer (Spire)

A system like this is best served when it can be seen as to what you can do with it with the right tooling. Here is examples of key explorer features. The release of this will likely shortly trail when the server feature is released.

Rich Data Viewing

Spire translates event data into viewable NPC, Item, Spell etc. cards to have contextual insight into the events and what they contain.

image

image

Event Raw

Any event you can see the richly formatted event or you can inspect the raw event data

event-raw

Flexible Filtering

You can filter by event type, zone_id, character_id which are top level filters always available and indexed regardless of event type

event-filter

Advanced Event Data Filtering

You can filter by JSON event data

event-data-filter

You can even filter by deeply nested data. For example if you're trying to search for a certain item that was traded by doing a nested wildcard search

event-deep-filter

Developer Docs

Where's the Code?

Most of the code managing player events is located @

./common/events
├── player_event_discord_formatter.cpp
├── player_event_discord_formatter.h
├── player_event_logs.cpp
├── player_event_logs.h
└── player_events.h

How Do I Add a New Event?

First you need an event defined. You do that in player_events.h in both EventType and EventName

You'll also need a struct representing the event data that you want to exist in the database, that is also defined in player_events.h

It is important to implement the cereal serialization fields because that is what is used to automatically handle transporting the data from zone to world and ultimately into JSON format in the database. Much of this is handled automatically for the developer.

struct SayEvent {
	std::string message;
	std::string target;

	// cereal
	template<class Archive>
	void serialize(Archive &ar)
	{
		ar(
			CEREAL_NVP(message),
			CEREAL_NVP(target)
		);
	}
};

You will also need to find the place in the code that you would like to record the event. There are two convenient macros that wrap a lot of other logic to keep things performant

RecordPlayerEventLog(event_type, event_data) // if client object is implied
RecordPlayerEventLogWithClient(c, event_type, event_data) // if you need to pass in the client object

For the message to properly show up in Discord, it will need a proper formatter to format the event data in a visually pleasing way.

Implement a formatter in ./common/player_event_discord_formatter.cpp that looks similar to

std::string PlayerEventDiscordFormatter::FormatDiscoverItemEvent(  
   const PlayerEvent::PlayerEventContainer &c,  
   const PlayerEvent::DiscoverItemEvent &e  
)

You can copy other formatter signatures fairly easy to understand what is happening. This will need to be hooked up in the formatter switch in PlayerEventLogs::GetDiscordPayloadFromEvent in common/events/player_event_logs.cpp

Example Diff

Below are two example of an event being implemented / added. These only contain what is required to add events, not format Discord messages

More Complex
diff --git a/common/events/player_events.h b/common/events/player_events.h  
index af0198ab9..6c988bb41 100644  
--- a/common/events/player_events.h  
+++ b/common/events/player_events.h  
@@ -42,7 +42,7 @@ namespace PlayerEvent {  
      COMBINE_FAILURE,  
      COMBINE_SUCCESS,  
      DROPPED_ITEM,  
-     SPLIT_MONEY, // unimplemented  
+     SPLIT_MONEY,  
      DZ_JOIN, // unimplemented  
      DZ_LEAVE, // unimplemented  
      TRADER_PURCHASE, // unimplemented  
@@ -574,6 +574,27 @@ namespace PlayerEvent {  
         );  
      }  
   };  
+  
+  struct SplitMoneyEvent {  
+     uint32 copper;  
+     uint32 silver;  
+     uint32 gold;  
+     uint32 platinum;  
+     uint64 player_money_balance;  
+  
+     // cereal  
+     template<class Archive>  
+     void serialize(Archive &ar)  
+     {  
+        ar(  
+           CEREAL_NVP(copper),  
+           CEREAL_NVP(silver),  
+           CEREAL_NVP(gold),  
+           CEREAL_NVP(platinum),  
+           CEREAL_NVP(player_money_balance)  
+        );  
+     }  
+  };  
 }  
   
 #endif //EQEMU_PLAYER_EVENTS_H  
diff --git a/zone/groups.cpp b/zone/groups.cpp  
index 910d5b21c..01d7c58bb 100644  
--- a/zone/groups.cpp  
+++ b/zone/groups.cpp  
@@ -26,6 +26,7 @@  
 #include "../common/strings.h"  
 #include "worldserver.h"  
 #include "string_ids.h"  
+#include "../common/events/player_event_logs.h"  
   
 extern EntityList entity_list;  
 extern WorldServer worldserver;  
@@ -177,6 +178,18 @@ void Group::SplitMoney(uint32 copper, uint32 silver, uint32 gold, uint32 platinu  
            true  
         );  
   
+        if (player_event_logs.IsEventEnabled(PlayerEvent::SPLIT_MONEY)) {  
+           auto e = PlayerEvent::SplitMoneyEvent{  
+              .copper = copper_split,  
+              .silver = silver_split,  
+              .gold = gold_split,  
+              .platinum = platinum_split,  
+              .player_money_balance = members[i]->CastToClient()->GetCarriedMoney(),  
+           };  
+  
+           RecordPlayerEventLogWithClient(members[i]->CastToClient(), PlayerEvent::SPLIT_MONEY, e);  
+        }  
+  
         members[i]->CastToClient()->MessageString(  
            Chat::MoneySplit,  
            YOU_RECEIVE_AS_SPLIT,  
diff --git a/zone/raids.cpp b/zone/raids.cpp  
index b27cae0e0..5fbc543c1 100644  
--- a/zone/raids.cpp  
+++ b/zone/raids.cpp  
@@ -17,6 +17,7 @@  
 */  
   
 #include "../common/strings.h"  
+#include "../common/events/player_event_logs.h"  
   
 #include "client.h"  
 #include "entity.h"  
@@ -814,6 +815,19 @@ void Raid::SplitMoney(uint32 gid, uint32 copper, uint32 silver, uint32 gold, uin  
            true  
         );  
   
+        if (player_event_logs.IsEventEnabled(PlayerEvent::SPLIT_MONEY)) {  
+           auto e = PlayerEvent::SplitMoneyEvent{  
+              .copper = copper_split,  
+              .silver = silver_split,  
+              .gold = gold_split,  
+              .platinum = platinum_split,  
+              .player_money_balance = members[i].member->GetCarriedMoney(),  
+           };  
+  
+           RecordPlayerEventLogWithClient(members[i].member, PlayerEvent::SPLIT_MONEY, e);  
+        }  
+  
+  
         members[i].member->MessageString(  
            Chat::MoneySplit,  
            YOU_RECEIVE_AS_SPLIT,

Simpler

diff --git a/common/events/player_events.h b/common/events/player_events.h  
index d9c3a98d4..07ee5aee5 100644  
--- a/common/events/player_events.h  
+++ b/common/events/player_events.h  
@@ -17,8 +17,8 @@ namespace PlayerEvent {  
      FISH_SUCCESS,  
      FISH_FAILURE,  
      ITEM_DESTROY,  
-     WENT_ONLINE, // unimplemented  
-     WENT_OFFLINE, // unimplemented  
+     WENT_ONLINE,  
+     WENT_OFFLINE,  
      LEVEL_GAIN, // unimplemented  
      LEVEL_LOSS, // unimplemented  
      LOOT_ITEM, // unimplemented  
diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp  
index ae75922c5..fbf7eca3b 100644  
--- a/zone/client_packet.cpp  
+++ b/zone/client_packet.cpp  
@@ -784,6 +784,7 @@ void Client::CompleteConnect()  
   /* This sub event is for if a player logs in for the first time since entering world. */  
   if (firstlogon == 1) {  
      parse->EventPlayer(EVENT_CONNECT, this, "", 0);  
+     RecordPlayerEventLog(PlayerEvent::WENT_ONLINE, PlayerEvent::EmptyEvent{});  
      /* QS: PlayerLogConnectDisconnect */  
      if (RuleB(QueryServ, PlayerLogConnectDisconnect)) {  
         std::string event_desc = StringFormat("Connect :: Logged into zoneid:%i instid:%i", GetZoneID(), GetInstanceID());  
diff --git a/zone/client_process.cpp b/zone/client_process.cpp  
index c6ff4e02f..2c5df69e8 100644  
--- a/zone/client_process.cpp  
+++ b/zone/client_process.cpp  
@@ -55,6 +55,7 @@  
 #include "zone.h"  
 #include "zonedb.h"  
 #include "../common/zone_store.h"  
+#include "../common/events/player_event_logs.h"  
   
 extern QueryServ* QServ;  
 extern Zone* zone;  
@@ -184,6 +185,7 @@ bool Client::Process() {  
         SetDynamicZoneMemberStatus(DynamicZoneMemberStatus::Offline);  
   
         parse->EventPlayer(EVENT_DISCONNECT, this, "", 0);  
+        RecordPlayerEventLog(PlayerEvent::WENT_OFFLINE, PlayerEvent::EmptyEvent{});  
   
         return false; //delete client  
      }  
@@ -693,6 +695,7 @@ void Client::OnDisconnect(bool hard_disconnect) {  
         MyRaid->MemberZoned(this);  
   
      parse->EventPlayer(EVENT_DISCONNECT, this, "", 0);  
+     RecordPlayerEventLog(PlayerEvent::WENT_OFFLINE, PlayerEvent::EmptyEvent{});  
   
      /* QS: PlayerLogConnectDisconnect */  
      if (RuleB(QueryServ, PlayerLogConnectDisconnect)){

Default Event Settings

Default event settings are defined in PlayerEventLogs::SetSettingsDefaults

void PlayerEventLogs::SetSettingsDefaults()  
{  
   m_settings[PlayerEvent::GM_COMMAND].event_enabled = 1;  
   m_settings[PlayerEvent::ZONING].event_enabled = 1;  
   m_settings[PlayerEvent::AA_GAIN].event_enabled = 1;  
   m_settings[PlayerEvent::AA_PURCHASE].event_enabled = 1;
   // ...truncated

This determines if the event is enabled by default and sets the defaults before it gets injected into the operators database automatically on next code update.

@Akkadius
Copy link
Member Author

Akkadius commented Feb 5, 2023

Please do not merge this. I will merge it after it's had review, testing, feedback and related branches are ready to go

zone/lua_general.cpp Outdated Show resolved Hide resolved
@Kinglykrab
Copy link
Contributor

Looks good being I already tested a lot of this functionality. Just curious how the repositories for eventlog and hackers work since they're pointed to the new player_event_logs table, are those manual repositories, or how does that work? Are they even necessary or are they just there so it's more understandable?

@Akkadius
Copy link
Member Author

Akkadius commented Feb 7, 2023

Looks good being I already tested a lot of this functionality. Just curious how the repositories for eventlog and hackers work since they're pointed to the new player_event_logs table, are those manual repositories, or how does that work? Are they even necessary or are they just there so it's more understandable?

Answered in DM; think this was a misunderstanding

@Akkadius Akkadius force-pushed the akkadius/player-event-logging branch from 88178dc to d1b9f02 Compare February 7, 2023 07:53
@Akkadius Akkadius force-pushed the akkadius/player-event-logging branch from d1b9f02 to c9ba95f Compare February 13, 2023 03:08
@Akkadius Akkadius merged commit d9f545a into master Feb 13, 2023
@Akkadius Akkadius deleted the akkadius/player-event-logging branch February 13, 2023 03:31
@Akkadius Akkadius mentioned this pull request Feb 16, 2023
joligario added a commit to ProjectEQ/peqphpeditor that referenced this pull request Feb 20, 2023
catapultam-habeo pushed a commit to catapultam-habeo/pyrelight-server that referenced this pull request Mar 27, 2023
* Plumbing

* Batch processing in world

* Cleanup

* Cleanup

* Update player_event_logs.cpp

* Add player zoning event

* Use generics

* Comments

* Add events

* Add more events

* AA_GAIN, AA_PURCHASE, FORAGE_SUCCESS, FORAGE_FAILURE

* FISH_SUCCESS, FISH_FAILURE, ITEM_DESTROY

* Add charges to ITEM_DESTROY

* WENT_ONLINE, WENT_OFFLINE

* LEVEL_GAIN, LEVEL_LOSS

* LOOT_ITEM

* MERCHANT_PURCHASE

* MERCHANT_SELL

* SKILL_UP

* Add events

* Add more events

* TASK_ACCEPT, TASK_COMPLETE, and TASK_UPDATE

* GROUNDSPAWN_PICKUP

* SAY

* REZ_ACCEPTED

* COMBINE_FAILURE and COMBINE_SUCCESS

* DROPPED_ITEM

* DEATH

* SPLIT_MONEY

* TRADER_PURCHASE and TRADER_SELL

* DISCOVER_ITEM

* Convert GM_COMMAND to use new macro

* Convert ZONING event to use macro

* Revert some code changes

* Revert "Revert some code changes"

This reverts commit d53682f.

* Add cereal generation support to repositories

* TRADE

* Formatting

* Cleanup

* Relocate discord_manager to discord folder

* Discord sending plumbing

* Rename UCS's Database class to UCSDatabase to be more specific and not collide with base Database class for repository usage

* More discord sending plumbing

* More discord message formatting work

* More discord formatting work

* Discord formatting of events

* Format WENT_ONLINE, WENT_OFFLINE

* Add merchant purchase event

* Handle Discord MERCHANT_SELL formatter

* Update player_event_discord_formatter.cpp

* Tweaks

* Implement retention truncation

* Put mutex locking on batch queue, put processor on its own thread

* Process on initial bootup

* Implement optional QS processing, implement keepalive from world to QS

* Reload player event settings when logs are reloaded in game

* Set settings defaults

* Update player_event_logs.cpp

* Update player_event_logs.cpp

* Set retention days on boot

* Update player_event_logs.cpp

* Player Handin Event Testing.

Testing player handin stuff.

* Cleanup.

* Finish NPC Handin.

* set a reference to the client inside of the trade object as well for plugins to process

* Fix for windows _inline

* Bump to cpp20 default, ignore excessive warnings on windows

* Bump FMT to 6.1.2 for cpp20 compat and swap fmt::join for Strings::Join

* Windows compile fixes

* Update CMakeLists.txt

* Update CMakeLists.txt

* Update CMakeLists.txt

* Create 2022_12_19_player_events_tables.sql

* [Formatters] Work on Discord Formatters

* Handin money.

* Format header

* [Formatters] Work on Discord Formatters

* Format

* Format

* [Formatters] More Formatter work, need to test further.

* [Formatters] More Work on Formatters.

* Add missing #endif

* [Formatters] Work on Formatters, fix Bot formatting in ^create help

* NPC Handin Discord Formatter

* Update player_event_logs.cpp

* Discover Item Discord Formatter

* Dropped Item Discord Formatter

* Split Money Discord Formatter

* Trader Discord Formatters

* Cleanup.

* Trade Event Discord Formatter Groundwork

* SAY don't record GM commands

* GM_Command don't record #help

* Update player_event_logs.cpp

* Fill in more event data

* Post rebase fixes

* Post rebase fix

* Discord formatting adjustments

* Add event deprecation or unimplemented tag support

* Trade events

* Add return money and sanity checks.

* Update schema

* Update ucs.cpp

* Update client.cpp

* Update 2022_12_19_player_events_tables.sql

* Implement archive single line

* Replace hackers table and functions with PossibleHack player event

* Replace very old eventlog table since the same events are covered by player event logs

* Update bot_command.cpp

* Record NPC kill events ALL / Named / Raid

* Add BatchEventProcessIntervalSeconds rule

* Naming

* Update CMakeLists.txt

* Update database_schema.h

* Remove logging function and methods

* DB version

* Cleanup SendPlayerHandinEvent

---------

Co-authored-by: Kinglykrab <kinglykrab@gmail.com>
Co-authored-by: Aeadoin <109764533+Aeadoin@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants