@@ -540,7 +540,7 @@ field_overlay_source::build_field_lines(const listview_curses& lv,
540540 auto * curr_elf = dynamic_cast <external_log_format*>(curr_format);
541541 const auto format_name = curr_format->get_name ().to_string ();
542542 attr_line_t al;
543- auto value_str = lv.to_string ();
543+ auto value_str = lv.to_humanized_string ();
544544
545545 if (curr_format != last_format) {
546546 this ->fos_lines .emplace_back (" Known message fields for table "
@@ -554,6 +554,7 @@ field_overlay_source::build_field_lines(const listview_curses& lv,
554554
555555 std::string field_name, orig_field_name;
556556 line_range hl_range;
557+ size_t prefix_len = 0 ;
557558 al.append (" " ).append (" |" , VC_GRAPHIC.value (NCACS_LTEE)).append (" " );
558559 if (meta.lvm_struct_name .empty ()) {
559560 if (curr_elf && curr_elf->elf_body_field == meta.lvm_name ) {
@@ -585,11 +586,11 @@ field_overlay_source::build_field_lines(const listview_curses& lv,
585586 al.append (" :bar_chart:" _emoji).append (" " );
586587 break ;
587588 }
588- auto prefix_len = al.column_width ();
589+ prefix_len = al.column_width () + this -> fos_known_key_size ;
589590 hl_range.lr_start = al.get_string ().length ();
590591 al.append (field_name);
591592 hl_range.lr_end = al.get_string ().length ();
592- al.pad_to (prefix_len + this -> fos_known_key_size );
593+ al.pad_to (prefix_len);
593594
594595 this ->fos_row_to_field_meta .emplace (this ->fos_lines .size (),
595596 row_info{meta, value_str});
@@ -599,6 +600,7 @@ field_overlay_source::build_field_lines(const listview_curses& lv,
599600 meta.lvm_name .get ());
600601 hl_range.lr_start = al.get_string ().length ();
601602 al.append (jget_str.in ());
603+ prefix_len = al.column_width ();
602604 hl_range.lr_end = al.get_string ().length ();
603605
604606 this ->fos_row_to_field_meta .emplace (
@@ -607,29 +609,6 @@ field_overlay_source::build_field_lines(const listview_curses& lv,
607609 readline_sql_highlighter_int (
608610 al, lnav::sql::dialect::sqlite, std::nullopt , hl_range);
609611
610- if (!meta.lvm_unit_suffix .empty ()) {
611- std::optional<double > numeric;
612- switch (meta.lvm_kind ) {
613- case value_kind_t ::VALUE_INTEGER:
614- numeric = (double ) lv.lv_value .i ;
615- break ;
616- case value_kind_t ::VALUE_FLOAT:
617- numeric = lv.lv_value .d ;
618- break ;
619- default :
620- break ;
621- }
622- if (numeric) {
623- if (meta.lvm_unit_divisor != 0.0
624- && meta.lvm_unit_divisor != 1.0 )
625- {
626- *numeric /= meta.lvm_unit_divisor ;
627- }
628- value_str = humanize::format (
629- *numeric, meta.lvm_unit_suffix .to_string_fragment ());
630- }
631- }
632-
633612 if (meta.lvm_kind == value_kind_t ::VALUE_TIMESTAMP) {
634613 auto dts = curr_format->build_time_scanner ();
635614 exttm tm;
@@ -653,8 +632,66 @@ field_overlay_source::build_field_lines(const listview_curses& lv,
653632
654633 al.append (" = " ).append (scrub_ws (value_str.c_str ()));
655634
635+ // Per-column stats summary: numeric columns get a min..max
636+ // range and total count; text columns get an HLL-estimated
637+ // distinct count. Both render in the column's unit if one is
638+ // declared, mirroring the value's own formatting above.
639+ const logline_value_stats* stats = nullptr ;
640+ const auto * curr_lf = this ->fos_log_helper .ldh_file .get ();
641+ if (curr_lf != nullptr ) {
642+ stats = curr_lf->stats_for_value (meta.lvm_name );
643+ }
644+
645+ if (stats != nullptr ) {
646+ std::string summary;
647+ if (stats->lvs_count > 0 ) {
648+ summary
649+ = fmt::format (FMT_STRING (" {}..{} of {}" ),
650+ meta.to_humanized_value (stats->lvs_min_value ),
651+ meta.to_humanized_value (stats->lvs_max_value ),
652+ stats->lvs_count );
653+ } else if (auto est = stats->distinct_estimate (); est) {
654+ summary = fmt::format (FMT_STRING (" ~{:.0f} distinct of {}" ),
655+ est.value (),
656+ stats->lvs_text_count );
657+ }
658+ if (!summary.empty ()) {
659+ al.append (attr_line_t (summary).with_attr_for_all (
660+ VC_ROLE.value (role_t ::VCR_COMMENT)));
661+ }
662+ }
663+
656664 this ->fos_lines .emplace_back (al);
657665
666+ // Numeric percentile sub-line: typical / tail / extreme.
667+ // Suppressed when the sample is too small to be statistically
668+ // meaningful, when the distribution is degenerate (single
669+ // value), or when the upper percentiles all collapse to the
670+ // max — in those cases the inline `min..max of N` already
671+ // tells the whole story.
672+ if (stats != nullptr && stats->lvs_count >= 20
673+ && stats->lvs_min_value < stats->lvs_max_value )
674+ {
675+ const auto p50 = stats->lvs_tdigest .quantile (50 );
676+ const auto p90 = stats->lvs_tdigest .quantile (90 );
677+ const auto p99 = stats->lvs_tdigest .quantile (99 );
678+ if (!(p50 == p99 && p99 == stats->lvs_max_value )) {
679+ attr_line_t pct_line;
680+ pct_line.append (" " )
681+ .with_attr (string_attr (line_range{1 , 2 },
682+ VC_GRAPHIC.value (NCACS_VLINE)))
683+ .with_attr (string_attr (line_range{1 , 2 },
684+ VC_ROLE.value (role_t ::VCR_COMMENT)))
685+ .pad_to (prefix_len + 5 )
686+ .append (fmt::format (FMT_STRING (" p50={} p90={} p99={}" ),
687+ meta.to_humanized_value (p50),
688+ meta.to_humanized_value (p90),
689+ meta.to_humanized_value (p99)));
690+ pct_line.with_attr_for_all (VC_ROLE.value (role_t ::VCR_COMMENT));
691+ this ->fos_lines .emplace_back (pct_line);
692+ }
693+ }
694+
658695 if (meta.lvm_kind == value_kind_t ::VALUE_STRUCT) {
659696 json_string js = extract (value_str.c_str ());
660697
0 commit comments