thingsboard-aplcache

Changes

Details

diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json
index ddb90db..803f1d3 100644
--- a/application/src/main/data/json/system/widget_bundles/cards.json
+++ b/application/src/main/data/json/system/widget_bundles/cards.json
@@ -66,7 +66,7 @@
         "controllerScript": "self.onInit = function() {\n    self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n    self.ctx.htmlSet = false;\n    \n    var cssParser = new cssjs();\n    cssParser.testMode = false;\n    var namespace = 'html-value-card-' + hashCode(self.ctx.settings.cardCss);\n    cssParser.cssPreviewNamespace = namespace;\n    cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n    self.ctx.$container.addClass(namespace);\n    self.ctx.html = self.ctx.settings.cardHtml;\n    self.ctx.replaceInfo = processHtmlPattern(self.ctx.html, self.ctx.data);\n    \n    updateHtml();\n    \n    function hashCode(str) {\n        var hash = 0;\n        var i, char;\n        if (str.length === 0) return hash;\n        for (i = 0; i < str.length; i++) {\n            char = str.charCodeAt(i);\n            hash = ((hash << 5) - hash) + char;\n            hash = hash & hash;\n        }\n        return hash;\n    }\n    \n    function processHtmlPattern(pattern, data) {\n        var match = self.ctx.varsRegex.exec(pattern);\n        var replaceInfo = {};\n        replaceInfo.variables = [];\n        while (match !== null) {\n            var variableInfo = {};\n            variableInfo.dataKeyIndex = -1;\n            var variable = match[0];\n            var label = match[1];\n            var valDec = 2;\n            var splitVals = label.split(':');\n            if (splitVals.length > 1) {\n                label = splitVals[0];\n                valDec = parseFloat(splitVals[1]);\n            }\n            variableInfo.variable = variable;\n            variableInfo.valDec = valDec;\n            if (label == 'entityName') {\n                variableInfo.isEntityName = true;\n            } else if (label.startsWith('#')) {\n                var keyIndexStr = label.substring(1);\n                var n = Math.floor(Number(keyIndexStr));\n                if (String(n) === keyIndexStr && n >= 0) {\n                    variableInfo.dataKeyIndex = n;\n                }\n            }\n            if (!variableInfo.isEntityName && variableInfo.dataKeyIndex === -1) {\n                for (var i = 0; i < data.length; i++) {\n                     var datasourceData = data[i];\n                     var dataKey = datasourceData.dataKey;\n                     if (dataKey.label === label) {\n                         variableInfo.dataKeyIndex = i;\n                         break;\n                     }\n                }\n            }\n            replaceInfo.variables.push(variableInfo);\n            match = self.ctx.varsRegex.exec(pattern);\n        }\n        return replaceInfo;\n    }    \n}\n\nself.onDataUpdated = function() {\n    updateHtml();\n}\n\nself.onDestroy = function() {\n}\n\nfunction isNumber(n) {\n    return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n    var i = 0;\n    var s, strVal, n;\n\n    val = parseFloat(val);\n    n = (val < 0);\n    val = Math.abs(val);\n\n    if (dec > 0) {\n        strVal = val.toFixed(dec).toString().split('.');\n        s = int - strVal[0].length;\n\n        for (; i < s; ++i) {\n            strVal[0] = '0' + strVal[0];\n        }\n\n        strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n    }\n\n    else {\n        strVal = Math.round(val).toString();\n        s = int - strVal.length;\n\n        for (; i < s; ++i) {\n            strVal = '0' + strVal;\n        }\n\n        strVal = (n ? '-' : '') + strVal;\n    }\n\n    return strVal;\n}\n\nfunction updateHtml() {\n    var text = self.ctx.html;\n    var updated = false;\n    for (var v in self.ctx.replaceInfo.variables) {\n        var variableInfo = self.ctx.replaceInfo.variables[v];\n        var txtVal = '';\n        if (variableInfo.dataKeyIndex > -1) {\n            var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n            if (varData.length > 0) {\n                var val = varData[varData.length-1][1];\n                if (isNumber(val)) {\n                    txtVal = padValue(val, variableInfo.valDec, 0);\n                } else {\n                    txtVal = val;\n                }\n            }\n        } else if (variableInfo.isEntityName) {\n            if (self.ctx.defaultSubscription.datasources.length) {\n                txtVal = self.ctx.defaultSubscription.datasources[0].entityName;\n            } else {\n                txtVal = 'Unknown';\n            }\n        }\n        if (typeof variableInfo.lastVal === undefined ||\n            variableInfo.lastVal !== txtVal) {\n            updated = true;\n            variableInfo.lastVal = txtVal;\n        }\n        text = text.split(variableInfo.variable).join(txtVal);\n    }\n    if (updated || !self.ctx.htmlSet) {\n        self.ctx.$container.html(text);\n        if (!self.ctx.htmlSet) {\n            self.ctx.htmlSet = true;\n        }\n    }\n}\n\n",
         "settingsSchema": "{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"Settings\",\n        \"required\": [\"cardHtml\"],\n        \"properties\": {\n            \"cardCss\": {\n                \"title\": \"CSS\",\n                \"type\": \"string\",\n                \"default\": \".card {\\n font-weight: bold; \\n}\"\n            },\n            \"cardHtml\": {\n                \"title\": \"HTML\",\n                \"type\": \"string\",\n                \"default\": \"<div class='card'>HTML code here</div>\"\n            }\n        }\n    },\n    \"form\": [\n        {\n            \"key\": \"cardCss\",\n            \"type\": \"css\"\n        },           \n        {\n            \"key\": \"cardHtml\",\n            \"type\": \"html\"\n        }    \n    ]\n}",
         "dataKeySettingsSchema": "{}\n",
-        "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"My value\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.random() * 5.45;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"cardCss\":\".card {\\n   width: 100%;\\n   height: 100%;\\n   border: 2px solid #ccc;\\n   box-sizing: border-box;\\n}\\n\\n.card .content {\\n   padding: 20px;\\n   display: flex;\\n   flex-direction: row;\\n   align-items: center;\\n   justify-content: space-around;\\n   height: 100%;\\n   box-sizing: border-box;\\n}\\n\\n.card .content .column {\\n   display: flex;\\n   flex-direction: column;    \\n   justify-content: space-around;\\n   height: 100%;\\n}\\n\\n.card h1 {\\n    text-transform: uppercase;\\n    color: #999;\\n    font-size: 20px;\\n    font-weight: bold;\\n    margin: 0;\\n    padding-bottom: 10px;\\n    line-height: 32px;\\n}\\n\\n.card .value {\\n    font-size: 38px;\\n    font-weight: 200;\\n}\\n\\n.card .description {\\n    font-size: 20px;\\n    color: #999;\\n}\\n\",\"cardHtml\":\"<div class='card'>\\n    <div class='content'>\\n        <div class='column'>\\n            <h1>Value title</h1>\\n            <div class='value'>\\n                ${My value:2} units.\\n            </div>    \\n            <div class='description'>\\n                Value description text\\n            </div>\\n        </div>\\n        <img height=\\\"80px\\\" src=\\\"https://thingsboard.io/images/logo_small.png\\\" />\\n    </div>\\n</div>\"},\"title\":\"HTML Value Card\",\"dropShadow\":false,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
+        "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"My value\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.random() * 5.45;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"cardCss\":\".card {\\n   width: 100%;\\n   height: 100%;\\n   border: 2px solid #ccc;\\n   box-sizing: border-box;\\n}\\n\\n.card .content {\\n   padding: 20px;\\n   display: flex;\\n   flex-direction: row;\\n   align-items: center;\\n   justify-content: space-around;\\n   height: 100%;\\n   box-sizing: border-box;\\n}\\n\\n.card .content .column {\\n   display: flex;\\n   flex-direction: column;    \\n   justify-content: space-around;\\n   height: 100%;\\n}\\n\\n.card h1 {\\n    text-transform: uppercase;\\n    color: #999;\\n    font-size: 20px;\\n    font-weight: bold;\\n    margin: 0;\\n    padding-bottom: 10px;\\n    line-height: 32px;\\n}\\n\\n.card .value {\\n    font-size: 38px;\\n    font-weight: 200;\\n}\\n\\n.card .description {\\n    font-size: 20px;\\n    color: #999;\\n}\\n\",\"cardHtml\":\"<div class='card'>\\n    <div class='content'>\\n        <div class='column'>\\n            <h1>Value title</h1>\\n            <div class='value'>\\n                ${My value:2} units.\\n            </div>    \\n            <div class='description'>\\n                Value description text\\n            </div>\\n        </div>\\n        <img height=\\\"80px\\\" src=\\\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMzIwIiB3aWR0aD0iMzIwIj48ZyBzdHJva2Utd2lkdGg9IjI4Ij48ZyBmaWxsPSIjMzA1NjgwIiBjb2xvcj0iIzAwMCIgd2hpdGUtc3BhY2U9Im5vcm1hbCI+PHBhdGggc3R5bGU9InRleHQtZGVjb3JhdGlvbi1jb2xvcjojMDAwO2lzb2xhdGlvbjphdXRvO21peC1ibGVuZC1tb2RlOm5vcm1hbDtibG9jay1wcm9ncmVzc2lvbjp0Yjt0ZXh0LWRlY29yYXRpb24tbGluZTpub25lO3RleHQtZGVjb3JhdGlvbi1zdHlsZTpzb2xpZDt0ZXh0LWluZGVudDowO3RleHQtdHJhbnNmb3JtOm5vbmUiIGQ9Ik0xNTEuMTMgMGMtMjguMzYzIDAtNTQuOTE1IDcuOTE1LTc3LjYxMyAyMS41MzdhMzYuNTc4IDM2LjU3OCAwIDAgMC0yMy4wNjctOC4xOTQgOC43NjYgOC43NjYgMCAwIDAtLjAwNCAwYy0yMC4xNTQuMDAxLTM2LjY3OSAxNi41MjgtMzYuNjc4IDM2LjY4MmE4Ljc2NiA4Ljc2NiAwIDAgMCAwIC4wMSAzNi42OSAzNi42OSAwIDAgMCA4LjEwNCAyMi45MjhjLTEzLjgzIDIyLjgzLTIxLjg3IDQ5LjU4LTIxLjg3IDc4LjE3YTguNzY2IDguNzY2IDAgMSAwIDE3LjUzIDBjMC0yNC43MDIgNi43Mi00Ny43NDggMTguMzc5LTY3LjU3NCA0LjU2NiAxLjk4NSA5LjQ3MiAzLjE1IDE0LjUxOSAzLjE1N2E4Ljc2NiA4Ljc2NiAwIDAgMCAuMDEyIDBjMjAuMTU1IDAgMzYuNjgzLTE2LjUyNyAzNi42ODItMzYuNjgyYTguNzY2IDguNzY2IDAgMCAwIDAtLjAwNGMtLjAwMS01LTEuMTM4LTkuODYzLTMuMDgzLTE0LjM5NyAxOS43MTctMTEuNDg0IDQyLjU4NS0xOC4wOTUgNjcuMDg1LTE4LjA5NWE4Ljc2NiA4Ljc2NiAwIDEgMCAwLTE3LjUzek01MC40NCAzMC44OGM1LjkxMy4wMDIgMTEuMTkxIDIuNTEyIDE0LjgzNiA3LjA3N2E4Ljc2NiA4Ljc2NiAwIDAgMCAuMTgzLjIxNCAxOS4xMzcgMTkuMTM3IDAgMCAxIDQuMTM0IDExLjg2M2MtLjAwMiAxMC42NzctOC40NjggMTkuMTQ0LTE5LjE0NCAxOS4xNDhhMTkuMTQ1IDE5LjE0NSAwIDAgMS0xMi00LjI1NCA4Ljc2NiA4Ljc2NiAwIDAgMC0uMDEzLS4wMSAxOS4xMzYgMTkuMTM2IDAgMCAxLTcuMTQ0LTE0Ljg5MmMuMDAzLTEwLjY3NyA4LjQ3LTE5LjE0NCAxOS4xNDgtMTkuMTQ2eiIvPjxwYXRoIHN0eWxlPSJ0ZXh0LWRlY29yYXRpb24tY29sb3I6IzAwMDtpc29sYXRpb246YXV0bzttaXgtYmxlbmQtbW9kZTpub3JtYWw7YmxvY2stcHJvZ3Jlc3Npb246dGI7dGV4dC1kZWNvcmF0aW9uLWxpbmU6bm9uZTt0ZXh0LWRlY29yYXRpb24tc3R5bGU6c29saWQ7dGV4dC1pbmRlbnQ6MDt0ZXh0LXRyYW5zZm9ybTpub25lIiBkPSJNNjYuOTkyIDEwMi44M2E4LjE4NyA4LjE4NyAwIDAgMC0yLjI1OCA2LjA3MSA4LjYwNCA4LjYwNCAwIDAgMCAyLjMzOCA1LjUxOGM2LjgwNSA2Ljg1NiAyMC4yMjMgMjAuMjIzIDIwLjIyMyAyMC4yMjNsMTEuODQ0LTExLjgzcy0xMi45NzMtMTIuOTYxLTIwLjE3Ni0yMC4xNzFjLTEuNjA0LTEuNjMyLTMuNzUtMi4zMTQtNi4wMTItMi4zMjRhOC4xNSA4LjE1IDAgMCAwLTUuOTYgMi41MTJ6bTMyLjE0NyAxOS45ODNMNjIuNSAxNTkuNDUyYy0zLjk3NSAzLjk3Ni0zLjk3NSAxMC40MjEgMCAxNC4zOTdsMTguMTU2IDE4LjE1NiAzMS43NTMgMzEuNzUzIDMwLjQ3OCAzMC40NzhjMy45NzYgMy45NzYgMTAuNDIyIDMuOTc2IDE0LjM5OCAwbDI0Ljc5MS0yNC43OTEgMzcuOTE0LTM3LjkxNCAzNi42MzktMzYuNjM5YzMuOTc1LTMuOTc2IDMuOTc1LTEwLjQyMiAwLTE0LjM5OGwtMTguNjMtMTguNjMtMzEuNzUtMzEuNzYtMzAuMDEtMzBjLTMuOTc3LTMuOTc1LTEwLjQyMi0zLjk3NS0xNC4zOTggMGwtMjQuNzkgMjQuNzktMzcuOTEgMzcuOTF6bTM3LjkxMS0zNy45MXMtMTIuOTczLTEyLjk2MS0yMC4xNzYtMjAuMTcxYy0xLjYwNC0xLjYzMi0zLjc1LTIuMzE0LTYuMDEyLTIuMzI0LTQuNzE3LS4wMjMtOC40MzQgMy44NjEtOC4yMTcgOC41ODNhOC42MDQgOC42MDQgMCAwIDAgMi4zMzcgNS41MThjNi44MDUgNi44NTYgMjAuMjIzIDIwLjIyMyAyMC4yMjMgMjAuMjIzbDExLjg0NC0xMS44M3ptNjkuMTkzIDUuMjEzczEyLjk2MS0xMi45NzMgMjAuMTcxLTIwLjE3NmMxLjYzMy0xLjYwNCAyLjMxNC0zLjc1IDIuMzI0LTYuMDEyLjAyMy00LjcxNi0zLjg2MS04LjQzNC04LjU4My04LjIxN2E4LjYwNCA4LjYwNCAwIDAgMC01LjUxOCAyLjMzOGMtNi44NTYgNi44MDUtMjAuMjIzIDIwLjIyMy0yMC4yMjMgMjAuMjIzbDExLjgzIDExLjg0NHptMzEuNzUzIDMxLjc1M3MxMi45NjEtMTIuOTczIDIwLjE3MS0yMC4xNzZjMS42MzMtMS42MDQgMi4zMTQtMy43NSAyLjMyNC02LjAxMi4wMjMtNC43MTYtMy44NjEtOC40MzQtOC41ODMtOC4yMTdhOC42MDQgOC42MDQgMCAwIDAtNS41MTggMi4zMzhjLTYuODU2IDYuODA1LTIwLjIyMyAyMC4yMjMtMjAuMjIzIDIwLjIyM2wxMS44MyAxMS44NDR6bS0xOC4wMDkgNjkuNjY3czEyLjk3MyAxMi45NjEgMjAuMTc4IDIwLjE3YzEuNjA0IDEuNjMyIDMuNzUgMi4zMTMgNi4wMTIgMi4zMjQgNC43MTcuMDIyIDguNDM0LTMuODYyIDguMjE3LTguNTg0bC0uMDAyLjAwMmE4LjYwNiA4LjYwNiAwIDAgMC0yLjMzOC01LjUxOGMtNi44MDUtNi44NTYtMjAuMjIyLTIwLjIyMi0yMC4yMjItMjAuMjIybC0xMS44NDQgMTEuODN6bS0zNy45MTQgMzcuOTE0czEyLjk3MyAxMi45NjEgMjAuMTc4IDIwLjE3YzEuNjA0IDEuNjMyIDMuNzUgMi4zMTMgNi4wMTIgMi4zMjMgNC43MTcuMDIzIDguNDM0LTMuODYxIDguMjE3LTguNTgzaC0uMDAyYTguNjAzIDguNjAzIDAgMCAwLTIuMzM3LTUuNTE4Yy02LjgwNS02Ljg1Ni0yMC4yMjMtMjAuMjIzLTIwLjIyMy0yMC4yMjNsLTExLjg0NCAxMS44M3ptLTY5LjY2Ny01LjY4N3MtMTIuOTYxIDEyLjk3My0yMC4xNjkgMjAuMTc4Yy0xLjYzMiAxLjYwNC0yLjMxNCAzLjc1LTIuMzI0IDYuMDEyLS4wMjMgNC43MTcgMy44NjEgOC40MzQgOC41ODMgOC4yMTdoLS4wMDJhOC42MDIgOC42MDIgMCAwIDAgNS41MTgtMi4zMzdjNi44NTYtNi44MDUgMjAuMjIzLTIwLjIyMyAyMC4yMjMtMjAuMjIzbC0xMS44Mi0xMS44NHptLTMxLjc0My0zMS43NHMtMTIuOTYxIDEyLjk3My0yMC4xNjkgMjAuMTc4Yy0xLjYzMiAxLjYwNC0yLjMxNCAzLjc1LTIuMzI0IDYuMDEyLS4wMjMgNC43MTcgMy44NjEgOC40MzQgOC41ODMgOC4yMTdoLS4wMDJhOC42MDQgOC42MDQgMCAwIDAgNS41MTgtMi4zMzdjNi44NTYtNi44MDUgMjAuMjIzLTIwLjIyMyAyMC4yMjMtMjAuMjIzbC0xMS44My0xMS44NXpNMTY3LjkgMTAxLjQ3YzEuNjgtMS43MDYgMy45NjctMi42NiA2LjI5Ny0yLjYyNmE3Ljg5IDcuODkgMCAwIDEgNC41NjMgMS41MWwxNi40OTkgMTIuMWMzLjIgMi4yOTcgNC4xNDQgNi42NTkgMi4yMyAxMC4zMTItMS45MTMgMy42NTMtNi4xMjMgNS41MjQtOS45NSA0LjQyM2w2LjEyNCAyMy45NDhjMS4xMTMgNC4zNTEtMS41NjQgOC45NjctNS45ODQgMTAuMzE3bC00NC42NDIgMTMuNjMgOC4yNDYgMzEuODg0YzEuMTczIDQuMzctMS41MDIgOS4wNDQtNS45NTUgMTAuNDA3cy04Ljk3NS0xLjExMS0xMC4wNjgtNS41MDVsLTEwLjI4Mi0zOS43N2MtMS4xMjYtNC4zNTUgMS41NS04Ljk4NCA1Ljk3Ni0xMC4zMzdsNDQuNjYxLTEzLjYzNy00LjEyMi0xNi4xMThjLTIuNzYzIDMuMDY0LTcuMjMzIDMuODA4LTEwLjU4NiAxLjc2MS0zLjM1My0yLjA0Ny00LjYxNC02LjI5LTIuOTg2LTEwLjA0N2w4LjExNy0xOS40NTRhOC44NzIgOC44NzIgMCAwIDEgMS44NjMtMi43OTd6IiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz48cGF0aCBzdHlsZT0idGV4dC1kZWNvcmF0aW9uLWNvbG9yOiMwMDA7aXNvbGF0aW9uOmF1dG87bWl4LWJsZW5kLW1vZGU6bm9ybWFsO2Jsb2NrLXByb2dyZXNzaW9uOnRiO3RleHQtZGVjb3JhdGlvbi1saW5lOm5vbmU7dGV4dC1kZWNvcmF0aW9uLXN0eWxlOnNvbGlkO3RleHQtaW5kZW50OjA7dGV4dC10cmFuc2Zvcm06bm9uZSIgZD0iTTE2OC44NyAzMjAuMDRjMjguMzYzIDAgNTQuOTE1LTcuOTE1IDc3LjYxNC0yMS41MzhhMzYuNTc4IDM2LjU3OCAwIDAgMCAyMy4wNjcgOC4xOTQgOC43NjYgOC43NjYgMCAwIDAgLjAwNCAwYzIwLjE1NSAwIDM2LjY4LTE2LjUyOCAzNi42NzktMzYuNjgyYTguNzY2IDguNzY2IDAgMCAwIDAtLjAxMSAzNi42ODggMzYuNjg4IDAgMCAwLTguMTAzLTIyLjkyN2MxMy44MjUtMjIuODIgMjEuODY2LTQ5LjU3MiAyMS44NjYtNzguMTYyYTguNzY2IDguNzY2IDAgMSAwLTE3LjUzMSAwYzAgMjQuNzAzLTYuNzIgNDcuNzQ5LTE4LjM3OCA2Ny41NzUtNC41NjctMS45ODUtOS40NzMtMy4xNS0xNC41Mi0zLjE1N2E4Ljc2NiA4Ljc2NiAwIDAgMC0uMDEyIDBjLTIwLjE1NS0uMDAxLTM2LjY4MyAxNi41MjctMzYuNjgyIDM2LjY4Mi4wMDIgNC45OTkgMS4xMzkgOS44NjIgMy4wODMgMTQuMzk3LTE5LjcxNyAxMS40ODQtNDIuNTg2IDE4LjA5NS02Ny4wODYgMTguMDk1YTguNzY2IDguNzY2IDAgMSAwIDAgMTcuNTN6bTEwMC42OS0zMC44NzVjLTUuOTEzIDAtMTEuMTkxLTIuNTEyLTE0LjgzNi03LjA3N2E4Ljc2NiA4Ljc2NiAwIDAgMC0uMTgzLS4yMTQgMTkuMTM2IDE5LjEzNiAwIDAgMS00LjEzNC0xMS44NjNjLjAwMi0xMC42NzcgOC40NjgtMTkuMTQ0IDE5LjE0NC0xOS4xNDhhMTkuMTQ1IDE5LjE0NSAwIDAgMSAxMiA0LjI1NCA4Ljc2NiA4Ljc2NiAwIDAgMCAuMDEzLjAxIDE5LjEzNiAxOS4xMzYgMCAwIDEgNy4xNDQgMTQuODkyYy0uMDAzIDEwLjY3Ny04LjQ3IDE5LjE0NS0xOS4xNDggMTkuMTQ2eiIvPjwvZz48L2c+PC9zdmc+\\\" />\\n    </div>\\n</div>\"},\"title\":\"HTML Value Card\",\"dropShadow\":false,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
       }
     },
     {
diff --git a/application/src/main/data/upgrade/1.4.0/schema_update.cql b/application/src/main/data/upgrade/1.4.0/schema_update.cql
new file mode 100644
index 0000000..c5b656f
--- /dev/null
+++ b/application/src/main/data/upgrade/1.4.0/schema_update.cql
@@ -0,0 +1,89 @@
+--
+-- 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.
+--
+
+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_customer_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, customer_id), id)
+);
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_user_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, user_id), 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'  };
diff --git a/application/src/main/data/upgrade/1.4.0/schema_update.sql b/application/src/main/data/upgrade/1.4.0/schema_update.sql
new file mode 100644
index 0000000..b2e3a97
--- /dev/null
+++ b/application/src/main/data/upgrade/1.4.0/schema_update.sql
@@ -0,0 +1,31 @@
+--
+-- 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.
+--
+
+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(1000000),
+    action_status varchar(255),
+    action_failure_details varchar(1000000)
+);
+
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index f45028a..a42bcab 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -40,6 +40,7 @@ import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
 import org.thingsboard.server.dao.alarm.AlarmService;
 import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.audit.AuditLogService;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.event.EventService;
@@ -114,6 +115,9 @@ public class ActorSystemContext {
     @Getter private RelationService relationService;
 
     @Autowired
+    @Getter private AuditLogService auditLogService;
+
+    @Autowired
     @Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint;
 
     @Value("${actors.session.sync.timeout}")
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
index 3f0d39f..762bb64 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
@@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.id.*;
 import org.thingsboard.server.common.data.kv.AttributeKey;
 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@@ -41,9 +42,7 @@ import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotific
 import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
 import org.thingsboard.server.extensions.api.plugins.PluginCallback;
 import org.thingsboard.server.extensions.api.plugins.PluginContext;
-import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
 import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
 import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
 import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
@@ -197,6 +196,52 @@ public final class PluginProcessingContext implements PluginContext {
     }
 
     @Override
+    public void logAttributesUpdated(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType,
+                                                                     List<AttributeKvEntry> attributes, Exception e) {
+        pluginCtx.auditLogService.logEntityAction(
+                ctx.getTenantId(),
+                ctx.getCustomerId(),
+                ctx.getUserId(),
+                ctx.getUserName(),
+                (UUIDBased & EntityId)entityId,
+                null,
+                ActionType.ATTRIBUTES_UPDATED,
+                e,
+                attributeType,
+                attributes);
+    }
+
+    @Override
+    public void logAttributesDeleted(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e) {
+        pluginCtx.auditLogService.logEntityAction(
+                ctx.getTenantId(),
+                ctx.getCustomerId(),
+                ctx.getUserId(),
+                ctx.getUserName(),
+                (UUIDBased & EntityId)entityId,
+                null,
+                ActionType.ATTRIBUTES_DELETED,
+                e,
+                attributeType,
+                keys);
+    }
+
+    @Override
+    public void logAttributesRead(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e) {
+        pluginCtx.auditLogService.logEntityAction(
+                ctx.getTenantId(),
+                ctx.getCustomerId(),
+                ctx.getUserId(),
+                ctx.getUserName(),
+                (UUIDBased & EntityId)entityId,
+                null,
+                ActionType.ATTRIBUTES_READ,
+                e,
+                attributeType,
+                keys);
+    }
+
+    @Override
     public void loadLatestTimeseries(final EntityId entityId, final Collection<String> keys, final PluginCallback<List<TsKvEntry>> callback) {
         validate(entityId, new ValidationCallback(callback, ctx -> {
             ListenableFuture<List<TsKvEntry>> rsListFuture = pluginCtx.tsService.findLatest(entityId, keys);
@@ -461,6 +506,29 @@ public final class PluginProcessingContext implements PluginContext {
     }
 
     @Override
+    public void logRpcRequest(PluginApiCallSecurityContext ctx, DeviceId deviceId, ToDeviceRpcRequestBody body, boolean oneWay, Optional<RpcError> rpcError, Exception e) {
+        String rpcErrorStr = "";
+        if (rpcError.isPresent()) {
+            rpcErrorStr = "RPC Error: " + rpcError.get().name();
+        }
+        String method = body.getMethod();
+        String params = body.getParams();
+        pluginCtx.auditLogService.logEntityAction(
+                ctx.getTenantId(),
+                ctx.getCustomerId(),
+                ctx.getUserId(),
+                ctx.getUserName(),
+                deviceId,
+                null,
+                ActionType.RPC_CALL,
+                e,
+                rpcErrorStr,
+                new Boolean(oneWay),
+                method,
+                params);
+    }
+
+    @Override
     public void scheduleTimeoutMsg(TimeoutMsg msg) {
         pluginCtx.scheduleTimeoutMsg(msg);
     }
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
index de859a2..a1cc835 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
@@ -27,6 +27,7 @@ import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
 import org.thingsboard.server.common.data.id.PluginId;
 import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.audit.AuditLogService;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.plugin.PluginService;
@@ -63,6 +64,7 @@ public final class SharedPluginProcessingContext {
     final ClusterRpcService rpcService;
     final ClusterRoutingService routingService;
     final RelationService relationService;
+    final AuditLogService auditLogService;
     final PluginId pluginId;
     final TenantId tenantId;
 
@@ -86,6 +88,7 @@ public final class SharedPluginProcessingContext {
         this.customerService = sysContext.getCustomerService();
         this.tenantService = sysContext.getTenantService();
         this.relationService = sysContext.getRelationService();
+        this.auditLogService = sysContext.getAuditLogService();
     }
 
     public PluginId getPluginId() {
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
index 6250897..72887cf 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
@@ -148,7 +148,7 @@ public class BasicRpcSessionListener implements GrpcSessionListener {
         DeviceId deviceId = new DeviceId(toUUID(msg.getDeviceId()));
 
         ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(msg.getMethod(), msg.getParams());
-        ToDeviceRpcRequest request = new ToDeviceRpcRequest(toUUID(msg.getMsgId()), deviceTenantId, deviceId, msg.getOneway(), msg.getExpTime(), requestBody);
+        ToDeviceRpcRequest request = new ToDeviceRpcRequest(toUUID(msg.getMsgId()), null, deviceTenantId, deviceId, msg.getOneway(), msg.getExpTime(), requestBody);
 
         return new ToDeviceRpcRequestPluginMsg(serverAddress, pluginId, pluginTenantId, request);
     }
diff --git a/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java b/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
new file mode 100644
index 0000000..ce622aa
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java
@@ -0,0 +1,43 @@
+/**
+ * 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.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+@ConfigurationProperties(prefix = "audit_log.logging_level")
+public class AuditLogLevelProperties {
+
+    private Map<String, String> mask = new HashMap<>();
+
+    public AuditLogLevelProperties() {
+        super();
+    }
+
+    public void setMask(Map<String, String> mask) {
+        this.mask = mask;
+    }
+
+    public Map<String, String> getMask() {
+        return this.mask;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
index 6755408..833c337 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -40,6 +40,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.web.cors.CorsUtils;
 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
 import org.springframework.web.filter.CorsFilter;
+import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
 import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
 import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider;
 import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
@@ -198,4 +199,9 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
             return new CorsFilter(source);
         }
     }
+
+    @Bean
+    public AuditLogLevelFilter auditLogLevelFilter(@Autowired AuditLogLevelProperties auditLogLevelProperties) {
+        return new AuditLogLevelFilter(auditLogLevelProperties.getMask());
+    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
index 194b7d8..4790b75 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -21,7 +21,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -73,8 +75,16 @@ public class AssetController extends BaseController {
                     checkCustomerId(asset.getCustomerId());
                 }
             }
-            return checkNotNull(assetService.saveAsset(asset));
+            Asset savedAsset  = checkNotNull(assetService.saveAsset(asset));
+
+            logEntityAction(savedAsset.getId(), savedAsset,
+                    savedAsset.getCustomerId(),
+                    asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
+
+            return  savedAsset;
         } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.ASSET), asset,
+                    null, asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
             throw handleException(e);
         }
     }
@@ -86,9 +96,18 @@ public class AssetController extends BaseController {
         checkParameter(ASSET_ID, strAssetId);
         try {
             AssetId assetId = new AssetId(toUUID(strAssetId));
-            checkAssetId(assetId);
+            Asset asset = checkAssetId(assetId);
             assetService.deleteAsset(assetId);
+
+            logEntityAction(assetId, asset,
+                    asset.getCustomerId(),
+                    ActionType.DELETED, null, strAssetId);
+
         } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.ASSET),
+                    null,
+                    null,
+                    ActionType.DELETED, e, strAssetId);
             throw handleException(e);
         }
     }
@@ -102,13 +121,24 @@ public class AssetController extends BaseController {
         checkParameter(ASSET_ID, strAssetId);
         try {
             CustomerId customerId = new CustomerId(toUUID(strCustomerId));
-            checkCustomerId(customerId);
+            Customer customer = checkCustomerId(customerId);
 
             AssetId assetId = new AssetId(toUUID(strAssetId));
             checkAssetId(assetId);
 
-            return checkNotNull(assetService.assignAssetToCustomer(assetId, customerId));
+            Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(assetId, customerId));
+
+            logEntityAction(assetId, savedAsset,
+                    savedAsset.getCustomerId(),
+                    ActionType.ASSIGNED_TO_CUSTOMER, null, strAssetId, strCustomerId, customer.getName());
+
+            return  savedAsset;
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.ASSET), null,
+                    null,
+                    ActionType.ASSIGNED_TO_CUSTOMER, e, strAssetId, strCustomerId);
+
             throw handleException(e);
         }
     }
@@ -124,8 +154,22 @@ public class AssetController extends BaseController {
             if (asset.getCustomerId() == null || asset.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
                 throw new IncorrectParameterException("Asset isn't assigned to any customer!");
             }
-            return checkNotNull(assetService.unassignAssetFromCustomer(assetId));
+
+            Customer customer = checkCustomerId(asset.getCustomerId());
+
+            Asset savedAsset = checkNotNull(assetService.unassignAssetFromCustomer(assetId));
+
+            logEntityAction(assetId, asset,
+                    asset.getCustomerId(),
+                    ActionType.UNASSIGNED_FROM_CUSTOMER, null, strAssetId, customer.getId().toString(), customer.getName());
+
+            return savedAsset;
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.ASSET), null,
+                    null,
+                    ActionType.UNASSIGNED_FROM_CUSTOMER, e, strAssetId);
+
             throw handleException(e);
         }
     }
@@ -139,8 +183,19 @@ public class AssetController extends BaseController {
             AssetId assetId = new AssetId(toUUID(strAssetId));
             Asset asset = checkAssetId(assetId);
             Customer publicCustomer = customerService.findOrCreatePublicCustomer(asset.getTenantId());
-            return checkNotNull(assetService.assignAssetToCustomer(assetId, publicCustomer.getId()));
+            Asset savedAsset = checkNotNull(assetService.assignAssetToCustomer(assetId, publicCustomer.getId()));
+
+            logEntityAction(assetId, savedAsset,
+                    savedAsset.getCustomerId(),
+                    ActionType.ASSIGNED_TO_CUSTOMER, null, strAssetId, publicCustomer.getId().toString(), publicCustomer.getName());
+
+            return savedAsset;
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.ASSET), null,
+                    null,
+                    ActionType.ASSIGNED_TO_CUSTOMER, e, strAssetId);
+
             throw handleException(e);
         }
     }
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..4d1d50e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
@@ -0,0 +1,114 @@
+/**
+ * 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.CustomerId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+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.exception.ThingsboardException;
+
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api")
+public class AuditLogController extends BaseController {
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"limit"}, method = RequestMethod.GET)
+    @ResponseBody
+    public TimePageData<AuditLog> getAuditLogsByCustomerId(
+            @PathVariable("customerId") String strCustomerId,
+            @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("CustomerId", strCustomerId);
+            TenantId tenantId = getCurrentUser().getTenantId();
+            TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
+            return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), pageLink));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/audit/logs/user/{userId}", params = {"limit"}, method = RequestMethod.GET)
+    @ResponseBody
+    public TimePageData<AuditLog> getAuditLogsByUserId(
+            @PathVariable("userId") String strUserId,
+            @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("UserId", strUserId);
+            TenantId tenantId = getCurrentUser().getTenantId();
+            TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
+            return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), pageLink));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"limit"}, method = RequestMethod.GET)
+    @ResponseBody
+    public TimePageData<AuditLog> getAuditLogsByEntityId(
+            @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 f581cc2..28489ca 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -15,9 +15,12 @@
  */
 package org.thingsboard.server.controller;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -27,6 +30,8 @@ import org.thingsboard.server.common.data.alarm.Alarm;
 import org.thingsboard.server.common.data.alarm.AlarmId;
 import org.thingsboard.server.common.data.alarm.AlarmInfo;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.audit.ActionStatus;
+import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.id.*;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.page.TimePageLink;
@@ -39,6 +44,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;
@@ -72,6 +78,7 @@ public abstract class BaseController {
 
     public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
     public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!";
+
     @Autowired
     private ThingsboardErrorResponseHandler errorResponseHandler;
 
@@ -117,6 +124,9 @@ public abstract class BaseController {
     @Autowired
     protected RelationService relationService;
 
+    @Autowired
+    protected AuditLogService auditLogService;
+
     @ExceptionHandler(ThingsboardException.class)
     public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
         errorResponseHandler.handle(ex, response);
@@ -540,4 +550,20 @@ public abstract class BaseController {
                 serverPort);
         return baseUrl;
     }
+
+    protected <I extends UUIDBased & EntityId> I emptyId(EntityType entityType) {
+        return (I)EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
+    }
+
+    protected <E extends BaseData<I> & HasName,
+            I extends UUIDBased & EntityId> void logEntityAction(I entityId, E entity, CustomerId customerId,
+                                                                 ActionType actionType, Exception e, Object... additionalInfo) throws ThingsboardException {
+        User user = getCurrentUser();
+        if (customerId == null || customerId.isNullUid()) {
+            customerId = user.getCustomerId();
+        }
+        auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
+    }
+
+
 }
diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
index d87ea5c..ee3fbfc 100644
--- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
@@ -22,6 +22,8 @@ import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
@@ -86,8 +88,18 @@ public class CustomerController extends BaseController {
     public Customer saveCustomer(@RequestBody Customer customer) throws ThingsboardException {
         try {
             customer.setTenantId(getCurrentUser().getTenantId());
-            return checkNotNull(customerService.saveCustomer(customer));
+            Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer));
+
+            logEntityAction(savedCustomer.getId(), savedCustomer,
+                    savedCustomer.getId(),
+                    customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
+
+            return savedCustomer;
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.CUSTOMER), customer,
+                    null, customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
+
             throw handleException(e);
         }
     }
@@ -99,9 +111,20 @@ public class CustomerController extends BaseController {
         checkParameter(CUSTOMER_ID, strCustomerId);
         try {
             CustomerId customerId = new CustomerId(toUUID(strCustomerId));
-            checkCustomerId(customerId);
+            Customer customer = checkCustomerId(customerId);
             customerService.deleteCustomer(customerId);
+
+            logEntityAction(customerId, customer,
+                    customer.getId(),
+                    ActionType.DELETED, null, strCustomerId);
+
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.CUSTOMER),
+                    null,
+                    null,
+                    ActionType.DELETED, e, strCustomerId);
+
             throw handleException(e);
         }
     }
diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
index 7bc05d7..34320b1 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -21,6 +21,8 @@ import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.Dashboard;
 import org.thingsboard.server.common.data.DashboardInfo;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DashboardId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -75,8 +77,17 @@ public class DashboardController extends BaseController {
     public Dashboard saveDashboard(@RequestBody Dashboard dashboard) throws ThingsboardException {
         try {
             dashboard.setTenantId(getCurrentUser().getTenantId());
-            return checkNotNull(dashboardService.saveDashboard(dashboard));
+            Dashboard savedDashboard = checkNotNull(dashboardService.saveDashboard(dashboard));
+
+            logEntityAction(savedDashboard.getId(), savedDashboard,
+                    savedDashboard.getCustomerId(),
+                    dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
+
+            return savedDashboard;
         } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.DASHBOARD), dashboard,
+                    null, dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
+
             throw handleException(e);
         }
     }
@@ -88,9 +99,20 @@ public class DashboardController extends BaseController {
         checkParameter(DASHBOARD_ID, strDashboardId);
         try {
             DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
-            checkDashboardId(dashboardId);
+            Dashboard dashboard = checkDashboardId(dashboardId);
             dashboardService.deleteDashboard(dashboardId);
+
+            logEntityAction(dashboardId, dashboard,
+                    dashboard.getCustomerId(),
+                    ActionType.DELETED, null, strDashboardId);
+
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.DASHBOARD),
+                    null,
+                    null,
+                    ActionType.DELETED, e, strDashboardId);
+
             throw handleException(e);
         }
     }
@@ -104,13 +126,25 @@ public class DashboardController extends BaseController {
         checkParameter(DASHBOARD_ID, strDashboardId);
         try {
             CustomerId customerId = new CustomerId(toUUID(strCustomerId));
-            checkCustomerId(customerId);
+            Customer customer = checkCustomerId(customerId);
 
             DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
             checkDashboardId(dashboardId);
             
-            return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, customerId));
+            Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, customerId));
+
+            logEntityAction(dashboardId, savedDashboard,
+                    savedDashboard.getCustomerId(),
+                    ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, strCustomerId, customer.getName());
+
+
+            return savedDashboard;
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.DASHBOARD), null,
+                    null,
+                    ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId, strCustomerId);
+
             throw handleException(e);
         }
     }
@@ -126,8 +160,22 @@ public class DashboardController extends BaseController {
             if (dashboard.getCustomerId() == null || dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
                 throw new IncorrectParameterException("Dashboard isn't assigned to any customer!");
             }
-            return checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId));
+
+            Customer customer = checkCustomerId(dashboard.getCustomerId());
+
+            Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId));
+
+            logEntityAction(dashboardId, dashboard,
+                    dashboard.getCustomerId(),
+                    ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customer.getId().toString(), customer.getName());
+
+            return savedDashboard;
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.DASHBOARD), null,
+                    null,
+                    ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDashboardId);
+
             throw handleException(e);
         }
     }
@@ -141,8 +189,19 @@ public class DashboardController extends BaseController {
             DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
             Dashboard dashboard = checkDashboardId(dashboardId);
             Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId());
-            return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId()));
+            Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId()));
+
+            logEntityAction(dashboardId, savedDashboard,
+                    savedDashboard.getCustomerId(),
+                    ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, publicCustomer.getId().toString(), publicCustomer.getName());
+
+            return savedDashboard;
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.DASHBOARD), null,
+                    null,
+                    ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId);
+
             throw handleException(e);
         }
     }
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..18ddfe5 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,10 @@ 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.EntityType;
+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 +33,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,14 +78,22 @@ public class DeviceController extends BaseController {
                 }
             }
             Device savedDevice = checkNotNull(deviceService.saveDevice(device));
+
             actorService
                     .onDeviceNameOrTypeUpdate(
                             savedDevice.getTenantId(),
                             savedDevice.getId(),
                             savedDevice.getName(),
                             savedDevice.getType());
+
+            logEntityAction(savedDevice.getId(), savedDevice,
+                    savedDevice.getCustomerId(),
+                    device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
+
             return savedDevice;
         } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.DEVICE), device,
+                    null, device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
             throw handleException(e);
         }
     }
@@ -94,9 +105,18 @@ 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);
+
+            logEntityAction(deviceId, device,
+                    device.getCustomerId(),
+                    ActionType.DELETED, null, strDeviceId);
+
         } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.DEVICE),
+                    null,
+                    null,
+                    ActionType.DELETED, e, strDeviceId);
             throw handleException(e);
         }
     }
@@ -110,13 +130,22 @@ public class DeviceController extends BaseController {
         checkParameter(DEVICE_ID, strDeviceId);
         try {
             CustomerId customerId = new CustomerId(toUUID(strCustomerId));
-            checkCustomerId(customerId);
+            Customer customer = checkCustomerId(customerId);
 
             DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
             checkDeviceId(deviceId);
 
-            return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId));
+            Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId));
+
+            logEntityAction(deviceId, savedDevice,
+                    savedDevice.getCustomerId(),
+                    ActionType.ASSIGNED_TO_CUSTOMER, null, strDeviceId, strCustomerId, customer.getName());
+
+            return savedDevice;
         } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.DEVICE), null,
+                    null,
+                    ActionType.ASSIGNED_TO_CUSTOMER, e, strDeviceId, strCustomerId);
             throw handleException(e);
         }
     }
@@ -132,8 +161,19 @@ public class DeviceController extends BaseController {
             if (device.getCustomerId() == null || device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
                 throw new IncorrectParameterException("Device isn't assigned to any customer!");
             }
-            return checkNotNull(deviceService.unassignDeviceFromCustomer(deviceId));
+            Customer customer = checkCustomerId(device.getCustomerId());
+
+            Device savedDevice = checkNotNull(deviceService.unassignDeviceFromCustomer(deviceId));
+
+            logEntityAction(deviceId, device,
+                    device.getCustomerId(),
+                    ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDeviceId, customer.getId().toString(), customer.getName());
+
+            return savedDevice;
         } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.DEVICE), null,
+                    null,
+                    ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDeviceId);
             throw handleException(e);
         }
     }
@@ -147,8 +187,17 @@ public class DeviceController extends BaseController {
             DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
             Device device = checkDeviceId(deviceId);
             Customer publicCustomer = customerService.findOrCreatePublicCustomer(device.getTenantId());
-            return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId()));
+            Device savedDevice = checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId()));
+
+            logEntityAction(deviceId, savedDevice,
+                    savedDevice.getCustomerId(),
+                    ActionType.ASSIGNED_TO_CUSTOMER, null, strDeviceId, publicCustomer.getId().toString(), publicCustomer.getName());
+
+            return savedDevice;
         } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.DEVICE), null,
+                    null,
+                    ActionType.ASSIGNED_TO_CUSTOMER, e, strDeviceId);
             throw handleException(e);
         }
     }
@@ -160,9 +209,16 @@ public class DeviceController extends BaseController {
         checkParameter(DEVICE_ID, strDeviceId);
         try {
             DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
-            checkDeviceId(deviceId);
-            return checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId));
+            Device device = checkDeviceId(deviceId);
+            DeviceCredentials deviceCredentials = checkNotNull(deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId));
+            logEntityAction(deviceId, device,
+                    device.getCustomerId(),
+                    ActionType.CREDENTIALS_READ, null, strDeviceId);
+            return deviceCredentials;
         } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.DEVICE), null,
+                    null,
+                    ActionType.CREDENTIALS_READ, e, strDeviceId);
             throw handleException(e);
         }
     }
@@ -173,11 +229,17 @@ 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());
+            logEntityAction(device.getId(), device,
+                    device.getCustomerId(),
+                    ActionType.CREDENTIALS_UPDATED, null, deviceCredentials);
             return result;
         } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.DEVICE), null,
+                    null,
+                    ActionType.CREDENTIALS_UPDATED, e, deviceCredentials);
             throw handleException(e);
         }
     }
@@ -306,5 +368,4 @@ public class DeviceController extends BaseController {
             throw handleException(e);
         }
     }
-
 }
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
index 1789da6..145b046 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
@@ -30,6 +30,7 @@ import org.springframework.web.context.request.async.DeferredResult;
 import org.thingsboard.server.actors.service.ActorService;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.controller.BaseController;
 import org.thingsboard.server.dao.model.ModelConstants;
@@ -68,7 +69,10 @@ public class PluginApiController extends BaseController {
                 if(tenantId != null && ModelConstants.NULL_UUID.equals(tenantId.getId())){
                     tenantId = null;
                 }
-                PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), tenantId, customerId);
+                UserId userId = getCurrentUser().getId();
+                String userName = getCurrentUser().getName();
+                PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(),
+                        tenantId, customerId, userId, userName);
                 actorService.process(new BasicPluginRestMsg(securityCtx, new RestRequest(requestEntity, request), result));
             } else {
                 result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN));
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java
index 727fd30..1312373 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java
@@ -28,6 +28,7 @@ import org.springframework.context.annotation.Lazy;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.config.WebSocketConfiguration;
 import org.thingsboard.server.extensions.api.plugins.PluginConstants;
 import org.thingsboard.server.service.security.model.SecurityUser;
@@ -151,8 +152,10 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug
             TenantId tenantId = currentUser.getTenantId();
             CustomerId customerId = currentUser.getCustomerId();
             if (PluginApiController.validatePluginAccess(pluginMd, tenantId, customerId)) {
+                UserId userId = currentUser.getId();
+                String userName = currentUser.getName();
                 PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), tenantId,
-                        currentUser.getCustomerId());
+                        currentUser.getCustomerId(), userId, userName);
                 return new BasicPluginWebsocketSessionRef(UUID.randomUUID().toString(), securityCtx, session.getUri(), session.getAttributes(),
                         session.getLocalAddress(), session.getRemoteAddress());
             } else {
diff --git a/application/src/main/java/org/thingsboard/server/controller/PluginController.java b/application/src/main/java/org/thingsboard/server/controller/PluginController.java
index 191fc6b..6217fef 100644
--- a/application/src/main/java/org/thingsboard/server/controller/PluginController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/PluginController.java
@@ -18,6 +18,8 @@ package org.thingsboard.server.controller;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.id.PluginId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
@@ -71,8 +73,17 @@ public class PluginController extends BaseController {
             PluginMetaData plugin = checkNotNull(pluginService.savePlugin(source));
             actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(),
                     created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+
+            logEntityAction(plugin.getId(), plugin,
+                    null,
+                    created ? ActionType.ADDED : ActionType.UPDATED, null);
+
             return plugin;
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.PLUGIN), source,
+                    null, source.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
+
             throw handleException(e);
         }
     }
@@ -87,7 +98,18 @@ public class PluginController extends BaseController {
             PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
             pluginService.activatePluginById(pluginId);
             actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.ACTIVATED);
+
+            logEntityAction(plugin.getId(), plugin,
+                    null,
+                    ActionType.ACTIVATED, null, strPluginId);
+
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.PLUGIN),
+                    null,
+                    null,
+                    ActionType.ACTIVATED, e, strPluginId);
+
             throw handleException(e);
         }
     }
@@ -102,7 +124,18 @@ public class PluginController extends BaseController {
             PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
             pluginService.suspendPluginById(pluginId);
             actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.SUSPENDED);
+
+            logEntityAction(plugin.getId(), plugin,
+                    null,
+                    ActionType.SUSPENDED, null, strPluginId);
+
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.PLUGIN),
+                    null,
+                    null,
+                    ActionType.SUSPENDED, e, strPluginId);
+
             throw handleException(e);
         }
     }
@@ -189,7 +222,16 @@ public class PluginController extends BaseController {
             PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
             pluginService.deletePluginById(pluginId);
             actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.DELETED);
+
+            logEntityAction(pluginId, plugin,
+                    null,
+                    ActionType.DELETED, null, strPluginId);
+
         } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.PLUGIN),
+                    null,
+                    null,
+                    ActionType.DELETED, e, strPluginId);
             throw handleException(e);
         }
     }
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleController.java b/application/src/main/java/org/thingsboard/server/controller/RuleController.java
index 84d1c8f..83e3b4b 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RuleController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleController.java
@@ -18,6 +18,8 @@ package org.thingsboard.server.controller;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.id.RuleId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
@@ -73,8 +75,17 @@ public class RuleController extends BaseController {
             RuleMetaData rule = checkNotNull(ruleService.saveRule(source));
             actorService.onRuleStateChange(rule.getTenantId(), rule.getId(),
                     created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+
+            logEntityAction(rule.getId(), rule,
+                    null,
+                    created ? ActionType.ADDED : ActionType.UPDATED, null);
+
             return rule;
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.RULE), source,
+                    null, source.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
+
             throw handleException(e);
         }
     }
@@ -89,7 +100,18 @@ public class RuleController extends BaseController {
             RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
             ruleService.activateRuleById(ruleId);
             actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.ACTIVATED);
+
+            logEntityAction(rule.getId(), rule,
+                    null,
+                    ActionType.ACTIVATED, null, strRuleId);
+
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.RULE),
+                    null,
+                    null,
+                    ActionType.ACTIVATED, e, strRuleId);
+
             throw handleException(e);
         }
     }
@@ -104,7 +126,18 @@ public class RuleController extends BaseController {
             RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
             ruleService.suspendRuleById(ruleId);
             actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.SUSPENDED);
+
+            logEntityAction(rule.getId(), rule,
+                    null,
+                    ActionType.SUSPENDED, null, strRuleId);
+
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.RULE),
+                    null,
+                    null,
+                    ActionType.SUSPENDED, e, strRuleId);
+
             throw handleException(e);
         }
     }
@@ -187,7 +220,18 @@ public class RuleController extends BaseController {
             RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
             ruleService.deleteRuleById(ruleId);
             actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.DELETED);
+
+            logEntityAction(ruleId, rule,
+                    null,
+                    ActionType.DELETED, null, strRuleId);
+
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.RULE),
+                    null,
+                    null,
+                    ActionType.DELETED, e, strRuleId);
+
             throw handleException(e);
         }
     }
diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java
index 05fca0b..95f5fdd 100644
--- a/application/src/main/java/org/thingsboard/server/controller/UserController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java
@@ -19,7 +19,9 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.id.UserId;
@@ -92,8 +94,17 @@ public class UserController extends BaseController {
                     throw e;
                 }
             }
+
+            logEntityAction(savedUser.getId(), savedUser,
+                    savedUser.getCustomerId(),
+                    user.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
+
             return savedUser;
         } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.USER), user,
+                    null, user.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
+
             throw handleException(e);
         }
     }
@@ -156,9 +167,18 @@ public class UserController extends BaseController {
         checkParameter(USER_ID, strUserId);
         try {
             UserId userId = new UserId(toUUID(strUserId));
-            checkUserId(userId);
+            User user = checkUserId(userId);
             userService.deleteUser(userId);
+
+            logEntityAction(userId, user,
+                    user.getCustomerId(),
+                    ActionType.DELETED, null, strUserId);
+
         } catch (Exception e) {
+            logEntityAction(emptyId(EntityType.USER),
+                    null,
+                    null,
+                    ActionType.DELETED, e, strUserId);
             throw handleException(e);
         }
     }
diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java
new file mode 100644
index 0000000..d0132f8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java
@@ -0,0 +1,34 @@
+/**
+ * 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.install;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
+
+import java.util.HashMap;
+
+@Configuration
+@Profile("install")
+public class ThingsboardInstallConfiguration {
+
+    @Bean
+    public AuditLogLevelFilter emptyAuditLogLevelFilter() {
+        return new AuditLogLevelFilter(new HashMap<>());
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
index bec7809..d918b22 100644
--- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
+++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
@@ -81,6 +81,8 @@ public class ThingsboardInstallService {
                     case "1.3.1":
                         log.info("Upgrading ThingsBoard from version 1.3.1 to 1.4.0 ...");
 
+                        databaseUpgradeService.upgradeDatabase("1.3.1");
+
                         log.info("Updating system data...");
 
                         systemDataLoaderService.deleteSystemWidgetBundle("charts");
diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
index 5b32374..dacf453 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
@@ -159,6 +159,12 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
                 break;
             case "1.3.0":
                 break;
+            case "1.3.1":
+                log.info("Updating schema ...");
+                schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.4.0", SCHEMA_UPDATE_CQL);
+                loadCql(schemaUpdateFile);
+                log.info("Schema updated.");
+                break;
             default:
                 throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion);
         }
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
index a8ffa99..098b4fe 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
@@ -61,6 +61,15 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
                 }
                 log.info("Schema updated.");
                 break;
+            case "1.3.1":
+                log.info("Updating schema ...");
+                schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.4.0", SCHEMA_UPDATE_SQL);
+                try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
+                    String sql = new String(Files.readAllBytes(schemaUpdateFile), Charset.forName("UTF-8"));
+                    conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
+                }
+                log.info("Schema updated.");
+                break;
             default:
                 throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
         }
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index de10135..f7a3a80 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -282,7 +282,6 @@ spring:
     username: "${SPRING_DATASOURCE_USERNAME:sa}"
     password: "${SPRING_DATASOURCE_PASSWORD:}"
 
-
 # PostgreSQL DAO Configuration
 #spring:
 #  data:
@@ -298,3 +297,23 @@ spring:
 #    url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard}"
 #    username: "${SPRING_DATASOURCE_USERNAME:postgres}"
 #    password: "${SPRING_DATASOURCE_PASSWORD:postgres}"
+
+# Audit log parameters
+audit_log:
+  # Enable/disable audit log functionality.
+  enabled: "${AUDIT_LOG_ENABLED:true}"
+  # Specify partitioning size for audit log by tenant id storage. Example MINUTES, HOURS, DAYS, MONTHS
+  by_tenant_partitioning: "${AUDIT_LOG_BY_TENANT_PARTITIONING:MONTHS}"
+  # Number of days as history period if startTime and endTime are not specified
+  default_query_period: "${AUDIT_LOG_DEFAULT_QUERY_PERIOD:30}"
+  # Logging levels per each entity type.
+  # Allowed values: OFF (disable), W (log write operations), RW (log read and write operations)
+  logging_level:
+    mask:
+      "device": "${AUDIT_LOG_MASK_DEVICE:W}"
+      "asset": "${AUDIT_LOG_MASK_ASSET:W}"
+      "dashboard": "${AUDIT_LOG_MASK_DASHBOARD:W}"
+      "customer": "${AUDIT_LOG_MASK_CUSTOMER:W}"
+      "user": "${AUDIT_LOG_MASK_USER:W}"
+      "rule": "${AUDIT_LOG_MASK_RULE:W}"
+      "plugin": "${AUDIT_LOG_MASK_PLUGIN:W}"
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 8d68bf8..2204fff 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;
@@ -336,6 +337,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..a826ee8
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java
@@ -0,0 +1,148 @@
+/**
+ * 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 org.thingsboard.server.dao.model.ModelConstants;
+
+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;
+    private User tenantAdmin;
+
+    @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);
+
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+
+        doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testAuditLogs() 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());
+
+        loadedAuditLogs = new ArrayList<>();
+        pageLink = new TimePageLink(23);
+        do {
+            pageData = doGetTypedWithTimePageLink("/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?",
+                    new TypeReference<TimePageData<AuditLog>>() {
+                    }, pageLink);
+            loadedAuditLogs.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Assert.assertEquals(178, loadedAuditLogs.size());
+
+        loadedAuditLogs = new ArrayList<>();
+        pageLink = new TimePageLink(23);
+        do {
+            pageData = doGetTypedWithTimePageLink("/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?",
+                    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 testAuditLogs_byTenantIdAndEntityId() 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/entity/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..7e1a976
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
@@ -0,0 +1,41 @@
+/**
+ * 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 lombok.Getter;
+
+@Getter
+public enum ActionType {
+    ADDED(false), // log entity
+    DELETED(false), // log string id
+    UPDATED(false), // log entity
+    ATTRIBUTES_UPDATED(false), // log attributes/values
+    ATTRIBUTES_DELETED(false), // log attributes
+    RPC_CALL(false), // log method and params
+    CREDENTIALS_UPDATED(false), // log new credentials
+    ASSIGNED_TO_CUSTOMER(false), // log customer name
+    UNASSIGNED_FROM_CUSTOMER(false), // log customer name
+    ACTIVATED(false), // log string id
+    SUSPENDED(false), // log string id
+    CREDENTIALS_READ(true), // log device id
+    ATTRIBUTES_READ(true); // log attributes
+
+    private final boolean isRead;
+
+    ActionType(boolean isRead) {
+        this.isRead = isRead;
+    }
+}
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..8562b6a
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java
@@ -0,0 +1,47 @@
+/**
+ * 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.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.UserId;
+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> saveByTenantIdAndCustomerId(AuditLog auditLog);
+
+    ListenableFuture<Void> saveByTenantIdAndUserId(AuditLog auditLog);
+
+    ListenableFuture<Void> savePartitionsByTenantId(AuditLog auditLog);
+
+    List<AuditLog> findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink);
+
+    List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink);
+
+    List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink);
+
+    List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java
new file mode 100644
index 0000000..0ae22e2
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java
@@ -0,0 +1,46 @@
+/**
+ * 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 org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AuditLogLevelFilter {
+
+    private Map<EntityType, AuditLogLevelMask> entityTypeMask = new HashMap<>();
+
+    public AuditLogLevelFilter(Map<String, String> mask) {
+        entityTypeMask.clear();
+        mask.forEach((entityTypeStr, logLevelMaskStr) -> {
+            EntityType entityType = EntityType.valueOf(entityTypeStr.toUpperCase());
+            AuditLogLevelMask logLevelMask = AuditLogLevelMask.valueOf(logLevelMaskStr.toUpperCase());
+            entityTypeMask.put(entityType, logLevelMask);
+        });
+    }
+
+    public boolean logEnabled(EntityType entityType, ActionType actionType) {
+        AuditLogLevelMask logLevelMask = entityTypeMask.get(entityType);
+        if (logLevelMask != null) {
+            return actionType.isRead() ? logLevelMask.isRead() : logLevelMask.isWrite();
+        } else {
+            return false;
+        }
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelMask.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelMask.java
new file mode 100644
index 0000000..ba5efb4
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelMask.java
@@ -0,0 +1,34 @@
+/**
+ * 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 lombok.Getter;
+
+@Getter
+public enum AuditLogLevelMask {
+
+    OFF(false, false),
+    W(true, false),
+    RW(true, true);
+
+    private final boolean write;
+    private final boolean read;
+
+    AuditLogLevelMask(boolean write, boolean read) {
+        this.write = write;
+        this.read = read;
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java
new file mode 100644
index 0000000..78b043b
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.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.dao.audit;
+
+import lombok.Getter;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.model.nosql.AuditLogEntity;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+public class AuditLogQueryCursor {
+    @Getter
+    private final UUID tenantId;
+    @Getter
+    private final List<AuditLogEntity> data;
+    @Getter
+    private final TimePageLink pageLink;
+
+    private final List<Long> partitions;
+
+    private int partitionIndex;
+    private int currentLimit;
+
+    public AuditLogQueryCursor(UUID tenantId, TimePageLink pageLink, List<Long> partitions) {
+        this.tenantId = tenantId;
+        this.partitions = partitions;
+        this.partitionIndex = partitions.size() - 1;
+        this.data = new ArrayList<>();
+        this.currentLimit = pageLink.getLimit();
+        this.pageLink = pageLink;
+    }
+
+    public boolean hasNextPartition() {
+        return partitionIndex >= 0;
+    }
+
+    public boolean isFull() {
+        return currentLimit <= 0;
+    }
+
+    public long getNextPartition() {
+        long partition = partitions.get(partitionIndex);
+        partitionIndex--;
+        return partition;
+    }
+
+    public int getCurrentLimit() {
+        return currentLimit;
+    }
+
+    public void addData(List<AuditLogEntity> newData) {
+        currentLimit -= newData.size();
+        data.addAll(newData);
+    }
+}
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..86cbbba
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java
@@ -0,0 +1,53 @@
+/**
+ * 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.BaseData;
+import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.User;
+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.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+
+import java.util.List;
+
+public interface AuditLogService {
+
+    TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink);
+
+    TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink);
+
+    TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink);
+
+    TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink);
+
+    <E extends BaseData<I> & HasName,
+            I extends UUIDBased & EntityId> ListenableFuture<List<Void>> logEntityAction(
+                                                        TenantId tenantId,
+                                                        CustomerId customerId,
+                                                        UserId userId,
+                                                        String userName,
+                                                        I entityId,
+                                                        E entity,
+                                                        ActionType actionType,
+                                                        Exception e, Object... additionalInfo);
+
+}
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..ab1c313
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
@@ -0,0 +1,316 @@
+/**
+ * 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.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+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.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.HasName;
+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.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.dao.entity.EntityService;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.service.DataValidator;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.List;
+
+import static org.thingsboard.server.dao.service.Validator.validateEntityId;
+import static org.thingsboard.server.dao.service.Validator.validateId;
+
+@Slf4j
+@Service
+@ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "true")
+public class AuditLogServiceImpl implements AuditLogService {
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    private static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
+    private static final int INSERTS_PER_ENTRY = 3;
+
+    @Autowired
+    private AuditLogLevelFilter auditLogLevelFilter;
+
+    @Autowired
+    private AuditLogDao auditLogDao;
+
+    @Autowired
+    private EntityService entityService;
+
+    @Override
+    public TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) {
+        log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink);
+        validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
+        validateId(customerId, "Incorrect customerId " + customerId);
+        List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndCustomerId(tenantId.getId(), customerId, pageLink);
+        return new TimePageData<>(auditLogs, pageLink);
+    }
+
+    @Override
+    public TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) {
+        log.trace("Executing findAuditLogsByTenantIdAndUserId [{}], [{}], [{}]", tenantId, userId, pageLink);
+        validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
+        validateId(userId, "Incorrect userId" + userId);
+        List<AuditLog> auditLogs = auditLogDao.findAuditLogsByTenantIdAndUserId(tenantId.getId(), userId, pageLink);
+        return new TimePageData<>(auditLogs, pageLink);
+    }
+
+    @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);
+    }
+
+    @Override
+    public <E extends BaseData<I> & HasName, I extends UUIDBased & EntityId> ListenableFuture<List<Void>>
+        logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity,
+                               ActionType actionType, Exception e, Object... additionalInfo) {
+        if (canLog(entityId.getEntityType(), actionType)) {
+            JsonNode actionData = constructActionData(entityId, entity, actionType, additionalInfo);
+            ActionStatus actionStatus = ActionStatus.SUCCESS;
+            String failureDetails = "";
+            String entityName = "";
+            if (entity != null) {
+                entityName = entity.getName();
+            } else {
+                try {
+                    entityName = entityService.fetchEntityNameAsync(entityId).get();
+                } catch (Exception ex) {}
+            }
+            if (e != null) {
+                actionStatus = ActionStatus.FAILURE;
+                failureDetails = getFailureStack(e);
+            }
+            if (actionType == ActionType.RPC_CALL) {
+                String rpcErrorString = extractParameter(String.class, additionalInfo);
+                if (!StringUtils.isEmpty(rpcErrorString)) {
+                    actionStatus = ActionStatus.FAILURE;
+                    failureDetails = rpcErrorString;
+                }
+            }
+            return logAction(tenantId,
+                    entityId,
+                    entityName,
+                    customerId,
+                    userId,
+                    userName,
+                    actionType,
+                    actionData,
+                    actionStatus,
+                    failureDetails);
+        } else {
+            return null;
+        }
+    }
+
+    private <E extends BaseData<I> & HasName, I extends UUIDBased & EntityId> JsonNode constructActionData(I entityId,
+                                                                                                           E entity,
+                                                                                                           ActionType actionType,
+                                                                                                           Object... additionalInfo) {
+        ObjectNode actionData = objectMapper.createObjectNode();
+        switch(actionType) {
+            case ADDED:
+            case UPDATED:
+                ObjectNode entityNode = objectMapper.valueToTree(entity);
+                if (entityId.getEntityType() == EntityType.DASHBOARD) {
+                    entityNode.put("configuration", "");
+                }
+                actionData.set("entity", entityNode);
+                break;
+            case DELETED:
+            case ACTIVATED:
+            case SUSPENDED:
+            case CREDENTIALS_READ:
+                String strEntityId = extractParameter(String.class, additionalInfo);
+                actionData.put("entityId", strEntityId);
+                break;
+            case ATTRIBUTES_UPDATED:
+                actionData.put("entityId", entityId.toString());
+                String scope = extractParameter(String.class, 0, additionalInfo);
+                List<AttributeKvEntry> attributes = extractParameter(List.class, 1, additionalInfo);
+                actionData.put("scope", scope);
+                ObjectNode attrsNode = objectMapper.createObjectNode();
+                if (attributes != null) {
+                    for (AttributeKvEntry attr : attributes) {
+                        attrsNode.put(attr.getKey(), attr.getValueAsString());
+                    }
+                }
+                actionData.set("attributes", attrsNode);
+                break;
+            case ATTRIBUTES_DELETED:
+            case ATTRIBUTES_READ:
+                actionData.put("entityId", entityId.toString());
+                scope = extractParameter(String.class, 0, additionalInfo);
+                actionData.put("scope", scope);
+                List<String> keys = extractParameter(List.class, 1, additionalInfo);
+                ArrayNode attrsArrayNode =  actionData.putArray("attributes");
+                if (keys != null) {
+                    keys.forEach(attrsArrayNode::add);
+                }
+                break;
+            case RPC_CALL:
+                actionData.put("entityId", entityId.toString());
+                Boolean oneWay = extractParameter(Boolean.class, 1, additionalInfo);
+                String method = extractParameter(String.class, 2, additionalInfo);
+                String params = extractParameter(String.class, 3, additionalInfo);
+                actionData.put("oneWay", oneWay);
+                actionData.put("method", method);
+                actionData.put("params", params);
+                break;
+            case CREDENTIALS_UPDATED:
+                actionData.put("entityId", entityId.toString());
+                DeviceCredentials deviceCredentials = extractParameter(DeviceCredentials.class, additionalInfo);
+                actionData.set("credentials", objectMapper.valueToTree(deviceCredentials));
+                break;
+            case ASSIGNED_TO_CUSTOMER:
+                strEntityId = extractParameter(String.class, 0, additionalInfo);
+                String strCustomerId = extractParameter(String.class, 1, additionalInfo);
+                String strCustomerName = extractParameter(String.class, 2, additionalInfo);
+                actionData.put("entityId", strEntityId);
+                actionData.put("assignedCustomerId", strCustomerId);
+                actionData.put("assignedCustomerName", strCustomerName);
+                break;
+            case UNASSIGNED_FROM_CUSTOMER:
+                strEntityId = extractParameter(String.class, 0, additionalInfo);
+                strCustomerId = extractParameter(String.class, 1, additionalInfo);
+                strCustomerName = extractParameter(String.class, 2, additionalInfo);
+                actionData.put("entityId", strEntityId);
+                actionData.put("unassignedCustomerId", strCustomerId);
+                actionData.put("unassignedCustomerName", strCustomerName);
+                break;
+        }
+        return actionData;
+    }
+
+    private <T> T extractParameter(Class<T> clazz, Object... additionalInfo) {
+        return extractParameter(clazz, 0, additionalInfo);
+    }
+
+    private <T> T extractParameter(Class<T> clazz, int index, Object... additionalInfo) {
+        T result = null;
+        if (additionalInfo != null && additionalInfo.length > index) {
+            Object paramObject = additionalInfo[index];
+            if (clazz.isInstance(paramObject)) {
+                result = clazz.cast(paramObject);
+            }
+        }
+        return result;
+    }
+
+    private String getFailureStack(Exception e) {
+        StringWriter sw = new StringWriter();
+        e.printStackTrace(new PrintWriter(sw));
+        return sw.toString();
+    }
+
+    private boolean canLog(EntityType entityType, ActionType actionType) {
+        return auditLogLevelFilter.logEnabled(entityType, actionType);
+    }
+
+    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.setId(new AuditLogId(UUIDs.timeBased()));
+        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;
+    }
+
+    private 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));
+        futures.add(auditLogDao.saveByTenantIdAndCustomerId(auditLogEntry));
+        futures.add(auditLogDao.saveByTenantIdAndUserId(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..fa32b5d
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
@@ -0,0 +1,349 @@
+/**
+ * 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.querybuilder.QueryBuilder;
+import com.datastax.driver.core.querybuilder.Select;
+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.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.UserId;
+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.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+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("${audit_log.by_tenant_partitioning}")
+    private String partitioning;
+    private TsPartitionDate tsFormat;
+
+    @Value("${audit_log.default_query_period}")
+    private Integer defaultQueryPeriodInDays;
+
+    private PreparedStatement partitionInsertStmt;
+    private PreparedStatement saveByTenantStmt;
+    private PreparedStatement saveByTenantIdAndUserIdStmt;
+    private PreparedStatement saveByTenantIdAndEntityIdStmt;
+    private PreparedStatement saveByTenantIdAndCustomerIdStmt;
+
+    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);
+
+        long partition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli());
+        BoundStatement stmt = getSaveByTenantStmt().bind();
+        stmt = setSaveStmtVariables(stmt, auditLog, partition);
+        return getFuture(executeAsyncWrite(stmt), rs -> null);
+    }
+
+    @Override
+    public ListenableFuture<Void> saveByTenantIdAndEntityId(AuditLog auditLog) {
+        log.debug("Save saveByTenantIdAndEntityId [{}] ", auditLog);
+
+        BoundStatement stmt = getSaveByTenantIdAndEntityIdStmt().bind();
+        stmt = setSaveStmtVariables(stmt, auditLog, -1);
+        return getFuture(executeAsyncWrite(stmt), rs -> null);
+    }
+
+    @Override
+    public ListenableFuture<Void> saveByTenantIdAndCustomerId(AuditLog auditLog) {
+        log.debug("Save saveByTenantIdAndCustomerId [{}] ", auditLog);
+
+        BoundStatement stmt = getSaveByTenantIdAndCustomerIdStmt().bind();
+        stmt = setSaveStmtVariables(stmt, auditLog, -1);
+        return getFuture(executeAsyncWrite(stmt), rs -> null);
+    }
+
+    @Override
+    public ListenableFuture<Void> saveByTenantIdAndUserId(AuditLog auditLog) {
+        log.debug("Save saveByTenantIdAndUserId [{}] ", auditLog);
+
+        BoundStatement stmt = getSaveByTenantIdAndUserIdStmt().bind();
+        stmt = setSaveStmtVariables(stmt, auditLog, -1);
+        return getFuture(executeAsyncWrite(stmt), rs -> null);
+    }
+
+    private BoundStatement setSaveStmtVariables(BoundStatement stmt, AuditLog auditLog, long partition) {
+         stmt.setUUID(0, auditLog.getId().getId())
+                .setUUID(1, auditLog.getTenantId().getId())
+                .setUUID(2, auditLog.getCustomerId().getId())
+                .setUUID(3, auditLog.getEntityId().getId())
+                .setString(4, auditLog.getEntityId().getEntityType().name())
+                .setString(5, auditLog.getEntityName())
+                .setUUID(6, auditLog.getUserId().getId())
+                .setString(7, auditLog.getUserName())
+                .setString(8, auditLog.getActionType().name())
+                .setString(9, auditLog.getActionData() != null ? auditLog.getActionData().toString() : null)
+                .setString(10, auditLog.getActionStatus().name())
+                .setString(11, auditLog.getActionFailureDetails());
+        if (partition > -1) {
+            stmt.setLong(12, partition);
+        }
+        return stmt;
+    }
+
+    @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 getSaveByTenantStmt() {
+        if (saveByTenantStmt == null) {
+            saveByTenantStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_TENANT_ID_CF, true);
+        }
+        return saveByTenantStmt;
+    }
+
+    private PreparedStatement getSaveByTenantIdAndEntityIdStmt() {
+        if (saveByTenantIdAndEntityIdStmt == null) {
+            saveByTenantIdAndEntityIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF, false);
+        }
+        return saveByTenantIdAndEntityIdStmt;
+    }
+
+    private PreparedStatement getSaveByTenantIdAndCustomerIdStmt() {
+        if (saveByTenantIdAndCustomerIdStmt == null) {
+            saveByTenantIdAndCustomerIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_CUSTOMER_ID_CF, false);
+        }
+        return saveByTenantIdAndCustomerIdStmt;
+    }
+
+    private PreparedStatement getSaveByTenantIdAndUserIdStmt() {
+        if (saveByTenantIdAndUserIdStmt == null) {
+            saveByTenantIdAndUserIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_USER_ID_CF, false);
+        }
+        return saveByTenantIdAndUserIdStmt;
+    }
+
+    private PreparedStatement getSaveByTenantIdAndCFName(String cfName, boolean hasPartition) {
+        List columnsList = new ArrayList();
+        columnsList.add(ModelConstants.AUDIT_LOG_ID_PROPERTY);
+        columnsList.add(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY);
+        columnsList.add(ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY);
+        columnsList.add(ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY);
+        columnsList.add(ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY);
+        columnsList.add(ModelConstants.AUDIT_LOG_ENTITY_NAME_PROPERTY);
+        columnsList.add(ModelConstants.AUDIT_LOG_USER_ID_PROPERTY);
+        columnsList.add(ModelConstants.AUDIT_LOG_USER_NAME_PROPERTY);
+        columnsList.add(ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY);
+        columnsList.add(ModelConstants.AUDIT_LOG_ACTION_DATA_PROPERTY);
+        columnsList.add(ModelConstants.AUDIT_LOG_ACTION_STATUS_PROPERTY);
+        columnsList.add(ModelConstants.AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY);
+        if (hasPartition) {
+            columnsList.add(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY);
+        }
+        StringJoiner values = new StringJoiner(",");
+        for (int i=0;i<columnsList.size();i++) {
+            values.add("?");
+        }
+        String statementString = INSERT_INTO + cfName + " (" + String.join(",", columnsList) + ") VALUES (" + values.toString() + ")";
+        return getSession().prepare(statementString);
+    }
+
+    private PreparedStatement getPartitionInsertStmt() {
+        if (partitionInsertStmt == null) {
+            partitionInsertStmt = getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF +
+                    "(" + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY +
+                    "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" +
+                    " VALUES(?, ?)");
+        }
+        return partitionInsertStmt;
+    }
+
+    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> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) {
+        log.trace("Try to find audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink);
+        List<AuditLogEntity> entities = findPageWithTimeSearch(AUDIT_LOG_BY_CUSTOMER_ID_CF,
+                Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId),
+                        eq(ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY, customerId.getId())),
+                pageLink);
+        log.trace("Found audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink);
+        return DaoUtil.convertDataList(entities);
+    }
+
+    @Override
+    public List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) {
+        log.trace("Try to find audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink);
+        List<AuditLogEntity> entities = findPageWithTimeSearch(AUDIT_LOG_BY_USER_ID_CF,
+                Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId),
+                        eq(ModelConstants.AUDIT_LOG_USER_ID_PROPERTY, userId.getId())),
+                pageLink);
+        log.trace("Found audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, 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);
+
+        long minPartition;
+        if (pageLink.getStartTime() != null && pageLink.getStartTime() != 0) {
+            minPartition = toPartitionTs(pageLink.getStartTime());
+        } else {
+            minPartition = toPartitionTs(LocalDate.now().minusDays(defaultQueryPeriodInDays).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli());
+        }
+
+        long maxPartition;
+        if (pageLink.getEndTime() != null && pageLink.getEndTime() != 0) {
+            maxPartition = toPartitionTs(pageLink.getEndTime());
+        } else {
+            maxPartition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli());
+        }
+
+        List<Long> partitions = fetchPartitions(tenantId, minPartition, maxPartition)
+                .all()
+                .stream()
+                .map(row -> row.getLong(ModelConstants.PARTITION_COLUMN))
+                .collect(Collectors.toList());
+
+        AuditLogQueryCursor cursor = new AuditLogQueryCursor(tenantId, pageLink, partitions);
+        List<AuditLogEntity> entities = fetchSequentiallyWithLimit(cursor);
+        log.trace("Found audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink);
+        return DaoUtil.convertDataList(entities);
+    }
+
+    private List<AuditLogEntity> fetchSequentiallyWithLimit(AuditLogQueryCursor cursor) {
+        if (cursor.isFull() || !cursor.hasNextPartition()) {
+            return cursor.getData();
+        } else {
+            cursor.addData(findPageWithTimeSearch(AUDIT_LOG_BY_TENANT_ID_CF,
+                    Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, cursor.getTenantId()),
+                            eq(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY, cursor.getNextPartition())),
+                    cursor.getPageLink()));
+            return fetchSequentiallyWithLimit(cursor);
+        }
+    }
+
+    private ResultSet fetchPartitions(UUID tenantId, long minPartition, long maxPartition) {
+        Select.Where select = QueryBuilder.select(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY).from(ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF)
+                .where(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId));
+        select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition));
+        select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition));
+        return getSession().execute(select);
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
new file mode 100644
index 0000000..885cd2f
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java
@@ -0,0 +1,63 @@
+/**
+ * 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.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.User;
+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.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+
+import java.util.Collections;
+import java.util.List;
+
+@ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "false")
+public class DummyAuditLogServiceImpl implements AuditLogService {
+
+    @Override
+    public TimePageData<AuditLog> findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) {
+        return new TimePageData<>(null, pageLink);
+    }
+
+    @Override
+    public TimePageData<AuditLog> findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) {
+        return new TimePageData<>(null, pageLink);
+    }
+
+    @Override
+    public TimePageData<AuditLog> findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) {
+        return new TimePageData<>(null, pageLink);
+    }
+
+    @Override
+    public TimePageData<AuditLog> findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) {
+        return new TimePageData<>(null, pageLink);
+    }
+
+    @Override
+    public <E extends BaseData<I> & HasName, I extends UUIDBased & EntityId> ListenableFuture<List<Void>> logEntityAction(TenantId tenantId, CustomerId customerId, UserId userId, String userName, I entityId, E entity, ActionType actionType, Exception e, Object... additionalInfo) {
+        return null;
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java b/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
index f37fa93..5d6c8e6 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
@@ -20,9 +20,6 @@ import com.datastax.driver.core.*;
 import com.datastax.driver.core.ProtocolOptions.Compression;
 import com.datastax.driver.mapping.Mapper;
 import com.datastax.driver.mapping.MappingManager;
-import lombok.AccessLevel;
-import lombok.Data;
-import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -36,7 +33,6 @@ import java.util.Collections;
 import java.util.List;
 
 @Slf4j
-@Data
 public abstract class AbstractCassandraCluster {
 
     private static final String COMMA = ",";
@@ -77,7 +73,7 @@ public abstract class AbstractCassandraCluster {
     private Cluster cluster;
     private Cluster.Builder clusterBuilder;
 
-    @Getter(AccessLevel.NONE) private Session session;
+    private Session session;
 
     private MappingManager mappingManager;
 
@@ -115,6 +111,10 @@ public abstract class AbstractCassandraCluster {
         }
     }
 
+    public Cluster getCluster() {
+        return cluster;
+    }
+
     public Session getSession() {
         if (!isInstall()) {
             return session;
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..66e03b0 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,31 @@ 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_CUSTOMER_ID_CF = "audit_log_by_customer_id";
+    public static final String AUDIT_LOG_BY_USER_ID_CF = "audit_log_by_user_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 +342,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..4ad54f5
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
@@ -0,0 +1,139 @@
+/**
+ * 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.core.utils.UUIDs;
+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));
+        auditLog.setCreatedTime(UUIDs.unixTimestamp(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..6dd70b2
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java
@@ -0,0 +1,135 @@
+/**
+ * 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.*;
+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();
+        }
+        if (auditLog.getUserId() != null) {
+            this.userId = toString(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(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()));
+        }
+        if (userId != null) {
+            auditLog.setUserId(new UserId(toUUID(entityId)));
+        }
+        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..52dbd67
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java
@@ -0,0 +1,24 @@
+/**
+ * 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.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.repository.CrudRepository;
+import org.thingsboard.server.dao.model.sql.AuditLogEntity;
+
+public interface AuditLogRepository extends CrudRepository<AuditLogEntity, String>, JpaSpecificationExecutor<AuditLogEntity> {
+
+}
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..14b5271
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
@@ -0,0 +1,155 @@
+/**
+ * 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.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.UUIDConverter;
+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.UserId;
+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.sql.JpaAbstractSearchTimeDao;
+import org.thingsboard.server.dao.util.SqlDao;
+
+import javax.annotation.PreDestroy;
+import javax.persistence.criteria.Predicate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+
+import static org.springframework.data.jpa.domain.Specifications.where;
+import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
+
+@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> saveByTenantIdAndCustomerId(AuditLog auditLog) {
+        return insertService.submit(() -> null);
+    }
+
+    @Override
+    public ListenableFuture<Void> saveByTenantIdAndUserId(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 findAuditLogs(tenantId, entityId, null, null, pageLink);
+    }
+
+    @Override
+    public List<AuditLog> findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) {
+        return findAuditLogs(tenantId, null, customerId, null, pageLink);
+    }
+
+    @Override
+    public List<AuditLog> findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) {
+        return findAuditLogs(tenantId, null, null, userId, pageLink);
+    }
+
+    @Override
+    public List<AuditLog> findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) {
+        return findAuditLogs(tenantId, null, null, null, pageLink);
+    }
+
+    private List<AuditLog> findAuditLogs(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId, TimePageLink pageLink) {
+        Specification<AuditLogEntity> timeSearchSpec = JpaAbstractSearchTimeDao.getTimeSearchPageSpec(pageLink, "id");
+        Specification<AuditLogEntity> fieldsSpec = getEntityFieldsSpec(tenantId, entityId, customerId, userId);
+        Sort.Direction sortDirection = pageLink.isAscOrder() ? Sort.Direction.ASC : Sort.Direction.DESC;
+        Pageable pageable = new PageRequest(0, pageLink.getLimit(), sortDirection, ID_PROPERTY);
+        return DaoUtil.convertDataList(auditLogRepository.findAll(where(timeSearchSpec).and(fieldsSpec), pageable).getContent());
+    }
+
+    private Specification<AuditLogEntity> getEntityFieldsSpec(UUID tenantId, EntityId entityId, CustomerId customerId, UserId userId) {
+        return (root, criteriaQuery, criteriaBuilder) -> {
+            List<Predicate> predicates = new ArrayList<>();
+            if (tenantId != null) {
+                Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("tenantId"), UUIDConverter.fromTimeUUID(tenantId));
+                predicates.add(tenantIdPredicate);
+            }
+            if (entityId != null) {
+                Predicate entityTypePredicate = criteriaBuilder.equal(root.get("entityType"), entityId.getEntityType());
+                predicates.add(entityTypePredicate);
+                Predicate entityIdPredicate = criteriaBuilder.equal(root.get("entityId"), UUIDConverter.fromTimeUUID(entityId.getId()));
+                predicates.add(entityIdPredicate);
+            }
+            if (customerId != null) {
+                Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("customerId"), UUIDConverter.fromTimeUUID(customerId.getId()));
+                predicates.add(tenantIdPredicate);
+            }
+            if (userId != null) {
+                Predicate tenantIdPredicate = criteriaBuilder.equal(root.get("userId"), UUIDConverter.fromTimeUUID(userId.getId()));
+                predicates.add(tenantIdPredicate);
+            }
+            return criteriaBuilder.and(predicates.toArray(new Predicate[]{}));
+        };
+    }
+}
diff --git a/dao/src/main/resources/cassandra/schema.cql b/dao/src/main/resources/cassandra/schema.cql
index dda8067..bc7884d 100644
--- a/dao/src/main/resources/cassandra/schema.cql
+++ b/dao/src/main/resources/cassandra/schema.cql
@@ -548,3 +548,78 @@ 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_customer_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, customer_id), id)
+);
+
+CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_user_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, user_id), 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..1f739f8 100644
--- a/dao/src/main/resources/sql/schema.sql
+++ b/dao/src/main/resources/sql/schema.sql
@@ -47,6 +47,21 @@ 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(1000000),
+    action_status varchar(255),
+    action_failure_details varchar(1000000)
+);
+
 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/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
index e0b5f24..7fc9c67 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.fasterxml.jackson.databind.node.TextNode;
 import org.junit.runner.RunWith;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.test.annotation.DirtiesContext;
@@ -29,6 +30,7 @@ import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringRunner;
 import org.springframework.test.context.support.AnnotationConfigContextLoader;
 import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.Event;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -40,6 +42,8 @@ import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.common.data.rule.RuleMetaData;
 import org.thingsboard.server.dao.alarm.AlarmService;
 import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
+import org.thingsboard.server.dao.audit.AuditLogLevelMask;
 import org.thingsboard.server.dao.component.ComponentDescriptorService;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.dashboard.DashboardService;
@@ -58,6 +62,8 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
 
 import java.io.IOException;
 import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ThreadLocalRandom;
 
@@ -227,4 +233,14 @@ public abstract class AbstractServiceTest {
         oNode.set("configuration", readFromResource(configuration));
         return oNode;
     }
+
+    @Bean
+    public AuditLogLevelFilter auditLogLevelFilter() {
+        Map<String,String> mask = new HashMap<>();
+        for (EntityType entityType : EntityType.values()) {
+            mask.put(entityType.name().toLowerCase(), AuditLogLevelMask.RW.name());
+        }
+        return new AuditLogLevelFilter(mask);
+    }
+
 }
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/application-test.properties b/dao/src/test/resources/application-test.properties
index 3466d66..21f1794 100644
--- a/dao/src/test/resources/application-test.properties
+++ b/dao/src/test/resources/application-test.properties
@@ -4,6 +4,10 @@ zk.zk_dir=/thingsboard
 
 updates.enabled=false
 
+audit_log.enabled=true
+audit_log.by_tenant_partitioning=MONTHS
+audit_log.default_query_period=30
+
 cache.type=caffeine
 #cache.type=redis
 
@@ -16,7 +20,10 @@ caffeine.specs.deviceCredentials.maxSize=100000
 caffeine.specs.devices.timeToLiveInMinutes=1440
 caffeine.specs.devices.maxSize=100000
 
+caching.specs.devices.timeToLiveInMinutes=1440
+caching.specs.devices.maxSize=100000
+
 redis.connection.host=localhost
 redis.connection.port=6379
 redis.connection.db=0
-redis.connection.password=
\ No newline at end of file
+redis.connection.password=
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;
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java
index f9269de..b81780b 100644
--- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.extensions.api.plugins.msg;
 import lombok.Data;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
 
 import java.io.Serializable;
 import java.util.UUID;
@@ -28,6 +29,7 @@ import java.util.UUID;
 @Data
 public class ToDeviceRpcRequest implements Serializable {
     private final UUID id;
+    private final PluginApiCallSecurityContext securityCtx;
     private final TenantId tenantId;
     private final DeviceId deviceId;
     private final boolean oneway;
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java
index 32600d3..3ca662d 100644
--- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java
@@ -15,10 +15,7 @@
  */
 package org.thingsboard.server.extensions.api.plugins;
 
-import org.thingsboard.server.common.data.id.CustomerId;
-import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.*;
 
 import java.io.Serializable;
 
@@ -30,13 +27,18 @@ public final class PluginApiCallSecurityContext implements Serializable {
     private final PluginId pluginId;
     private final TenantId tenantId;
     private final CustomerId customerId;
+    private final UserId userId;
+    private final String userName;
 
-    public PluginApiCallSecurityContext(TenantId pluginTenantId, PluginId pluginId, TenantId tenantId, CustomerId customerId) {
+    public PluginApiCallSecurityContext(TenantId pluginTenantId, PluginId pluginId, TenantId tenantId, CustomerId customerId,
+                                        UserId userId, String userName) {
         super();
         this.pluginTenantId = pluginTenantId;
         this.pluginId = pluginId;
         this.tenantId = tenantId;
         this.customerId = customerId;
+        this.userId = userId;
+        this.userName = userName;
     }
 
     public TenantId getPluginTenantId(){
@@ -67,4 +69,12 @@ public final class PluginApiCallSecurityContext implements Serializable {
         return customerId;
     }
 
+    public UserId getUserId() {
+        return userId;
+    }
+
+    public String getUserName() {
+        return userName;
+    }
+
 }
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
index c825594..d1a2927 100644
--- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
@@ -24,9 +24,7 @@ import org.thingsboard.server.common.data.kv.TsKvQuery;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
 import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
 import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
 import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
@@ -60,6 +58,7 @@ public interface PluginContext {
 
     void scheduleTimeoutMsg(TimeoutMsg<?> timeoutMsg);
 
+    void logRpcRequest(PluginApiCallSecurityContext ctx, DeviceId deviceId, ToDeviceRpcRequestBody body, boolean oneWay, Optional<RpcError> rpcError, Exception e);
 
     /*
         Websocket API
@@ -96,6 +95,12 @@ public interface PluginContext {
         Attributes API
      */
 
+    void logAttributesUpdated(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<AttributeKvEntry> attributes, Exception e);
+
+    void logAttributesDeleted(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e);
+
+    void logAttributesRead(PluginApiCallSecurityContext ctx, EntityId entityId, String attributeType, List<String> keys, Exception e);
+
     void saveAttributes(TenantId tenantId, EntityId entityId, String attributeType, List<AttributeKvEntry> attributes, PluginCallback<Void> callback);
 
     void removeAttributes(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys, PluginCallback<Void> callback);
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java
index 4684337..7438a27 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java
@@ -152,7 +152,7 @@ public class DeviceMessagingRuleMsgHandler implements RuleMsgHandler {
                         pendingMsgs.put(uid, requestMd);
                         log.trace("[{}] Forwarding {} to [{}]", uid, params, targetDeviceId);
                         ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(ON_MSG_METHOD_NAME, GSON.toJson(params.get("body")));
-                        ctx.sendRpcRequest(new ToDeviceRpcRequest(uid, targetDevice.getTenantId(), targetDeviceId, oneWay, System.currentTimeMillis() + timeout, requestBody));
+                        ctx.sendRpcRequest(new ToDeviceRpcRequest(uid, null, targetDevice.getTenantId(), targetDeviceId, oneWay, System.currentTimeMillis() + timeout, requestBody));
                     } else {
                         replyWithError(ctx, requestMd, RpcError.FORBIDDEN);
                     }
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java
index f88b796..476154d 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java
@@ -94,11 +94,12 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler {
 
     private boolean handleDeviceRPCRequest(PluginContext ctx, final PluginRestMsg msg, TenantId tenantId, DeviceId deviceId, RpcRequest cmd, boolean oneWay) throws JsonProcessingException {
         long timeout = System.currentTimeMillis() + (cmd.getTimeout() != null ? cmd.getTimeout() : defaultTimeout);
+        ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData());
         ctx.checkAccess(deviceId, new PluginCallback<Void>() {
             @Override
             public void onSuccess(PluginContext ctx, Void value) {
-                ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData());
                 ToDeviceRpcRequest rpcRequest = new ToDeviceRpcRequest(UUID.randomUUID(),
+                        msg.getSecurityCtx(),
                         tenantId,
                         deviceId,
                         oneWay,
@@ -116,15 +117,17 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler {
                 } else {
                     response = new ResponseEntity(HttpStatus.UNAUTHORIZED);
                 }
+                ctx.logRpcRequest(msg.getSecurityCtx(), deviceId, body, oneWay, Optional.empty(), e);
                 msg.getResponseHolder().setResult(response);
             }
         });
         return true;
     }
 
-    public void reply(PluginContext ctx, DeferredResult<ResponseEntity> responseWriter, FromDeviceRpcResponse response) {
+    public void reply(PluginContext ctx, ToDeviceRpcRequest rpcRequest, DeferredResult<ResponseEntity> responseWriter, FromDeviceRpcResponse response) {
         Optional<RpcError> rpcError = response.getError();
         if (rpcError.isPresent()) {
+            ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, null);
             RpcError error = rpcError.get();
             switch (error) {
                 case TIMEOUT:
@@ -142,12 +145,15 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler {
             if (responseData.isPresent() && !StringUtils.isEmpty(responseData.get())) {
                 String data = responseData.get();
                 try {
+                    ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, null);
                     responseWriter.setResult(new ResponseEntity<>(jsonMapper.readTree(data), HttpStatus.OK));
                 } catch (IOException e) {
                     log.debug("Failed to decode device response: {}", data, e);
+                    ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, e);
                     responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE));
                 }
             } else {
+                ctx.logRpcRequest(rpcRequest.getSecurityCtx(), rpcRequest.getDeviceId(), rpcRequest.getBody(), rpcRequest.isOneway(), rpcError, null);
                 responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK));
             }
         }
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java
index f193c01..7db2acb 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRuleMsgHandler.java
@@ -77,7 +77,7 @@ public class RpcRuleMsgHandler implements RuleMsgHandler {
                             @Override
                             public void onSuccess(PluginContext ctx, Void value) {
                                 ctx.sendRpcRequest(new ToDeviceRpcRequest(UUID.randomUUID(),
-                                        tenantId, tmpId, true, expirationTime, body)
+                                        null, tenantId, tmpId, true, expirationTime, body)
                                 );
                                 log.trace("[{}] Sent RPC Call Action msg", tmpId);
                             }
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java
index 25f7238..193d528 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java
@@ -49,7 +49,7 @@ public class RpcManager {
         LocalRequestMetaData md = localRpcRequests.remove(requestId);
         if (md != null) {
             log.trace("[{}] Processing local rpc response from device [{}]", requestId, md.getRequest().getDeviceId());
-            restHandler.reply(ctx, md.getResponseWriter(), response);
+            restHandler.reply(ctx, md.getRequest(), md.getResponseWriter(), response);
         } else {
             log.trace("[{}] Unknown or stale rpc response received [{}]", requestId, response);
         }
@@ -62,7 +62,7 @@ public class RpcManager {
             LocalRequestMetaData md = localRpcRequests.remove(requestId);
             if (md != null) {
                 log.trace("[{}] Processing rpc timeout for local device [{}]", requestId, md.getRequest().getDeviceId());
-                restHandler.reply(ctx, md.getResponseWriter(), timeoutReponse);
+                restHandler.reply(ctx, md.getRequest(), md.getResponseWriter(), timeoutReponse);
             }
         }
     }
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
index 7e92fc1..1fa034b 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.data.kv.*;
 import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
 import org.thingsboard.server.common.transport.adaptor.JsonConverter;
@@ -150,18 +151,19 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
     private void handleHttpGetAttributesValues(PluginContext ctx, PluginRestMsg msg,
                                                RestRequest request, String scope, EntityId entityId) throws ServletException {
         String keys = request.getParameter("keys", "");
-
-        PluginCallback<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg);
+        List<String> keyList = null;
+        if (!StringUtils.isEmpty(keys)) {
+            keyList = Arrays.asList(keys.split(","));
+        }
+        PluginCallback<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg, scope, entityId, keyList);
         if (!StringUtils.isEmpty(scope)) {
-            if (!StringUtils.isEmpty(keys)) {
-                List<String> keyList = Arrays.asList(keys.split(","));
+            if (keyList != null && !keyList.isEmpty()) {
                 ctx.loadAttributes(entityId, scope, keyList, callback);
             } else {
                 ctx.loadAttributes(entityId, scope, callback);
             }
         } else {
-            if (!StringUtils.isEmpty(keys)) {
-                List<String> keyList = Arrays.asList(keys.split(","));
+            if (keyList != null && !keyList.isEmpty()) {
                 ctx.loadAttributes(entityId, Arrays.asList(DataConstants.allScopes()), keyList, callback);
             } else {
                 ctx.loadAttributes(entityId, Arrays.asList(DataConstants.allScopes()), callback);
@@ -230,9 +232,11 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
                 if (attributes.isEmpty()) {
                     throw new IllegalArgumentException("No attributes data found in request body!");
                 }
+
                 ctx.saveAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, attributes, new PluginCallback<Void>() {
                     @Override
                     public void onSuccess(PluginContext ctx, Void value) {
+                        ctx.logAttributesUpdated(msg.getSecurityCtx(), entityId, scope, attributes, null);
                         msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
                         subscriptionManager.onAttributesUpdateFromServer(ctx, entityId, scope, attributes);
                     }
@@ -240,6 +244,7 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
                     @Override
                     public void onFailure(PluginContext ctx, Exception e) {
                         log.error("Failed to save attributes", e);
+                        ctx.logAttributesUpdated(msg.getSecurityCtx(), entityId, scope, attributes, e);
                         handleError(e, msg, HttpStatus.BAD_REQUEST);
                     }
                 });
@@ -334,15 +339,18 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
                 String keysParam = request.getParameter("keys");
                 if (!StringUtils.isEmpty(keysParam)) {
                     String[] keys = keysParam.split(",");
-                    ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, Arrays.asList(keys), new PluginCallback<Void>() {
+                    List<String> keyList = Arrays.asList(keys);
+                    ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(IllegalArgumentException::new).getTenantId(), entityId, scope, keyList, new PluginCallback<Void>() {
                         @Override
                         public void onSuccess(PluginContext ctx, Void value) {
+                            ctx.logAttributesDeleted(msg.getSecurityCtx(), entityId, scope, keyList, null);
                             msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
                         }
 
                         @Override
                         public void onFailure(PluginContext ctx, Exception e) {
                             log.error("Failed to remove attributes", e);
+                            ctx.logAttributesDeleted(msg.getSecurityCtx(), entityId, scope, keyList, e);
                             handleError(e, msg, HttpStatus.INTERNAL_SERVER_ERROR);
                         }
                     });
@@ -373,18 +381,21 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
         };
     }
 
-    private PluginCallback<List<AttributeKvEntry>> getAttributeValuesPluginCallback(final PluginRestMsg msg) {
+    private PluginCallback<List<AttributeKvEntry>> getAttributeValuesPluginCallback(final PluginRestMsg msg, final String scope,
+                                                                                    final EntityId entityId, final List<String> keyList) {
         return new PluginCallback<List<AttributeKvEntry>>() {
             @Override
             public void onSuccess(PluginContext ctx, List<AttributeKvEntry> attributes) {
                 List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
                         attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
+                ctx.logAttributesRead(msg.getSecurityCtx(), entityId, scope, keyList, null);
                 msg.getResponseHolder().setResult(new ResponseEntity<>(values, HttpStatus.OK));
             }
 
             @Override
             public void onFailure(PluginContext ctx, Exception e) {
                 log.error("Failed to fetch attributes", e);
+                ctx.logAttributesRead(msg.getSecurityCtx(), entityId, scope, keyList, e);
                 handleError(e, msg, HttpStatus.INTERNAL_SERVER_ERROR);
             }
         };
diff --git a/ui/src/app/alarm/alarm-table.tpl.html b/ui/src/app/alarm/alarm-table.tpl.html
index 98d42a2..361b9ba 100644
--- a/ui/src/app/alarm/alarm-table.tpl.html
+++ b/ui/src/app/alarm/alarm-table.tpl.html
@@ -29,7 +29,7 @@
     </section>
     <div flex layout="column" class="tb-alarm-container md-whiteframe-z1">
         <md-list flex layout="column" class="tb-alarm-table">
-            <md-list class="tb-row tb-header" layout="row" tb-alarm-header>
+            <md-list class="tb-row tb-header" layout="row" layout-align="start center" tb-alarm-header>
             </md-list>
             <md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
                                 ng-show="$root.loading"></md-progress-linear>
@@ -39,7 +39,7 @@
                   class="tb-prompt" ng-show="noData()">alarm.no-alarms-prompt</span>
             <md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
                 <md-list-item md-virtual-repeat="alarm in theAlarms" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
-                    <md-list class="tb-row" flex layout="row" tb-alarm-row alarm="{{alarm}}">
+                    <md-list class="tb-row" flex layout="row" layout-align="start center" tb-alarm-row alarm="{{alarm}}">
                     </md-list>
                     <md-divider flex></md-divider>
                 </md-list-item>
diff --git a/ui/src/app/api/audit-log.service.js b/ui/src/app/api/audit-log.service.js
new file mode 100644
index 0000000..b8f73b9
--- /dev/null
+++ b/ui/src/app/api/audit-log.service.js
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+export default angular.module('thingsboard.api.auditLog', [])
+    .factory('auditLogService', AuditLogService)
+    .name;
+
+/*@ngInject*/
+function AuditLogService($http, $q) {
+
+    var service = {
+        getAuditLogsByEntityId: getAuditLogsByEntityId,
+        getAuditLogsByUserId: getAuditLogsByUserId,
+        getAuditLogsByCustomerId: getAuditLogsByCustomerId,
+        getAuditLogs: getAuditLogs
+    }
+
+    return service;
+
+    function getAuditLogsByEntityId (entityType, entityId, pageLink) {
+        var deferred = $q.defer();
+        var url = `/api/audit/logs/entity/${entityType}/${entityId}?limit=${pageLink.limit}`;
+
+        if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
+            url += '&startTime=' + pageLink.startTime;
+        }
+        if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
+            url += '&endTime=' + pageLink.endTime;
+        }
+        if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
+            url += '&offset=' + pageLink.idOffset;
+        }
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getAuditLogsByUserId (userId, pageLink) {
+        var deferred = $q.defer();
+        var url = `/api/audit/logs/user/${userId}?limit=${pageLink.limit}`;
+
+        if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
+            url += '&startTime=' + pageLink.startTime;
+        }
+        if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
+            url += '&endTime=' + pageLink.endTime;
+        }
+        if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
+            url += '&offset=' + pageLink.idOffset;
+        }
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getAuditLogsByCustomerId (customerId, pageLink) {
+        var deferred = $q.defer();
+        var url = `/api/audit/logs/customer/${customerId}?limit=${pageLink.limit}`;
+
+        if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
+            url += '&startTime=' + pageLink.startTime;
+        }
+        if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
+            url += '&endTime=' + pageLink.endTime;
+        }
+        if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
+            url += '&offset=' + pageLink.idOffset;
+        }
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getAuditLogs (pageLink) {
+        var deferred = $q.defer();
+        var url = `/api/audit/logs?limit=${pageLink.limit}`;
+
+        if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) {
+            url += '&startTime=' + pageLink.startTime;
+        }
+        if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) {
+            url += '&endTime=' + pageLink.endTime;
+        }
+        if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) {
+            url += '&offset=' + pageLink.idOffset;
+        }
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+}
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
index bb4249e..f2ae8cd 100644
--- a/ui/src/app/api/device.service.js
+++ b/ui/src/app/api/device.service.js
@@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
     .name;
 
 /*@ngInject*/
-function DeviceService($http, $q, attributeService, customerService, types) {
+function DeviceService($http, $q, $window, userService, attributeService, customerService, types) {
 
     var service = {
         assignDeviceToCustomer: assignDeviceToCustomer,
@@ -181,14 +181,27 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         return deferred.promise;
     }
 
-    function getDeviceCredentials(deviceId) {
+    function getDeviceCredentials(deviceId, sync) {
         var deferred = $q.defer();
         var url = '/api/device/' + deviceId + '/credentials';
-        $http.get(url, null).then(function success(response) {
-            deferred.resolve(response.data);
-        }, function fail() {
-            deferred.reject();
-        });
+        if (sync) {
+            var request = new $window.XMLHttpRequest();
+            request.open('GET', url, false);
+            request.setRequestHeader("Accept", "application/json, text/plain, */*");
+            userService.setAuthorizationRequestHeader(request);
+            request.send(null);
+            if (request.status === 200) {
+                deferred.resolve(angular.fromJson(request.responseText));
+            } else {
+                deferred.reject();
+            }
+        } else {
+            $http.get(url, null).then(function success(response) {
+                deferred.resolve(response.data);
+            }, function fail() {
+                deferred.reject();
+            });
+        }
         return deferred.promise;
     }
 
diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js
index 1a146b4..ce5e673 100644
--- a/ui/src/app/api/user.service.js
+++ b/ui/src/app/api/user.service.js
@@ -54,6 +54,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
         refreshJwtToken: refreshJwtToken,
         refreshTokenPending: refreshTokenPending,
         updateAuthorizationHeader: updateAuthorizationHeader,
+        setAuthorizationRequestHeader: setAuthorizationRequestHeader,
         gotoDefaultPlace: gotoDefaultPlace,
         forceDefaultPlace: forceDefaultPlace,
         updateLastPublicDashboardId: updateLastPublicDashboardId,
@@ -367,6 +368,14 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
         return jwtToken;
     }
 
+    function setAuthorizationRequestHeader(request) {
+        var jwtToken = store.get('jwt_token');
+        if (jwtToken) {
+            request.setRequestHeader('X-Authorization', 'Bearer ' + jwtToken);
+        }
+        return jwtToken;
+    }
+
     function getTenantAdmins(tenantId, pageLink) {
         var deferred = $q.defer();
         var url = '/api/tenant/' + tenantId + '/users?limit=' + pageLink.limit;
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 5b19089..23f6933 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -63,6 +63,7 @@ import thingsboardApiTime from './api/time.service';
 import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
 import thingsboardHelp from './help/help.directive';
 import thingsboardToast from './services/toast';
+import thingsboardClipboard from './services/clipboard.service';
 import thingsboardHome from './layout';
 import thingsboardApiLogin from './api/login.service';
 import thingsboardApiDevice from './api/device.service';
@@ -72,6 +73,7 @@ import thingsboardApiAsset from './api/asset.service';
 import thingsboardApiAttribute from './api/attribute.service';
 import thingsboardApiEntity from './api/entity.service';
 import thingsboardApiAlarm from './api/alarm.service';
+import thingsboardApiAuditLog from './api/audit-log.service';
 
 import 'typeface-roboto';
 import 'font-awesome/css/font-awesome.min.css';
@@ -123,6 +125,7 @@ angular.module('thingsboard', [
     thingsboardKeyboardShortcut,
     thingsboardHelp,
     thingsboardToast,
+    thingsboardClipboard,
     thingsboardHome,
     thingsboardApiLogin,
     thingsboardApiDevice,
@@ -132,6 +135,7 @@ angular.module('thingsboard', [
     thingsboardApiAttribute,
     thingsboardApiEntity,
     thingsboardApiAlarm,
+    thingsboardApiAuditLog,
     uiRouter])
     .config(AppConfig)
     .factory('globalInterceptor', GlobalInterceptor)
diff --git a/ui/src/app/asset/assets.tpl.html b/ui/src/app/asset/assets.tpl.html
index c4c0267..f16e1cd 100644
--- a/ui/src/app/asset/assets.tpl.html
+++ b/ui/src/app/asset/assets.tpl.html
@@ -66,4 +66,10 @@
                                 entity-type="{{vm.types.entityType.asset}}">
             </tb-relation-table>
         </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+            <tb-audit-log-table flex entity-type="vm.types.entityType.asset"
+                                entity-id="vm.grid.operatingItem().id.id"
+                                audit-log-mode="{{vm.types.auditLogMode.entity}}">
+            </tb-audit-log-table>
+        </md-tab>
 </tb-grid>
diff --git a/ui/src/app/audit/audit-log.routes.js b/ui/src/app/audit/audit-log.routes.js
new file mode 100644
index 0000000..be26b9c
--- /dev/null
+++ b/ui/src/app/audit/audit-log.routes.js
@@ -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.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import auditLogsTemplate from './audit-logs.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function AuditLogRoutes($stateProvider) {
+    $stateProvider
+        .state('home.auditLogs', {
+            url: '/auditLogs',
+            module: 'private',
+            auth: ['TENANT_ADMIN'],
+            views: {
+                "content@home": {
+                    templateUrl: auditLogsTemplate,
+                    controller: 'AuditLogsController',
+                    controllerAs: 'vm'
+                }
+            },
+            data: {
+                searchEnabled: false,
+                pageTitle: 'audit-log.audit-logs'
+            },
+            ncyBreadcrumb: {
+                label: '{"icon": "track_changes", "label": "audit-log.audit-logs"}'
+            }
+        });
+}
diff --git a/ui/src/app/audit/audit-log.scss b/ui/src/app/audit/audit-log.scss
new file mode 100644
index 0000000..1642cbe
--- /dev/null
+++ b/ui/src/app/audit/audit-log.scss
@@ -0,0 +1,91 @@
+/**
+ * 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.
+ */
+
+.tb-audit-logs {
+  background-color: #fff;
+  .tb-audit-log-margin-18px {
+    margin-bottom: 18px;
+  }
+  .tb-audit-log-toolbar {
+    font-size: 20px;
+  }
+  md-input-container.tb-audit-log-search-input {
+    .md-errors-spacer {
+      min-height: 0px;
+    }
+  }
+}
+
+.tb-audit-log-container {
+  overflow-x: auto;
+}
+
+
+
+md-list.tb-audit-log-table {
+  padding: 0px;
+  min-width: 700px;
+  &.tb-audit-log-table-full {
+    min-width: 900px;
+  }
+
+  md-list-item {
+    padding: 0px;
+  }
+
+  .tb-row {
+    height: 48px;
+    padding: 0px;
+    overflow: hidden;
+  }
+
+  .tb-row:hover {
+    background-color: #EEEEEE;
+  }
+
+  .tb-header:hover {
+    background: none;
+  }
+
+  .tb-header {
+    .tb-cell {
+      color: rgba(0,0,0,.54);
+      font-size: 12px;
+      font-weight: 700;
+      white-space: nowrap;
+      background: none;
+    }
+  }
+
+  .tb-cell {
+    padding: 0 24px;
+    margin: auto 0;
+    color: rgba(0,0,0,.87);
+    font-size: 13px;
+    vertical-align: middle;
+    text-align: left;
+    overflow: hidden;
+    .md-button {
+      padding: 0;
+      margin: 0;
+    }
+  }
+
+  .tb-cell.tb-number {
+    text-align: right;
+  }
+
+}
diff --git a/ui/src/app/audit/audit-log-details-dialog.controller.js b/ui/src/app/audit/audit-log-details-dialog.controller.js
new file mode 100644
index 0000000..88cb10d
--- /dev/null
+++ b/ui/src/app/audit/audit-log-details-dialog.controller.js
@@ -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.
+ */
+import $ from 'jquery';
+import 'brace/ext/language_tools';
+import 'brace/mode/java';
+import 'brace/theme/github';
+
+/* eslint-disable angular/angularelement */
+
+import './audit-log-details-dialog.scss';
+
+/*@ngInject*/
+export default function AuditLogDetailsDialogController($mdDialog, types, auditLog, showingCallback) {
+
+    var vm = this;
+
+    showingCallback.onShowing = function(scope, element) {
+        updateEditorSize(element, vm.actionData, 'tb-audit-log-action-data');
+        vm.actionDataEditor.resize();
+        if (vm.displayFailureDetails) {
+            updateEditorSize(element, vm.actionFailureDetails, 'tb-audit-log-failure-details');
+            vm.failureDetailsEditor.resize();
+        }
+    };
+
+    vm.types = types;
+    vm.auditLog = auditLog;
+    vm.displayFailureDetails = auditLog.actionStatus == types.auditLogActionStatus.FAILURE.value;
+    vm.actionData = auditLog.actionDataText;
+    vm.actionFailureDetails = auditLog.actionFailureDetails;
+
+    vm.actionDataContentOptions = {
+        useWrapMode: false,
+        mode: 'java',
+        showGutter: false,
+        showPrintMargin: false,
+        theme: 'github',
+        advanced: {
+            enableSnippets: false,
+            enableBasicAutocompletion: false,
+            enableLiveAutocompletion: false
+        },
+        onLoad: function (_ace) {
+            vm.actionDataEditor = _ace;
+        }
+    };
+
+    vm.failureDetailsContentOptions = {
+        useWrapMode: false,
+        mode: 'java',
+        showGutter: false,
+        showPrintMargin: false,
+        theme: 'github',
+        advanced: {
+            enableSnippets: false,
+            enableBasicAutocompletion: false,
+            enableLiveAutocompletion: false
+        },
+        onLoad: function (_ace) {
+            vm.failureDetailsEditor = _ace;
+        }
+    };
+
+    function updateEditorSize(element, content, editorId) {
+        var newHeight = 200;
+        var newWidth = 600;
+        if (content && content.length > 0) {
+            var lines = content.split('\n');
+            newHeight = 16 * lines.length + 16;
+            var maxLineLength = 0;
+            for (var i in lines) {
+                var line = lines[i].replace(/\t/g, '    ').replace(/\n/g, '');
+                var lineLength = line.length;
+                maxLineLength = Math.max(maxLineLength, lineLength);
+            }
+            newWidth = 8 * maxLineLength + 16;
+        }
+        $('#'+editorId, element).height(newHeight.toString() + "px").css('min-height', newHeight.toString() + "px")
+            .width(newWidth.toString() + "px");
+    }
+
+    vm.close = close;
+
+    function close () {
+        $mdDialog.hide();
+    }
+
+}
+
+/* eslint-enable angular/angularelement */
diff --git a/ui/src/app/audit/audit-log-details-dialog.scss b/ui/src/app/audit/audit-log-details-dialog.scss
new file mode 100644
index 0000000..83aaf40
--- /dev/null
+++ b/ui/src/app/audit/audit-log-details-dialog.scss
@@ -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.
+ */
+
+#tb-audit-log-action-data, #tb-audit-log-failure-details {
+  min-width: 400px;
+  min-height: 50px;
+  width: 100%;
+  height: 100%;
+  border: 1px solid #C0C0C0;
+}
\ No newline at end of file
diff --git a/ui/src/app/audit/audit-log-details-dialog.tpl.html b/ui/src/app/audit/audit-log-details-dialog.tpl.html
new file mode 100644
index 0000000..b1e1e63
--- /dev/null
+++ b/ui/src/app/audit/audit-log-details-dialog.tpl.html
@@ -0,0 +1,49 @@
+<!--
+
+    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.
+
+-->
+<md-dialog aria-label="{{ 'audit-log.audit-log-details' | translate }}">
+    <md-toolbar>
+        <div class="md-toolbar-tools">
+            <h2 translate>audit-log.audit-log-details</h2>
+            <span flex></span>
+            <md-button class="md-icon-button" ng-click="vm.close()">
+                <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+            </md-button>
+        </div>
+    </md-toolbar>
+    <md-dialog-content>
+        <div class="md-dialog-content" layout="column">
+            <label translate class="tb-title no-padding">audit-log.action-data</label>
+            <div flex id="tb-audit-log-action-data" readonly
+                 ui-ace="vm.actionDataContentOptions"
+                 ng-model="vm.actionData">
+            </div>
+            <span style="height: 30px;"></span>
+            <label ng-show="vm.displayFailureDetails" translate class="tb-title no-padding">audit-log.failure-details</label>
+            <div ng-show="vm.displayFailureDetails" flex id="tb-audit-log-failure-details" readonly
+                 ui-ace="vm.failureDetailsContentOptions"
+                 ng-model="vm.actionFailureDetails">
+            </div>
+        </div>
+    </md-dialog-content>
+    <md-dialog-actions layout="row">
+        <span flex></span>
+        <md-button ng-disabled="$root.loading" ng-click="vm.close()" style="margin-right:20px;">{{ 'action.close' |
+            translate }}
+        </md-button>
+    </md-dialog-actions>
+</md-dialog>
diff --git a/ui/src/app/audit/audit-log-header.directive.js b/ui/src/app/audit/audit-log-header.directive.js
new file mode 100644
index 0000000..504e96c
--- /dev/null
+++ b/ui/src/app/audit/audit-log-header.directive.js
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import auditLogHeaderTemplate from './audit-log-header.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function AuditLogHeaderDirective($compile, $templateCache, types) {
+
+    var linker = function (scope, element, attrs) {
+
+        var template = $templateCache.get(auditLogHeaderTemplate);
+        element.html(template);
+        scope.auditLogMode = attrs.auditLogMode;
+        scope.types = types;
+        $compile(element.contents())(scope);
+
+    };
+
+    return {
+        restrict: "A",
+        replace: false,
+        link: linker,
+        scope: false
+    };
+}
diff --git a/ui/src/app/audit/audit-log-header.tpl.html b/ui/src/app/audit/audit-log-header.tpl.html
new file mode 100644
index 0000000..6927ad7
--- /dev/null
+++ b/ui/src/app/audit/audit-log-header.tpl.html
@@ -0,0 +1,24 @@
+<!--
+
+    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.
+
+-->
+<div translate class="tb-cell" flex="30">audit-log.timestamp</div>
+<div ng-if="auditLogMode != types.auditLogMode.entity" translate class="tb-cell" flex="10">audit-log.entity-type</div>
+<div ng-if="auditLogMode != types.auditLogMode.entity" translate class="tb-cell" flex="30">audit-log.entity-name</div>
+<div ng-if="auditLogMode != types.auditLogMode.user" translate class="tb-cell" flex="30">audit-log.user</div>
+<div translate class="tb-cell" flex="15">audit-log.type</div>
+<div translate class="tb-cell" flex="15">audit-log.status</div>
+<div translate class="tb-cell" flex="10">audit-log.details</div>
diff --git a/ui/src/app/audit/audit-log-row.directive.js b/ui/src/app/audit/audit-log-row.directive.js
new file mode 100644
index 0000000..1cb6d5f
--- /dev/null
+++ b/ui/src/app/audit/audit-log-row.directive.js
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import auditLogDetailsDialogTemplate from './audit-log-details-dialog.tpl.html';
+
+import auditLogRowTemplate from './audit-log-row.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function AuditLogRowDirective($compile, $templateCache, types, $mdDialog, $document) {
+
+    var linker = function (scope, element, attrs) {
+
+        var template = $templateCache.get(auditLogRowTemplate);
+        element.html(template);
+
+        scope.auditLog = attrs.auditLog;
+        scope.auditLogMode = attrs.auditLogMode;
+        scope.types = types;
+
+        scope.showAuditLogDetails = function($event) {
+            var onShowingCallback = {
+                onShowing: function(){}
+            }
+            $mdDialog.show({
+                controller: 'AuditLogDetailsDialogController',
+                controllerAs: 'vm',
+                templateUrl: auditLogDetailsDialogTemplate,
+                locals: {
+                    auditLog: scope.auditLog,
+                    showingCallback: onShowingCallback
+                },
+                parent: angular.element($document[0].body),
+                targetEvent: $event,
+                fullscreen: true,
+                skipHide: true,
+                onShowing: function(scope, element) {
+                    onShowingCallback.onShowing(scope, element);
+                }
+            });
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "A",
+        replace: false,
+        link: linker,
+        scope: false
+    };
+}
diff --git a/ui/src/app/audit/audit-log-row.tpl.html b/ui/src/app/audit/audit-log-row.tpl.html
new file mode 100644
index 0000000..fd0af2d
--- /dev/null
+++ b/ui/src/app/audit/audit-log-row.tpl.html
@@ -0,0 +1,36 @@
+<!--
+
+    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.
+
+-->
+<div class="tb-cell" flex="30">{{ auditLog.createdTime | date :  'yyyy-MM-dd HH:mm:ss' }}</div>
+<div ng-if="auditLogMode != types.auditLogMode.entity" class="tb-cell" flex="10">{{ auditLog.entityTypeText }}</div>
+<div ng-if="auditLogMode != types.auditLogMode.entity" class="tb-cell" flex="30">{{ auditLog.entityName }}</div>
+<div ng-if="auditLogMode != types.auditLogMode.user" class="tb-cell" flex="30">{{ auditLog.userName }}</div>
+<div class="tb-cell" flex="15">{{ auditLog.actionTypeText }}</div>
+<div class="tb-cell" flex="15">{{ auditLog.actionStatusText }}</div>
+<div class="tb-cell" flex="10">
+    <md-button class="md-icon-button md-primary"
+               ng-click="showAuditLogDetails($event)"
+               aria-label="{{ 'action.view' | translate }}">
+        <md-tooltip md-direction="top">
+            {{ 'audit-log.details' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'action.view' | translate }}"
+                 class="material-icons">
+            more_horiz
+        </md-icon>
+    </md-button>
+</div>
diff --git a/ui/src/app/audit/audit-logs.controller.js b/ui/src/app/audit/audit-logs.controller.js
new file mode 100644
index 0000000..d2cb1e3
--- /dev/null
+++ b/ui/src/app/audit/audit-logs.controller.js
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+/*@ngInject*/
+export default function AuditLogsController(types) {
+
+    var vm = this;
+
+    vm.types = types;
+
+}
\ No newline at end of file
diff --git a/ui/src/app/audit/audit-logs.tpl.html b/ui/src/app/audit/audit-logs.tpl.html
new file mode 100644
index 0000000..088c339
--- /dev/null
+++ b/ui/src/app/audit/audit-logs.tpl.html
@@ -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.
+
+-->
+
+<tb-audit-log-table class="md-whiteframe-z1"
+                    flex
+                    audit-log-mode="{{vm.types.auditLogMode.tenant}}"
+                    page-mode="true">
+</tb-audit-log-table>
diff --git a/ui/src/app/audit/audit-log-table.directive.js b/ui/src/app/audit/audit-log-table.directive.js
new file mode 100644
index 0000000..ba15249
--- /dev/null
+++ b/ui/src/app/audit/audit-log-table.directive.js
@@ -0,0 +1,262 @@
+/*
+ * 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.
+ */
+import './audit-log.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import auditLogTableTemplate from './audit-log-table.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function AuditLogTableDirective($compile, $templateCache, $rootScope, $filter, $translate, types, auditLogService) {
+
+    var linker = function (scope, element) {
+
+        var template = $templateCache.get(auditLogTableTemplate);
+
+        element.html(template);
+
+        scope.types = types;
+
+        var pageSize = 20;
+        var startTime = 0;
+        var endTime = 0;
+
+        scope.timewindow = {
+            history: {
+                timewindowMs: 24 * 60 * 60 * 1000 // 1 day
+            }
+        }
+
+        scope.topIndex = 0;
+        scope.searchText = '';
+
+        scope.theAuditLogs = {
+            getItemAtIndex: function (index) {
+                if (index > scope.auditLogs.filtered.length) {
+                    scope.theAuditLogs.fetchMoreItems_(index);
+                    return null;
+                }
+                return scope.auditLogs.filtered[index];
+            },
+
+            getLength: function () {
+                if (scope.auditLogs.hasNext) {
+                    return scope.auditLogs.filtered.length + scope.auditLogs.nextPageLink.limit;
+                } else {
+                    return scope.auditLogs.filtered.length;
+                }
+            },
+
+            fetchMoreItems_: function () {
+                if (scope.auditLogs.hasNext && !scope.auditLogs.pending) {
+                    var promise = getAuditLogsPromise(scope.auditLogs.nextPageLink);
+                    if (promise) {
+                        scope.auditLogs.pending = true;
+                        promise.then(
+                            function success(auditLogs) {
+                                scope.auditLogs.data = scope.auditLogs.data.concat(prepareAuditLogsData(auditLogs.data));
+                                scope.auditLogs.filtered = $filter('filter')(scope.auditLogs.data, {$: scope.searchText});
+                                scope.auditLogs.nextPageLink = auditLogs.nextPageLink;
+                                scope.auditLogs.hasNext = auditLogs.hasNext;
+                                if (scope.auditLogs.hasNext) {
+                                    scope.auditLogs.nextPageLink.limit = pageSize;
+                                }
+                                scope.auditLogs.pending = false;
+                            },
+                            function fail() {
+                                scope.auditLogs.hasNext = false;
+                                scope.auditLogs.pending = false;
+                            });
+                    } else {
+                        scope.auditLogs.hasNext = false;
+                    }
+                }
+            }
+        };
+
+        function prepareAuditLogsData(data) {
+            data.forEach(
+                auditLog => {
+                    auditLog.entityTypeText = $translate.instant(types.entityTypeTranslations[auditLog.entityId.entityType].type);
+                    auditLog.actionTypeText = $translate.instant(types.auditLogActionType[auditLog.actionType].name);
+                    auditLog.actionStatusText = $translate.instant(types.auditLogActionStatus[auditLog.actionStatus].name);
+                    auditLog.actionDataText = auditLog.actionData ? angular.toJson(auditLog.actionData, true) : '';
+                }
+            );
+            return data;
+        }
+
+        scope.$watch("entityId", function(newVal, prevVal) {
+            if (newVal && !angular.equals(newVal, prevVal)) {
+                resetFilter();
+                scope.reload();
+            }
+        });
+
+        scope.$watch("userId", function(newVal, prevVal) {
+            if (newVal && !angular.equals(newVal, prevVal)) {
+                resetFilter();
+                scope.reload();
+            }
+        });
+
+        scope.$watch("customerId", function(newVal, prevVal) {
+            if (newVal && !angular.equals(newVal, prevVal)) {
+                resetFilter();
+                scope.reload();
+            }
+        });
+
+        function getAuditLogsPromise(pageLink) {
+            switch(scope.auditLogMode) {
+                case types.auditLogMode.tenant:
+                    return auditLogService.getAuditLogs(pageLink);
+                case types.auditLogMode.entity:
+                    if (scope.entityType && scope.entityId) {
+                        return auditLogService.getAuditLogsByEntityId(scope.entityType, scope.entityId,
+                            pageLink);
+                    } else {
+                        return null;
+                    }
+                case types.auditLogMode.user:
+                    if (scope.userId) {
+                        return auditLogService.getAuditLogsByUserId(scope.userId, pageLink);
+                    } else {
+                        return null;
+                    }
+                case types.auditLogMode.customer:
+                    if (scope.customerId) {
+                        return auditLogService.getAuditLogsByCustomerId(scope.customerId, pageLink);
+                    } else {
+                        return null;
+                    }
+            }
+        }
+
+        function destroyWatchers() {
+            if (scope.timewindowWatchHandle) {
+                scope.timewindowWatchHandle();
+                scope.timewindowWatchHandle = null;
+            }
+            if (scope.searchTextWatchHandle) {
+                scope.searchTextWatchHandle();
+                scope.searchTextWatchHandle = null;
+            }
+        }
+
+        function initWatchers() {
+            scope.timewindowWatchHandle = scope.$watch("timewindow", function(newVal, prevVal) {
+                if (newVal && !angular.equals(newVal, prevVal)) {
+                    scope.reload();
+                }
+            }, true);
+
+            scope.searchTextWatchHandle = scope.$watch("searchText", function(newVal, prevVal) {
+                if (!angular.equals(newVal, prevVal)) {
+                    scope.searchTextUpdated();
+                }
+            }, true);
+        }
+
+        function resetFilter() {
+            destroyWatchers();
+            scope.timewindow = {
+                history: {
+                    timewindowMs: 24 * 60 * 60 * 1000 // 1 day
+                }
+            };
+            scope.searchText = '';
+            initWatchers();
+        }
+
+        function updateTimeWindowRange () {
+            if (scope.timewindow.history.timewindowMs) {
+                var currentTime = (new Date).getTime();
+                startTime = currentTime - scope.timewindow.history.timewindowMs;
+                endTime = currentTime;
+            } else {
+                startTime = scope.timewindow.history.fixedTimewindow.startTimeMs;
+                endTime = scope.timewindow.history.fixedTimewindow.endTimeMs;
+            }
+        }
+
+        scope.reload = function() {
+            scope.topIndex = 0;
+            updateTimeWindowRange();
+            scope.auditLogs = {
+                data: [],
+                filtered: [],
+                nextPageLink: {
+                    limit: pageSize,
+                    startTime: startTime,
+                    endTime: endTime
+                },
+                hasNext: true,
+                pending: false
+            };
+            scope.theAuditLogs.getItemAtIndex(pageSize);
+        }
+
+        scope.searchTextUpdated = function() {
+            scope.auditLogs.filtered = $filter('filter')(scope.auditLogs.data, {$: scope.searchText});
+            scope.theAuditLogs.getItemAtIndex(pageSize);
+        }
+
+        scope.noData = function() {
+            return scope.auditLogs.data.length == 0 && !scope.auditLogs.hasNext;
+        }
+
+        scope.hasData = function() {
+            return scope.auditLogs.data.length > 0;
+        }
+
+        scope.loading = function() {
+            return $rootScope.loading;
+        }
+
+        scope.hasScroll = function() {
+            var repeatContainer = scope.repeatContainer[0];
+            if (repeatContainer) {
+                var scrollElement = repeatContainer.children[0];
+                if (scrollElement) {
+                    return scrollElement.scrollHeight > scrollElement.clientHeight;
+                }
+            }
+            return false;
+        }
+
+        scope.reload();
+
+        initWatchers();
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        link: linker,
+        scope: {
+            entityType: '=?',
+            entityId: '=?',
+            userId: '=?',
+            customerId: '=?',
+            auditLogMode: '@',
+            pageMode: '@?'
+        }
+    };
+}
diff --git a/ui/src/app/audit/audit-log-table.tpl.html b/ui/src/app/audit/audit-log-table.tpl.html
new file mode 100644
index 0000000..88a1d93
--- /dev/null
+++ b/ui/src/app/audit/audit-log-table.tpl.html
@@ -0,0 +1,68 @@
+<!--
+
+    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.
+
+-->
+<md-content flex class="md-padding tb-absolute-fill" layout="column">
+    <div flex layout="column" class="tb-audit-logs" ng-class="{'md-whiteframe-z1': pageMode}">
+        <div layout="column" layout-gt-sm="row" layout-align-gt-sm="start center" class="tb-audit-log-toolbar" ng-class="{'md-padding': pageMode, 'tb-audit-log-margin-18px': !pageMode}">
+            <tb-timewindow ng-model="timewindow" history-only as-button="true"></tb-timewindow>
+            <div flex layout="row" layout-align="start center">
+                <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
+                    <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{'audit-log.search' | translate}}
+                    </md-tooltip>
+                </md-button>
+                <md-input-container flex class="tb-audit-log-search-input">
+                    <label>&nbsp;</label>
+                    <input ng-model="searchText" placeholder="{{'audit-log.search' | translate}}"/>
+                </md-input-container>
+                <md-button ng-disabled="$root.loading" class="md-icon-button" aria-label="Close" ng-click="searchText = ''">
+                    <md-icon aria-label="Close" class="material-icons">close</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'audit-log.clear-search' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-button ng-disabled="$root.loading"
+                           class="md-icon-button" ng-click="reload()">
+                    <md-icon>refresh</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.refresh' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+        </div>
+        <div flex layout="column" class="tb-audit-log-container" ng-class="{'md-whiteframe-z1': !pageMode}">
+            <md-list flex layout="column" class="tb-audit-log-table" ng-class="{'tb-audit-log-table-full': pageMode}">
+                <md-list class="tb-row tb-header" layout="row" layout-align="start center" tb-audit-log-header audit-log-mode="{{auditLogMode}}">
+                </md-list>
+                <md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
+                                    ng-show="$root.loading"></md-progress-linear>
+                <md-divider></md-divider>
+                <span translate layout-align="center center"
+                      style="margin-top: 25px;"
+                      class="tb-prompt" ng-show="noData()">audit-log.no-audit-logs-prompt</span>
+                <md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
+                    <md-list-item md-virtual-repeat="auditLog in theAuditLogs" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
+                        <md-list class="tb-row" flex layout="row" layout-align="start center" tb-audit-log-row audit-log-mode="{{auditLogMode}}" audit-log="{{auditLog}}">
+                        </md-list>
+                        <md-divider flex></md-divider>
+                    </md-list-item>
+                </md-virtual-repeat-container>
+            </md-list>
+        </div>
+    </div>
+</md-content>
diff --git a/ui/src/app/audit/index.js b/ui/src/app/audit/index.js
new file mode 100644
index 0000000..aebcd33
--- /dev/null
+++ b/ui/src/app/audit/index.js
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+import AuditLogRoutes from './audit-log.routes';
+import AuditLogsController from './audit-logs.controller';
+import AuditLogDetailsDialogController from './audit-log-details-dialog.controller';
+import AuditLogHeaderDirective from './audit-log-header.directive';
+import AuditLogRowDirective from './audit-log-row.directive';
+import AuditLogTableDirective from './audit-log-table.directive';
+
+export default angular.module('thingsboard.auditLog', [])
+    .config(AuditLogRoutes)
+    .controller('AuditLogsController', AuditLogsController)
+    .controller('AuditLogDetailsDialogController', AuditLogDetailsDialogController)
+    .directive('tbAuditLogHeader', AuditLogHeaderDirective)
+    .directive('tbAuditLogRow', AuditLogRowDirective)
+    .directive('tbAuditLogTable', AuditLogTableDirective)
+    .name;
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 5b3c9e4..d82add1 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -156,6 +156,63 @@ export default angular.module('thingsboard.types', [])
                     color: "green"
                 }
             },
+            auditLogActionType: {
+                "ADDED": {
+                    name: "audit-log.type-added"
+                },
+                "DELETED": {
+                    name: "audit-log.type-deleted"
+                },
+                "UPDATED": {
+                    name: "audit-log.type-updated"
+                },
+                "ATTRIBUTES_UPDATED": {
+                    name: "audit-log.type-attributes-updated"
+                },
+                "ATTRIBUTES_DELETED": {
+                    name: "audit-log.type-attributes-deleted"
+                },
+                "RPC_CALL": {
+                    name: "audit-log.type-rpc-call"
+                },
+                "CREDENTIALS_UPDATED": {
+                    name: "audit-log.type-credentials-updated"
+                },
+                "ASSIGNED_TO_CUSTOMER": {
+                    name: "audit-log.type-assigned-to-customer"
+                },
+                "UNASSIGNED_FROM_CUSTOMER": {
+                    name: "audit-log.type-unassigned-from-customer"
+                },
+                "ACTIVATED": {
+                    name: "audit-log.type-activated"
+                },
+                "SUSPENDED": {
+                    name: "audit-log.type-suspended"
+                },
+                "CREDENTIALS_READ": {
+                    name: "audit-log.type-credentials-read"
+                },
+                "ATTRIBUTES_READ": {
+                    name: "audit-log.type-attributes-read"
+                }
+            },
+            auditLogActionStatus: {
+                "SUCCESS": {
+                    value: "SUCCESS",
+                    name: "audit-log.status-success"
+                },
+                "FAILURE": {
+                    value: "FAILURE",
+                    name: "audit-log.status-failure"
+                }
+            },
+            auditLogMode: {
+                tenant: "tenant",
+                entity: "entity",
+                user: "user",
+                customer: "customer"
+            },
             aliasFilterType: {
                 singleEntity: {
                     value: 'singleEntity',
diff --git a/ui/src/app/components/grid.directive.js b/ui/src/app/components/grid.directive.js
index 727b999..290431c 100644
--- a/ui/src/app/components/grid.directive.js
+++ b/ui/src/app/components/grid.directive.js
@@ -125,7 +125,7 @@ function Grid() {
 }
 
 /*@ngInject*/
-function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $timeout, $translate, $mdMedia, $templateCache, $window) {
+function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $timeout, $translate, $mdMedia, $templateCache, $window, userService) {
 
     var vm = this;
 
@@ -157,6 +157,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $time
     vm.saveItem = saveItem;
     vm.toggleItemSelection = toggleItemSelection;
     vm.triggerResize = triggerResize;
+    vm.isTenantAdmin = isTenantAdmin;
 
     $scope.$watch(function () {
         return $mdMedia('xs') || $mdMedia('sm');
@@ -634,6 +635,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $time
         w.triggerHandler('resize');
     }
 
+    function isTenantAdmin() {
+        return userService.getAuthority() == 'TENANT_ADMIN';
+    }
+
     function moveToTop() {
         moveToIndex(0, true);
     }
diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html
index da0a2e9..0a0ca84 100644
--- a/ui/src/app/customer/customers.tpl.html
+++ b/ui/src/app/customer/customers.tpl.html
@@ -66,5 +66,10 @@
 							   entity-type="{{vm.types.entityType.customer}}">
 			</tb-relation-table>
 		</md-tab>
+		<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+			<tb-audit-log-table flex customer-id="vm.grid.operatingItem().id.id"
+								audit-log-mode="{{vm.types.auditLogMode.customer}}">
+			</tb-audit-log-table>
+		</md-tab>
 	</md-tabs>
 </tb-grid>
diff --git a/ui/src/app/dashboard/dashboards.tpl.html b/ui/src/app/dashboard/dashboards.tpl.html
index dde2f86..8149821 100644
--- a/ui/src/app/dashboard/dashboards.tpl.html
+++ b/ui/src/app/dashboard/dashboards.tpl.html
@@ -19,13 +19,24 @@
 	<details-buttons tb-help="'dashboards'" help-container-id="help-container">
 		<div id="help-container"></div>
 	</details-buttons>
-	<tb-dashboard-details dashboard="vm.grid.operatingItem()"
-						  is-edit="vm.grid.detailsConfig.isDetailsEditMode"
-						  dashboard-scope="vm.dashboardsScope"
-						  the-form="vm.grid.detailsForm"
-						  on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
-						  on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
-						  on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
-						  on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
-						  on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
+	<md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
+			 id="tabs" md-border-bottom flex class="tb-absolute-fill">
+		<md-tab label="{{ 'dashboard.details' | translate }}">
+			<tb-dashboard-details dashboard="vm.grid.operatingItem()"
+								  is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+								  dashboard-scope="vm.dashboardsScope"
+								  the-form="vm.grid.detailsForm"
+								  on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
+								  on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
+								  on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
+								  on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
+								  on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
+		</md-tab>
+		<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+			<tb-audit-log-table flex entity-type="vm.types.entityType.dashboard"
+								entity-id="vm.grid.operatingItem().id.id"
+								audit-log-mode="{{vm.types.auditLogMode.entity}}">
+			</tb-audit-log-table>
+		</md-tab>
+	</md-tabs>
 </tb-grid>
diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js
index eda4fb2..a40e4da 100644
--- a/ui/src/app/device/device.directive.js
+++ b/ui/src/app/device/device.directive.js
@@ -20,7 +20,7 @@ import deviceFieldsetTemplate from './device-fieldset.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function DeviceDirective($compile, $templateCache, toast, $translate, types, deviceService, customerService) {
+export default function DeviceDirective($compile, $templateCache, toast, $translate, types, clipboardService, deviceService, customerService) {
     var linker = function (scope, element) {
         var template = $templateCache.get(deviceFieldsetTemplate);
         element.html(template);
@@ -30,17 +30,8 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
         scope.isPublic = false;
         scope.assignedCustomer = null;
 
-        scope.deviceCredentials = null;
-
         scope.$watch('device', function(newVal) {
             if (newVal) {
-                if (scope.device.id) {
-                    deviceService.getDeviceCredentials(scope.device.id.id).then(
-                        function success(credentials) {
-                            scope.deviceCredentials = credentials;
-                        }
-                    );
-                }
                 if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
                     scope.isAssignedToCustomer = true;
                     customerService.getShortCustomerInfo(scope.device.customerId.id).then(
@@ -61,8 +52,20 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
             toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
         };
 
-        scope.onAccessTokenCopied = function() {
-            toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+        scope.copyAccessToken = function(e) {
+            const trigger = e.delegateTarget || e.currentTarget;
+            if (scope.device.id) {
+                deviceService.getDeviceCredentials(scope.device.id.id, true).then(
+                    function success(credentials) {
+                        var credentialsId = credentials.credentialsId;
+                        clipboardService.copyToClipboard(trigger, credentialsId).then(
+                            () => {
+                                toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+                            }
+                        );
+                    }
+                );
+            }
         };
 
         $compile(element.contents())(scope);
diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html
index a4d15f3..758c77f 100644
--- a/ui/src/app/device/device-fieldset.tpl.html
+++ b/ui/src/app/device/device-fieldset.tpl.html
@@ -39,10 +39,8 @@
         <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
         <span translate>device.copyId</span>
     </md-button>
-    <md-button ngclipboard data-clipboard-action="copy"
-               ngclipboard-success="onAccessTokenCopied(e)"
-               data-clipboard-text="{{deviceCredentials.credentialsId}}" ng-show="!isEdit"
-               class="md-raised">
+    <md-button ng-show="!isEdit"
+               class="md-raised" ng-click="copyAccessToken($event)">
         <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
         <span translate>device.copyAccessToken</span>
     </md-button>
diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
index 1ec0134..513465d 100644
--- a/ui/src/app/device/devices.tpl.html
+++ b/ui/src/app/device/devices.tpl.html
@@ -74,4 +74,10 @@
                                 entity-type="{{vm.types.entityType.device}}">
             </tb-extension-table>
         </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+            <tb-audit-log-table flex entity-type="vm.types.entityType.device"
+                            entity-id="vm.grid.operatingItem().id.id"
+                            audit-log-mode="{{vm.types.auditLogMode.entity}}">
+            </tb-audit-log-table>
+        </md-tab>
 </tb-grid>
diff --git a/ui/src/app/event/event-table.tpl.html b/ui/src/app/event/event-table.tpl.html
index 760b55b..e056671 100644
--- a/ui/src/app/event/event-table.tpl.html
+++ b/ui/src/app/event/event-table.tpl.html
@@ -26,9 +26,16 @@
             </md-select>
         </md-input-container>
         <tb-timewindow flex ng-model="timewindow" history-only as-button="true"></tb-timewindow>
+        <md-button ng-disabled="$root.loading"
+                   class="md-icon-button" ng-click="reload()">
+            <md-icon>refresh</md-icon>
+            <md-tooltip md-direction="top">
+                {{ 'action.refresh' | translate }}
+            </md-tooltip>
+        </md-button>
     </section>
     <md-list flex layout="column" class="md-whiteframe-z1 tb-event-table">
-           <md-list class="tb-row tb-header" layout="row" tb-event-header event-type="{{eventType}}">
+           <md-list class="tb-row tb-header" layout="row" layout-align="start center" tb-event-header event-type="{{eventType}}">
            </md-list>
         <md-progress-linear style="max-height: 0px;" md-mode="indeterminate" ng-disabled="!$root.loading"
                             ng-show="$root.loading"></md-progress-linear>
@@ -38,7 +45,7 @@
               class="tb-prompt" ng-show="noData()">event.no-events-prompt</span>
         <md-virtual-repeat-container ng-show="hasData()" flex md-top-index="topIndex" tb-scope-element="repeatContainer">
            <md-list-item md-virtual-repeat="event in theEvents" md-on-demand flex ng-style="hasScroll() ? {'margin-right':'-15px'} : {}">
-               <md-list class="tb-row" flex layout="row" tb-event-row event-type="{{eventType}}" event="{{event}}">
+               <md-list class="tb-row" flex layout="row" layout-align="start center" tb-event-row event-type="{{eventType}}" event="{{event}}">
                </md-list>
                <md-divider flex></md-divider>
            </md-list-item>
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index c23b008..9102928 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive';
 import thingsboardEntity from '../entity';
 import thingsboardEvent from '../event';
 import thingsboardAlarm from '../alarm';
+import thingsboardAuditLog from '../audit';
 import thingsboardExtension from '../extension';
 import thingsboardTenant from '../tenant';
 import thingsboardCustomer from '../customer';
@@ -67,6 +68,7 @@ export default angular.module('thingsboard.home', [
     thingsboardEntity,
     thingsboardEvent,
     thingsboardAlarm,
+    thingsboardAuditLog,
     thingsboardExtension,
     thingsboardTenant,
     thingsboardCustomer,
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index b9977f6..96dc152 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -286,6 +286,38 @@ export default angular.module('thingsboard.locale', [])
                     "selected-attributes": "{ count, select, 1 {1 attribute} other {# attributes} } selected",
                     "selected-telemetry": "{ count, select, 1 {1 telemetry unit} other {# telemetry units} } selected"
                 },
+                "audit-log": {
+                    "audit": "Audit",
+                    "audit-logs": "Audit Logs",
+                    "timestamp": "Timestamp",
+                    "entity-type": "Entity Type",
+                    "entity-name": "Entity Name",
+                    "user": "User",
+                    "type": "Type",
+                    "status": "Status",
+                    "details": "Details",
+                    "type-added": "Added",
+                    "type-deleted": "Deleted",
+                    "type-updated": "Updated",
+                    "type-attributes-updated": "Attributes updated",
+                    "type-attributes-deleted": "Attributes deleted",
+                    "type-rpc-call": "RPC call",
+                    "type-credentials-updated": "Credentials updated",
+                    "type-assigned-to-customer": "Assigned to Customer",
+                    "type-unassigned-from-customer": "Unassigned from Customer",
+                    "type-activated": "Activated",
+                    "type-suspended": "Suspended",
+                    "type-credentials-read": "Credentials read",
+                    "type-attributes-read": "Attributes read",
+                    "status-success": "Success",
+                    "status-failure": "Failure",
+                    "audit-log-details": "Audit log details",
+                    "no-audit-logs-prompt": "No logs found",
+                    "action-data": "Action data",
+                    "failure-details": "Failure details",
+                    "search": "Search audit logs",
+                    "clear-search": "Clear search"
+                },
                 "confirm-on-exit": {
                     "message": "You have unsaved changes. Are you sure you want to leave this page?",
                     "html-message": "You have unsaved changes.<br/>Are you sure you want to leave this page?",
@@ -1183,7 +1215,8 @@ export default angular.module('thingsboard.locale', [])
                     "activation-link": "User activation link",
                     "activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> :",
                     "copy-activation-link": "Copy activation link",
-                    "activation-link-copied-message": "User activation link has been copied to clipboard"
+                    "activation-link-copied-message": "User activation link has been copied to clipboard",
+                    "details": "Details"
                 },
                 "value": {
                     "type": "Value type",
diff --git a/ui/src/app/plugin/plugins.tpl.html b/ui/src/app/plugin/plugins.tpl.html
index b3563e9..eafb9f7 100644
--- a/ui/src/app/plugin/plugins.tpl.html
+++ b/ui/src/app/plugin/plugins.tpl.html
@@ -66,5 +66,12 @@
                                entity-type="{{vm.types.entityType.plugin}}">
             </tb-relation-table>
         </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isPluginEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
+                md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+            <tb-audit-log-table flex entity-type="vm.types.entityType.plugin"
+                                entity-id="vm.grid.operatingItem().id.id"
+                                audit-log-mode="{{vm.types.auditLogMode.entity}}">
+            </tb-audit-log-table>
+        </md-tab>
     </md-tabs>
 </tb-grid>
diff --git a/ui/src/app/rule/rules.tpl.html b/ui/src/app/rule/rules.tpl.html
index 064728b..f46d124 100644
--- a/ui/src/app/rule/rules.tpl.html
+++ b/ui/src/app/rule/rules.tpl.html
@@ -66,5 +66,12 @@
                                entity-type="{{vm.types.entityType.rule}}">
             </tb-relation-table>
         </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
+                md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+            <tb-audit-log-table flex entity-type="vm.types.entityType.rule"
+                                entity-id="vm.grid.operatingItem().id.id"
+                                audit-log-mode="{{vm.types.auditLogMode.entity}}">
+            </tb-audit-log-table>
+        </md-tab>
     </md-tabs>
 </tb-grid>
diff --git a/ui/src/app/services/clipboard.service.js b/ui/src/app/services/clipboard.service.js
new file mode 100644
index 0000000..7c6d960
--- /dev/null
+++ b/ui/src/app/services/clipboard.service.js
@@ -0,0 +1,128 @@
+/*
+ * 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.
+ */
+export default angular.module('thingsboard.clipboard', [])
+    .factory('clipboardService', ClipboardService)
+    .name;
+
+/*@ngInject*/
+function ClipboardService($q) {
+
+    var fakeHandler, fakeHandlerCallback, fakeElem;
+
+    var service = {
+        copyToClipboard: copyToClipboard
+    };
+
+    return service;
+
+    /* eslint-disable */
+    function copyToClipboard(trigger, text) {
+        var deferred = $q.defer();
+        const isRTL = document.documentElement.getAttribute('dir') == 'rtl';
+        removeFake();
+        fakeHandlerCallback = () => removeFake();
+        fakeHandler = document.body.addEventListener('click', fakeHandlerCallback) || true;
+        fakeElem = document.createElement('textarea');
+        fakeElem.style.fontSize = '12pt';
+        fakeElem.style.border = '0';
+        fakeElem.style.padding = '0';
+        fakeElem.style.margin = '0';
+        fakeElem.style.position = 'absolute';
+        fakeElem.style[ isRTL ? 'right' : 'left' ] = '-9999px';
+        let yPosition = window.pageYOffset || document.documentElement.scrollTop;
+        fakeElem.style.top = `${yPosition}px`;
+        fakeElem.setAttribute('readonly', '');
+        fakeElem.value = text;
+        document.body.appendChild(fakeElem);
+        var selectedText = select(fakeElem);
+
+        let succeeded;
+        try {
+            succeeded = document.execCommand('copy');
+        }
+        catch (err) {
+            succeeded = false;
+        }
+        if (trigger) {
+            trigger.focus();
+        }
+        window.getSelection().removeAllRanges();
+        removeFake();
+        if (succeeded) {
+            deferred.resolve(selectedText);
+        } else {
+            deferred.reject();
+        }
+        return deferred.promise;
+    }
+
+    function removeFake() {
+        if (fakeHandler) {
+            document.body.removeEventListener('click', fakeHandlerCallback);
+            fakeHandler = null;
+            fakeHandlerCallback = null;
+        }
+        if (fakeElem) {
+            document.body.removeChild(fakeElem);
+            fakeElem = null;
+        }
+    }
+
+    function select(element) {
+        var selectedText;
+
+        if (element.nodeName === 'SELECT') {
+            element.focus();
+
+            selectedText = element.value;
+        }
+        else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
+            var isReadOnly = element.hasAttribute('readonly');
+
+            if (!isReadOnly) {
+                element.setAttribute('readonly', '');
+            }
+
+            element.select();
+            element.setSelectionRange(0, element.value.length);
+
+            if (!isReadOnly) {
+                element.removeAttribute('readonly');
+            }
+
+            selectedText = element.value;
+        }
+        else {
+            if (element.hasAttribute('contenteditable')) {
+                element.focus();
+            }
+
+            var selection = window.getSelection();
+            var range = document.createRange();
+
+            range.selectNodeContents(element);
+            selection.removeAllRanges();
+            selection.addRange(range);
+
+            selectedText = selection.toString();
+        }
+
+        return selectedText;
+    }
+
+    /* eslint-enable */
+
+}
\ No newline at end of file
diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js
index 24db768..a492585 100644
--- a/ui/src/app/services/menu.service.js
+++ b/ui/src/app/services/menu.service.js
@@ -211,6 +211,12 @@ function Menu(userService, $state, $rootScope) {
                             type: 'link',
                             state: 'home.dashboards',
                             icon: 'dashboards'
+                        },
+                        {
+                            name: 'audit-log.audit-logs',
+                            type: 'link',
+                            state: 'home.auditLogs',
+                            icon: 'track_changes'
                         }];
 
                     homeSections =
@@ -273,6 +279,16 @@ function Menu(userService, $state, $rootScope) {
                                         state: 'home.dashboards'
                                     }
                                 ]
+                            },
+                            {
+                                name: 'audit-log.audit',
+                                places: [
+                                    {
+                                        name: 'audit-log.audit-logs',
+                                        icon: 'track_changes',
+                                        state: 'home.auditLogs'
+                                    }
+                                ]
                             }];
 
                 } else if (authority === 'CUSTOMER_USER') {
diff --git a/ui/src/app/user/user.controller.js b/ui/src/app/user/user.controller.js
index a9f4ce3..6a9fdde 100644
--- a/ui/src/app/user/user.controller.js
+++ b/ui/src/app/user/user.controller.js
@@ -42,6 +42,8 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d
 
     var vm = this;
 
+    vm.types = types;
+
     vm.userGridConfig = {
         deleteItemTitleFunc: deleteUserTitle,
         deleteItemContentFunc: deleteUserText,
diff --git a/ui/src/app/user/users.tpl.html b/ui/src/app/user/users.tpl.html
index 72f67d2..03a5da2 100644
--- a/ui/src/app/user/users.tpl.html
+++ b/ui/src/app/user/users.tpl.html
@@ -19,10 +19,20 @@
 	<details-buttons tb-help="'users'" help-container-id="help-container">
 		<div id="help-container"></div>
 	</details-buttons>
-	<tb-user user="vm.grid.operatingItem()"
-			 is-edit="vm.grid.detailsConfig.isDetailsEditMode"
-			 the-form="vm.grid.detailsForm"
-			 on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)"
-			 on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"
-			 on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user>
+	<md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
+			 id="tabs" md-border-bottom flex class="tb-absolute-fill">
+		<md-tab label="{{ 'user.details' | translate }}">
+			<tb-user user="vm.grid.operatingItem()"
+					 is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+					 the-form="vm.grid.detailsForm"
+					 on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)"
+					 on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"
+					 on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user>
+		</md-tab>
+		<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.isTenantAdmin()" md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+			<tb-audit-log-table flex user-id="vm.grid.operatingItem().id.id"
+								audit-log-mode="{{vm.types.auditLogMode.user}}">
+			</tb-audit-log-table>
+		</md-tab>
+	</md-tabs>
 </tb-grid>
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 93418b2..a961ab5 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -203,6 +203,19 @@ md-sidenav {
  * THINGSBOARD SPECIFIC
  ***********************/
 
+label {
+  &.tb-title {
+    pointer-events: none;
+    color: #666;
+    font-size: 13px;
+    font-weight: 400;
+    padding-bottom: 15px;
+    &.no-padding {
+      padding-bottom: 0px;
+    }
+  }
+}
+
 .tb-noselect {
   -webkit-touch-callout: none; /* iOS Safari */
   -webkit-user-select: none; /* Safari */
diff --git a/ui/src/vendor/css.js/css.js b/ui/src/vendor/css.js/css.js
index 8c4160a..254e6b5 100644
--- a/ui/src/vendor/css.js/css.js
+++ b/ui/src/vendor/css.js/css.js
@@ -147,7 +147,8 @@ fi.prototype.parseRules = function (rules) {
     rules = rules.split('\r\n').join('\n');
     var ret = [];
 
-    rules = rules.split(';');
+    // Split all rules but keep semicolon for base64 url data
+    rules = rules.split(/;(?!base64)/);
 
     //proccess rules line by line
     for (var i = 0; i < rules.length; i++) {