Skip to content

Commit c84df9a

Browse files
Merge pull request #17 from EvotecIT/create-iislogrecord-class-with-legacy-support
2 parents 4ae247e + b2057f3 commit c84df9a

5 files changed

Lines changed: 290 additions & 65 deletions

File tree

IISParser.PowerShell/CmdletGetIISParsedLog.cs

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,16 @@ namespace IISParser.PowerShell;
2929
/// <code>Get-IISParsedLog -FilePath "C:\\Logs\\u_ex230101.log" -Expand</code>
3030
/// <para>Field names such as <c>X-Forwarded-For</c> become <c>X_Forwarded_For</c>.</para>
3131
/// </example>
32+
/// <example>
33+
/// <summary>Return legacy property names.</summary>
34+
/// <prefix>PS&gt; </prefix>
35+
/// <code>Get-IISParsedLog -FilePath "C:\\Logs\\u_ex230101.log" -Legacy</code>
36+
/// <para>Outputs entries using the original property names.</para>
37+
/// </example>
3238
/// <seealso href="https://learn.microsoft.com/iis/configuration/system.webserver/httplogging" />
33-
/// <seealso href="https://github.com/EvotecIT/IISParser" />
34-
[Cmdlet(VerbsCommon.Get, "IISParsedLog", DefaultParameterSetName = "Default")]
35-
public class CmdletGetIISParsedLog : AsyncPSCmdlet {
39+
/// <seealso href="https://github.com/EvotecIT/IISParser" />
40+
[Cmdlet(VerbsCommon.Get, "IISParsedLog", DefaultParameterSetName = "Default")]
41+
public class CmdletGetIISParsedLog : AsyncPSCmdlet {
3642
private ParserEngine? _parser;
3743

3844
/// <summary>Path to the IIS log file.</summary>
@@ -65,6 +71,10 @@ public class CmdletGetIISParsedLog : AsyncPSCmdlet {
6571
/// </summary>
6672
[Parameter]
6773
public SwitchParameter Expand { get; set; }
74+
75+
/// <summary>Outputs objects with legacy property names.</summary>
76+
[Parameter]
77+
public SwitchParameter Legacy { get; set; }
6878

6979
/// <inheritdoc />
7080
protected override Task BeginProcessingAsync() {
@@ -81,28 +91,49 @@ protected override Task ProcessRecordAsync() {
8191
return Task.CompletedTask;
8292
}
8393

84-
IEnumerable<IISLogEvent> events = _parser.ParseLog();
94+
if (Legacy) {
95+
IEnumerable<IISLogEvent> events = _parser.ParseLogLegacy();
96+
if (ParameterSetName == "FirstLastSkip") {
97+
if (Skip.HasValue && Skip.Value > 0) {
98+
events = events.Skip(Skip.Value);
99+
}
100+
101+
if (First.HasValue) {
102+
events = events.Take(First.Value);
103+
}
85104

86-
if (ParameterSetName == "FirstLastSkip") {
87-
if (Skip.HasValue && Skip.Value > 0) {
88-
events = events.Skip(Skip.Value);
105+
if (Last.HasValue && Last.Value > 0) {
106+
events = events.TakeLastLazy(Last.Value);
107+
}
108+
} else if (ParameterSetName == "SkipLast" && SkipLast.HasValue && SkipLast.Value > 0) {
109+
events = events.SkipLastLazy(SkipLast.Value);
89110
}
90111

91-
if (First.HasValue) {
92-
events = events.Take(First.Value);
112+
foreach (var evt in events) {
113+
WriteEvent(evt);
93114
}
115+
} else {
116+
IEnumerable<IISLogRecord> records = _parser.ParseLog();
117+
if (ParameterSetName == "FirstLastSkip") {
118+
if (Skip.HasValue && Skip.Value > 0) {
119+
records = records.Skip(Skip.Value);
120+
}
121+
122+
if (First.HasValue) {
123+
records = records.Take(First.Value);
124+
}
94125

95-
if (Last.HasValue && Last.Value > 0) {
96-
events = events.TakeLastLazy(Last.Value);
126+
if (Last.HasValue && Last.Value > 0) {
127+
records = records.TakeLastLazy(Last.Value);
128+
}
129+
} else if (ParameterSetName == "SkipLast" && SkipLast.HasValue && SkipLast.Value > 0) {
130+
records = records.SkipLastLazy(SkipLast.Value);
97131
}
98-
} else if (ParameterSetName == "SkipLast" && SkipLast.HasValue && SkipLast.Value > 0) {
99-
events = events.SkipLastLazy(SkipLast.Value);
100-
}
101132

102-
foreach (var evt in events) {
103-
WriteEvent(evt);
133+
foreach (var record in records) {
134+
WriteRecord(record);
135+
}
104136
}
105-
106137
return Task.CompletedTask;
107138
}
108139

@@ -138,6 +169,26 @@ private void WriteEvent(IISLogEvent evt) {
138169
WriteObject(evt);
139170
}
140171
}
172+
173+
private void WriteRecord(IISLogRecord record) {
174+
if (Expand) {
175+
var psObj = new PSObject();
176+
foreach (var prop in PSObject.AsPSObject(record).Properties) {
177+
if (!prop.Name.Equals("Fields", StringComparison.OrdinalIgnoreCase)) {
178+
psObj.Properties.Add(new PSNoteProperty(prop.Name, prop.Value));
179+
}
180+
}
181+
182+
foreach (var kv in record.Fields) {
183+
var key = ToPsIdentifier(kv.Key);
184+
psObj.Properties.Add(new PSNoteProperty(key, kv.Value));
185+
}
186+
187+
WriteObject(psObj);
188+
} else {
189+
WriteObject(record);
190+
}
191+
}
141192

142193
/// <inheritdoc />
143194
protected override Task EndProcessingAsync() {

IISParser.Tests/ParserEngineTests.cs

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,34 @@ namespace IISParser.Tests;
88

99
public class ParserEngineTests {
1010
[Fact]
11-
public void ParseLog_ReturnsEvents() {
11+
public void ParseLog_ReturnsRecords() {
1212
var path = Path.Combine(AppContext.BaseDirectory, "TestData", "sample.log");
1313
var engine = new ParserEngine(path);
14-
var eventsList = engine.ParseLog().ToList();
15-
Assert.Single(eventsList);
16-
var evt = eventsList[0];
17-
Assert.Equal("/index.html", evt.csUriStem);
18-
Assert.Equal(200, evt.scStatus);
19-
Assert.Equal("192.168.0.1", evt.Fields["X-Forwarded-For"]);
14+
var recordsList = engine.ParseLog().ToList();
15+
Assert.Single(recordsList);
16+
var record = recordsList[0];
17+
Assert.Equal("/index.html", record.UriPath);
18+
Assert.Equal(200, record.StatusCode);
19+
Assert.Equal("192.168.0.1", record.Fields["X-Forwarded-For"]);
2020
}
2121

2222
[Fact]
2323
public void ParseLog_RemovesKnownFieldsFromDictionary() {
2424
var path = Path.Combine(AppContext.BaseDirectory, "TestData", "sample.log");
2525
var engine = new ParserEngine(path);
26-
var evt = engine.ParseLog().Single();
27-
Assert.False(evt.Fields.ContainsKey("cs-uri-stem"));
28-
Assert.False(evt.Fields.ContainsKey("date"));
26+
var record = engine.ParseLog().Single();
27+
Assert.False(record.Fields.ContainsKey("cs-uri-stem"));
28+
Assert.False(record.Fields.ContainsKey("date"));
2929
}
3030

3131
[Fact]
3232
public void ParseLog_HandlesValuesAboveIntMax() {
3333
var path = Path.Combine(AppContext.BaseDirectory, "TestData", "large_values.log");
3434
var engine = new ParserEngine(path);
35-
var evt = engine.ParseLog().Single();
36-
Assert.Equal(3000000000L, evt.scBytes);
37-
Assert.Equal(4000000000L, evt.csBytes);
38-
Assert.Equal(5000000000L, evt.timeTaken);
35+
var record = engine.ParseLog().Single();
36+
Assert.Equal(3000000000L, record.BytesSent);
37+
Assert.Equal(4000000000L, record.BytesReceived);
38+
Assert.Equal(5000000000L, record.TimeTakenMs);
3939
}
4040

4141
[Fact]
@@ -45,8 +45,8 @@ public void ParseLog_ParsesDateTimeUnderDifferentCulture() {
4545
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR");
4646
var path = Path.Combine(AppContext.BaseDirectory, "TestData", "sample.log");
4747
var engine = new ParserEngine(path);
48-
var evt = engine.ParseLog().Single();
49-
Assert.Equal(new DateTime(2024, 1, 1, 0, 0, 0), evt.DateTimeEvent);
48+
var record = engine.ParseLog().Single();
49+
Assert.Equal(new DateTime(2024, 1, 1, 0, 0, 0), record.Timestamp);
5050
} finally {
5151
CultureInfo.CurrentCulture = originalCulture;
5252
}
@@ -58,34 +58,45 @@ public void ParseLog_YieldsEventsLazily() {
5858
var engine = new ParserEngine(path);
5959
using var enumerator = engine.ParseLog().GetEnumerator();
6060
Assert.True(enumerator.MoveNext());
61-
Assert.Equal("/index0.html", enumerator.Current.csUriStem);
61+
Assert.Equal("/index0.html", enumerator.Current.UriPath);
6262
Assert.Equal(1, engine.CurrentFileRecord);
63-
}
63+
}
6464

6565
[Fact]
6666
public void ParseLog_HandlesShortLogLineGracefully() {
6767
var path = Path.Combine(AppContext.BaseDirectory, "TestData", "short_line.log");
6868
var engine = new ParserEngine(path);
69-
var evt = engine.ParseLog().Single();
70-
Assert.True(evt.Fields.ContainsKey("X-Forwarded-For"));
71-
Assert.Null(evt.Fields["X-Forwarded-For"]);
69+
var record = engine.ParseLog().Single();
70+
Assert.True(record.Fields.ContainsKey("X-Forwarded-For"));
71+
Assert.Null(record.Fields["X-Forwarded-For"]);
7272
}
7373

7474
[Fact]
7575
public void ParseLog_MalformedDateTime_ReturnsMinValue() {
7676
var path = Path.Combine(AppContext.BaseDirectory, "TestData", "malformed_datetime.log");
7777
var engine = new ParserEngine(path);
78-
var evt = engine.ParseLog().Single();
79-
Assert.Equal(DateTime.MinValue, evt.DateTimeEvent);
80-
Assert.Equal("/index.html", evt.csUriStem);
78+
var record = engine.ParseLog().Single();
79+
Assert.Equal(DateTime.MinValue, record.Timestamp);
80+
Assert.Equal("/index.html", record.UriPath);
8181
}
8282

8383
[Fact]
8484
public void ParseLog_MissingDateTime_ReturnsMinValue() {
8585
var path = Path.Combine(AppContext.BaseDirectory, "TestData", "missing_datetime.log");
8686
var engine = new ParserEngine(path);
87-
var evt = engine.ParseLog().Single();
88-
Assert.Equal(DateTime.MinValue, evt.DateTimeEvent);
87+
var record = engine.ParseLog().Single();
88+
Assert.Equal(DateTime.MinValue, record.Timestamp);
89+
Assert.Equal("/index.html", record.UriPath);
90+
}
91+
92+
[Fact]
93+
public void ParseLogLegacy_ReturnsEvents() {
94+
var path = Path.Combine(AppContext.BaseDirectory, "TestData", "sample.log");
95+
var engine = new ParserEngine(path);
96+
var eventsList = engine.ParseLogLegacy().ToList();
97+
Assert.Single(eventsList);
98+
var evt = eventsList[0];
8999
Assert.Equal("/index.html", evt.csUriStem);
100+
Assert.Equal(200, evt.scStatus);
90101
}
91102
}

IISParser/IISLogRecord.cs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace IISParser;
5+
6+
/// <summary>
7+
/// Represents a single entry in an IIS log file with user-friendly property names.
8+
/// </summary>
9+
public class IISLogRecord {
10+
/// <summary>
11+
/// Gets or sets the timestamp of the log entry.
12+
/// </summary>
13+
public DateTime Timestamp { get; set; }
14+
15+
/// <summary>
16+
/// Gets or sets the site name reported by the server (<c>s-sitename</c>).
17+
/// </summary>
18+
public string? SiteName { get; set; }
19+
20+
/// <summary>
21+
/// Gets or sets the computer name for the server (<c>s-computername</c>).
22+
/// </summary>
23+
public string? ComputerName { get; set; }
24+
25+
/// <summary>
26+
/// Gets or sets the server IP address (<c>s-ip</c>).
27+
/// </summary>
28+
public string? ServerIp { get; set; }
29+
30+
/// <summary>
31+
/// Gets or sets the HTTP method used by the request (<c>cs-method</c>).
32+
/// </summary>
33+
public string? HttpMethod { get; set; }
34+
35+
/// <summary>
36+
/// Gets or sets the requested resource path (<c>cs-uri-stem</c>).
37+
/// </summary>
38+
public string? UriPath { get; set; }
39+
40+
/// <summary>
41+
/// Gets or sets the query portion of the requested URI (<c>cs-uri-query</c>).
42+
/// </summary>
43+
public string? UriQuery { get; set; }
44+
45+
/// <summary>
46+
/// Gets or sets the server port (<c>s-port</c>).
47+
/// </summary>
48+
public int? ServerPort { get; set; }
49+
50+
/// <summary>
51+
/// Gets or sets the authenticated user name (<c>cs-username</c>).
52+
/// </summary>
53+
public string? Username { get; set; }
54+
55+
/// <summary>
56+
/// Gets or sets the client IP address (<c>c-ip</c>).
57+
/// </summary>
58+
public string? ClientIp { get; set; }
59+
60+
/// <summary>
61+
/// Gets or sets the protocol version (<c>cs-version</c>).
62+
/// </summary>
63+
public string? HttpVersion { get; set; }
64+
65+
/// <summary>
66+
/// Gets or sets the user agent string (<c>cs(User-Agent)</c>).
67+
/// </summary>
68+
public string? UserAgent { get; set; }
69+
70+
/// <summary>
71+
/// Gets or sets the HTTP cookie value (<c>cs(Cookie)</c>).
72+
/// </summary>
73+
public string? Cookie { get; set; }
74+
75+
/// <summary>
76+
/// Gets or sets the referrer URL (<c>cs(Referer)</c>).
77+
/// </summary>
78+
public string? Referer { get; set; }
79+
80+
/// <summary>
81+
/// Gets or sets the host header value (<c>cs-host</c>).
82+
/// </summary>
83+
public string? Host { get; set; }
84+
85+
/// <summary>
86+
/// Gets or sets the HTTP status code (<c>sc-status</c>).
87+
/// </summary>
88+
public int? StatusCode { get; set; }
89+
90+
/// <summary>
91+
/// Gets or sets the substatus error code (<c>sc-substatus</c>).
92+
/// </summary>
93+
public int? SubStatusCode { get; set; }
94+
95+
/// <summary>
96+
/// Gets or sets the Windows status code (<c>sc-win32-status</c>).
97+
/// </summary>
98+
public long? Win32Status { get; set; }
99+
100+
/// <summary>
101+
/// Gets or sets the number of bytes sent (<c>sc-bytes</c>).
102+
/// </summary>
103+
public long? BytesSent { get; set; }
104+
105+
/// <summary>
106+
/// Gets or sets the number of bytes received (<c>cs-bytes</c>).
107+
/// </summary>
108+
public long? BytesReceived { get; set; }
109+
110+
/// <summary>
111+
/// Gets or sets the time taken to service the request, in milliseconds (<c>time-taken</c>).
112+
/// </summary>
113+
public long? TimeTakenMs { get; set; }
114+
115+
/// <summary>
116+
/// Gets a dictionary containing all fields from the log line keyed by their original names.
117+
/// </summary>
118+
public Dictionary<string, string?> Fields { get; } = new(StringComparer.OrdinalIgnoreCase);
119+
}
120+

0 commit comments

Comments
 (0)