UNPKG

5.78 kBPlain TextView Raw
1/*
2 * Copyright © 2019 Atomist, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {
18 Configuration,
19 defaultHttpClientFactory,
20 HttpClient,
21 HttpMethod,
22 logger,
23} from "@atomist/automation-client";
24import { redact } from "@atomist/automation-client/lib/util/redact";
25import {
26 format,
27 ProgressLog,
28} from "@atomist/sdm";
29import * as _ from "lodash";
30import os = require("os");
31
32function* timestampGenerator(): Iterator<Date> {
33 while (true) {
34 yield new Date();
35 }
36}
37
38/**
39 * Post log to Atomist Rolar service for it to persist
40 */
41export class RolarProgressLog implements ProgressLog {
42
43 private readonly httpClient: HttpClient;
44 private localLogs: LogData[] = [];
45 private readonly timer: any;
46 private readonly rolarBaseUrl: string;
47 private readonly bufferSizeLimit: number;
48 private readonly timerInterval: number;
49 private readonly redact: boolean;
50
51 constructor(private readonly logPath: string[],
52 configuration: Configuration,
53 private readonly logLevel: string = "info",
54 private readonly timestamper: Iterator<Date> = timestampGenerator()) {
55 this.rolarBaseUrl = _.get(configuration, "sdm.rolar.url", "https://rolar.atomist.com");
56 this.bufferSizeLimit = _.get(configuration, "sdm.rolar.bufferSize", 10240);
57 this.timerInterval = _.get(configuration, "sdm.rolar.flushInterval", 2000);
58 this.redact = _.get(configuration, "redact.log", false);
59 if (this.timerInterval > 0) {
60 this.timer = setInterval(() => this.flush(), this.timerInterval).unref();
61 }
62 this.httpClient = _.get(configuration, "http.client.factory", defaultHttpClientFactory()).create(this.rolarBaseUrl);
63 }
64
65 get name(): string {
66 return this.logPath.join("/");
67 }
68
69 get url(): string {
70 return `${this.rolarBaseUrl}/logs/${this.name}`;
71 }
72
73 public async isAvailable(): Promise<boolean> {
74 const url = `${this.rolarBaseUrl}/api/logs`;
75 try {
76 await this.httpClient.exchange(url, { method: HttpMethod.Head });
77 return true;
78 } catch (e) {
79 logger.warn(`Rolar logger is not available at ${url}: ${e}`);
80 return false;
81 }
82 }
83
84 public write(msg: string = "", ...args: string[]): void {
85 const fmsg = format(msg, ...args);
86 const line = this.redact ? redact(fmsg) : fmsg;
87 const now: Date = this.timestamper.next().value;
88 this.localLogs.push({
89 level: this.logLevel,
90 message: line,
91 timestamp: this.constructUtcTimestamp(now),
92 timestampMillis: this.constructMillisTimestamp(now),
93 });
94 const bufferSize = this.localLogs.reduce((acc, logData) => acc + logData.message.length, 0);
95 if (bufferSize > this.bufferSizeLimit) {
96 // tslint:disable-next-line:no-floating-promises
97 this.flush();
98 }
99 }
100
101 public flush(): Promise<any> {
102 return this.postLogs(false);
103 }
104
105 public close(): Promise<any> {
106 if (this.timer) {
107 clearInterval(this.timer);
108 }
109 return this.postLogs(true);
110 }
111
112 private async postLogs(isClosed: boolean): Promise<any> {
113 const postingLogs = this.localLogs;
114 this.localLogs = [];
115
116 if (isClosed === true || (!!postingLogs && postingLogs.length > 0)) {
117 const closedRequestParam = isClosed ? "?closed=true" : "";
118 const url = `${this.rolarBaseUrl}/api/logs/${this.logPath.join("/")}${closedRequestParam}`;
119 let result;
120 try {
121 result = await this.httpClient.exchange(url, {
122 method: HttpMethod.Post,
123 body: {
124 host: os.hostname(),
125 content: postingLogs || [],
126 },
127 headers: { "Content-Type": "application/json" },
128 retry: {
129 retries: 0,
130 },
131 options: {
132 timeout: 2500,
133 },
134 });
135 } catch (err) {
136 this.localLogs = postingLogs.concat(this.localLogs);
137 if (!/timeout.*exceeded/i.test(err.message)) {
138 logger.error(err.message);
139 } else {
140 logger.debug("Calling rolar timed out");
141 }
142 }
143 return result;
144 }
145 return Promise.resolve();
146 }
147
148 private constructUtcTimestamp(d: Date): string {
149 const now: Date = d;
150 const date = [now.getUTCMonth() + 1, now.getUTCDate(), now.getUTCFullYear()]
151 .map(t => _.padStart(t.toString(), 2, "0"));
152 const time = [now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds()]
153 .map(t => _.padStart(t.toString(), 2, "0"));
154 return `${date.join("/")} ${time.join(":")}.${_.padStart(now.getUTCMilliseconds().toString(), 3, "0")}`;
155 }
156
157 private constructMillisTimestamp(d: Date): number {
158 return d.valueOf();
159 }
160}
161
162interface LogData {
163 level: string;
164 message: string;
165 timestamp: string;
166 timestampMillis: number;
167}