private readonly bool _showDeltaColumn;
private const int Indent = 4; // Counter name indent size.
private const int CounterValueLength = 15;
+ private const int MinimalColumnHeaderLength = 5;
private int _nameColumnWidth; // fixed width of the name column. Names will be truncated if needed to fit in this space.
private int _statusRow; // Row # of where we print the status of dotnet-counters
_consoleHeight = _console.WindowHeight;
// Truncate the name column if needed to prevent line wrapping
int numValueColumns = _showDeltaColumn ? 2 : 1;
- _nameColumnWidth = Math.Max(Math.Min(80, _consoleWidth) - numValueColumns * (CounterValueLength + 1), 0);
+ _nameColumnWidth = Math.Max(_consoleWidth - numValueColumns * (CounterValueLength + 1), 0);
int row = _console.CursorTop;
counter.Row = row;
if (counter.RenderValueInline)
{
- if (!RenderCounterValueRow(ref row, indentLevel:1, counter.DisplayName, counter.LastValue, 0))
+ if (!RenderCounterValueRow(ref row, indentLevel: 1, counter.DisplayName, counter.LastValue, 0))
{
break;
}
{
break;
}
- foreach (ObservedTagSet tagSet in counter.TagSets.Values.OrderBy(t => t.Tags))
+ if (!RenderTagSetsInColumnMode(ref row, counter))
{
- tagSet.Row = row;
- if (!RenderCounterValueRow(ref row, indentLevel: 2, tagSet.Tags, tagSet.LastValue, 0))
- {
- break;
- }
+ break;
}
}
}
_maxRow = Math.Max(_maxRow, row);
}
+ private bool RenderTagSetsInColumnMode(ref int row, ObservedCounter counter)
+ {
+ List<(string header, string[] values)> observedTags = new();
+ List<int> columnHeaderLen = new();
+ List<int> maxValueColumnLen = new();
+ int tagsCount = 0;
+ foreach (ObservedTagSet tagSet in counter.TagSets.Values.OrderBy(t => t.Tags))
+ {
+ string[] tags = tagSet.DisplayTags.Split(',');
+ for (int i = 0; i < tags.Length; i++)
+ {
+ string tag = tags[i];
+ string[] keyValue = tag.Split("=");
+ int posTag = observedTags.FindIndex (tag => tag.header == keyValue[0]);
+ if (posTag == -1)
+ {
+ observedTags.Add((keyValue[0], new string[counter.TagSets.Count]));
+ columnHeaderLen.Add(keyValue[0].Length);
+ maxValueColumnLen.Add(default(int));
+ posTag = observedTags.Count - 1;
+ }
+ observedTags[posTag].values[tagsCount] = keyValue[1];
+ maxValueColumnLen[posTag] = Math.Max(keyValue[1].Length, maxValueColumnLen[posTag]);
+ }
+ tagsCount++;
+ }
+ AdjustColumnsLength(columnHeaderLen, maxValueColumnLen);
+ //print the header
+ string headerRow = "";
+ for (int i = 0; i < observedTags.Count; i++)
+ {
+ (string header, string[] values) observedTag = observedTags[i];
+ string headerWithSpaces = MakeFixedWidth(observedTag.header, Math.Max(maxValueColumnLen[i], columnHeaderLen[i]), truncateLeft: true);
+ headerWithSpaces += " ";
+ headerRow += headerWithSpaces;
+ }
+ if (!RenderCounterNameRow(ref row, headerRow, indentLevel: 2))
+ {
+ return false;
+ }
+ //print each line
+ int linePos = 0;
+ foreach (ObservedTagSet tagSet in counter.TagSets.Values.OrderBy(t => t.Tags))
+ {
+ tagSet.Row = row;
+ string tagRow = "";
+ for (int i = 0; i < observedTags.Count; i++)
+ {
+ (string header, string[] values) observedTag = observedTags[i];
+ string tagItem = observedTag.values[linePos];
+ string tagWithSpaces = MakeFixedWidth(tagItem, Math.Max(maxValueColumnLen[i], columnHeaderLen[i]));
+ tagWithSpaces += " ";
+ tagRow += tagWithSpaces;
+ }
+ if (!RenderCounterValueRow(ref row, indentLevel: 2, tagRow, tagSet.LastValue, 0))
+ {
+ return false;
+ }
+ linePos++;
+ }
+ return true;
+ }
+ // This method attempts to truncate column header content while prioritizing the actual content.
+ // Initially, we evenly divide the available space among columns, aiming to reduce the size
+ // of each column header until it fits on the screen. If this isn't possible due to headers
+ // reaching a minimal length (MinimalColumnHeaderLength), we then truncate the values
+ // themselves until we achieve the desired width that fits within the screen.
+ private void AdjustColumnsLength(List<int> columnHeaderLen, List<int> maxValueColumnLen)
+ {
+ int totalColumnLength = 0;
+ bool startReduceValueColumnLength = false;
+ for (int i = 0; i < columnHeaderLen.Count; i++)
+ {
+ totalColumnLength += Math.Max(columnHeaderLen[i] + 1, maxValueColumnLen[i] + 1);
+ }
+
+ int needsToReduce = totalColumnLength - (_nameColumnWidth - Indent * 2);
+ while (needsToReduce > 0)
+ {
+ bool changed = false;
+ int needsToReducePerColumn = Math.Max((needsToReduce / columnHeaderLen.Count), 1);
+ for (int i = 0; needsToReduce > 0 && i < columnHeaderLen.Count; i++)
+ {
+ if (columnHeaderLen[i] > maxValueColumnLen[i] && columnHeaderLen[i] - needsToReducePerColumn > MinimalColumnHeaderLength)
+ {
+ columnHeaderLen[i] -= needsToReducePerColumn;
+ needsToReduce -= needsToReducePerColumn;
+ changed = true;
+ }
+ if (startReduceValueColumnLength)
+ {
+ maxValueColumnLen[i] -= needsToReducePerColumn;
+ needsToReduce -= needsToReducePerColumn;
+ }
+ }
+ if (!changed) //cannot reduce header anymore, start reducing the value
+ {
+ startReduceValueColumnLength = true;
+ }
+ }
+ }
+
public void ToggleStatus(bool pauseCmdSet)
{
if (_paused == pauseCmdSet)
return RenderTableRow(ref row, $"{new string(' ', Indent * indentLevel)}{name}", FormatValue(value), deltaText);
}
- private bool RenderCounterNameRow(ref int row, string name)
+ private bool RenderCounterNameRow(ref int row, string name, int indentLevel = 1)
{
- return RenderTableRow(ref row, $"{new string(' ', Indent)}{name}");
+ return RenderTableRow(ref row, $"{new string(' ', Indent * indentLevel)}{name}");
}
private bool RenderTableRow(ref int row, string name, string value = null, string delta = null)
return valueText;
}
- private static string MakeFixedWidth(string text, int width, bool alignRight = false)
+ private static string MakeFixedWidth(string text, int width, bool alignRight = false, bool truncateLeft = false)
{
if (text == null)
{
}
else if (text.Length > width)
{
+ if (truncateLeft)
+ {
+ return text.Substring(text.Length-width, width);
+ }
return text.Substring(0, width);
}
else
"Name Current Value",
"[Provider1]",
" Counter1 ({widget} / 1 sec)",
- " color=blue 87",
- " color=red 0.1",
+ " color",
+ " blue 87",
+ " red 0.1",
" Counter2 ({widget} / 1 sec)",
- " size=1 14",
- " temp=hot 160");
+ " size temp",
+ " 1 14",
+ " hot 160");
}
[Fact]
"Name Current Value",
"[Provider1]",
" Counter1 ({widget} / 1 sec)",
- " color=blue,LongNameTag=Thi 87",
- " color=red 0.1",
+ " color LongNameTag herOne",
+ " blue ThisDoesNotFi Hi 87",
+ " red 0.1",
" Counter2 ({widget} / 1 sec)",
- " size=1 14",
- " temp=hot 160");
+ " size temp",
+ " 1 14",
+ " hot 160");
}
[Fact]
"Name Current Value",
"[Provider1]",
" Counter1 ({widget} / 1 sec)",
- " color=blue 87");
+ " color");
}
[Fact]
exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "size=1", 14), false);
exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter2", "{widget}", "temp=hot", 160), false);
exporter.SetErrorText("Uh-oh, a bad thing happened");
-
console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
" Status: Running",
"Uh-oh, a bad thing happened",
"Name Current Value",
"[Provider1]",
" Counter1 ({widget} / 1 sec)",
- " color=blue 87",
- " color=red 0.1",
+ " color",
+ " blue 87",
+ " red 0.1",
" Counter2 ({widget} / 1 sec)",
- " size=1 14",
- " temp=hot 160");
+ " size temp",
+ " 1 14",
+ " hot 160");
}
[Fact]
" Allocation Rate (B / 1 sec) 1,731",
"[Provider1]",
" Counter1 ({widget} / 1 sec)",
- " color=blue 87",
- " color=red 0.1",
+ " color",
+ " blue 87",
+ " red 0.1",
" Counter2 ({widget} / 1 sec)",
- " size=1 14",
- " temp=hot 160");
+ " size temp",
+ " 1 14",
+ " hot 160");
}
[Fact]
" Allocation Rate (B / 1 sec) 1,731",
"[Provider1]",
" Counter1 ({widget} / 1 sec)",
- " color=blue 87",
- " color=red 0.1",
+ " color",
+ " blue 87",
+ " red 0.1",
" Counter2 ({widget} / 1 sec)",
- " size=1 14",
- " temp=hot 160");
+ " size temp",
+ " 1 14",
+ " hot 160");
exporter.CounterPayloadReceived(CreateIncrementingEventCounter("System.Runtime", "Allocation Rate", "B", 1732), false);
exporter.CounterPayloadReceived(CreateMeterCounterPreNet8("Provider1", "Counter1", "{widget}", "color=red", 0.2), false);
" Allocation Rate (B / 1 sec) 1,732 1",
"[Provider1]",
" Counter1 ({widget} / 1 sec)",
- " color=blue 87 0",
- " color=red 0.2 0.1",
+ " color",
+ " blue 87 0",
+ " red 0.2 0.1",
" Counter2 ({widget} / 1 sec)",
- " size=1 10 -4",
- " temp=hot 160");
+ " size temp",
+ " 1 10 -4",
+ " hot 160");
}
// Starting in .NET 8 MetricsEventSource, Meter counter instruments report both rate of change and
"Name Current Value Last Delta",
"[Provider1]",
" Counter1 ({widget})", // There is no longer (unit / 1 sec) here
- " color=blue 87",
- " color=red 0.1",
+ " color",
+ " blue 87",
+ " red 0.1",
" Counter2 ({widget}) 14");
exporter.CounterPayloadReceived(CreateMeterCounterPostNet8("Provider1", "Counter1", "{widget}", "color=red", 0.2), false);
"Name Current Value Last Delta",
"[Provider1]",
" Counter1 ({widget})", // There is no longer (unit / 1 sec) here
- " color=blue 87 0",
- " color=red 0.2 0.1",
+ " color",
+ " blue 87 0",
+ " red 0.2 0.1",
" Counter2 ({widget}) 10 -4");
}