2018-08-27 00:17:11 +00:00
|
|
|
import * as Bluebird from 'bluebird';
|
|
|
|
import * as es from 'event-stream';
|
|
|
|
import * as _ from 'lodash';
|
|
|
|
import * as Lock from 'rwlock';
|
|
|
|
|
|
|
|
import { EventTracker } from './event-tracker';
|
|
|
|
import Docker = require('./lib/docker-utils');
|
|
|
|
import { LogType } from './lib/log-types';
|
2018-10-09 11:02:38 +00:00
|
|
|
import {
|
|
|
|
LocalLogBackend,
|
|
|
|
LogBackend,
|
|
|
|
LogMessage,
|
2018-10-18 18:52:35 +00:00
|
|
|
BalenaLogBackend,
|
2018-10-09 11:02:38 +00:00
|
|
|
} from './logging-backends';
|
2018-08-27 00:17:11 +00:00
|
|
|
|
|
|
|
interface LoggerSetupOptions {
|
|
|
|
apiEndpoint: string;
|
|
|
|
uuid: string;
|
|
|
|
deviceApiKey: string;
|
|
|
|
offlineMode: boolean;
|
|
|
|
enableLogs: boolean;
|
2018-10-09 11:02:38 +00:00
|
|
|
localMode: boolean;
|
2018-08-27 00:17:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type LogEventObject = Dictionary<any> | null;
|
|
|
|
|
|
|
|
enum OutputStream {
|
|
|
|
Stdout,
|
|
|
|
Stderr,
|
|
|
|
}
|
|
|
|
|
2018-09-18 14:36:59 +00:00
|
|
|
interface LoggerConstructOptions {
|
|
|
|
eventTracker: EventTracker;
|
|
|
|
}
|
|
|
|
|
2018-08-27 00:17:11 +00:00
|
|
|
export class Logger {
|
|
|
|
private writeLock: (key: string) => Bluebird<() => void> = Bluebird.promisify(
|
|
|
|
new Lock().async.writeLock,
|
|
|
|
);
|
2018-10-09 11:02:38 +00:00
|
|
|
|
2018-08-27 00:17:11 +00:00
|
|
|
private backend: LogBackend | null = null;
|
2018-10-18 18:52:35 +00:00
|
|
|
private balenaBackend: BalenaLogBackend | null = null;
|
2018-10-09 11:02:38 +00:00
|
|
|
private localBackend: LocalLogBackend | null = null;
|
|
|
|
|
2018-08-27 00:17:11 +00:00
|
|
|
private eventTracker: EventTracker;
|
|
|
|
private attached: {
|
|
|
|
[key in OutputStream]: { [containerId: string]: boolean }
|
|
|
|
} = {
|
2018-11-02 14:17:58 +00:00
|
|
|
[OutputStream.Stderr]: {},
|
|
|
|
[OutputStream.Stdout]: {},
|
2018-08-27 00:17:11 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
public constructor({ eventTracker }: LoggerConstructOptions) {
|
|
|
|
this.backend = null;
|
|
|
|
this.eventTracker = eventTracker;
|
|
|
|
}
|
|
|
|
|
|
|
|
public init({
|
|
|
|
apiEndpoint,
|
|
|
|
uuid,
|
|
|
|
deviceApiKey,
|
|
|
|
offlineMode,
|
|
|
|
enableLogs,
|
2018-10-09 11:02:38 +00:00
|
|
|
localMode,
|
2018-11-02 14:17:58 +00:00
|
|
|
}: LoggerSetupOptions) {
|
2018-10-18 18:52:35 +00:00
|
|
|
this.balenaBackend = new BalenaLogBackend(apiEndpoint, uuid, deviceApiKey);
|
2018-10-09 11:02:38 +00:00
|
|
|
this.localBackend = new LocalLogBackend();
|
|
|
|
|
2018-10-18 18:52:35 +00:00
|
|
|
this.backend = localMode ? this.localBackend : this.balenaBackend;
|
2018-10-09 11:02:38 +00:00
|
|
|
|
2018-08-27 00:17:11 +00:00
|
|
|
this.backend.offlineMode = offlineMode;
|
2018-08-27 20:58:20 +00:00
|
|
|
this.backend.publishEnabled = enableLogs;
|
2018-08-27 00:17:11 +00:00
|
|
|
}
|
|
|
|
|
2018-10-09 11:02:38 +00:00
|
|
|
public switchBackend(localMode: boolean) {
|
|
|
|
if (localMode) {
|
|
|
|
// Use the local mode backend
|
|
|
|
this.backend = this.localBackend;
|
|
|
|
console.log('Switching logging backend to LocalLogBackend');
|
|
|
|
} else {
|
2018-10-18 18:52:35 +00:00
|
|
|
// Use the balena backend
|
|
|
|
this.backend = this.balenaBackend;
|
|
|
|
console.log('Switching logging backend to BalenaLogBackend');
|
2018-10-09 11:02:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public getLocalBackend(): LocalLogBackend {
|
|
|
|
// TODO: Think about this interface a little better, it would be
|
|
|
|
// nicer to proxy the logs via the logger module
|
|
|
|
if (this.localBackend == null) {
|
|
|
|
// TODO: Type this as an internal inconsistency error
|
|
|
|
throw new Error('Local backend logger is not defined.');
|
|
|
|
}
|
|
|
|
return this.localBackend;
|
|
|
|
}
|
|
|
|
|
2018-08-27 00:17:11 +00:00
|
|
|
public enable(value: boolean = true) {
|
|
|
|
if (this.backend != null) {
|
|
|
|
this.backend.publishEnabled = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public logDependent(message: LogMessage, device: { uuid: string }) {
|
|
|
|
if (this.backend != null) {
|
|
|
|
message.uuid = device.uuid;
|
|
|
|
this.backend.log(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public log(message: LogMessage) {
|
|
|
|
if (this.backend != null) {
|
|
|
|
this.backend.log(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public logSystemMessage(
|
|
|
|
message: string,
|
|
|
|
eventObj?: LogEventObject,
|
|
|
|
eventName?: string,
|
|
|
|
) {
|
|
|
|
const msgObj: LogMessage = { message, isSystem: true };
|
|
|
|
if (eventObj != null && eventObj.error != null) {
|
|
|
|
msgObj.isStdErr = true;
|
|
|
|
}
|
|
|
|
this.log(msgObj);
|
|
|
|
this.eventTracker.track(
|
|
|
|
eventName != null ? eventName : message,
|
2018-11-02 14:17:58 +00:00
|
|
|
eventObj != null ? eventObj : {},
|
2018-08-27 00:17:11 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public lock(containerId: string): Bluebird.Disposer<() => void> {
|
2018-11-02 14:17:58 +00:00
|
|
|
return this.writeLock(containerId).disposer(release => {
|
|
|
|
release();
|
|
|
|
});
|
2018-08-27 00:17:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public attach(
|
|
|
|
docker: Docker,
|
|
|
|
containerId: string,
|
2018-11-02 14:17:58 +00:00
|
|
|
serviceInfo: { serviceId: string; imageId: string },
|
2018-08-27 00:17:11 +00:00
|
|
|
): Bluebird<void> {
|
|
|
|
return Bluebird.using(this.lock(containerId), () => {
|
2018-11-02 14:17:58 +00:00
|
|
|
return this.attachStream(
|
|
|
|
docker,
|
|
|
|
OutputStream.Stdout,
|
|
|
|
containerId,
|
|
|
|
serviceInfo,
|
|
|
|
).then(() => {
|
|
|
|
return this.attachStream(
|
|
|
|
docker,
|
|
|
|
OutputStream.Stderr,
|
|
|
|
containerId,
|
|
|
|
serviceInfo,
|
|
|
|
);
|
|
|
|
});
|
2018-08-27 00:17:11 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public logSystemEvent(logType: LogType, obj: LogEventObject): void {
|
|
|
|
let message = logType.humanName;
|
|
|
|
const objectName = this.objectNameForLogs(obj);
|
|
|
|
if (objectName != null) {
|
|
|
|
message += ` '${objectName}'`;
|
|
|
|
}
|
|
|
|
if (obj && obj.error != null) {
|
|
|
|
let errorMessage = obj.error.message;
|
|
|
|
if (_.isEmpty(errorMessage)) {
|
|
|
|
errorMessage = 'Unknown cause';
|
|
|
|
console.error('Warning: invalid error message', obj.error);
|
|
|
|
}
|
|
|
|
message += ` due to '${errorMessage}'`;
|
|
|
|
}
|
|
|
|
this.logSystemMessage(message, obj, logType.eventName);
|
|
|
|
}
|
|
|
|
|
|
|
|
public logConfigChange(
|
|
|
|
config: { [configName: string]: string },
|
2018-11-02 15:37:47 +00:00
|
|
|
{ success = false, err }: { success?: boolean; err?: Error } = {},
|
2018-08-27 00:17:11 +00:00
|
|
|
) {
|
|
|
|
const obj: LogEventObject = { config };
|
|
|
|
let message: string;
|
|
|
|
let eventName: string;
|
|
|
|
if (success) {
|
|
|
|
message = `Applied configuration change ${JSON.stringify(config)}`;
|
|
|
|
eventName = 'Apply config change success';
|
2018-11-02 14:17:58 +00:00
|
|
|
} else if (err != null) {
|
2018-08-27 00:17:11 +00:00
|
|
|
message = `Error applying configuration change: ${err}`;
|
|
|
|
eventName = 'Apply config change error';
|
|
|
|
obj.error = err;
|
|
|
|
} else {
|
2018-09-13 13:44:20 +00:00
|
|
|
message = `Applying configuration change ${JSON.stringify(config)}`;
|
2018-08-27 00:17:11 +00:00
|
|
|
eventName = 'Apply config change in progress';
|
|
|
|
}
|
|
|
|
|
|
|
|
this.logSystemMessage(message, obj, eventName);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: This function interacts with the docker daemon directly,
|
|
|
|
// using the container id, but it would be better if this was provided
|
|
|
|
// by the Compose/Service-Manager module, as an accessor
|
|
|
|
private attachStream(
|
|
|
|
docker: Docker,
|
|
|
|
streamType: OutputStream,
|
|
|
|
containerId: string,
|
2018-11-02 14:17:58 +00:00
|
|
|
{ serviceId, imageId }: { serviceId: string; imageId: string },
|
2018-08-27 00:17:11 +00:00
|
|
|
): Bluebird<void> {
|
2018-10-09 11:02:38 +00:00
|
|
|
return Bluebird.try(() => {
|
|
|
|
if (this.attached[streamType][containerId]) {
|
|
|
|
return;
|
|
|
|
}
|
2018-08-27 00:17:11 +00:00
|
|
|
|
2018-10-09 11:02:38 +00:00
|
|
|
const logsOpts = {
|
|
|
|
follow: true,
|
|
|
|
stdout: streamType === OutputStream.Stdout,
|
|
|
|
stderr: streamType === OutputStream.Stderr,
|
|
|
|
timestamps: true,
|
|
|
|
since: Math.floor(Date.now() / 1000),
|
|
|
|
};
|
2018-08-27 00:17:11 +00:00
|
|
|
|
2018-11-02 14:17:58 +00:00
|
|
|
return docker
|
|
|
|
.getContainer(containerId)
|
|
|
|
.logs(logsOpts)
|
|
|
|
.then(stream => {
|
2018-10-09 11:02:38 +00:00
|
|
|
this.attached[streamType][containerId] = true;
|
2018-08-27 00:17:11 +00:00
|
|
|
|
2018-10-09 11:02:38 +00:00
|
|
|
stream
|
2018-11-02 14:17:58 +00:00
|
|
|
.on('error', err => {
|
2018-08-27 00:17:11 +00:00
|
|
|
console.error('Error on container logs', err);
|
|
|
|
this.attached[streamType][containerId] = false;
|
|
|
|
})
|
|
|
|
.pipe(es.split())
|
2018-09-21 14:49:43 +00:00
|
|
|
.on('data', (logBuf: Buffer | string) => {
|
|
|
|
if (_.isString(logBuf)) {
|
|
|
|
logBuf = Buffer.from(logBuf);
|
|
|
|
}
|
|
|
|
const logMsg = Logger.extractContainerMessage(logBuf);
|
|
|
|
if (logMsg != null) {
|
2018-08-27 00:17:11 +00:00
|
|
|
const message: LogMessage = {
|
2018-09-21 14:49:43 +00:00
|
|
|
message: logMsg.message,
|
|
|
|
timestamp: logMsg.timestamp,
|
2018-08-27 00:17:11 +00:00
|
|
|
serviceId,
|
|
|
|
imageId,
|
|
|
|
};
|
|
|
|
if (streamType === OutputStream.Stderr) {
|
|
|
|
message.isStdErr = true;
|
|
|
|
}
|
|
|
|
this.log(message);
|
|
|
|
}
|
|
|
|
})
|
2018-11-02 14:17:58 +00:00
|
|
|
.on('error', err => {
|
2018-08-27 00:17:11 +00:00
|
|
|
console.error('Error on container logs', err);
|
|
|
|
this.attached[streamType][containerId] = false;
|
|
|
|
})
|
|
|
|
.on('end', () => {
|
|
|
|
this.attached[streamType][containerId] = false;
|
|
|
|
});
|
2018-10-09 11:02:38 +00:00
|
|
|
});
|
|
|
|
});
|
2018-08-27 00:17:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private objectNameForLogs(eventObj: LogEventObject): string | null {
|
|
|
|
if (eventObj == null) {
|
|
|
|
return null;
|
|
|
|
}
|
2018-11-02 14:17:58 +00:00
|
|
|
if (
|
|
|
|
eventObj.service != null &&
|
2018-08-27 00:17:11 +00:00
|
|
|
eventObj.service.serviceName != null &&
|
2018-10-12 12:44:23 +00:00
|
|
|
eventObj.service.config != null &&
|
2018-09-18 14:36:59 +00:00
|
|
|
eventObj.service.config.image != null
|
2018-08-27 00:17:11 +00:00
|
|
|
) {
|
2018-09-18 14:36:59 +00:00
|
|
|
return `${eventObj.service.serviceName} ${eventObj.service.config.image}`;
|
2018-08-27 00:17:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (eventObj.image != null) {
|
|
|
|
return eventObj.image.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (eventObj.network != null && eventObj.network.name != null) {
|
|
|
|
return eventObj.network.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (eventObj.volume != null && eventObj.volume.name != null) {
|
|
|
|
return eventObj.volume.name;
|
|
|
|
}
|
|
|
|
|
2018-09-18 14:36:59 +00:00
|
|
|
if (eventObj.fields != null) {
|
|
|
|
return eventObj.fields.join(',');
|
|
|
|
}
|
|
|
|
|
2018-08-27 00:17:11 +00:00
|
|
|
return null;
|
|
|
|
}
|
2018-09-21 14:49:43 +00:00
|
|
|
|
|
|
|
private static extractContainerMessage(
|
|
|
|
msgBuf: Buffer,
|
2018-11-02 14:17:58 +00:00
|
|
|
): { message: string; timestamp: number } | null {
|
2018-09-21 14:49:43 +00:00
|
|
|
// Non-tty message format from:
|
|
|
|
// https://docs.docker.com/engine/api/v1.30/#operation/ContainerAttach
|
2018-11-02 14:17:58 +00:00
|
|
|
if (msgBuf[0] in [0, 1, 2] && _.every(msgBuf.slice(1, 7), c => c === 0)) {
|
2018-09-21 14:49:43 +00:00
|
|
|
// Take the header from this message, and parse it as normal
|
|
|
|
msgBuf = msgBuf.slice(8);
|
|
|
|
}
|
|
|
|
const logLine = msgBuf.toString();
|
|
|
|
const space = logLine.indexOf(' ');
|
|
|
|
if (space > 0) {
|
2018-11-02 14:17:58 +00:00
|
|
|
let timestamp = new Date(logLine.substr(0, space)).getTime();
|
2018-10-09 11:04:50 +00:00
|
|
|
if (_.isNaN(timestamp)) {
|
|
|
|
timestamp = Date.now();
|
|
|
|
}
|
2018-09-21 14:49:43 +00:00
|
|
|
return {
|
2018-10-11 13:25:38 +00:00
|
|
|
timestamp,
|
2018-09-21 14:49:43 +00:00
|
|
|
message: logLine.substr(space + 1),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
2018-08-27 00:17:11 +00:00
|
|
|
}
|