1// Copyright 2022 The Pigweed Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4// use this file except in compliance with the License. You may obtain a copy of 5// the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations under 13// the License. 14 15// eslint-disable-next-line no-undef 16const { createLogViewer, LogSource, LogEntry, Severity } = PigweedLogging; 17 18let currentTheme = {}; 19let defaultLogStyleRule = 'color: #ffffff;'; 20let columnStyleRules = {}; 21let defaultColumnStyles = []; 22let logLevelStyles = {}; 23 24const logLevelToString = { 25 10: 'DBG', 26 20: 'INF', 27 21: 'OUT', 28 30: 'WRN', 29 40: 'ERR', 30 50: 'CRT', 31 70: 'FTL', 32}; 33 34const logLevelToSeverity = { 35 10: Severity.DEBUG, 36 20: Severity.INFO, 37 21: Severity.INFO, 38 30: Severity.WARNING, 39 40: Severity.ERROR, 40 50: Severity.CRITICAL, 41 70: Severity.CRITICAL, 42}; 43 44let nonAdditionalDataFields = [ 45 '_hosttime', 46 'levelname', 47 'levelno', 48 'args', 49 'fields', 50 'message', 51 'time', 52]; 53let additionalHeaders = []; 54 55// New LogSource to consume pw-console log json messages 56class PwConsoleLogSource extends LogSource { 57 constructor() { 58 super(); 59 } 60 append_log(data) { 61 var fields = [ 62 { key: 'severity', value: logLevelToSeverity[data.levelno] }, 63 { key: 'time', value: data.time }, 64 ]; 65 Object.keys(data.fields).forEach((columnName) => { 66 if ( 67 nonAdditionalDataFields.indexOf(columnName) === -1 && 68 additionalHeaders.indexOf(columnName) === -1 69 ) { 70 fields.push({ key: columnName, value: data.fields[columnName] }); 71 } 72 }); 73 fields.push({ key: 'message', value: data.message }); 74 fields.push({ key: 'py_file', value: data.py_file }); 75 fields.push({ key: 'py_logger', value: data.py_logger }); 76 this.publishLogEntry({ 77 severity: logLevelToSeverity[data.levelno], 78 timestamp: new Date(), 79 fields: fields, 80 }); 81 } 82} 83 84// Setup the pigweedjs log-viewer 85const logSource = new PwConsoleLogSource(); 86const containerEl = document.querySelector('#log-viewer-container'); 87let unsubscribe = createLogViewer(logSource, containerEl); 88 89// Format a date in the standard pw_cli style YYYY-mm-dd HH:MM:SS 90function formatDate(dt) { 91 function pad2(n) { 92 return (n < 10 ? '0' : '') + n; 93 } 94 95 return ( 96 dt.getFullYear() + 97 pad2(dt.getMonth() + 1) + 98 pad2(dt.getDate()) + 99 ' ' + 100 pad2(dt.getHours()) + 101 ':' + 102 pad2(dt.getMinutes()) + 103 ':' + 104 pad2(dt.getSeconds()) 105 ); 106} 107 108// Return the value for the given # parameter name. 109function getUrlHashParameter(param) { 110 var params = getUrlHashParameters(); 111 return params[param]; 112} 113 114// Capture all # parameters from the current URL. 115function getUrlHashParameters() { 116 var sPageURL = window.location.hash; 117 if (sPageURL) sPageURL = sPageURL.split('#')[1]; 118 var pairs = sPageURL.split('&'); 119 var object = {}; 120 pairs.forEach(function (pair, i) { 121 pair = pair.split('='); 122 if (pair[0] !== '') object[pair[0]] = pair[1]; 123 }); 124 return object; 125} 126 127// Update web page CSS styles based on a pw-console color json log message. 128function setCurrentTheme(newTheme) { 129 currentTheme = newTheme; 130 defaultLogStyleRule = parsePromptToolkitStyle(newTheme.default); 131 // Set body background color 132 // document.querySelector('body').setAttribute('style', defaultLogStyleRule); 133 134 // Apply default font styles to columns 135 let styles = []; 136 Object.keys(newTheme).forEach((key) => { 137 if (key.startsWith('log-table-column-')) { 138 styles.push(newTheme[key]); 139 } 140 if (key.startsWith('log-level-')) { 141 logLevelStyles[parseInt(key.replace('log-level-', ''))] = 142 parsePromptToolkitStyle(newTheme[key]); 143 } 144 }); 145 defaultColumnStyles = styles; 146} 147 148// Convert prompt_toolkit color format strings to CSS. 149// 'bg:#BG-HEX #FG-HEX STYLE' where STYLE is either 'bold' or 'underline' 150function parsePromptToolkitStyle(rule) { 151 const ruleList = rule.split(' '); 152 let outputStyle = ruleList.map((fragment) => { 153 if (fragment.startsWith('bg:')) { 154 return `background-color: ${fragment.replace('bg:', '')}`; 155 } else if (fragment === 'bold') { 156 return `font-weight: bold`; 157 } else if (fragment === 'underline') { 158 return `text-decoration: underline`; 159 } else if (fragment.startsWith('#')) { 160 return `color: ${fragment}`; 161 } 162 }); 163 return outputStyle.join(';'); 164} 165 166// Inject styled spans into the log message column values. 167function applyStyling(data, applyColors = false) { 168 let colIndex = 0; 169 Object.keys(data).forEach((key) => { 170 if (columnStyleRules[key] && typeof data[key] === 'string') { 171 Object.keys(columnStyleRules[key]).forEach((token) => { 172 data[key] = data[key].replaceAll( 173 token, 174 `<span 175 style="${defaultLogStyleRule};${ 176 applyColors 177 ? defaultColumnStyles[colIndex % defaultColumnStyles.length] 178 : '' 179 };${parsePromptToolkitStyle(columnStyleRules[key][token])};"> 180 ${token} 181 </span>`, 182 ); 183 }); 184 } else if (key === 'fields') { 185 data[key] = applyStyling(data.fields, true); 186 } 187 if (applyColors) { 188 data[key] = `<span 189 style="${parsePromptToolkitStyle( 190 defaultColumnStyles[colIndex % defaultColumnStyles.length], 191 )}"> 192 ${data[key]} 193 </span>`; 194 } 195 colIndex++; 196 }); 197 return data; 198} 199 200// Connect to the pw-console websocket and start emitting logs. 201(function () { 202 const container = document.querySelector('.log-container'); 203 const height = window.innerHeight - 50; 204 let follow = true; 205 206 const port = getUrlHashParameter('ws'); 207 const hostname = location.hostname || '127.0.0.1'; 208 var ws = new WebSocket(`ws://${hostname}:${port}/`); 209 ws.onmessage = function (event) { 210 let dataObj; 211 try { 212 dataObj = JSON.parse(event.data); 213 } catch (e) { 214 // empty 215 } 216 if (!dataObj) return; 217 218 if (dataObj.__pw_console_colors) { 219 // If this is a color theme message, update themes. 220 const colors = dataObj.__pw_console_colors; 221 setCurrentTheme(colors.classes); 222 if (colors.column_values) { 223 columnStyleRules = { ...colors.column_values }; 224 } 225 } else { 226 // Normal log message. 227 const currentData = { ...dataObj, time: formatDate(new Date()) }; 228 logSource.append_log(currentData); 229 } 230 }; 231})(); 232