mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-04-13 22:22:58 +00:00
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:
commit
811262ed8b
1
.gitignore
vendored
1
.gitignore
vendored
@ -30,7 +30,6 @@ package-lock.json
|
||||
.balenaconf
|
||||
resinrc.yml
|
||||
balenarc.yml
|
||||
tslint.json
|
||||
|
||||
.DS_Store
|
||||
.idea
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
128
lib/actions/logs.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
21
lib/utils/cloud.ts
Normal 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(),
|
||||
);
|
@ -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();
|
||||
|
@ -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
6
tslint.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./node_modules/resin-lint/config/tslint-prettier.json",
|
||||
"rules": {
|
||||
"ignoreDefinitionFiles": false
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user