Hi there,
Can you be more specific about what you call user activity ?
If you’re talking about which page they went on, that could be use with action composition
If you want to track what change they made in database, I’ve done such a thing using ebean in the past.
Ebean will try to load any class that implement ServerConfigStartup
. So you can do something like that.
public class ActionLoggerConfigStartup implements ServerConfigStartup {
@Override
public void onStart(final ServerConfig serverConfig) {
serverConfig.add(new ActionLoggerPersistController());
}
}
public class ActionLoggerPersistController extends DefaultPersistController {
@Override
public int getExecutionOrder() {
return 100000;
}
@Override
public boolean isRegisterFor(final Class<?> cls) {
return BaseModel.class.isAssignableFrom(cls);
}
@Override
public void postInsert(final BeanPersistRequest<?> request) {
ActionLoggerHelper.created(request);
}
@Override
public boolean preUpdate(final BeanPersistRequest<?> request) {
ActionLoggerHelper.modified(request);
return true;
}
@Override
public void preDelete(final BeanDeleteIdRequest request) {
return;
}
@Override
public void postDelete(final BeanPersistRequest<?> request) {
ActionLoggerHelper.delete(request);
}
@Override
public void postSoftDelete(final BeanPersistRequest<?> request) {
ActionLoggerHelper.softDelete(request);
}
}
public class ActionLoggerHelper {
public final static Logger.ALogger LOGGER = Logger.of(ActionLoggerHelper.class);
public static void created(final BeanPersistRequest<?> request) {
executeWithContext(request, (account, organization, model) -> {
final ActionLogModel actionLog = new ActionLogModel(
account,
organization,
ActionLogModel.ActionType.CREATE,
model
);
final EntityBean entityBean = (EntityBean) model;
final List<ActionLogChangeEntity> changes = new ArrayList<>();
for (int i = 0; i < entityBean._ebean_getPropertyNames().length; i++) {
final Object o = entityBean._ebean_getField(i);
changes.add(new ActionLogChangeEntity(
entityBean._ebean_getPropertyName(i),
null,
o));
}
actionLog.setChanges(changes);
return actionLog;
});
}
public static void modified(final BeanPersistRequest<?> request) {
executeWithContext(request, (account, organization, model) -> {
final ActionLogModel actionLog = new ActionLogModel(
account,
organization,
ActionLogModel.ActionType.UPDATE,
model
);
final EntityBean entityBean = (EntityBean) model;
final List<ActionLogChangeEntity> changes = new ArrayList<>();
entityBean._ebean_getIntercept().getDirtyValues().forEach((key, valuePair) -> {
if ("lastUpdate".equals(key) || "dirty".equals(key)) {
return;
}
changes.add(new ActionLogChangeEntity(key, valuePair));
});
if (changes.size() == 0) {
return null;
}
actionLog.setChanges(changes);
return actionLog;
});
}
public static void delete(final BeanPersistRequest<?> request) {
executeWithContext(request, (account, organization, model) ->
new ActionLogModel(account, organization, ActionLogModel.ActionType.DELETE, model)
);
}
public static void softDelete(final BeanPersistRequest<?> request) {
executeWithContext(request, (account, organization, model) ->
new ActionLogModel(account, organization, ActionLogModel.ActionType.SOFT_DELETE, model)
);
}
private static void executeWithContext(final BeanPersistRequest<?> request,
final LoggerFunction function) {
final AccountModel account = SessionHelper.getAccount();
final OrganizationModel organization = SessionHelper.getOrganization();
if (account == null || organization == null) {
return;
}
final Object bean = request.getBean();
if (!(bean instanceof BaseModel)) {
LOGGER.warn("Bean is not an instance of BaseModel. Aborting.");
return;
}
final BaseModel model = (BaseModel) bean;
final PlayRedis playRedis = ApplicationService.application
.injector()
.instanceOf(PlayRedis.class);
try (Jedis jedis = playRedis.getConnection()) {
final ActionLogModel actionLog = function.apply(account, organization, model);
if (actionLog != null) {
jedis.rpush(RedisKeys.ACTION_LOGGER_FIFO, actionLog.getAsJson());
}
} catch (final Exception e) {
LOGGER.error("An error occurred.", e);
}
}
@FunctionalInterface
private interface LoggerFunction {
ActionLogModel apply(AccountModel account, OrganizationModel organization, BaseModel model);
}
}
Please not that my models extends @MappedSuperclass BaseModel
which itself extends Model
. Allowing the logging process to only affect classes that extends BaseModel
.
Please also not that my logging is backed by redis as a temporary cache before being flushed in my postgres database. The reason is if I remember correctly, that it avoid messing with Ebean life cycle as you’re in the middle of a transaction with an object being created, modified or deleted.
Hope that give you some insights as to what can be done.