You’re a curious person. You want to know how your router is performing. But you don’t want to have to slog through a thousand and one command-line commands with their various inscrutable flags.
Let’s work on that a bit. We’re going to create a sub-tab under Network >> DNS >> Stats which will display the dnsmasq cache statistics.
First, edit the ACL file:
vi /usr/share/rpcd/acl.d/luci-mod-network.json
… press i to enter editing mode, then change this:
"luci-mod-network-dns": { "description": "Grant access to DNS configuration", "read": { "ubus": { "luci-rpc": [ "getHostHints" ] }, "uci": [ "dhcp" ] }, "write": { "uci": [ "dhcp" ] } },
… to this:
"luci-mod-network-dns": { "description": "Grant access to DNS configuration", "read": { "ubus": { "luci-rpc": [ "getHostHints" ], "dnsmasq": [ "metrics" ] }, "uci": [ "dhcp" ] }, "write": { "uci": [ "dhcp" ] } },
… press Esc to exit editing mode, then :wq to save and exit.
Restart the relevant service:
/etc/init.d/rpcd restart
You’ll have to log back in after that service restart.
Now we’re going to edit the file which creates the page at Network >> DNS.
vi /www/luci-static/resources/view/network/dns.js
… press i to enter editing mode.
Look for the following code:
s.tab('ipsets',_('IP Sets'));o=s.taboption('general',form.Value,...
… and inject the Stats tab in there thusly:
s.tab('ipsets',_('IP Sets'));s.tab('stats', _('Stats'));o=s.taboption('general',form.Value,
Now look at the very end of the file for:
return m.render();}});
Line your cursor up at the beginning of that code blurb, and press Enter several times to create some space for our code.
Now we inject our code above that last line, in the blank space you just created:
o = s.taboption('stats', form.DummyValue, '_stats_render'); o.title = _('DNSmasq Statistics'); o.render = function() { var container = E('div', { 'id': 'dnsmasq-stats-wrapper', 'style': 'margin-top: 15px;' }); // Create local memory anchors to track subtracted values var offsets = { hits: 0, forwarded: 0, total_inserted: 0, disc: 0, off: 0, req: 0, ack: 0 }; var getMetrics = L.rpc.declare({ object: 'dnsmasq', method: 'metrics', params: [] }); // Extracts active dynamic leases var getLiveLeases = L.rpc.declare({ object: 'luci-rpc', method: 'getDHCPLeases', params: [] }); function updateDashboard(res) { // Upon reset button push, apply the zeroing across variables var hits = Math.max(0, (res.dns_local_answered || 0) - offsets.hits); var forwarded = Math.max(0, (res.dns_queries_forwarded || 0) - offsets.forwarded); var total = hits + forwarded; var hitRate = total > 0 ? ((hits / total) * 100).toFixed(2) + '%' : '0.00%'; var inserted = Math.max(0, (res.dns_cache_inserted || 0) - offsets.total_inserted); var disc = Math.max(0, (res.dhcp_discover || 0) - offsets.disc); var off = Math.max(0, (res.dhcp_offer || 0) - offsets.off); var req = Math.max(0, (res.dhcp_request || 0) - offsets.req); var ack = Math.max(0, (res.dhcp_ack || 0) - offsets.ack); container.innerHTML = ''; container.appendChild(E('div', { 'class': 'cbi-map-descr' }, _('DNSmasq Statistics'))); // Create the tables container var section = E('div', { 'class': 'cbi-section' }, [ E('h3', {}, _('DNS Performance Summary')), E('div', { 'class': 'table', 'style': 'max-width:550px; margin-bottom:20px;' }, [ E('div', { 'class': 'tr' }, [E('div', { 'class': 'td font-weight-bold' }, _('Cache Hit Rate')), E('div', { 'class': 'td', 'style': 'font-size:1.4em; color:#22c55e; font-weight:bold;' }, hitRate)]), E('div', { 'class': 'tr' }, [E('div', { 'class': 'td' }, _('Cache Hits (Local Answered)')), E('div', { 'class': 'td' }, hits.toString())]), E('div', { 'class': 'tr' }, [E('div', { 'class': 'td' }, _('Cache Misses (Forwarded)')), E('div', { 'class': 'td' }, forwarded.toString())]), E('div', { 'class': 'tr' }, [E('div', { 'class': 'td' }, _('Total Queries Evaluated')), E('div', { 'class': 'td' }, total.toString())]), E('div', { 'class': 'tr' }, [E('div', { 'class': 'td' }, _('Cache Insertions (Since Last Reset)')), E('div', { 'class': 'td' }, inserted.toString())]), E('div', { 'class': 'tr' }, [E('div', { 'class': 'td' }, _('Evictions (Forced Live Frees)')), E('div', { 'class': 'td', 'style': res.dns_cache_live_freed > 0 ? 'color:#ef4444; font-weight:bold;' : '' }, (res.dns_cache_live_freed || 0).toString())]) ]), E('h3', {}, _('DHCP Server Counters')), E('div', { 'class': 'table', 'style': 'max-width:550px; margin-bottom:20px;' }, [ E('div', { 'class': 'tr' }, [E('div', { 'class': 'td' }, _('Active DHCPv4 Leases')), E('div', { 'class': 'td' }, (res.leases_allocated_4 || 0).toString())]), E('div', { 'class': 'tr' }, [E('div', { 'class': 'td' }, _('Active DHCPv6 Leases')), E('div', { 'class': 'td' }, (res.leases_allocated_6 || 0).toString())]), E('div', { 'class': 'tr' }, [E('div', { 'class': 'td' }, _('DHCP Discovers / Offers')), E('div', { 'class': 'td' }, (res.dhcp_discover || 0) + ' / ' + (res.dhcp_offer || 0))]), E('div', { 'class': 'tr' }, [E('div', { 'class': 'td' }, _('DHCP Requests / ACKs')), E('div', { 'class': 'td' }, (res.dhcp_request || 0) + ' / ' + (res.dhcp_ack || 0))]) ]) ]); // Reset button var resetBtn = E('button', { 'type': 'button', 'class': 'cbi-button cbi-button-reset', 'style': 'margin-top: 10px;', 'click': function() { // Offsets to zero out dashboard columns offsets.hits = res.dns_local_answered || 0; offsets.forwarded = res.dns_queries_forwarded || 0; offsets.total_inserted = res.dns_cache_inserted || 0; offsets.disc = res.dhcp_discover || 0; offsets.off = res.dhcp_offer || 0; offsets.req = res.dhcp_request || 0; offsets.ack = res.dhcp_ack || 0; // Force a repaint to show zeroes right away updateDashboard(res); ui.addNotification(null, E('p', _('DNSmasq statistics display has been reset to zero.')), 'info'); } }, _('Reset Statistics')); section.appendChild(E('div', { 'style': 'margin-top: 15px; margin-bottom: 25px;' }, resetBtn)); container.appendChild(section); } function fetchLiveMetrics() { return Promise.all([ L.resolveDefault(getMetrics(), {}), L.resolveDefault(getLiveLeases(), {}) ]).then(function(results) { var res = results[0] || {}; var leaseObj = results[1] || {}; // Extract IPv4 and IPv6 DHCP lease numbers var ipv4Leases = Array.isArray(leaseObj.dhcp_leases) ? leaseObj.dhcp_leases : []; var ipv6Leases = Array.isArray(leaseObj.dhcp6_leases) ? leaseObj.dhcp6_leases : []; res.leases_allocated_4 = ipv4Leases.length; res.leases_allocated_6 = ipv6Leases.length; return res; }); } // Execute initial paint fetchLiveMetrics().then(updateDashboard); // 5-second polling L.Poll.add(function() { if (document.getElementById('dnsmasq-stats-wrapper') && container.parentNode.style.display !== 'none') { return fetchLiveMetrics().then(updateDashboard); } }, 5); return container; };
Press Esc to exit editing mode, then :wq to save and exit.
Then restart your browser and load that page.
The Reset button doesn’t actually ‘reset’ anything, it just zeroes the values for that session by offsetting the existing values. If you navigate away from that tab, then navigate back to that tab, the original values will display. This is handy for short-term testing of dnsmasq, while preserving the underlying statistics. Besides, it is incredibly difficult to get a service to actually restart (and thus zero the values) from the LuCI code.
Make the changes impervious to firmware updates. Add:
/usr/share/rpcd/acl.d/luci-mod-network.json /www/luci-static/resources/view/network/dns.js
… to System >> Backup / Flash firmware >> Configuration tab, then click the Save button.
