1 /**
2 	Handles all the console output of the Dub package manager, by providing useful
3 	methods for handling colored text. The module also disables colors when stdout
4 	and stderr are not a TTY in order to avoid ASCII escape sequences in piped
5 	output. The module can autodetect and configure itself in this regard by
6 	calling initLogging() at the beginning of the program. But, whether to color
7 	text or not can also be set manually with setLoggingColorsEnabled(bool).
8 
9 	The output for the log levels error, warn and info is formatted like this:
10 
11 	"			 <tag> <text>"
12 	 '----------'
13 	 fixed width
14 
15 	the "tag" part can be colored (most oftenly will be) and always has a fixed
16 	width, which is defined as a const at the beginning of this module.
17 
18 	The output for the log levels debug and diagnostic will be just the plain
19 	string.
20 
21 	There are some default tag string and color values for some logging levels:
22 	- warn: "Warning", yellow bold
23 	- error: "Error", red bold
24 
25 	Actually, for error and warn levels, the tag color is fixed to the ones listed
26 	above.
27 
28 	Also, the default tag string for the info level is "" (the empty string) and
29 	the default color is white (usually it's manually set when calling logInfo
30 	with the wanted tag string, but this allows to just logInfo("text") without
31 	having to worry about the tag if it's not needed).
32 
33 	Usage:
34 		After initializing the logging module with initLogging(), the functions
35 		logDebug(..), logDiagnostic(..), logInfo(..), logWarning(..) and logError(..)
36 		can be used to print log messages. Whether the messages are printed on stdout
37 		or stderr depends on the log level (warning and error go to stderr).
38 		The log(..) function can also be used. Check the signature and documentation
39 		of the functions for more information.
40 
41 		The minimum log level to print can be configured using setLogLevel(..),
42 		and whether to color outputted text or not can be set with
43 		setLoggingColorsEnabled(..)
44 
45 		The color(str, color) function can be used to color text within a log
46 		message, for instance like this:
47 
48 		logInfo("Tag", Color.green, "My %s message", "colored".color(Color.red))
49 
50 	Copyright: © 2018 Giacomo De Lazzari
51 	License: Subject to the terms of the MIT license, as written in the included LICENSE file.
52 	Authors: Giacomo De Lazzari
53 */
54 
55 module dub.internal.logging;
56 
57 import std.stdio;
58 import std.array;
59 import std.format;
60 import std.string;
61 
62 import dub.internal.colorize : fg, mode;
63 
64 /**
65 	An enum listing possible colors for terminal output, useful to set the color
66 	of a tag. Re-exported from d-colorize in dub.internal.colorize. See the enum
67 	definition there for a list of possible values.
68 */
69 public alias Color = fg;
70 
71 /**
72 	An enum listing possible text "modes" for terminal output, useful to set the
73 	text to bold, underline, blinking, etc...
74 	Re-exported from d-colorize in dub.internal.colorize. See the enum definition
75 	there for a list of possible values.
76 */
77 public alias Mode = mode;
78 
79 /// Defines the current width of logging tags for justifying in chars.
80 /// Can be manipulated through push and pop.
81 struct TagWidth {
82 	import core.atomic;
83 
84 	private shared int value = 12;
85 	private shared int index;
86 	private shared int[16] stack;
87 
88 	/// Gets the tag width in chars
89 	public int get() {
90 		return value;
91 	}
92 
93 	/// Changes the tag width for all following logging calls, until $(LREF pop) is called.
94 	public void push(int width) {
95 		int currentIndex = index;
96 		index.atomicOp!"+="(1);
97 		stack[currentIndex] = value;
98 		assert(index < stack.length, "too many TagWidth.push without pop");
99 		value = width;
100 	}
101 
102 	/// Reverts the last $(LREF push) call.
103 	public void pop() {
104 		assert(index > 0);
105 		value = stack[index.atomicOp!"-="(1)];
106 	}
107 }
108 
109 /// The global tag width instance used for logging.
110 public __gshared TagWidth tagWidth;
111 
112 /// Possible log levels supported
113 enum LogLevel {
114 	debug_,
115 	diagnostic,
116 	info,
117 	warn,
118 	error,
119 	none
120 }
121 
122 // The current minimum log level to be printed
123 private shared LogLevel _minLevel = LogLevel.info;
124 
125 /*
126 	Whether to print text with colors or not, defaults to true but will be set
127 	to false in initLogging() if stdout or stderr are not a TTY (which means the
128 	output is probably being piped and we don't want ASCII escape chars in it)
129 */
130 private shared bool _printColors = true;
131 
132 /// Ditto
133 public bool hasColors () @trusted nothrow @nogc { return _printColors; }
134 
135 // isatty() is used in initLogging() to detect whether or not we are on a TTY
136 extern (C) int isatty(int);
137 
138 /**
139 	This function must be called at the beginning for the program, before any
140 	logging occurs. It will detect whether or not stdout/stderr are a console/TTY
141 	and will consequently disable colored output if needed. Also, if a NO_COLOR
142 	environment variable is defined, colors are disabled (https://no-color.org/).
143 
144 	Forgetting to call the function will result in ASCII escape sequences in the
145 	piped output, probably an undesirable thing.
146 */
147 void initLogging()
148 {
149 	import std.process : environment;
150 	import core.stdc.stdio;
151 
152 	_printColors = environment.get("NO_COLOR") == "";
153 	version (Windows)
154 	{
155 		version (CRuntime_DigitalMars)
156 		{
157 			if (!isatty(core.stdc.stdio.stdout._file) ||
158 					!isatty(core.stdc.stdio.stderr._file))
159 				_printColors = false;
160 		}
161 		else version (CRuntime_Microsoft)
162 		{
163 			if (!isatty(fileno(core.stdc.stdio.stdout)) ||
164 					!isatty(fileno(core.stdc.stdio.stderr)))
165 				_printColors = false;
166 		}
167 		else
168 			_printColors = false;
169 	}
170 	else version (Posix)
171 	{
172 		import core.sys.posix.unistd;
173 
174 		if (!isatty(STDERR_FILENO) || !isatty(STDOUT_FILENO))
175 			_printColors = false;
176 	}
177 }
178 
179 /// Sets the minimum log level to be printed
180 void setLogLevel(LogLevel level) nothrow
181 {
182 	_minLevel = level;
183 }
184 
185 /// Gets the minimum log level to be printed
186 LogLevel getLogLevel()
187 {
188 	return _minLevel;
189 }
190 
191 /// Set whether to print colors or not
192 void setLoggingColorsEnabled(bool enabled)
193 {
194 	_printColors = enabled;
195 }
196 
197 /**
198 	Shorthand function to log a message with debug/diagnostic level, no tag string
199 	or tag color required (since there will be no tag).
200 
201 	Params:
202 		level = The log level for the logged message
203 		fmt = See http://dlang.org/phobos/std_format.html#format-string
204 */
205 void logDebug(T...)(string fmt, lazy T args) nothrow
206 {
207 	log(LogLevel.debug_, false, "", Color.init, fmt, args);
208 }
209 
210 /// ditto
211 void logDiagnostic(T...)(string fmt, lazy T args) nothrow
212 {
213 	log(LogLevel.diagnostic, false, "", Color.init, fmt, args);
214 }
215 
216 /**
217 	Shorthand function to log a message with info level, with custom tag string
218 	and tag color.
219 
220 	Params:
221 		tag = The string the tag at the beginning of the line should contain
222 		tagColor = The color the tag string should have
223 		level = The log level for the logged message
224 		fmt = See http://dlang.org/phobos/std_format.html#format-string
225 */
226 void logInfo(T...)(string tag, Color tagColor, string fmt, lazy T args) nothrow
227 {
228 	log(LogLevel.info, false, tag, tagColor, fmt, args);
229 }
230 
231 /**
232 	Shorthand function to log a message with info level, this version prints an
233 	empty tag automatically (which is different from not having a tag - in this
234 	case there will be an identation of tagWidth chars on the left anyway).
235 
236 	Params:
237 		level = The log level for the logged message
238 		fmt = See http://dlang.org/phobos/std_format.html#format-string
239 */
240 void logInfo(T...)(string fmt, lazy T args) nothrow if (!is(T[0] : Color))
241 {
242 	log(LogLevel.info, false, "", Color.init, fmt, args);
243 }
244 
245 /**
246 	Shorthand function to log a message with info level, this version doesn't
247 	print a tag at all, it effectively just prints the given string.
248 
249 	Params:
250 		level = The log level for the logged message
251 		fmt = See http://dlang.org/phobos/std_format.html#format-string
252 */
253 void logInfoNoTag(T...)(string fmt, lazy T args) nothrow if (!is(T[0] : Color))
254 {
255 	log(LogLevel.info, true, "", Color.init, fmt, args);
256 }
257 
258 /**
259 	Shorthand function to log a message with warning level, with custom tag string.
260 	The tag color is fixed to yellow.
261 
262 	Params:
263 		tag = The string the tag at the beginning of the line should contain
264 		level = The log level for the logged message
265 		fmt = See http://dlang.org/phobos/std_format.html#format-string
266 */
267 void logWarnTag(T...)(string tag, string fmt, lazy T args) nothrow
268 {
269 	log(LogLevel.warn, false, tag, Color.yellow, fmt, args);
270 }
271 
272 /**
273 	Shorthand function to log a message with warning level, using the default
274 	tag "Warning". The tag color is also fixed to yellow.
275 
276 	Params:
277 		level = The log level for the logged message
278 		fmt = See http://dlang.org/phobos/std_format.html#format-string
279 */
280 void logWarn(T...)(string fmt, lazy T args) nothrow
281 {
282 	log(LogLevel.warn, false, "Warning", Color.yellow, fmt, args);
283 }
284 
285 /**
286 	Shorthand function to log a message with error level, with custom tag string.
287 	The tag color is fixed to red.
288 
289 	Params:
290 		tag = The string the tag at the beginning of the line should contain
291 		level = The log level for the logged message
292 		fmt = See http://dlang.org/phobos/std_format.html#format-string
293 */
294 void logErrorTag(T...)(string tag, string fmt, lazy T args) nothrow
295 {
296 	log(LogLevel.error, false, tag, Color.red, fmt, args);
297 }
298 
299 /**
300 	Shorthand function to log a message with error level, using the default
301 	tag "Error". The tag color is also fixed to red.
302 
303 	Params:
304 		level = The log level for the logged message
305 		fmt = See http://dlang.org/phobos/std_format.html#format-string
306 */
307 void logError(T...)(string fmt, lazy T args) nothrow
308 {
309 	log(LogLevel.error, false, "Error", Color.red, fmt, args);
310 }
311 
312 /**
313 	Log a message with the specified log level and with the specified tag string
314 	and color. If the log level is debug or diagnostic, the tag is not printed
315 	thus the tag string and tag color will be ignored. If the log level is error
316 	or warning, the tag will be in bold text. Also the tag can be disabled (for
317 	any log level) by passing true as the second argument.
318 
319 	Params:
320 		level = The log level for the logged message
321 		disableTag = Setting this to true disables the tag, no matter what
322 		tag = The string the tag at the beginning of the line should contain
323 		tagColor = The color the tag string should have
324 		fmt = See http://dlang.org/phobos/std_format.html#format-string
325 */
326 void log(T...)(
327 	LogLevel level,
328 	bool disableTag,
329 	string tag,
330 	Color tagColor,
331 	string fmt,
332 	lazy T args
333 ) nothrow
334 {
335 	if (level < _minLevel)
336 		return;
337 
338 	auto hasTag = true;
339 	if (level <= LogLevel.diagnostic)
340 		hasTag = false;
341 	if (disableTag)
342 		hasTag = false;
343 
344 	auto boldTag = false;
345 	if (level >= LogLevel.warn)
346 		boldTag = true;
347 
348 	try
349 	{
350 		string result = format(fmt, args);
351 
352 		if (hasTag)
353 			result = tag.rightJustify(tagWidth.get, ' ').color(tagColor, boldTag ? Mode.bold : Mode.init) ~ " " ~ result;
354 
355 		import dub.internal.colorize : cwrite;
356 
357 		File output = (level <= LogLevel.info) ? stdout : stderr;
358 
359 		if (output.isOpen)
360 		{
361 			output.cwrite(result, "\n");
362 			output.flush();
363 		}
364 	}
365 	catch (Exception e)
366 	{
367 		debug assert(false, e.msg);
368 	}
369 }
370 
371 /**
372 	Colors the specified string with the specified color. The function is used to
373 	print colored text within a log message. The function also checks whether
374 	color output is enabled or disabled (when not outputting to a TTY) and, in the
375 	last case, just returns the plain string. This allows to use it like so:
376 
377 	logInfo("Tag", Color.green, "My %s log message", "colored".color(Color.red));
378 
379 	without worring whether or not colored output is enabled or not.
380 
381 	Also a mode can be specified, such as bold/underline/etc...
382 
383 	Params:
384 		str = The string to color
385 		color = The color to apply
386 		mode = An optional mode, such as bold/underline/etc...
387 */
388 string color(const string str, const Color c, const Mode m = Mode.init)
389 {
390 	import dub.internal.colorize;
391 
392 	if (_printColors)
393 		return dub.internal.colorize.color(str, c, bg.init, m);
394 	else
395 		return str;
396 }
397 
398 /**
399 	This function is the same as the above one, but just accepts a mode.
400 	It's useful, for instance, when outputting bold text without changing the
401 	color.
402 
403 	Params:
404 		str = The string to color
405 		mode = The mode, such as bold/underline/etc...
406 */
407 string color(const string str, const Mode m = Mode.init)
408 {
409 	import dub.internal.colorize;
410 
411 	if (_printColors)
412 		return dub.internal.colorize.color(str, fg.init, bg.init, m);
413 	else
414 		return str;
415 }