-
-
Save rsmets/655d7afb91483bb0691ac045a26df864 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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, '&').replace(/</g, '<').replace(/>/g, '>'); | |
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,' '); | |
} | |
</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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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: [] | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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