xref: /aosp_15_r20/external/pigweed/pw_web/log-viewer/src/console.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1// Copyright 2024 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
15import { WebSocketRPCReplKernel, WebSocketRPCClient } from './repl-kernel';
16import { createLogViewer } from './createLogViewer';
17import { LogSource } from './log-source';
18import { Severity } from './shared/interfaces';
19import { Repl } from './components/repl/repl';
20
21const logLevelToSeverity = {
22  10: Severity.DEBUG,
23  20: Severity.INFO,
24  21: Severity.INFO,
25  30: Severity.WARNING,
26  40: Severity.ERROR,
27  50: Severity.CRITICAL,
28  70: Severity.CRITICAL,
29};
30
31const nonAdditionalDataFields = [
32  '_hosttime',
33  'levelname',
34  'levelno',
35  'args',
36  'fields',
37  'message',
38  'time',
39];
40
41// Format a date in the standard pw_cli style YYYY-mm-dd HH:MM:SS
42function formatDate(dt: Date) {
43  function pad2(n: number) {
44    return (n < 10 ? '0' : '') + n;
45  }
46
47  return (
48    dt.getFullYear() +
49    pad2(dt.getMonth() + 1) +
50    pad2(dt.getDate()) +
51    ' ' +
52    pad2(dt.getHours()) +
53    ':' +
54    pad2(dt.getMinutes()) +
55    ':' +
56    pad2(dt.getSeconds())
57  );
58}
59
60// New LogSource to consume pw-console log json messages
61class PwConsoleLogSource extends LogSource {
62  // @ts-ignore
63  appendLog(data) {
64    const fields = [
65      // @ts-ignore
66      { key: 'severity', value: logLevelToSeverity[data.levelno] },
67      { key: 'time', value: data.time },
68    ];
69    Object.keys(data.fields || {}).forEach((columnName) => {
70      if (nonAdditionalDataFields.indexOf(columnName) === -1) {
71        fields.push({ key: columnName, value: data.fields[columnName] });
72      }
73    });
74    fields.push({ key: 'message', value: data.message });
75    fields.push({ key: 'py_file', value: data.py_file });
76    fields.push({ key: 'py_logger', value: data.py_logger });
77    this.publishLogEntry({
78      // @ts-ignore
79      severity: logLevelToSeverity[data.levelno],
80      timestamp: new Date(),
81      fields: fields,
82    });
83  }
84}
85
86export async function renderPWConsole(containerEl: HTMLElement, wsUrl = '/ws') {
87  const replContainerEl = document.createElement('div');
88  replContainerEl.id = 'repl-container';
89  const logsContainerEl = document.createElement('div');
90  logsContainerEl.id = 'logs-container';
91  createSplitPanel(replContainerEl, logsContainerEl, containerEl, 40);
92
93  const ws = new WebSocket(wsUrl);
94  const kernel = new WebSocketRPCClient(ws);
95  const allLogSourceNames: string[] = await kernel.call('log_source_list', {
96    filter: '',
97  });
98  const logSources = allLogSourceNames.map(
99    (name) => new PwConsoleLogSource(name),
100  );
101  logSources.forEach((source, index) => {
102    kernel.openStream(
103      'log_source_subscribe',
104      { name: allLogSourceNames[index] },
105      (data) => {
106        if (data.log_line)
107          source.appendLog({ ...data.log_line, time: formatDate(new Date()) });
108      },
109    );
110  });
111
112  const unsubscribe = createLogViewer(logSources, logsContainerEl);
113
114  const rpc = new WebSocketRPCReplKernel(kernel);
115  const repl = new Repl(rpc, 'Python Shell');
116  replContainerEl.appendChild(repl);
117  (window as any).rpc = rpc;
118  const resizeObserver = new ResizeObserver((entries) => {
119    for (const entry of entries) {
120      const splitPanel = document.querySelector('sl-split-panel');
121      if (splitPanel) {
122        // Stack vertically for smaller viewport sizes
123        splitPanel.vertical = entry.contentRect.width < 800;
124      }
125    }
126  });
127  resizeObserver.observe(document.body);
128  return () => {
129    unsubscribe();
130  };
131}
132
133export function createSplitPanel(
134  startEl: HTMLElement,
135  endEl: HTMLElement,
136  containerEl: HTMLElement,
137  initialPosition = 50,
138) {
139  const splitPanel = document.createElement('sl-split-panel');
140
141  startEl.setAttribute('slot', 'start');
142  endEl.setAttribute('slot', 'end');
143  splitPanel.setAttribute('position', `${initialPosition}`);
144
145  splitPanel.appendChild(startEl);
146  splitPanel.appendChild(endEl);
147  containerEl.appendChild(splitPanel);
148}
149