Skip to content

Commit

Permalink
Polish HTML views for common Langfun objects.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 684661406
  • Loading branch information
daiyip authored and langfun authors committed Oct 11, 2024
1 parent ec3cfbe commit fbc42cd
Show file tree
Hide file tree
Showing 20 changed files with 853 additions and 178 deletions.
187 changes: 144 additions & 43 deletions langfun/core/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,13 @@
# limitations under the License.
"""Langfun event logging."""

from collections.abc import Iterator
import contextlib
import datetime
import io
import typing
from typing import Any, Literal
from typing import Any, Iterator, Literal, Sequence

from langfun.core import component
from langfun.core import console
from langfun.core import repr_utils
import pyglove as pg


Expand Down Expand Up @@ -56,49 +53,153 @@ class LogEntry(pg.Object):
def should_output(self, min_log_level: LogLevel) -> bool:
return _LOG_LEVELS.index(self.level) >= _LOG_LEVELS.index(min_log_level)

def _repr_html_(self) -> str:
s = io.StringIO()
padding_left = 50 * self.indent
s.write(f'<div style="padding-left: {padding_left}px;">')
s.write(self._message_display)
if self.metadata:
s.write(repr_utils.html_repr(self.metadata))
s.write('</div>')
return s.getvalue()

@property
def _message_text_bgcolor(self) -> str:
match self.level:
case 'debug':
return '#EEEEEE'
case 'info':
return '#A3E4D7'
case 'warning':
return '#F8C471'
case 'error':
return '#F5C6CB'
case 'fatal':
return '#F19CBB'
case _:
raise ValueError(f'Unknown log level: {self.level}')

@property
def _time_display(self) -> str:
display_text = self.time.strftime('%H:%M:%S')
alt_text = self.time.strftime('%Y-%m-%d %H:%M:%S.%f')
return (
'<span style="background-color: #BBBBBB; color: white; '
'border-radius:5px; padding:0px 5px 0px 5px;" '
f'title="{alt_text}">{display_text}</span>'
def _html_tree_view_summary(
self,
view: pg.views.HtmlTreeView,
title: str | pg.Html | None = None,
max_str_len_for_summary: int = pg.View.PresetArgValue(80), # pytype: disable=annotation-type-mismatch
**kwargs
) -> str:
if len(self.message) > max_str_len_for_summary:
message = self.message[:max_str_len_for_summary] + '...'
else:
message = self.message

s = pg.Html(
pg.Html.element(
'span',
[self.time.strftime('%H:%M:%S')],
css_class=['log-time']
),
pg.Html.element(
'span',
[pg.Html.escape(message)],
css_class=['log-summary'],
),
)
return view.summary(
self,
title=title or s,
max_str_len_for_summary=max_str_len_for_summary,
**kwargs,
)

@property
def _message_display(self) -> str:
return repr_utils.html_round_text(
self._time_display + '&nbsp;' + self.message,
background_color=self._message_text_bgcolor,
# pytype: disable=annotation-type-mismatch
def _html_tree_view_content(
self,
view: pg.views.HtmlTreeView,
root_path: pg.KeyPath,
collapse_log_metadata_level: int = pg.View.PresetArgValue(0),
max_str_len_for_summary: int = pg.View.PresetArgValue(80),
**kwargs
) -> pg.Html:
# pytype: enable=annotation-type-mismatch
def render_message_text():
if len(self.message) < max_str_len_for_summary:
return None
return pg.Html.element(
'span',
[pg.Html.escape(self.message)],
css_class=['log-text'],
)

def render_metadata():
if not self.metadata:
return None
return pg.Html.element(
'div',
[
view.render(
self.metadata,
name='metadata',
root_path=root_path + 'metadata',
parent=self,
collapse_level=(
root_path.depth + collapse_log_metadata_level + 1
)
)
],
css_class=['log-metadata'],
)

return pg.Html.element(
'div',
[
render_message_text(),
render_metadata(),
],
css_class=['complex_value'],
)

def _html_style(self) -> list[str]:
return super()._html_style() + [
"""
.log-time {
color: #222;
font-size: 12px;
padding-right: 10px;
}
.log-summary {
font-weight: normal;
font-style: italic;
padding: 4px;
}
.log-debug > summary > .summary_title::before {
content: '🛠️ '
}
.log-info > summary > .summary_title::before {
content: '💡 '
}
.log-warning > summary > .summary_title::before {
content: '❗ '
}
.log-error > summary > .summary_title::before {
content: '❌ '
}
.log-fatal > summary > .summary_title::before {
content: '💀 '
}
.log-text {
display: block;
color: black;
font-style: italic;
padding: 20px;
border-radius: 5px;
background: rgba(255, 255, 255, 0.5);
white-space: pre-wrap;
}
details.log-entry {
margin: 0px 0px 10px;
border: 0px;
}
div.log-metadata {
margin: 10px 0px 0px 0px;
}
.log-metadata > details {
background-color: rgba(255, 255, 255, 0.5);
border: 1px solid transparent;
}
.log-debug {
background-color: #EEEEEE
}
.log-warning {
background-color: #F8C471
}
.log-info {
background-color: #A3E4D7
}
.log-error {
background-color: #F5C6CB
}
.log-fatal {
background-color: #F19CBB
}
"""
]

def _html_element_class(self) -> Sequence[str] | None:
return super()._html_element_class() + [f'log-{self.level}']


def log(level: LogLevel,
message: str,
Expand Down
33 changes: 33 additions & 0 deletions langfun/core/logging_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
# limitations under the License.
"""Tests for langfun.core.logging."""

import datetime
import inspect
import unittest

from langfun.core import logging
Expand Down Expand Up @@ -52,6 +54,37 @@ def assert_color(entry, color):
assert_color(logging.error('hi', indent=2, x=1, y=2), '#F5C6CB')
assert_color(logging.fatal('hi', indent=2, x=1, y=2), '#F19CBB')

def assert_html_content(self, html, expected):
expected = inspect.cleandoc(expected).strip()
actual = html.content.strip()
if actual != expected:
print(actual)
self.assertEqual(actual, expected)

def test_html(self):
time = datetime.datetime(2024, 10, 10, 12, 30, 45)
self.assert_html_content(
logging.LogEntry(
level='info', message='5 + 2 > 3',
time=time, metadata={}
).to_html(enable_summary_tooltip=False),
"""
<details open class="pyglove log-entry log-info"><summary><div class="summary_title"><span class="log-time">12:30:45</span><span class="log-summary">5 + 2 &gt; 3</span></div></summary><div class="complex_value"></div></details>
"""
)
self.assert_html_content(
logging.LogEntry(
level='error', message='This is a longer message: 5 + 2 > 3',
time=time, metadata=dict(x=1, y=2)
).to_html(
max_str_len_for_summary=10,
enable_summary_tooltip=False,
collapse_log_metadata_level=1
),
"""
<details open class="pyglove log-entry log-error"><summary><div class="summary_title"><span class="log-time">12:30:45</span><span class="log-summary">This is a ...</span></div></summary><div class="complex_value"><span class="log-text">This is a longer message: 5 + 2 &gt; 3</span><div class="log-metadata"><details open class="pyglove dict"><summary><div class="summary_name">metadata</div><div class="summary_title">Dict(...)</div></summary><div class="complex_value dict"><table><tr><td><span class="object_key str">x</span><span class="tooltip key-path">metadata.x</span></td><td><div><span class="simple_value int">1</span></div></td></tr><tr><td><span class="object_key str">y</span><span class="tooltip key-path">metadata.y</span></td><td><div><span class="simple_value int">2</span></div></td></tr></table></div></details></div></div></details>
"""
)

if __name__ == '__main__':
unittest.main()
Loading

0 comments on commit fbc42cd

Please sign in to comment.