Merge pull request #1184 from balena-io/local-logs

Refactor and improve balena logs, with consistent interface and local mode support
This commit is contained in:
CameronDiver 2019-04-24 14:00:54 +01:00 committed by GitHub
commit 811262ed8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 208 additions and 92 deletions

1
.gitignore vendored
View File

@ -30,7 +30,6 @@ package-lock.json
.balenaconf
resinrc.yml
balenarc.yml
tslint.json
.DS_Store
.idea

View File

@ -146,7 +146,7 @@ environment variable (in the same standard URL format).
- Logs
- [logs <uuid>](#logs-uuid)
- [logs <uuidOrDevice>](#logs-uuidordevice)
- Sync
@ -853,18 +853,24 @@ Examples:
# Logs
## logs <uuid>
## logs <uuidOrDevice>
Use this command to show logs for a specific device.
By default, the command prints all log messages and exit.
By default, the command prints all log messages and exits.
To continuously stream output, and see new logs in real time, use the `--tail` option.
If an IP address is passed to this command, logs are displayed from
a local mode device with that address. Note that --tail is implied
when this command is provided an IP address.
Examples:
$ balena logs 23c73a1
$ balena logs 23c73a1
$ balena logs 23c73a1 --tail
$ balena logs 192.168.0.31
### Options

View File

@ -1,61 +0,0 @@
###
Copyright 2016-2017 Balena
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.
###
{ normalizeUuidProp } = require('../utils/normalization')
module.exports =
signature: 'logs <uuid>'
description: 'show device logs'
help: '''
Use this command to show logs for a specific device.
By default, the command prints all log messages and exit.
To continuously stream output, and see new logs in real time, use the `--tail` option.
Examples:
$ balena logs 23c73a1
$ balena logs 23c73a1
'''
options: [
{
signature: 'tail'
description: 'continuously stream output'
boolean: true
alias: 't'
}
]
permission: 'user'
primary: true
action: (params, options, done) ->
normalizeUuidProp(params)
balena = require('balena-sdk').fromSharedOptions()
moment = require('moment')
printLine = (line) ->
timestamp = moment(line.timestamp).format('DD.MM.YY HH:mm:ss (ZZ)')
console.log("#{timestamp} #{line.message}")
if options.tail
balena.logs.subscribe(params.uuid, { count: 100 }).then (logs) ->
logs.on('line', printLine)
logs.on('error', done)
.catch(done)
else
balena.logs.history(params.uuid)
.each(printLine)
.catch(done)

128
lib/actions/logs.ts Normal file
View File

@ -0,0 +1,128 @@
/*
Copyright 2016-2019 Balena
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 { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
import { normalizeUuidProp } from '../utils/normalization';
type CloudLog =
| {
isSystem: false;
serviceId: number;
timestamp: number;
message: string;
}
| {
isSystem: true;
timestamp: number;
message: string;
};
export const logs: CommandDefinition<
{
uuidOrDevice: string;
},
{ tail: boolean }
> = {
signature: 'logs <uuidOrDevice>',
description: 'show device logs',
help: stripIndent`
Use this command to show logs for a specific device.
By default, the command prints all log messages and exits.
To continuously stream output, and see new logs in real time, use the \`--tail\` option.
If an IP address is passed to this command, logs are displayed from
a local mode device with that address. Note that --tail is implied
when this command is provided an IP address.
Examples:
$ balena logs 23c73a1
$ balena logs 23c73a1 --tail
$ balena logs 192.168.0.31`,
options: [
{
signature: 'tail',
description: 'continuously stream output',
boolean: true,
alias: 't',
},
],
permission: 'user',
primary: true,
async action(params, options, done) {
normalizeUuidProp(params);
const balena = (await import('balena-sdk')).fromSharedOptions();
const { serviceIdToName } = await import('../utils/cloud');
const { displayDeviceLogs, displayLogObject } = await import(
'../utils/device/logs'
);
const { validateIPAddress } = await import('../utils/validation');
const { exitWithExpectedError } = await import('../utils/patterns');
const Logger = await import('../utils/logger');
const logger = new Logger();
const displayCloudLog = async (line: CloudLog) => {
if (!line.isSystem) {
let serviceName = await serviceIdToName(balena, line.serviceId);
if (serviceName == null) {
serviceName = 'Unknown service';
}
displayLogObject({ serviceName, ...line }, logger);
} else {
displayLogObject(line, logger);
}
};
if (validateIPAddress(params.uuidOrDevice)) {
const { DeviceAPI } = await import('../utils/device/api');
const deviceApi = new DeviceAPI(logger, params.uuidOrDevice);
logger.logDebug('Checking we can access device');
try {
await deviceApi.ping();
} catch (e) {
exitWithExpectedError(
new Error(
`Cannot access local mode device at address ${params.uuidOrDevice}`,
),
);
}
const logStream = await deviceApi.getLogStream();
displayDeviceLogs(logStream, logger);
} else {
if (options.tail) {
return balena.logs
.subscribe(params.uuidOrDevice, { count: 100 })
.then(function(logStream) {
logStream.on('line', displayCloudLog);
logStream.on('error', done);
})
.catch(done);
} else {
return balena.logs
.history(params.uuidOrDevice)
.each(displayCloudLog)
.catch(done);
}
}
},
};

View File

@ -19,11 +19,10 @@ import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
import { registrySecretsHelp } from '../utils/messages';
// An regex to detect an IP address, from https://www.regular-expressions.info/ip.html
const IP_REGEX = new RegExp(
/\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/,
);
import {
validateApplicationName,
validateIPAddress,
} from '../utils/validation';
enum BuildTarget {
Cloud,
@ -32,11 +31,11 @@ enum BuildTarget {
function getBuildTarget(appOrDevice: string): BuildTarget | null {
// First try the application regex from the api
if (/^[a-zA-Z0-9_-]+$/.test(appOrDevice)) {
if (validateApplicationName(appOrDevice)) {
return BuildTarget.Cloud;
}
if (IP_REGEX.test(appOrDevice)) {
if (validateIPAddress(appOrDevice)) {
return BuildTarget.Device;
}

View File

@ -174,7 +174,7 @@ capitano.command(actions.config.generate)
capitano.command(actions.settings.list)
# ---------- Logs Module ----------
capitano.command(actions.logs)
capitano.command(actions.logs.logs)
# ---------- Sync Module ----------
capitano.command(actions.sync)

21
lib/utils/cloud.ts Normal file
View File

@ -0,0 +1,21 @@
import { BalenaSDK } from 'balena-sdk';
import memoize = require('lodash/memoize');
export const serviceIdToName = memoize(
async (sdk: BalenaSDK, serviceId: number): Promise<string | undefined> => {
const serviceName = await sdk.pine.get({
resource: 'service',
id: serviceId,
options: {
$select: 'service_name',
},
});
if (serviceName != null) {
return serviceName.service_name;
}
return;
},
// Memoize the call based on service id
(_sdk, id) => id.toString(),
);

View File

@ -51,28 +51,31 @@ export function displayBuildLog(log: BuildLog, logger: Logger): void {
function displayLogLine(log: string | Buffer, logger: Logger): void {
try {
const obj: Log = JSON.parse(log.toString());
let toPrint: string;
if (obj.timestamp != null) {
toPrint = `[${new Date(obj.timestamp).toLocaleString()}]`;
} else {
toPrint = `[${new Date().toLocaleString()}]`;
}
if (obj.serviceName != null) {
const colourFn = getServiceColourFn(obj.serviceName);
toPrint += ` ${colourFn(`[${obj.serviceName}]`)}`;
}
toPrint += ` ${obj.message}`;
logger.logLogs(toPrint);
displayLogObject(obj, logger);
} catch (e) {
logger.logDebug(`Dropping device log due to failed parsing: ${e}`);
}
}
export function displayLogObject<T extends Log>(obj: T, logger: Logger): void {
let toPrint: string;
if (obj.timestamp != null) {
toPrint = `[${new Date(obj.timestamp).toLocaleString()}]`;
} else {
toPrint = `[${new Date().toLocaleString()}]`;
}
if (obj.serviceName != null) {
const colourFn = getServiceColourFn(obj.serviceName);
toPrint += ` ${colourFn(`[${obj.serviceName}]`)}`;
}
toPrint += ` ${obj.message}`;
logger.logLogs(toPrint);
}
const getServiceColourFn = _.memoize(_getServiceColourFn);
const colorHash = new ColorHash();

View File

@ -16,6 +16,13 @@ limitations under the License.
import validEmail = require('@resin.io/valid-email');
const APPNAME_REGEX = new RegExp(/^[a-zA-Z0-9_-]+$/);
// An regex to detect an IP address, from https://www.regular-expressions.info/ip.html
const IP_REGEX = new RegExp(
/\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/,
);
const DOTLOCAL_REGEX = new RegExp(/^[a-zA-Z0-9-]+\.local$/);
export function validateEmail(input: string) {
if (!validEmail(input)) {
return 'Email is not valid';
@ -37,5 +44,13 @@ export function validateApplicationName(input: string) {
return 'The application name should be at least 4 characters';
}
return true;
return APPNAME_REGEX.test(input);
}
export function validateIPAddress(input: string): boolean {
return IP_REGEX.test(input);
}
export function validateDotLocalUrl(input: string): boolean {
return DOTLOCAL_REGEX.test(input);
}

6
tslint.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "./node_modules/resin-lint/config/tslint-prettier.json",
"rules": {
"ignoreDefinitionFiles": false
}
}