thingsboard-aplcache

Added audit log base impl

2/9/2018 1:17:44 PM

Changes

Details

diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
new file mode 100644
index 0000000..b79359b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.exception.ThingsboardException;
+
+@RestController
+@RequestMapping("/api")
+public class AuditLogController extends BaseController {
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/audit/logs/{entityType}/{entityId}", params = {"limit"}, method = RequestMethod.GET)
+    @ResponseBody
+    public TimePageData<AuditLog> getAuditLogs(
+            @PathVariable("entityType") String strEntityType,
+            @PathVariable("entityId") String strEntityId,
+            @RequestParam int limit,
+            @RequestParam(required = false) Long startTime,
+            @RequestParam(required = false) Long endTime,
+            @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
+            @RequestParam(required = false) String offset) throws ThingsboardException {
+        try {
+            checkParameter("EntityId", strEntityId);
+            checkParameter("EntityType", strEntityType);
+            TenantId tenantId = getCurrentUser().getTenantId();
+            TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
+            return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), pageLink));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/audit/logs", params = {"limit"}, method = RequestMethod.GET)
+    @ResponseBody
+    public TimePageData<AuditLog> getAuditLogs(
+            @RequestParam int limit,
+            @RequestParam(required = false) Long startTime,
+            @RequestParam(required = false) Long endTime,
+            @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
+            @RequestParam(required = false) String offset) throws ThingsboardException {
+        try {
+            TenantId tenantId = getCurrentUser().getTenantId();
+            TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
+            return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, pageLink));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index 16ba93e..ed5975a 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.widget.WidgetType;
 import org.thingsboard.server.common.data.widget.WidgetsBundle;
 import org.thingsboard.server.dao.alarm.AlarmService;
 import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.audit.AuditLogService;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.device.DeviceCredentialsService;
@@ -117,6 +118,9 @@ public abstract class BaseController {
     @Autowired
     protected RelationService relationService;
 
+    @Autowired
+    protected AuditLogService auditLogService;
+
 
     @ExceptionHandler(ThingsboardException.class)
     public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index eeb10c8..8d08706 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -22,6 +22,9 @@ import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.device.DeviceSearchQuery;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -29,7 +32,6 @@ import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
-import org.thingsboard.server.common.data.device.DeviceSearchQuery;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.exception.ThingsboardErrorCode;
@@ -75,12 +77,21 @@ public class DeviceController extends BaseController {
                 }
             }
             Device savedDevice = checkNotNull(deviceService.saveDevice(device));
+
             actorService
                     .onDeviceNameOrTypeUpdate(
                             savedDevice.getTenantId(),
                             savedDevice.getId(),
                             savedDevice.getName(),
                             savedDevice.getType());
+
+            // TODO: refactor to ANNOTATION usage
+            if (device.getId() == null) {
+                logDeviceAction(savedDevice, ActionType.ADDED);
+            } else {
+                logDeviceAction(savedDevice, ActionType.UPDATED);
+            }
+
             return savedDevice;
         } catch (Exception e) {
             throw handleException(e);
@@ -94,8 +105,10 @@ public class DeviceController extends BaseController {
         checkParameter(DEVICE_ID, strDeviceId);
         try {
             DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
-            checkDeviceId(deviceId);
+            Device device = checkDeviceId(deviceId);
             deviceService.deleteDevice(deviceId);
+            // TODO: refactor to ANNOTATION usage
+            logDeviceAction(device, ActionType.DELETED);
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -173,9 +186,11 @@ public class DeviceController extends BaseController {
     public DeviceCredentials saveDeviceCredentials(@RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException {
         checkNotNull(deviceCredentials);
         try {
-            checkDeviceId(deviceCredentials.getDeviceId());
+            Device device = checkDeviceId(deviceCredentials.getDeviceId());
             DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
             actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId());
+            // TODO: refactor to ANNOTATION usage
+            logDeviceAction(device, ActionType.CREDENTIALS_UPDATED);
             return result;
         } catch (Exception e) {
             throw handleException(e);
@@ -307,4 +322,18 @@ public class DeviceController extends BaseController {
         }
     }
 
+    // TODO: refactor to ANNOTATION usage
+    private void logDeviceAction(Device device, ActionType actionType) throws ThingsboardException {
+        auditLogService.logAction(
+                getCurrentUser().getTenantId(),
+                device.getId(),
+                device.getName(),
+                device.getCustomerId(),
+                getCurrentUser().getId(),
+                getCurrentUser().getName(),
+                actionType,
+                null,
+                ActionStatus.SUCCESS,
+                null);
+    }
 }
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
index 19e4329..36d736d 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -66,6 +66,7 @@ import org.thingsboard.server.common.data.User;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.page.TimePageLink;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
 import org.thingsboard.server.service.mail.TestMailService;
@@ -331,6 +332,35 @@ public abstract class AbstractControllerTest {
         return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType);
     }
 
+    protected <T> T  doGetTypedWithTimePageLink(String urlTemplate, TypeReference<T> responseType,
+                                                TimePageLink pageLink,
+                                                Object... urlVariables) throws Exception {
+        List<Object> pageLinkVariables = new ArrayList<>();
+        urlTemplate += "limit={limit}";
+        pageLinkVariables.add(pageLink.getLimit());
+        if (pageLink.getStartTime() != null) {
+            urlTemplate += "&startTime={startTime}";
+            pageLinkVariables.add(pageLink.getStartTime());
+        }
+        if (pageLink.getEndTime() != null) {
+            urlTemplate += "&endTime={endTime}";
+            pageLinkVariables.add(pageLink.getEndTime());
+        }
+        if (pageLink.getIdOffset() != null) {
+            urlTemplate += "&offset={offset}";
+            pageLinkVariables.add(pageLink.getIdOffset().toString());
+        }
+        if (pageLink.isAscOrder()) {
+            urlTemplate += "&ascOrder={ascOrder}";
+            pageLinkVariables.add(pageLink.isAscOrder());
+        }
+        Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()];
+        System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length);
+        System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size());
+
+        return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType);
+    }
+
     protected <T> T doPost(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
         return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass);
     }
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java
new file mode 100644
index 0000000..5e74416
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.security.Authority;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public abstract class BaseAuditLogControllerTest extends AbstractControllerTest {
+
+    private Tenant savedTenant;
+
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+
+        User tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+
+        createUserAndLogin(tenantAdmin, "testPassword1");
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+
+        doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testSaveDeviceAuditLogs() throws Exception {
+        for (int i = 0; i < 178; i++) {
+            Device device = new Device();
+            device.setName("Device" + i);
+            device.setType("default");
+            doPost("/api/device", device, Device.class);
+        }
+
+        List<AuditLog> loadedAuditLogs = new ArrayList<>();
+        TimePageLink pageLink = new TimePageLink(23);
+        TimePageData<AuditLog> pageData;
+        do {
+            pageData = doGetTypedWithTimePageLink("/api/audit/logs?",
+                    new TypeReference<TimePageData<AuditLog>>() {
+                    }, pageLink);
+            loadedAuditLogs.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Assert.assertEquals(178, loadedAuditLogs.size());
+    }
+
+    @Test
+    public void testUpdateDeviceAuditLogs() throws Exception {
+        Device device = new Device();
+        device.setName("Device name");
+        device.setType("default");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        for (int i = 0; i < 178; i++) {
+            savedDevice.setName("Device name" + i);
+            doPost("/api/device", savedDevice, Device.class);
+        }
+
+        List<AuditLog> loadedAuditLogs = new ArrayList<>();
+        TimePageLink pageLink = new TimePageLink(23);
+        TimePageData<AuditLog> pageData;
+        do {
+            pageData = doGetTypedWithTimePageLink("/api/audit/logs/DEVICE/" + savedDevice.getId().getId() + "?",
+                    new TypeReference<TimePageData<AuditLog>>() {
+                    }, pageLink);
+            loadedAuditLogs.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Assert.assertEquals(179, loadedAuditLogs.size());
+    }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java
new file mode 100644
index 0000000..4692afe
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller.nosql;
+
+import org.thingsboard.server.controller.BaseAuditLogControllerTest;
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
+
+@DaoNoSqlTest
+public class AuditLogControllerNoSqlTest extends BaseAuditLogControllerTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/AuditLogControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/AuditLogControllerSqlTest.java
new file mode 100644
index 0000000..df6804e
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/sql/AuditLogControllerSqlTest.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller.sql;
+
+import org.thingsboard.server.controller.BaseAuditLogControllerTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+@DaoSqlTest
+public class AuditLogControllerSqlTest extends BaseAuditLogControllerTest {
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionStatus.java
new file mode 100644
index 0000000..5ee8beb
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionStatus.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.audit;
+
+public enum ActionStatus {
+    SUCCESS, FAILURE
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
new file mode 100644
index 0000000..495f80d
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.audit;
+
+public enum ActionType {
+    ADDED, DELETED, UPDATED, ATTRIBUTE_UPDATED, ATTRIBUTE_DELETED, ATTRIBUTE_ADDED, RPC_CALL, CREDENTIALS_UPDATED
+}
\ No newline at end of file
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java
new file mode 100644
index 0000000..62d4bfa
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.audit;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.id.*;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class AuditLog extends BaseData<AuditLogId> {
+
+    private TenantId tenantId;
+    private CustomerId customerId;
+    private EntityId entityId;
+    private String entityName;
+    private UserId userId;
+    private String userName;
+    private ActionType actionType;
+    private JsonNode actionData;
+    private ActionStatus actionStatus;
+    private String actionFailureDetails;
+
+    public AuditLog() {
+        super();
+    }
+
+    public AuditLog(AuditLogId id) {
+        super(id);
+    }
+
+    public AuditLog(AuditLog auditLog) {
+        super(auditLog);
+        this.tenantId = auditLog.getTenantId();
+        this.customerId = auditLog.getCustomerId();
+        this.entityId = auditLog.getEntityId();
+        this.entityName = auditLog.getEntityName();
+        this.userId = auditLog.getUserId();
+        this.userName = auditLog.getUserName();
+        this.actionType = auditLog.getActionType();
+        this.actionData = auditLog.getActionData();
+        this.actionStatus = auditLog.getActionStatus();
+        this.actionFailureDetails = auditLog.getActionFailureDetails();
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AuditLogId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AuditLogId.java
new file mode 100644
index 0000000..5327212
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AuditLogId.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.UUID;
+
+public class AuditLogId extends UUIDBased {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonCreator
+    public AuditLogId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+
+    public static AuditLogId fromString(String auditLogId) {
+        return new AuditLogId(UUID.fromString(auditLogId));
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java
new file mode 100644
index 0000000..f401ad1
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.audit;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.page.TimePageLink;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface AuditLogDao {
+
+    ListenableFuture<Void> saveByTenantId(AuditLog auditLog);
+
+    ListenableFuture<Void> saveByTenantIdAndEntityId(AuditLog auditLog);
+
+    ListenableFuture<Void> savePartitionsByTenantId(AuditLog auditLog);
+
+    List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink);
+
+    List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
new file mode 100644
index 0000000..5593ede
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.audit;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+
+import java.util.List;
+
+public interface AuditLogService {
+
+    TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink);
+
+    TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink);
+
+    ListenableFuture<List<Void>> logAction(TenantId tenantId,
+                                           EntityId entityId,
+                                           String entityName,
+                                           CustomerId customerId,
+                                           UserId userId,
+                                           String userName,
+                                           ActionType actionType,
+                                           JsonNode actionData,
+                                           ActionStatus actionStatus,
+                                           String actionFailureDetails);
+
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
new file mode 100644
index 0000000..1baf573
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.audit;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.service.DataValidator;
+
+import java.util.List;
+
+import static org.thingsboard.server.dao.service.Validator.validateEntityId;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+
+@Slf4j
+@Service
+public class AuditLogServiceImpl implements AuditLogService {
+
+    private static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
+    private static final int INSERTS_PER_ENTRY = 3;
+
+    @Autowired
+    private AuditLogDao auditLogDao;
+
+    @Override
+    public TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) {
+        log.trace("Executing findAuditLogsByTenantIdAndEntityId [{}], [{}], [{}]", tenantId, entityId, pageLink);
+        validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
+        validateEntityId(entityId, INCORRECT_TENANT_ID + entityId);
+        List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndEntityId(tenantId.getId(), entityId, pageLink);
+        return new TimePageData<>(auditLogs, pageLink);
+    }
+
+    @Override
+    public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) {
+        log.trace("Executing findAuditLogs [{}]", pageLink);
+        validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
+        List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantId(tenantId.getId(), pageLink);
+        return new TimePageData<>(auditLogs, pageLink);
+    }
+
+    private AuditLog createAuditLogEntry(TenantId tenantId,
+                                         EntityId entityId,
+                                         String entityName,
+                                         CustomerId customerId,
+                                         UserId userId,
+                                         String userName,
+                                         ActionType actionType,
+                                         JsonNode actionData,
+                                         ActionStatus actionStatus,
+                                         String actionFailureDetails) {
+        AuditLog result = new AuditLog();
+        result.setTenantId(tenantId);
+        result.setEntityId(entityId);
+        result.setEntityName(entityName);
+        result.setCustomerId(customerId);
+        result.setUserId(userId);
+        result.setUserName(userName);
+        result.setActionType(actionType);
+        result.setActionData(actionData);
+        result.setActionStatus(actionStatus);
+        result.setActionFailureDetails(actionFailureDetails);
+        return result;
+    }
+
+    @Override
+    public ListenableFuture<List<Void>> logAction(TenantId tenantId,
+                                                  EntityId entityId,
+                                                  String entityName,
+                                                  CustomerId customerId,
+                                                  UserId userId,
+                                                  String userName,
+                                                  ActionType actionType,
+                                                  JsonNode actionData,
+                                                  ActionStatus actionStatus,
+                                                  String actionFailureDetails) {
+        AuditLog auditLogEntry = createAuditLogEntry(tenantId, entityId, entityName, customerId, userId, userName,
+                actionType, actionData, actionStatus, actionFailureDetails);
+        log.trace("Executing logAction [{}]", auditLogEntry);
+        auditLogValidator.validate(auditLogEntry);
+        List<ListenableFuture<Void>> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY);
+        futures.add(auditLogDao.savePartitionsByTenantId(auditLogEntry));
+        futures.add(auditLogDao.saveByTenantId(auditLogEntry));
+        futures.add(auditLogDao.saveByTenantIdAndEntityId(auditLogEntry));
+        return Futures.allAsList(futures);
+    }
+
+    private DataValidator<AuditLog> auditLogValidator =
+            new DataValidator<AuditLog>() {
+                @Override
+                protected void validateDataImpl(AuditLog auditLog) {
+                    if (auditLog.getEntityId() == null) {
+                        throw new DataValidationException("Entity Id should be specified!");
+                    }
+                    if (auditLog.getTenantId() == null) {
+                        throw new DataValidationException("Tenant Id should be specified!");
+                    }
+                    if (auditLog.getUserId() == null) {
+                        throw new DataValidationException("User Id should be specified!");
+                    }
+                }
+            };
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
new file mode 100644
index 0000000..2fcb732
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
@@ -0,0 +1,242 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.audit;
+
+import com.datastax.driver.core.BoundStatement;
+import com.datastax.driver.core.PreparedStatement;
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.AuditLogId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.model.nosql.AuditLogEntity;
+import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTimeDao;
+import org.thingsboard.server.dao.timeseries.TsPartitionDate;
+import org.thingsboard.server.dao.util.NoSqlDao;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Component
+@Slf4j
+@NoSqlDao
+public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLogEntity, AuditLog> implements AuditLogDao {
+
+    private static final String INSERT_INTO = "INSERT INTO ";
+
+    @Autowired
+    private Environment environment;
+
+    @Override
+    protected Class<AuditLogEntity> getColumnFamilyClass() {
+        return AuditLogEntity.class;
+    }
+
+    @Override
+    protected String getColumnFamilyName() {
+        return AUDIT_LOG_COLUMN_FAMILY_NAME;
+    }
+
+    protected ExecutorService readResultsProcessingExecutor;
+
+    @Value("${cassandra.query.ts_key_value_partitioning}")
+    private String partitioning;
+    private TsPartitionDate tsFormat;
+
+    private PreparedStatement[] saveStmts;
+
+    private boolean isInstall() {
+        return environment.acceptsProfiles("install");
+    }
+
+    @PostConstruct
+    public void init() {
+        if (!isInstall()) {
+            Optional<TsPartitionDate> partition = TsPartitionDate.parse(partitioning);
+            if (partition.isPresent()) {
+                tsFormat = partition.get();
+            } else {
+                log.warn("Incorrect configuration of partitioning {}", partitioning);
+                throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!");
+            }
+        }
+        readResultsProcessingExecutor = Executors.newCachedThreadPool();
+    }
+
+    @PreDestroy
+    public void stopExecutor() {
+        if (readResultsProcessingExecutor != null) {
+            readResultsProcessingExecutor.shutdownNow();
+        }
+    }
+
+    private <T> ListenableFuture<T> getFuture(ResultSetFuture future, java.util.function.Function<ResultSet, T> transformer) {
+        return Futures.transform(future, new Function<ResultSet, T>() {
+            @Nullable
+            @Override
+            public T apply(@Nullable ResultSet input) {
+                return transformer.apply(input);
+            }
+        }, readResultsProcessingExecutor);
+    }
+
+    @Override
+    public ListenableFuture<Void> saveByTenantId(AuditLog auditLog) {
+        log.debug("Save saveByTenantId [{}] ", auditLog);
+
+        AuditLogId auditLogId = new AuditLogId(UUIDs.timeBased());
+
+        long partition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli());
+        BoundStatement stmt = getSaveByTenantStmt().bind();
+        stmt.setUUID(0, auditLogId.getId())
+                .setUUID(1, auditLog.getTenantId().getId())
+                .setUUID(2, auditLog.getEntityId().getId())
+                .setString(3, auditLog.getEntityId().getEntityType().name())
+                .setString(4, auditLog.getActionType().name())
+                .setLong(5, partition);
+        return getFuture(executeAsyncWrite(stmt), rs -> null);
+    }
+
+    @Override
+    public ListenableFuture<Void> saveByTenantIdAndEntityId(AuditLog auditLog) {
+        log.debug("Save saveByTenantIdAndEntityId [{}] ", auditLog);
+
+        AuditLogId auditLogId = new AuditLogId(UUIDs.timeBased());
+
+        BoundStatement stmt = getSaveByTenantIdAndEntityIdStmt().bind();
+        stmt.setUUID(0, auditLogId.getId())
+                .setUUID(1, auditLog.getTenantId().getId())
+                .setUUID(2, auditLog.getEntityId().getId())
+                .setString(3, auditLog.getEntityId().getEntityType().name())
+                .setString(4, auditLog.getActionType().name());
+        return getFuture(executeAsyncWrite(stmt), rs -> null);
+    }
+
+    @Override
+    public ListenableFuture<Void> savePartitionsByTenantId(AuditLog auditLog) {
+        log.debug("Save savePartitionsByTenantId [{}] ", auditLog);
+
+        long partition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli());
+
+        BoundStatement stmt = getPartitionInsertStmt().bind();
+        stmt = stmt.setUUID(0, auditLog.getTenantId().getId())
+                .setLong(1, partition);
+        return getFuture(executeAsyncWrite(stmt), rs -> null);
+    }
+
+    private PreparedStatement getPartitionInsertStmt() {
+        // TODO: ADD CACHE LOGIC
+        return getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF +
+                "(" + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY +
+                "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" +
+                " VALUES(?, ?)");
+    }
+
+    private PreparedStatement getSaveByTenantIdAndEntityIdStmt() {
+        // TODO: ADD CACHE LOGIC
+        return getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF +
+                "(" + ModelConstants.AUDIT_LOG_ID_PROPERTY +
+                "," + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY +
+                "," + ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY +
+                "," + ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY +
+                "," + ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY + ")" +
+                " VALUES(?, ?, ?, ?, ?)");
+    }
+
+    private PreparedStatement getSaveByTenantStmt() {
+        // TODO: ADD CACHE LOGIC
+        return getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_CF +
+                "(" + ModelConstants.AUDIT_LOG_ID_PROPERTY +
+                "," + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY +
+                "," + ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY +
+                "," + ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY +
+                "," + ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY +
+                "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" +
+                " VALUES(?, ?, ?, ?, ?, ?)");
+    }
+
+    private long toPartitionTs(long ts) {
+        LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC);
+        return tsFormat.truncatedTo(time).toInstant(ZoneOffset.UTC).toEpochMilli();
+    }
+
+
+    @Override
+    public List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) {
+        log.trace("Try to find audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink);
+        List<AuditLogEntity> entities = findPageWithTimeSearch(AUDIT_LOG_BY_ENTITY_ID_CF,
+                Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId),
+                        eq(ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY, entityId.getEntityType()),
+                        eq(ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY, entityId.getId())),
+                pageLink);
+        log.trace("Found audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink);
+        return DaoUtil.convertDataList(entities);
+    }
+
+    @Override
+    public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) {
+        log.trace("Try to find audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink);
+
+        // TODO: ADD AUDIT LOG PARTITION CURSOR LOGIC
+
+        long minPartition;
+        long maxPartition;
+
+        if (pageLink.getStartTime() != null && pageLink.getStartTime() != 0) {
+            minPartition = toPartitionTs(pageLink.getStartTime());
+        } else {
+            minPartition = toPartitionTs(LocalDate.now().minusMonths(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli());
+        }
+
+        if (pageLink.getEndTime() != null && pageLink.getEndTime() != 0) {
+            maxPartition = toPartitionTs(pageLink.getEndTime());
+        } else {
+            maxPartition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli());
+        }
+        List<AuditLogEntity> entities = findPageWithTimeSearch(AUDIT_LOG_BY_TENANT_ID_CF,
+                Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId),
+                        eq(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY, maxPartition)),
+                pageLink);
+        log.trace("Found audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink);
+        return DaoUtil.convertDataList(entities);
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index 9b596fc..58c8d2f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -44,6 +44,13 @@ public class ModelConstants {
     public static final String ADDITIONAL_INFO_PROPERTY = "additional_info";
     public static final String ENTITY_TYPE_PROPERTY = "entity_type";
 
+    public static final String ENTITY_TYPE_COLUMN = ENTITY_TYPE_PROPERTY;
+    public static final String ENTITY_ID_COLUMN = "entity_id";
+    public static final String ATTRIBUTE_TYPE_COLUMN = "attribute_type";
+    public static final String ATTRIBUTE_KEY_COLUMN = "attribute_key";
+    public static final String LAST_UPDATE_TS_COLUMN = "last_update_ts";
+
+
     /**
      * Cassandra user constants.
      */
@@ -135,6 +142,29 @@ public class ModelConstants {
     public static final String DEVICE_TYPES_BY_TENANT_VIEW_NAME = "device_types_by_tenant";
 
     /**
+     * Cassandra audit log constants.
+     */
+    public static final String AUDIT_LOG_COLUMN_FAMILY_NAME = "audit_log";
+
+    public static final String AUDIT_LOG_BY_ENTITY_ID_CF = "audit_log_by_entity_id";
+    public static final String AUDIT_LOG_BY_TENANT_ID_CF = "audit_log_by_tenant_id";
+    public static final String AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF = "audit_log_by_tenant_id_partitions";
+
+    public static final String AUDIT_LOG_ID_PROPERTY = ID_PROPERTY;
+    public static final String AUDIT_LOG_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
+    public static final String AUDIT_LOG_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
+    public static final String AUDIT_LOG_ENTITY_TYPE_PROPERTY = ENTITY_TYPE_PROPERTY;
+    public static final String AUDIT_LOG_ENTITY_ID_PROPERTY = ENTITY_ID_COLUMN;
+    public static final String AUDIT_LOG_ENTITY_NAME_PROPERTY = "entity_name";
+    public static final String AUDIT_LOG_USER_ID_PROPERTY = USER_ID_PROPERTY;
+    public static final String AUDIT_LOG_PARTITION_PROPERTY = "partition";
+    public static final String AUDIT_LOG_USER_NAME_PROPERTY = "user_name";
+    public static final String AUDIT_LOG_ACTION_TYPE_PROPERTY = "action_type";
+    public static final String AUDIT_LOG_ACTION_DATA_PROPERTY = "action_data";
+    public static final String AUDIT_LOG_ACTION_STATUS_PROPERTY = "action_status";
+    public static final String AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY = "action_failure_details";
+
+    /**
      * Cassandra asset constants.
      */
     public static final String ASSET_COLUMN_FAMILY_NAME = "asset";
@@ -310,13 +340,6 @@ public class ModelConstants {
     public static final String TS_KV_PARTITIONS_CF = "ts_kv_partitions_cf";
     public static final String TS_KV_LATEST_CF = "ts_kv_latest_cf";
 
-
-    public static final String ENTITY_TYPE_COLUMN = ENTITY_TYPE_PROPERTY;
-    public static final String ENTITY_ID_COLUMN = "entity_id";
-    public static final String ATTRIBUTE_TYPE_COLUMN = "attribute_type";
-    public static final String ATTRIBUTE_KEY_COLUMN = "attribute_key";
-    public static final String LAST_UPDATE_TS_COLUMN = "last_update_ts";
-
     public static final String PARTITION_COLUMN = "partition";
     public static final String KEY_COLUMN = "key";
     public static final String TS_COLUMN = "ts";
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
new file mode 100644
index 0000000..25b6a21
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
@@ -0,0 +1,137 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.nosql;
+
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.Table;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.dao.model.BaseEntity;
+import org.thingsboard.server.dao.model.type.ActionStatusCodec;
+import org.thingsboard.server.dao.model.type.ActionTypeCodec;
+import org.thingsboard.server.dao.model.type.EntityTypeCodec;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Table(name = AUDIT_LOG_COLUMN_FAMILY_NAME)
+@Data
+@NoArgsConstructor
+public class AuditLogEntity implements BaseEntity<AuditLog> {
+
+    @Column(name = ID_PROPERTY)
+    private UUID id;
+
+    @Column(name = AUDIT_LOG_TENANT_ID_PROPERTY)
+    private UUID tenantId;
+
+    @Column(name = AUDIT_LOG_CUSTOMER_ID_PROPERTY)
+    private UUID customerId;
+
+    @Column(name = AUDIT_LOG_ENTITY_TYPE_PROPERTY, codec = EntityTypeCodec.class)
+    private EntityType entityType;
+
+    @Column(name = AUDIT_LOG_ENTITY_ID_PROPERTY)
+    private UUID entityId;
+
+    @Column(name = AUDIT_LOG_ENTITY_NAME_PROPERTY)
+    private String entityName;
+
+    @Column(name = AUDIT_LOG_USER_ID_PROPERTY)
+    private UUID userId;
+
+    @Column(name = AUDIT_LOG_USER_NAME_PROPERTY)
+    private String userName;
+
+    @Column(name = AUDIT_LOG_ACTION_TYPE_PROPERTY, codec = ActionTypeCodec.class)
+    private ActionType actionType;
+
+    @Column(name = AUDIT_LOG_ACTION_DATA_PROPERTY, codec = JsonCodec.class)
+    private JsonNode actionData;
+
+    @Column(name = AUDIT_LOG_ACTION_STATUS_PROPERTY, codec = ActionStatusCodec.class)
+    private ActionStatus actionStatus;
+
+    @Column(name = AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY)
+    private String actionFailureDetails;
+
+    @Override
+    public UUID getId() {
+        return id;
+    }
+
+    @Override
+    public void setId(UUID id) {
+        this.id = id;
+    }
+
+    public AuditLogEntity(AuditLog auditLog) {
+        if (auditLog.getId() != null) {
+            this.id = auditLog.getId().getId();
+        }
+        if (auditLog.getTenantId() != null) {
+            this.tenantId = auditLog.getTenantId().getId();
+        }
+        if (auditLog.getEntityId() != null) {
+            this.entityType = auditLog.getEntityId().getEntityType();
+            this.entityId = auditLog.getEntityId().getId();
+        }
+        if (auditLog.getCustomerId() != null) {
+            this.customerId = auditLog.getCustomerId().getId();
+        }
+        if (auditLog.getUserId() != null) {
+            this.userId = auditLog.getUserId().getId();
+        }
+        this.entityName = auditLog.getEntityName();
+        this.userName = auditLog.getUserName();
+        this.actionType = auditLog.getActionType();
+        this.actionData = auditLog.getActionData();
+        this.actionStatus = auditLog.getActionStatus();
+        this.actionFailureDetails = auditLog.getActionFailureDetails();
+    }
+
+    @Override
+    public AuditLog toData() {
+        AuditLog auditLog = new AuditLog(new AuditLogId(id));
+        if (tenantId != null) {
+            auditLog.setTenantId(new TenantId(tenantId));
+        }
+        if (entityId != null & entityType != null) {
+            auditLog.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId));
+        }
+        if (customerId != null) {
+            auditLog.setCustomerId(new CustomerId(customerId));
+        }
+        if (userId != null) {
+            auditLog.setUserId(new UserId(userId));
+        }
+        auditLog.setEntityName(this.entityName);
+        auditLog.setUserName(this.userName);
+        auditLog.setActionType(this.actionType);
+        auditLog.setActionData(this.actionData);
+        auditLog.setActionStatus(this.actionStatus);
+        auditLog.setActionFailureDetails(this.actionFailureDetails);
+        return auditLog;
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
new file mode 100644
index 0000000..0f71091
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
@@ -0,0 +1,132 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.sql;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.hibernate.annotations.Type;
+import org.hibernate.annotations.TypeDef;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.AuditLogId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.model.BaseEntity;
+import org.thingsboard.server.dao.model.BaseSqlEntity;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.util.mapping.JsonStringType;
+
+import javax.persistence.*;
+
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Entity
+@TypeDef(name = "json", typeClass = JsonStringType.class)
+@Table(name = ModelConstants.AUDIT_LOG_COLUMN_FAMILY_NAME)
+public class AuditLogEntity extends BaseSqlEntity<AuditLog> implements BaseEntity<AuditLog> {
+
+    @Column(name = AUDIT_LOG_TENANT_ID_PROPERTY)
+    private String tenantId;
+
+    @Column(name = AUDIT_LOG_CUSTOMER_ID_PROPERTY)
+    private String customerId;
+
+    @Enumerated(EnumType.STRING)
+    @Column(name = AUDIT_LOG_ENTITY_TYPE_PROPERTY)
+    private EntityType entityType;
+
+    @Column(name = AUDIT_LOG_ENTITY_ID_PROPERTY)
+    private String entityId;
+
+    @Column(name = AUDIT_LOG_ENTITY_NAME_PROPERTY)
+    private String entityName;
+
+    @Column(name = AUDIT_LOG_USER_ID_PROPERTY)
+    private String userId;
+
+    @Column(name = AUDIT_LOG_USER_NAME_PROPERTY)
+    private String userName;
+
+    @Enumerated(EnumType.STRING)
+    @Column(name = AUDIT_LOG_ACTION_TYPE_PROPERTY)
+    private ActionType actionType;
+
+    @Type(type = "json")
+    @Column(name = AUDIT_LOG_ACTION_DATA_PROPERTY)
+    private JsonNode actionData;
+
+    @Enumerated(EnumType.STRING)
+    @Column(name = AUDIT_LOG_ACTION_STATUS_PROPERTY)
+    private ActionStatus actionStatus;
+
+    @Column(name = AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY)
+    private String actionFailureDetails;
+
+    public AuditLogEntity() {
+        super();
+    }
+
+    public AuditLogEntity(AuditLog auditLog) {
+        if (auditLog.getId() != null) {
+            this.setId(auditLog.getId().getId());
+        }
+        if (auditLog.getTenantId() != null) {
+            this.tenantId = toString(auditLog.getTenantId().getId());
+        }
+        if (auditLog.getCustomerId() != null) {
+            this.customerId = toString(auditLog.getCustomerId().getId());
+        }
+        if (auditLog.getEntityId() != null) {
+            this.entityId = toString(auditLog.getEntityId().getId());
+            this.entityType = auditLog.getEntityId().getEntityType();
+        }
+        this.entityName = auditLog.getEntityName();
+        this.userName = auditLog.getUserName();
+        this.actionType = auditLog.getActionType();
+        this.actionData = auditLog.getActionData();
+        this.actionStatus = auditLog.getActionStatus();
+        this.actionFailureDetails = auditLog.getActionFailureDetails();
+    }
+
+    @Override
+    public AuditLog toData() {
+        AuditLog auditLog = new AuditLog(new AuditLogId(getId()));
+        auditLog.setCreatedTime(UUIDs.unixTimestamp(getId()));
+        if (tenantId != null) {
+            auditLog.setTenantId(new TenantId(toUUID(tenantId)));
+        }
+        if (customerId != null) {
+            auditLog.setCustomerId(new CustomerId(toUUID(customerId)));
+        }
+        if (entityId != null) {
+            auditLog.setEntityId(EntityIdFactory.getByTypeAndId(entityType.name(), toUUID(entityId).toString()));
+        }
+        auditLog.setEntityName(this.entityName);
+        auditLog.setUserName(this.userName);
+        auditLog.setActionType(this.actionType);
+        auditLog.setActionData(this.actionData);
+        auditLog.setActionStatus(this.actionStatus);
+        auditLog.setActionFailureDetails(this.actionFailureDetails);
+        return auditLog;
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java
new file mode 100644
index 0000000..a207c40
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.type;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+
+public class ActionStatusCodec extends EnumNameCodec<ActionStatus> {
+
+    public ActionStatusCodec() {
+        super(ActionStatus.class);
+    }
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java
new file mode 100644
index 0000000..9f22d5f
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.model.type;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+import org.thingsboard.server.common.data.audit.ActionType;
+
+public class ActionTypeCodec extends EnumNameCodec<ActionType> {
+
+    public ActionTypeCodec() {
+        super(ActionType.class);
+    }
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java
new file mode 100644
index 0000000..7e4fdc1
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.sql.audit;
+
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.repository.query.Param;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.dao.model.sql.AuditLogEntity;
+
+import java.util.List;
+
+public interface AuditLogRepository extends CrudRepository<AuditLogEntity, String> {
+
+    @Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " +
+            "AND al.id > :idOffset ORDER BY al.id")
+    List<AuditLogEntity> findByTenantId(@Param("tenantId") String tenantId,
+                                        @Param("idOffset") String idOffset,
+                                        Pageable pageable);
+
+    @Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " +
+            "AND al.entityType = :entityType " +
+            "AND al.entityId = :entityId " +
+            "AND al.id > :idOffset ORDER BY al.id")
+    List<AuditLogEntity> findByTenantIdAndEntityId(@Param("tenantId") String tenantId,
+                                                   @Param("entityId") String entityId,
+                                                   @Param("entityType") EntityType entityType,
+                                                   @Param("idOffset") String idOffset,
+                                                   Pageable pageable);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
new file mode 100644
index 0000000..fe7edf8
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.sql.audit;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.audit.AuditLogDao;
+import org.thingsboard.server.dao.model.sql.AuditLogEntity;
+import org.thingsboard.server.dao.sql.JpaAbstractDao;
+import org.thingsboard.server.dao.util.SqlDao;
+
+import javax.annotation.PreDestroy;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+
+import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR;
+
+@Component
+@SqlDao
+public class JpaAuditLogDao extends JpaAbstractDao<AuditLogEntity, AuditLog> implements AuditLogDao {
+
+    private ListeningExecutorService insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
+
+    @Autowired
+    private AuditLogRepository auditLogRepository;
+
+    @Override
+    protected Class<AuditLogEntity> getEntityClass() {
+        return AuditLogEntity.class;
+    }
+
+    @Override
+    protected CrudRepository<AuditLogEntity, String> getCrudRepository() {
+        return auditLogRepository;
+    }
+
+    @PreDestroy
+    void onDestroy() {
+        insertService.shutdown();
+    }
+
+    @Override
+    public ListenableFuture<Void> saveByTenantId(AuditLog auditLog) {
+        return insertService.submit(() -> {
+            save(auditLog);
+            return null;
+        });
+    }
+
+    @Override
+    public ListenableFuture<Void> saveByTenantIdAndEntityId(AuditLog auditLog) {
+        return insertService.submit(() -> null);
+    }
+
+    @Override
+    public ListenableFuture<Void> savePartitionsByTenantId(AuditLog auditLog) {
+        return insertService.submit(() -> null);
+    }
+
+    @Override
+    public List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) {
+        return DaoUtil.convertDataList(
+                auditLogRepository.findByTenantIdAndEntityId(
+                        fromTimeUUID(tenantId),
+                        fromTimeUUID(entityId.getId()),
+                        entityId.getEntityType(),
+                        pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),
+                        new PageRequest(0, pageLink.getLimit())));
+    }
+
+    @Override
+    public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) {
+        return DaoUtil.convertDataList(
+                auditLogRepository.findByTenantId(
+                        fromTimeUUID(tenantId),
+                        pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),
+                        new PageRequest(0, pageLink.getLimit())));
+    }
+}
diff --git a/dao/src/main/resources/cassandra/schema.cql b/dao/src/main/resources/cassandra/schema.cql
index dda8067..6dc2bec 100644
--- a/dao/src/main/resources/cassandra/schema.cql
+++ b/dao/src/main/resources/cassandra/schema.cql
@@ -548,3 +548,44 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.event_by_id AS
     AND event_type IS NOT NULL AND event_uid IS NOT NULL
     PRIMARY KEY ((tenant_id, entity_type, entity_id), id, event_type, event_uid)
     WITH CLUSTERING ORDER BY (id ASC, event_type ASC, event_uid ASC);
+
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_entity_id (
+  tenant_id timeuuid,
+  id timeuuid,
+  customer_id timeuuid,
+  entity_id timeuuid,
+  entity_type text,
+  entity_name text,
+  user_id timeuuid,
+  user_name text,
+  action_type text,
+  action_data text,
+  action_status text,
+  action_failure_details text,
+  PRIMARY KEY ((tenant_id, entity_id, entity_type), id)
+);
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id (
+  tenant_id timeuuid,
+  id timeuuid,
+  partition bigint,
+  customer_id timeuuid,
+  entity_id timeuuid,
+  entity_type text,
+  entity_name text,
+  user_id timeuuid,
+  user_name text,
+  action_type text,
+  action_data text,
+  action_status text,
+  action_failure_details text,
+  PRIMARY KEY ((tenant_id, partition), id)
+);
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id_partitions (
+  tenant_id timeuuid,
+  partition bigint,
+  PRIMARY KEY (( tenant_id ), partition)
+) WITH CLUSTERING ORDER BY ( partition ASC )
+AND compaction = { 'class' :  'LeveledCompactionStrategy'  };
\ No newline at end of file
diff --git a/dao/src/main/resources/sql/schema.sql b/dao/src/main/resources/sql/schema.sql
index 26b314c..7c0f172 100644
--- a/dao/src/main/resources/sql/schema.sql
+++ b/dao/src/main/resources/sql/schema.sql
@@ -47,6 +47,22 @@ CREATE TABLE IF NOT EXISTS asset (
     type varchar(255)
 );
 
+CREATE TABLE IF NOT EXISTS audit_log (
+    id varchar(31) NOT NULL CONSTRAINT audit_log_pkey PRIMARY KEY,
+    tenant_id varchar(31),
+    customer_id varchar(31),
+    entity_id varchar(31),
+    entity_type varchar(255),
+    entity_name varchar(255),
+    user_id varchar(31),
+    user_name varchar(255),
+    action_type varchar(255),
+    action_data varchar(255),
+    action_status varchar(255),
+    search_text varchar(255),
+    action_failure_details varchar
+);
+
 CREATE TABLE IF NOT EXISTS attribute_kv (
   entity_type varchar(255),
   entity_id varchar(31),
diff --git a/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java
index d936a38..817f6ee 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java
@@ -24,7 +24,7 @@ import java.util.Arrays;
 
 @RunWith(ClasspathSuite.class)
 @ClassnameFilters({
-        "org.thingsboard.server.dao.sql.*AAATest"
+        "org.thingsboard.server.dao.sql.*THIS_MUST_BE_FIXED_Test"
 })
 public class JpaDaoTestSuite {
 
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDaoTest.java
new file mode 100644
index 0000000..ed174a7
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDaoTest.java
@@ -0,0 +1,21 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.sql.audit;
+
+import org.thingsboard.server.dao.AbstractJpaDaoTest;
+
+public class JpaAuditLogDaoTest extends AbstractJpaDaoTest {
+}
diff --git a/dao/src/test/resources/sql/drop-all-tables.sql b/dao/src/test/resources/sql/drop-all-tables.sql
index 49a3774..dfdc90f 100644
--- a/dao/src/test/resources/sql/drop-all-tables.sql
+++ b/dao/src/test/resources/sql/drop-all-tables.sql
@@ -1,6 +1,7 @@
 DROP TABLE IF EXISTS admin_settings;
 DROP TABLE IF EXISTS alarm;
 DROP TABLE IF EXISTS asset;
+DROP TABLE IF EXISTS audit_log;
 DROP TABLE IF EXISTS attribute_kv;
 DROP TABLE IF EXISTS component_descriptor;
 DROP TABLE IF EXISTS customer;