Skip to content

Instantly share code, notes, and snippets.

@rsmets
Forked from avimar/admin_view_logs.svelte
Created December 22, 2020 18:06
Show Gist options
  • Save rsmets/655d7afb91483bb0691ac045a26df864 to your computer and use it in GitHub Desktop.
Save rsmets/655d7afb91483bb0691ac045a26df864 to your computer and use it in GitHub Desktop.
<script>
export let params={};
export let service=null;
export let id=null;
import Fa from 'svelte-fa'
import { faEdit, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
import feathers from '../web_feathers.js';
const rest = feathers.service('logs');
import {display_date_seconds} from '../web_display_date.js';
let loading=false;
let data = [];
let header=[];
//const sql_select =['method','provider','ip','userEmail','duration','timeStart','data','query','error']; //$select:sql_select
var sql_select =['trace','timeStart','duration','method','provider','ip','userEmail','query','data','error'];
var methods=['update','patch','create','remove'];
var searchKey={path:service};
var limit=null;
if(id) searchKey.id=id;//allow null id to query all
else if (params.trace) {
searchKey={trace:params.trace}; //overwrite with just this... so many different uses right now.
sql_select.push('path');
sql_select.push('id');//also show the ID for all the related calls
methods.push('get');//for related, see all query types.
methods.push('find');
}
else {
sql_select.push('id');//if not searching for an ID, then show one!
limit=20;
}
if(service=='hotmobile-line') sql_select.push('result');//it's not long, show it for clarity.
if(service=='hotmobile_numbers_available') methods.push('remove');
var query = {query:{$sort:{timeStart: -1}, method: {$in:methods}, ...searchKey,$select:sql_select, $limit:limit }}
function refreshLookup(){
loading=true;
rest.find(query)
.then(function(result){
if(result.length>0) {
data=result;
header = Object.keys(data[0]);
}
})
.finally(s=>loading=false);
}
refreshLookup();//onLoad
function syntaxHighlight(json) {//code mostly comes from https://stackoverflow.com/a/7220510/1278519
if (typeof json != 'string') {
json = JSON.stringify(json, undefined, 4);
}
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
} else {
cls = 'string';
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return '<span class="' + cls + '">' + match + '</span>';
});
return json.replace(/\n/g,'\n<br>').replace(/\\n/g,'<br>').replace(/ /g,'&nbsp;');
}
</script>
<style>
.top{vertical-align:top;}
</style>
<main>
Logs {service}:<br>
<table border=1>
<tr>
{#each header as col}
<th>{col}</th>
{/each}
</tr>
{#each data as row}
<tr class:alert-danger={row.error}>
{#each header as key}
{#if key=="trace"}<td class="top"><a href="#/logs/{row[key]}">Related</a></td>
{:else if key=="timeStart"}<td class="top">{display_date_seconds(row[key])}</td>
{:else if key=='duration'}<td class="top">{(row[key]/1000)}s</td>
{:else if key=='method'}<td class="top">
{#if row[key]=="remove"}
<Fa icon={faTrash}/>
{:else if row[key]=="create"}
<Fa icon={faPlus}/>
{:else if row[key]=="update"||row[key]=="patch"}
<Fa icon={faEdit}/>
{/if}
{row[key]}
</td>
{:else if ['data','query','result','error'].includes(key)}<td class="top">{@html syntaxHighlight(row[key])}</td>
{:else} <td class="top">{row[key]}</td>
{/if}
{/each}
</tr>
{/each}
</table>
</main>
// Application hooks that run for every service
const fp= require('lodash/fp');
const os = require('os');
const { Conflict } = require('@feathersjs/errors');
const {iff} = require('feathers-hooks-common');
const jsonifyError = require("jsonify-error");
function debugError(context){ //https://docs.feathersjs.com/api/errors.html#error-handling
if(context.app.get('verbose_errors')===false) return;//defaults to being on
console.error(context.error.stack);
console.error(fp.omit(['hook.service','hook.app'],context.error));
//jsonifyError.log(context.error);
// console.error(context.error.code,context.error.message);
}
const { v4 } = require('uuid');
async function setLoggingInformation(context){
context.messages = [];
//defensive: set here for internal service calls, which don't run middleware. Prefer global middleware (where we NEED to do it for the IP) so we trigger the time start sooner.
if(!context.params.trace) context.params.trace=v4();
if(!context.params.timeStart) context.params.timeStart=new Date().toISOString();
return context;
}
const loggerService='logs';//extract so we never hit a loop
async function saveRequest(context){
if(context.path==loggerService) return context;//IMPORTANT: avoid recursion from this service call! Logs all the way down!
if(context.path=='authentication' && context.method=='create') return context;//login noise
if(context.path=='users' && context.method=='get') return context;//probably login noise
//trace, IP, timeStart I added with middleware
//params.messages is an array we can push message into
var data = fp.pick([
'path','method','id','error'
,'messages'//things I added
,'data','result'// --BIG STUFF!
],context);
if(data.method=='find') delete data.result;//don't log the actual results when there's so much data coming back
if(data.error) data.error = jsonifyError(data.error);//errors don't coerce nicely to objects on their own.
if(data.error && fp.has('error.enumerableFields.hook',data)){
delete data.error.enumerableFields.hook;//we saved the parts we wanted already.
}
if(context.path=='hotmobile-line' && fp.has('error.enumerableFields.data',data)){
data.error.data = data.error.enumerableFields.data;
delete data.error.enumerableFields;
}
if(context.path=='webhook-hotmobile' || context.path=='hotmobile-historical'){ //very bulky, no need to save
delete data.data;
delete data.result;
}
if(data.result && typeof data.result=="string") data.result = JSON.stringify(data.result);//turn into JSON
data.messages = data.messages.join('\n');
data = fp.assign(data,fp.pick(['trace','ip','timeStart','provider','query'],context.params));
if(context.params.user) {//shouldn't need `authenticated` - just check if there's a user ID
data.user = fp.get('id',context.params.user);
data.userEmail = fp.get('email',context.params.user);
data.userPermissions = fp.get('permissions',context.params.user);
}
data['user-agent'] = fp.get('params.user-agent',context) || fp.get('params.headers.user-agent',context);//allow an internal service to set the user-agent, e.g. the script name
data.duration = Date.now()-new Date(data.timeStart).getTime();
data.machine=os.hostname();
const dates = ['createdAt','orderPlaced','orderFulfilled']; //dates must be converted before comparison
if(['update','patch'].includes(data.method) && context.params.before){//we have a stashBefore
var updated = {};
for (const key in context.data) {
if(key=='updatedAt') ;
else if(dates.includes(key)){//need to compare something other than the raw Date object.
if(context.params.before[key]==context.data[key]) ; //both the same, e.g. null
else if ( (!context.params.before[key] && context.data[key]) //wasn't set but now it is
|| (new Date(context.params.before[key]).getTime()!=new Date(context.data[key]).getTime())//actual update
) {
updated[key]={submitted: context.data[key], saved: context.params.before[key]};
}
}
else if(context.params.before[key]!=context.data[key]) {
updated[key]={submitted: context.data[key], saved: context.params.before[key]};
}
}
data.data = updated;
//console.log(updated);
}
//console.log(data);
await context.app.service(loggerService).create(data,{query: {$noSelect:true}}).catch(console.error);//have to wait for scripts that automatically exit. don't try to query the actual input
}
const excludedPathsForStash=['hotmobile-line','new_password']
function safeForStash(context){
return !excludedPathsForStash.includes(context.path);
}
function checkIfUpdateIsSafe(context){
if(context.params.before.updatedAt!=context.data.updatedAt) {
var updated = {};
for (const key in context.data) {
if(key=='updatedAt') ;
else if(context.params.before[key]!=context.data[key]) {
updated[key]={submitted: context.data[key], saved: context.params.before[key]};
}
}
throw new Conflict(updated)
}
return context;
}
//standard stashBefore doesn't pass along full params.
async function stashBeforeCustom(context){
return context.service.get(context.id, {trace: context.params.trace})//pass along user data and IP too? or just the trace so we know it's internal.
.then(data => {
context.params.before = data;
return context;
})
.catch(() => context);
}
module.exports = {
before: {
all: [setLoggingInformation],
find: [],
get: [],
create: [],
//checkIfUpdateIsSafe -- we're not properly returning the modifiedAt time, so we can't do subsequent updates
update: [iff(safeForStash,stashBeforeCustom)], //stash the before so we can have better logs of what changed
patch: [iff(safeForStash,stashBeforeCustom)],
remove: []
},
after: {
all: [saveRequest],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
error: {
all: [saveRequest,debugError],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
};
// See https://vincit.github.io/objection.js/#models
// for more of what you can do here.
const { Model } = require('objection');
class logs extends Model {
static get tableName(){
return 'logs';
}
static get idColumn(){
return 'log';
}
}
module.exports = function (app) {
const db = app.get('knex');
db.schema.hasTable('logs').then(exists => {
if (!exists) {
db.schema.createTable('logs', table => {
table.increments('log');
table.uuid('trace')
table.string('path');
table.string('method');
table.string('provider');
table.string('id');
table.string('ip');
table.text('messages');
table.jsonb('query')
table.jsonb('error');
table.jsonb('data');
table.jsonb('result');
table.timestamp('timeStart');
table.integer('user');
table.string('userEmail');
table.string('userPermissions');
table.string('user-agent');
table.integer('duration');
table.string('machine');
})
.then(() => console.log('Created logs table')) // eslint-disable-line no-console
.catch(e => console.error('Error creating logs table', e)); // eslint-disable-line no-console
}
})
.catch(e => console.error('Error creating logs table', e)); // eslint-disable-line no-console
return logs;
};
import feathers from '@feathersjs/feathers';
const app = feathers();
//Auth
import auth from '@feathersjs/authentication-client';
app.configure(auth());
//import axios from 'axios';
import rest from '@feathersjs/rest-client';
const restClient = rest() //configure with connection URL base
app.configure(restClient.fetch(window.fetch));
import { writable } from 'svelte/store';
app.perms = writable(false);//store currently logged in user
app.perms.set(false);
app.reAuthenticate()//safe to always try using a stored token on load.
.then(async function(result){
app.perms.set(result.user.permissions);
})
.catch(s=>'noop');
export default app;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment