Merge pull request #18 from dxos/tel-m1a

Logging
This commit is contained in:
Thomas E Lackey 2020-06-11 01:31:27 -05:00 committed by GitHub
commit 3887b2a834
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 440 additions and 256 deletions

View File

@ -15,6 +15,7 @@ const useStyles = makeStyles(theme => ({
},
log: {
width: '100%',
display: 'flex',
// Pin to bottom (render in time order).
flexDirection: 'column-reverse',
@ -106,6 +107,9 @@ const Log = ({ log = [] }) => {
);
};
// TODO(telackey): Why do these display in reverse?
log.reverse();
return (
<div className={classes.root}>
<div className={classes.log}>

View File

@ -0,0 +1,54 @@
//
// Copyright 2020 DxOS.org
//
import React, { useContext } from 'react';
import { useQuery } from '@apollo/react-hooks';
import { ConsoleContext, useQueryStatusReducer } from '../hooks';
import LOGS from '../gql/logs.graphql';
import Log from './Log';
const MAX_LINES = 1000;
const _logBuffers = new Map();
const getLogBuffer = (name) => {
let buffer = _logBuffers.get(name);
if (!buffer) {
buffer = [];
_logBuffers.set(name, buffer);
}
return buffer;
};
const LogPoller = ({ service }) => {
const { config } = useContext(ConsoleContext);
const logBuffer = getLogBuffer(service);
const data = useQueryStatusReducer(useQuery(LOGS, {
pollInterval: config.api.intervalLog,
variables: { service, incremental: logBuffer.length !== 0 }
}));
if (!data) {
return null;
}
const { incremental, lines } = JSON.parse(data.logs.json);
if (!incremental && lines.length) {
logBuffer.length = 0;
}
logBuffer.push(...lines);
if (logBuffer.length > MAX_LINES) {
logBuffer.splice(0, logBuffer.length - MAX_LINES);
}
return (
<Log log={logBuffer.slice(0)} />
);
};
export default LogPoller;

View File

@ -2,11 +2,16 @@
// Copyright 2020 DxOS.org
//
import React, { useContext } from 'react';
import React, { useContext, useState } from 'react';
import { useQuery } from '@apollo/react-hooks';
import { makeStyles } from '@material-ui/core';
import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs';
import TabContext from '@material-ui/lab/TabContext';
import Json from '../../components/Json';
import SERVICE_STATUS from '../../gql/service_status.graphql';
import SYSTEM_STATUS from '../../gql/system_status.graphql';
import { ConsoleContext, useQueryStatusReducer } from '../../hooks';
@ -14,22 +19,64 @@ import { ConsoleContext, useQueryStatusReducer } from '../../hooks';
import Panel from '../../components/Panel';
import Toolbar from '../../components/Toolbar';
const TAB_SYSTEM = 'system';
const TAB_SERVICES = 'services';
const useStyles = makeStyles(() => ({
expand: {
flex: 1
},
panel: {
display: 'flex',
overflow: 'hidden',
flex: 1
},
paper: {
display: 'flex',
overflow: 'hidden',
flex: 1
}
}));
const Status = () => {
const classes = useStyles();
const { config } = useContext(ConsoleContext);
const [tab, setTab] = useState(TAB_SYSTEM);
const systemResponse = useQueryStatusReducer(useQuery(SYSTEM_STATUS, { pollInterval: config.api.intervalQuery }));
if (!systemResponse) {
const serviceResponse = useQueryStatusReducer(useQuery(SERVICE_STATUS, { pollInterval: config.api.intervalQuery }));
if (!systemResponse || !serviceResponse) {
return null;
}
const data = JSON.parse(systemResponse.system_status.json);
const systemData = JSON.parse(systemResponse.system_status.json);
const serviceData = JSON.parse(serviceResponse.service_status.json);
return (
<Panel
toolbar={
<Toolbar />
<Toolbar>
<Tabs value={tab} onChange={(_, value) => setTab(value)}>
<Tab value={TAB_SYSTEM} label='System' />
<Tab value={TAB_SERVICES} label='Services' />
</Tabs>
</Toolbar>
}
>
<Json data={data} />
<TabContext value={tab}>
{tab === TAB_SYSTEM && (
<div className={classes.panel}>
<Json data={systemData} />
</div>
)}
{tab === TAB_SERVICES && (
<div className={classes.panel}>
<Json data={serviceData} />
</div>
)}
</TabContext>
</Panel>
);
};

View File

@ -11,8 +11,10 @@ import Panel from '../../../components/Panel';
import Toolbar from '../../../components/Toolbar';
import AppRecords from './AppRecords';
import LogPoller from '../../../components/LogPoller';
const TAB_RECORDS = 'records';
const TAB_LOG = 'log';
const useStyles = makeStyles(theme => ({
root: {}
@ -29,6 +31,7 @@ const Apps = () => {
<Toolbar>
<Tabs value={tab} onChange={(_, value) => setTab(value)}>
<Tab value={TAB_RECORDS} label='Records' />
<Tab value={TAB_LOG} label='Log' />
</Tabs>
</Toolbar>
}
@ -36,6 +39,10 @@ const Apps = () => {
{tab === TAB_RECORDS && (
<AppRecords />
)}
{tab === TAB_LOG && (
<LogPoller service='app-server' />
)}
</Panel>
);
};

View File

@ -29,7 +29,7 @@ const AppRecords = () => {
return null;
}
const records = data.wns_records.json;
const records = JSON.parse(data.wns_records.json);
return (
<Table>

View File

@ -3,122 +3,44 @@
//
import React, { useState } from 'react';
import get from 'lodash.get';
import { useQuery } from '@apollo/react-hooks';
import { makeStyles } from '@material-ui/core';
import Paper from '@material-ui/core/Paper';
import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs';
import TableBody from '@material-ui/core/TableBody';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import TabContext from '@material-ui/lab/TabContext';
import IPFS_STATUS from '../../../gql/ipfs_status.graphql';
import WNS_RECORDS from '../../../gql/wns_records.graphql';
import { useQueryStatusReducer } from '../../../hooks';
import Json from '../../../components/Json';
import Panel from '../../../components/Panel';
import Table from '../../../components/Table';
import TableCell from '../../../components/TableCell';
import Toolbar from '../../../components/Toolbar';
import { BooleanIcon } from '../../../components/BooleanIcon';
const RECORD_TYPE = 'wrn:service';
const SERVICE_TYPE = 'ipfs';
const useStyles = makeStyles((theme) => ({
tableContainer: {
flex: 1,
overflowY: 'scroll'
},
table: {
tableLayout: 'fixed',
'& th': {
fontVariant: 'all-small-caps',
fontSize: 18,
cursor: 'ns-resize'
}
},
connected: {
fontWeight: 'bold'
},
disconnected: {
fontStyle: 'italic'
},
colShort: {
width: '30%'
},
colWide: {},
colBoolean: {
width: '10%'
},
caption: {
backgroundColor: theme.palette.primary[500],
color: theme.palette.primary.contrastText,
paddingLeft: '1em',
margin: 0
}
}));
import LogPoller from '../../../components/LogPoller';
import IPFSStatus from './IPFSStatus';
const TAB_STATUS = 'status';
const TAB_LOG = 'log';
const TAB_SWARM_LOG = 'swarm';
const useStyles = makeStyles(() => ({
expand: {
flex: 1
},
panel: {
display: 'flex',
overflow: 'hidden',
flex: 1
},
paper: {
display: 'flex',
overflow: 'hidden',
flex: 1
}
}));
const IPFS = () => {
const classes = useStyles();
const [tab, setTab] = useState(TAB_STATUS);
const ipfsResponse = useQueryStatusReducer(useQuery(IPFS_STATUS));
const wnsResponse = useQueryStatusReducer(useQuery(WNS_RECORDS, {
variables: { attributes: { type: RECORD_TYPE, service: SERVICE_TYPE } }
}));
if (!wnsResponse || !ipfsResponse) {
return null;
}
const ipfsData = JSON.parse(ipfsResponse.ipfs_status.json);
const registeredServers = JSON.parse(wnsResponse.wns_records.json);
const displayServers = registeredServers.map((service) => {
console.error(service);
const addresses = get(service, 'attributes.ipfs.addresses');
let connected = false;
for (const address of addresses) {
const parts = address.split('/');
const nodeId = parts[parts.length - 1];
connected = !!ipfsData.swarm.peers.find(({ peer }) => peer === nodeId);
if (connected) {
break;
}
}
return {
name: get(service, 'name'),
version: get(service, 'version'),
description: get(service, 'attributes.description'),
ipfs: get(service, 'attributes.ipfs'),
connected
};
});
displayServers.sort((a, b) => {
return a.connected && !b.connected ? -1 : b.connected && !a.connected ? 1 : b.name < a.name ? 1 : -1;
});
if (displayServers.length === 0) {
displayServers.push({ name: 'None' });
}
return (
<Panel
toolbar={
@ -126,46 +48,32 @@ const IPFS = () => {
<Tabs value={tab} onChange={(_, value) => setTab(value)}>
<Tab value={TAB_STATUS} label='Status' />
<Tab value={TAB_LOG} label='Log' />
<Tab value={TAB_SWARM_LOG} label='Connection Log' />
</Tabs>
</Toolbar>
}
>
<h4 className={classes.caption}>WNS-registered IPFS Servers</h4>
<Table stickyHeader size='small' className={classes.table}>
<TableHead>
<TableRow>
<TableCell>Identifier</TableCell>
<TableCell size='medium'>Description</TableCell>
<TableCell size='icon'>Connected</TableCell>
<TableCell>Address</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayServers.map(({ name, description, ipfs, connected }) => (
<TableRow key={name}>
<TableCell>{name}</TableCell>
<TableCell>{description}</TableCell>
<TableCell>
<BooleanIcon yes={connected} />
</TableCell>
<TableCell>
{ipfs.addresses}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<TabContext value={tab}>
{tab === TAB_STATUS && (
<div className={classes.panel}>
<Paper className={classes.paper}>
<IPFSStatus />
</Paper>
</div>
)}
<h4 className={classes.caption}>Local IPFS Server</h4>
<Json data={{
id: ipfsData.id.id,
version: ipfsData.id.agentVersion,
addresses: ipfsData.id.addresses,
peers: ipfsData.swarm.peers.length,
numObjects: ipfsData.repo.stats.numObjects,
repoSize: ipfsData.repo.stats.repoSize
}}
/>
{tab === TAB_LOG && (
<div className={classes.panel}>
<LogPoller service='ipfs' />
</div>
)}
{tab === TAB_SWARM_LOG && (
<div className={classes.panel}>
<LogPoller service='ipfs-swarm-connect' />
</div>
)}
</TabContext>
</Panel>
);
};

View File

@ -0,0 +1,157 @@
//
// Copyright 2020 DxOS.org
//
import React from 'react';
import get from 'lodash.get';
import { useQuery } from '@apollo/react-hooks';
import { makeStyles } from '@material-ui/core';
import TableBody from '@material-ui/core/TableBody';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import IPFS_STATUS from '../../../gql/ipfs_status.graphql';
import WNS_RECORDS from '../../../gql/wns_records.graphql';
import { useQueryStatusReducer } from '../../../hooks';
import Json from '../../../components/Json';
import Panel from '../../../components/Panel';
import Table from '../../../components/Table';
import TableCell from '../../../components/TableCell';
import { BooleanIcon } from '../../../components/BooleanIcon';
const RECORD_TYPE = 'wrn:service';
const SERVICE_TYPE = 'ipfs';
const useStyles = makeStyles((theme) => ({
tableContainer: {
flex: 1,
overflowY: 'scroll'
},
table: {
tableLayout: 'fixed',
'& th': {
fontVariant: 'all-small-caps',
fontSize: 18,
cursor: 'ns-resize'
}
},
connected: {
fontWeight: 'bold'
},
disconnected: {
fontStyle: 'italic'
},
colShort: {
width: '30%'
},
colWide: {},
colBoolean: {
width: '10%'
},
caption: {
backgroundColor: theme.palette.primary[500],
color: theme.palette.primary.contrastText,
paddingLeft: '1em',
margin: 0
}
}));
const IPFSStatus = () => {
const classes = useStyles();
const ipfsResponse = useQueryStatusReducer(useQuery(IPFS_STATUS));
const wnsResponse = useQueryStatusReducer(useQuery(WNS_RECORDS, {
variables: { attributes: { type: RECORD_TYPE, service: SERVICE_TYPE } }
}));
if (!wnsResponse || !ipfsResponse) {
return null;
}
const ipfsData = JSON.parse(ipfsResponse.ipfs_status.json);
const registeredServers = JSON.parse(wnsResponse.wns_records.json);
const displayServers = registeredServers.map((service) => {
console.error(service);
const addresses = get(service, 'attributes.ipfs.addresses');
let connected = false;
for (const address of addresses) {
const parts = address.split('/');
const nodeId = parts[parts.length - 1];
connected = !!ipfsData.swarm.peers.find(({ peer }) => peer === nodeId);
if (connected) {
break;
}
}
return {
name: get(service, 'name'),
version: get(service, 'version'),
description: get(service, 'attributes.description'),
ipfs: get(service, 'attributes.ipfs'),
connected
};
});
displayServers.sort((a, b) => {
return a.connected && !b.connected ? -1 : b.connected && !a.connected ? 1 : b.name < a.name ? 1 : -1;
});
if (displayServers.length === 0) {
displayServers.push({ name: 'None' });
}
return (
<Panel>
<h4 className={classes.caption}>WNS-registered IPFS Servers</h4>
<Table stickyHeader size='small' className={classes.table}>
<TableHead>
<TableRow>
<TableCell>Identifier</TableCell>
<TableCell size='medium'>Description</TableCell>
<TableCell size='icon'>Connected</TableCell>
<TableCell>Address</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayServers.map(({ name, description, ipfs, connected }) => (
<TableRow key={name}>
<TableCell>{name}</TableCell>
<TableCell>{description}</TableCell>
<TableCell>
<BooleanIcon yes={connected} />
</TableCell>
<TableCell>
{ipfs.addresses}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<h4 className={classes.caption}>Local IPFS Server</h4>
<Json data={{
id: ipfsData.id.id,
version: ipfsData.id.agentVersion,
addresses: ipfsData.id.addresses,
peers: ipfsData.swarm.peers.length,
numObjects: ipfsData.repo.stats.numObjects,
repoSize: ipfsData.repo.stats.repoSize
}}
/>
</Panel>
);
};
export default IPFSStatus;

View File

@ -1,26 +0,0 @@
//
// Copyright 2020 DxOS.org
//
import React, { useContext } from 'react';
import { useQuery } from '@apollo/react-hooks';
import SIGNAL_LOG from '../../../gql/signal_log.graphql';
import { ConsoleContext, useQueryStatusReducer } from '../../../hooks';
import Log from '../../../components/Log';
const SignalLog = () => {
const { config } = useContext(ConsoleContext);
const data = useQueryStatusReducer(useQuery(SIGNAL_LOG, { pollInterval: config.api.intervalLog }));
if (!data) {
return null;
}
return (
<Log log={data.signal_log.log} />
);
};
export default SignalLog;

View File

@ -12,8 +12,8 @@ import TabContext from '@material-ui/lab/TabContext';
import Panel from '../../../components/Panel';
import Toolbar from '../../../components/Toolbar';
import SignalLog from './SignalLog';
import SignalStatus from './SignalStatus';
import LogPoller from '../../../components/LogPoller';
const TAB_STATUS = 'status';
const TAB_LOG = 'log';
@ -39,7 +39,6 @@ const useStyles = makeStyles(() => ({
const Signal = () => {
const classes = useStyles();
const [tab, setTab] = useState(TAB_STATUS);
const [type, setType] = useState();
return (
<Panel
@ -63,7 +62,7 @@ const Signal = () => {
{tab === TAB_LOG && (
<div className={classes.panel}>
<SignalLog />
<LogPoller service='signal' />
</div>
)}
</TabContext>

View File

@ -9,10 +9,10 @@ import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs';
import TabContext from '@material-ui/lab/TabContext';
import LogPoller from '../../../components/LogPoller';
import Panel from '../../../components/Panel';
import Toolbar from '../../../components/Toolbar';
import WNSLog from './WNSLog';
import WNSRecords, { WNSRecordType } from './WNSRecords';
import WNSStatus from './WNSStatus';
@ -76,7 +76,7 @@ const WNS = () => {
{tab === TAB_LOG && (
<div className={classes.panel}>
<WNSLog />
<LogPoller service='wns-lite' />
</div>
)}
</TabContext>

View File

@ -1,26 +0,0 @@
//
// Copyright 2020 DxOS.org
//
import React, { useContext } from 'react';
import { useQuery } from '@apollo/react-hooks';
import WNS_LOG from '../../../gql/wns_log.graphql';
import { ConsoleContext, useQueryStatusReducer } from '../../../hooks';
import Log from '../../../components/Log';
const WNSLog = () => {
const { config } = useContext(ConsoleContext);
const data = useQueryStatusReducer(useQuery(WNS_LOG, { pollInterval: config.api.intervalLog }));
if (!data) {
return null;
}
return (
<Log log={data.wns_log.log} />
);
};
export default WNSLog;

View File

@ -0,0 +1,10 @@
#
# Copyright 2020 DxOS.org
#
query ($service: String!, $incremental: Boolean) {
logs(service: $service, incremental: $incremental) {
timestamp
json
}
}

View File

@ -3,8 +3,8 @@
#
query {
wns_log @client {
timestamp
log
service_status {
timestamp,
json
}
}

View File

@ -1,10 +0,0 @@
#
# Copyright 2020 DxOS.org
#
query {
signal_log @client {
timestamp
log
}
}

View File

@ -105,7 +105,7 @@ export const createResolvers = config => {
timestamp: timestamp(),
log: []
};
},
}
}
};
};

View File

@ -0,0 +1,7 @@
{
"build": {
"name": "@dxos/console-app",
"buildDate": "2020-06-11T06:21:53.153Z",
"version": "1.0.0-beta.0"
}
}

View File

@ -8,33 +8,21 @@ type JSONResult {
json: String!
}
type Result {
timestamp: String!
code: Int!
}
type Log {
timestamp: String!
log: [String]!
}
#
# Schema
#
type Mutation {
action(command: String!): Result!
}
type Query {
system_status: SystemStatus!
logs(service: String!, incremental: Boolean): JSONResult!
app_status: JSONResult!
ipfs_status: JSONResult!
ipfs_swarm_status: JSONResult!
service_status: JSONResult!
signal_status: JSONResult!
system_status: JSONResult!
wns_status: JSONResult!
wns_records(type: String): JSONResult!
wns_log: Log!
}
schema {
mutation: Mutation
query: Query
}

View File

@ -1,8 +0,0 @@
#
# Copyright 2020 DxOS.org
#
type SystemStatus {
timestamp: String!
json: String!
}

View File

@ -7,7 +7,9 @@ import defaultsDeep from 'lodash.defaultsdeep';
import { ipfsResolvers } from './ipfs';
import { systemResolvers } from './system';
import { logResolvers } from './log';
// eslint-disable-next-line
const log = debug('dxos:console:server:resolvers');
/**
@ -19,14 +21,4 @@ export const resolvers = defaultsDeep({
// TODO(burdon): Auth.
// https://www.apollographql.com/docs/apollo-server/data/errors/#codes
Mutation: {
action: async (_, { command }) => {
log(`WNS action: ${command}`);
return {
timestamp: new Date().toUTCString(),
code: 0
};
}
}
}, ipfsResolvers, systemResolvers);
}, ipfsResolvers, systemResolvers, logResolvers);

View File

@ -0,0 +1,64 @@
//
// Copyright 2020 DxOS.org
//
import { spawnSync } from 'child_process';
class LogCache {
constructor (maxLines = 500) {
// Sets in JS iterate in insertion order.
this.buffer = new Set();
this.maxLines = maxLines;
}
append (lines) {
const added = [];
for (const line of lines) {
if (!this.buffer.has(line)) {
this.buffer.add(line);
added.push(line);
}
}
if (this.buffer.size > this.maxLines) {
this.buffer = new Set(Array.from(this.buffer).slice(parseInt(this.maxLines / 2)));
}
return added;
}
}
const _caches = new Map();
const getLogCache = (name) => {
let cache = _caches.get(name);
if (!cache) {
cache = new LogCache();
_caches.set(name, cache);
}
return cache;
};
const getLogs = async (name, incremental = false, lines = 100) => {
const command = 'wire';
const args = ['service', 'logs', '--lines', lines, name];
const child = spawnSync(command, args, { encoding: 'utf8' });
const logLines = child.stdout.split(/\n/);
const cache = getLogCache(name);
const added = cache.append(logLines);
return incremental ? added : Array.from(cache.buffer);
};
export const logResolvers = {
Query: {
logs: async (_, { service, incremental }) => {
const lines = await getLogs(service, incremental);
return {
timestamp: new Date().toUTCString(),
json: JSON.stringify({ incremental, lines })
};
},
}
};

View File

@ -6,6 +6,7 @@ import moment from 'moment';
import pick from 'lodash.pick';
import os from 'os';
import si from 'systeminformation';
import { spawnSync } from "child_process";
const num = new Intl.NumberFormat('en', { maximumSignificantDigits: 3 });
@ -77,6 +78,18 @@ const getSystemInfo = async () => {
};
};
/**
* Get system inforamtion.
* https://www.npmjs.com/package/systeminformation
*/
const getServiceInfo = async () => {
const command = 'wire';
const args = ['service', '--json'];
const child = spawnSync(command, args, { encoding: 'utf8' });
return JSON.parse(child.stdout);
}
export const systemResolvers = {
Query: {
system_status: async () => {
@ -86,6 +99,14 @@ export const systemResolvers = {
timestamp: new Date().toUTCString(),
json: JSON.stringify(system)
};
}
},
service_status: async () => {
const serviceInfo = await getServiceInfo();
return {
timestamp: new Date().toUTCString(),
json: JSON.stringify(serviceInfo)
};
},
}
};

View File

@ -22,7 +22,6 @@ import SYSTEM_STATUS from '@dxos/console-app/src/gql/system_status.graphql';
import { resolvers } from '../resolvers';
import API_SCHEMA from '../gql/api.graphql';
import SYSTEM_SCHEMA from '../gql/system.graphql';
const argv = yargs
.option('config', {
@ -113,10 +112,7 @@ app.get(publicUrl, (req, res) => {
const server = new ApolloServer({
typeDefs: [
API_SCHEMA,
SYSTEM_SCHEMA
// WNS_EXTENSIONS,
// WNS_SCHEMA
API_SCHEMA
],
// https://www.apollographql.com/docs/graphql-tools/resolvers