The Microsoft IIS pack in the Logit.io Dashboard Library installs one OpenSearch Dashboards view, Logit.io IIS web activity, that reads two Windows logs at once: the W3C access log each IIS site writes and the HTTPERR log that HTTP.sys writes for requests it rejects before IIS ever sees them. It fills when Filebeat ships both through its iis module to your Logs stack, which parses each line into status codes, IIS sub-status codes, client addresses and HTTP.sys reason phrases. The one setup choice that decides whether it works is which W3C fields you log: the defaults and the full standard set parse, but adding just the byte counts makes every line fail to index.
This guide is for teams running ASP.NET, ASP.NET Core, classic ASP or static sites on Windows Server who want to see the 500.19 after a config change, the 503s from a stopped application pool and the clients HTTP.sys threw away, without logging on to each server and opening u_ex*.log in Notepad. It covers how the two logs differ, which W3C field sets the parser reads, how to ship both with Filebeat on Windows, how to record the real client behind a load balancer, how to prove the whole path with two requests, how to read sub-status and Win32 codes, how to turn the signals into alerts and what to check when the panels look wrong.
Contents
- What the Microsoft IIS pack shows
- Two logs from two layers of the same server
- Choose W3C fields the parser can read
- Ship both logs with Filebeat on Windows
- Check both logs end to end in five minutes
- Install the pack and read it top down
- Read the sub-status and Win32 codes
- Read the HTTP.sys reasons in HTTPERR
- Alert on 5xx share and turned-away requests
- When the IIS panels look wrong
- Limits and trade-offs
- FAQ
- Next steps
What the Microsoft IIS pack shows
The pack is listed as Microsoft IIS in the Dashboard Library and installs a single dashboard, Logit.io IIS web activity. It reads the filebeat-* index pattern with @timestamp as the time field, opens on the last 24 hours and refreshes every 60 seconds. Every panel starts from the same Lucene base query:
event.module:iis AND agent.type:filebeat AND event.dataset:(iis.access OR iis.error)
The access panels narrow that to event.dataset:iis.access, and the HTTP API error panels narrow it to event.dataset:iis.error. The eleven panels are laid out top to bottom in the order you'd use them:
| Panel | What it counts | Field it reads |
|---|---|---|
| HTTP requests | Access log records | event.dataset |
| Server errors | Access log records with a 500 to 599 status | http.response.status_code |
| HTTP API error entries | HTTPERR records | event.dataset |
| Last event received | Newest IIS event in the range | @timestamp |
| Web log activity over time | Access and HTTPERR records, stacked | event.dataset |
| Requests by response status | Top five exact status codes | http.response.status_code |
| Requests by method | Top five HTTP methods | http.request.method |
| HTTP API errors by reason | Top five HTTP.sys reason phrases | iis.error.reason_phrase |
| Activity by log dataset | Access versus HTTPERR share | event.dataset |
| Requests to investigate | Latest 4xx and 5xx requests | Time, method, status, user, client and host |
| HTTP API error detail | Latest HTTPERR records | Time, method, status, reason, client and host |
Two things are deliberately missing. There's no response time panel, because only the access log has a duration and mixing it with HTTPERR records would mislead. There's no map either, because IIS servers behind a load balancer log private addresses, which can't be geolocated. You can add both to a copy of the dashboard once your data supports them, and this guide shows the queries.
Two logs from two layers of the same server
IIS doesn't accept connections itself. HTTP.sys, the kernel-mode HTTP listener in Windows, takes every connection first, checks that the request is well formed, works out which site and application pool it belongs to and queues it for the worker process. Only then does IIS run the request and write a line to the site's W3C log. If HTTP.sys refuses the request, or the application pool can't take it, IIS never hears about it and the W3C log has no line. HTTP.sys writes it to its own log instead:
| W3C access log | HTTPERR log | |
|---|---|---|
| Written by | IIS, one folder per site | HTTP.sys, one folder per server |
| Default location | C:\inetpub\logs\LogFiles\W3SVC<site-id>\ | C:\Windows\System32\LogFiles\HTTPERR\ |
| File names | u_ex<yymmdd>.log | httperr<n>.log |
| Filebeat dataset | iis.access | iis.error |
| Typical content | Every request IIS handled, including its 4xx and 5xx | Malformed requests, bad Host headers, stopped or overloaded app pools, idle and slow connections |
| Time zone | UTC | UTC |
This is why the pack has two error signals. Server errors counts 5xx responses that IIS sent, usually your application failing. HTTP API error entries counts what HTTP.sys rejected or dropped, which includes the worst outage of all: an application pool that has stopped, where every user gets a 503 and the W3C log goes quiet. If you only ship the access log, that outage looks like a sudden drop in traffic.
Choose W3C fields the parser can read
When Filebeat's iis module ships a line, your Logs stack's Logstash sends it to an IIS access or IIS error ingest pipeline before it's stored. The access pipeline uses fixed patterns that expect W3C fields in IIS's own column order. IIS lets you tick any combination of fields in IIS Manager, and not every combination fits a pattern.
We ran lines built from each common field selection through the same IIS access pipeline definition and Beats index template that current Logit.io Logs stacks use, on a local OpenSearch node. The results fall into three groups, and one of them is a trap:
| Fields selected in IIS Manager | Result |
|---|---|
| The IIS 8.5 and later default set (15 fields, shown below) | Fully parsed: time, server address, method, path, query, port, user, client, user agent, referrer, status, sub-status, Win32 status and duration |
The IIS 7 default set (no cs(Referer)) | Fully parsed, without the referrer |
Either default set plus an X-Forwarded-For custom field | Fully parsed, and the first forwarded address lands in network.forwarded_ip |
| All 22 standard fields | Fully parsed, plus site, server name, host header, HTTP version and request and response bytes |
| All standard fields except Server Name, Server IP and Protocol Version | Fully parsed, as above without those three |
| Default set plus Host, Site Name or Protocol Version | Stored, but not parsed: the document has error.message and no HTTP fields |
| Default set without Time Taken, or all fields except Cookie | Stored, but not parsed |
| Default set plus Bytes Sent, with or without Bytes Received | Rejected: the line is never stored |
The last row needs explaining, because it fails silently. IIS writes sc-bytes and cs-bytes just before time-taken. The default pattern still matches, but it reads the response size as the duration and the next number as an optional forwarded client address. That field is mapped as an IP address, a number such as 412 isn't one, and OpenSearch refuses the whole document with a mapper_parsing_exception. Logstash then drops it, or writes it to the dead letter queue if you've enabled one. Your server is logging, Filebeat is shipping and nothing appears.
So pick one of two configurations and keep every site on the same one:
- Keep the default fields. Simplest, and enough for every panel in the pack. Add
X-Forwarded-Foras a custom field if you're behind a load balancer. - Tick all 22 standard fields. Choose this if you want response sizes, the site and host header per request. Again, add
X-Forwarded-Forif you need it. Note that Cookie is part of the set and can contain session identifiers, so think about who can readfilebeat-*before you turn it on.
The fastest way to see what a server is actually writing is the #Fields header IIS puts at the top of every log file. Run this in PowerShell on the web server:
Get-ChildItem C:\inetpub\logs\LogFiles\W3SVC*\*.log |
Sort-Object LastWriteTime | Select-Object -Last 1 |
Get-Content -TotalCount 4
On a default IIS 10 server the fourth line is:
#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken
With all 22 standard fields it reads:
#Fields: date time s-sitename s-computername s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs-version cs(User-Agent) cs(Cookie) cs(Referer) cs-host sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken
Anything else, check against the table before you rely on it. The format must also be W3C. The older IIS and NCSA formats and custom log providers don't match any pattern, and a site set to log to ETW events only writes no file at all. HTTPERR has a fixed format you can't change, and the error pipeline reads both the older layout and the newer one that adds a streamid column, so there's nothing to choose there.
Ship both logs with Filebeat on Windows
Find every site's log folder
The module's default access path is C:/inetpub/logs/LogFiles/*/*.log, which covers every site that logs to the default folder, one W3SVC<site-id> subfolder each. Many teams move logs to a data drive, and some move one site but not others. List where each site really logs, and in what format:
Import-Module WebAdministration
Get-ChildItem IIS:\Sites | Select-Object name, id,
@{n='logFolder'; e={[Environment]::ExpandEnvironmentVariables($_.logFile.directory)}},
@{n='format'; e={$_.logFile.logFormat}}
Every format should say W3C. Note the id as well: the folder for site 2 is W3SVC2, and that's also what IIS logs as the site name if you choose all fields, so it's worth keeping a note of which number is which site.
Configure the iis module
Start with the IIS integration guide in your Logit.io account. Choose Install Integration first, which prepares your stack for this source, then install Filebeat for Windows as the guide shows. From the Filebeat folder in an elevated PowerShell prompt (C:\Program Files\filebeat if you used the zip, or the folder the installer opened), enable the module:
cd 'C:\Program Files\filebeat'
.\filebeat.exe modules enable iis
Then edit modules.d\iis.yml. Both filesets are disabled by default, so you must enable them. Listing the paths explicitly makes the file obvious to the next person, and lets you add any site that logs elsewhere. Use forward slashes:
- module: iis
access:
enabled: true
var.paths:
- C:/inetpub/logs/LogFiles/*/*.log
- D:/IISLogs/*/*.log # any site you moved, from the list above
error:
enabled: true
var.paths:
- C:/Windows/System32/LogFiles/HTTPERR/*.log
Folders that don't exist are skipped, and the module already ignores the # header lines and .gz files. Set the output in filebeat.yml. Copy the block from the integration guide while signed in, because it fills in your stack's Logstash host and Beats SSL port:
filebeat.config.modules:
path: ${path.config}/modules.d/*.yml
reload.enabled: false
output.logstash:
hosts: ["your-logstash-host:your-beats-ssl-port"]
loadbalance: true
ssl.enabled: true
Test and start the service:
.\filebeat.exe test config
.\filebeat.exe test output
Start-Service filebeat
Get-Service filebeat
test output should end with talk to server... OK. The Filebeat service runs as Local System, which can read both log folders. Beats data sent through your stack's Logstash lands in daily filebeat-* indices, which is what the pack reads. If you have renamed the index in your Logs stack, the pack won't find your data.
Use the module rather than a plain filestream input on the same folders. The module sets event.module:iis and tells your stack which ingest pipeline to use. A plain input sends raw lines that are never parsed into HTTP fields, and the pack never sees them.
Record the real client with X-Forwarded-For
Behind Azure Application Gateway, an AWS load balancer, a CDN or Application Request Routing, c-ip is the address of the device in front of IIS. Every request then seems to come from a handful of private addresses, and the client column in Requests to investigate is no help.
IIS 8.5 and later can log a request header as a custom field. In IIS Manager, open the server or site, choose Logging, then Select Fields, then Add Field and set Field Name to X-Forwarded-For, Source Type to Request Header and Source to X-Forwarded-For. Or set it for every site at once:
Add-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' `
-Filter 'system.applicationHost/sites/siteDefaults/logFile/customFields' `
-Name '.' -Value @{logFieldName='X-Forwarded-For'; sourceName='X-Forwarded-For'; sourceType='RequestHeader'}
IIS appends custom fields after the standard ones, which is exactly where the access pipeline looks. The first address in the header goes into network.forwarded_ip. Two caveats. source.ip still holds the load balancer's address, because that's what c-ip says, so the pack's tables keep showing it: add network.forwarded_ip as a column in a copy of the dashboard. And the header is only trustworthy if your load balancer sets or appends it itself. A client can send its own X-Forwarded-For, so don't use the value for access decisions.
Check both logs end to end in five minutes
You don't need to wait for real traffic or a real outage. Two requests from the web server itself prove both halves, one per log:
curl.exe -s -o NUL -w "%{http_code}`n" "http://localhost/logit-iis-test-$(Get-Date -Format yyyyMMdd)"
curl.exe -s -o NUL -w "%{http_code}`n" -H "Host: bad host" "http://localhost/"
netsh http flush logbuffer
The first should print 404 and write a W3C line for a path nothing real uses. If your default site only binds HTTPS, use https://localhost/ and add -k. The second should print 400. A space isn't allowed in a host name, so HTTP.sys rejects the request before IIS sees it and writes an HTTPERR line with the reason Hostname. IIS and HTTP.sys buffer their log writes, so netsh http flush logbuffer pushes both lines to disk now instead of in a minute or so.
Open Discover in OpenSearch Dashboards, choose filebeat-*, set the query language to Lucene and the time picker to the last 15 minutes, and search:
event.dataset:iis.access AND url.original:\/logit-iis-test-*
Open the document and check that http.response.status_code is 404, iis.access.sub_status is 0, iis.access.win32_status is 2 (file not found) if IIS served the 404 itself or 0 if your application did, event.outcome is failure and source.ip is ::1 or 127.0.0.1. The path is in url.original, exactly as IIS logged it in cs-uri-stem. Then the HTTPERR half:
event.dataset:iis.error AND iis.error.reason_phrase:Hostname
That document should have http.response.status_code 400 and destination.port 80, plus the client's source.ip and source.port. If either document has error.message and no HTTP fields, it arrived but didn't parse: compare the server's #Fields line with the table above. If the 404 never arrives at all and Filebeat's log shows no errors, look for the Bytes Sent trap first.
Next, confirm the field types the panels need, using the OpenSearch API on your stack. The endpoint and stack API key are on the Endpoints page in your stack settings, and the guide to connecting to your stack's OpenSearch API covers both authentication modes. In API key mode:
curl -s "https://<stack-id>-es.logit.io/filebeat-*/_field_caps?fields=event.dataset,http.response.status_code,http.request.method,iis.access.sub_status,iis.access.win32_status,iis.error.reason_phrase,url.original,source.ip,network.forwarded_ip,event.duration&apikey=<stack-api-key>&pretty"
You want http.response.status_code, both IIS status fields and event.duration as long, source.ip and network.forwarded_ip as ip and the rest as keyword, all aggregatable. On filebeat-*, string fields are mapped as keyword directly, so you group on iis.error.reason_phrase, not iis.error.reason_phrase.keyword as you would on logstash-*. Index templates and mappings on Logit.io stacks are managed for you (the index templates guide shows how to read them). The default Filebeat template is applied when an index is created, so if a type looks wrong, fix it in the pipeline or the shipper rather than editing the live mapping; existing daily indices keep their first type until they age out.
Finally, count what the Library will see. Detection looks for recent documents in filebeat-* with event.module of iis, and needs event.dataset and http.response.status_code to be aggregatable. Save this as iis-detect.json:
{
"size": 0,
"query": { "bool": { "filter": [
{ "range": { "@timestamp": { "gte": "now-24h" } } },
{ "term": { "event.module": "iis" } }
] } },
"aggs": {
"datasets": { "terms": { "field": "event.dataset" } },
"hosts": { "terms": { "field": "host.name", "size": 50 } },
"unparsed": { "filter": { "exists": { "field": "error.message" } } }
}
}
Then run it from any machine with curl, including Windows, where it's curl.exe:
curl -s -H "Content-Type: application/json" --data-binary @iis-detect.json \
"https://<stack-id>-es.logit.io/filebeat-*/_search?apikey=<stack-api-key>&pretty"
Expect an iis.access bucket, an iis.error bucket once anything has been rejected and one hosts bucket per web server. If a server you expect is missing from hosts, its Filebeat isn't shipping. If unparsed is more than a handful, one server or site is using a field set the parser doesn't know.
Install the pack and read it top down
Open your Logs stack's card in your Logit.io account and choose Dashboard Library. Search for Microsoft IIS. Once matching events have arrived, the pack shows a Data detected badge. If you've only just started shipping, press Rescan stack. Tick the IIS web activity dashboard, choose Install dashboards and open it in OpenSearch Dashboards a minute or two later. The Dashboard Library guide covers the permissions you need and what gets installed, and the post on how Dashboard Library detection works explains the badge in more depth.
Read the dashboard in this order:
- Last event received. On a site with steady traffic, a timestamp more than a few minutes old means collection has stopped, not that your users went home. Remember the minute or so of log buffering before you worry about a gap.
- Server errors next to HTTP requests. Any 5xx at all deserves a look, but the trend matters more than the total.
- HTTP API error entries. A steady trickle is normal on a public site. A jump, especially with a fall in HTTP requests at the same time, is the stopped-pool pattern described below.
- Web log activity over time. The stacked chart shows whether the access and HTTPERR series move together. When HTTPERR rises as access falls, requests are being turned away before IIS.
- The three donuts. Status codes, methods and HTTP.sys reasons. A new code or reason in the top five is often the first sign of a change.
- The two tables. Requests to investigate lists the latest 4xx and 5xx requests with method, status, user, client and server. HTTP API error detail does the same for HTTPERR records, which don't carry a user name.
The table of requests doesn't include the path or the sub-status. Most teams make a copy of the dashboard straight away and add url.original, iis.access.sub_status and, behind a load balancer, network.forwarded_ip as columns. Save the copy under a new name, so reinstalling the pack later doesn't overwrite it.
Read the sub-status and Win32 codes
IIS logs three numbers for every request: the HTTP status (sc-status), an IIS sub-status (sc-substatus) and a Windows error code (sc-win32-status). The pipeline stores them as http.response.status_code, iis.access.sub_status and iis.access.win32_status. The status tells you what the client saw. The other two tell you why, which is what saves you a trip to the server. These are the combinations worth recognising:
| Status.sub-status | Meaning | Usual cause |
|---|---|---|
401.2 | Logon failed due to server configuration | Usually harmless: the first, anonymous leg of Windows authentication. Only worth a look if it isn't followed by a 200 from the same client |
401.1 | Logon failed | Wrong credentials. Many from one client is a password guessing pattern |
403.14 | Directory listing denied | No default document, or a request for a folder rather than a page |
404.0 | Not found | Missing file or route, or scanning |
404.3 | MIME map policy | A static file type with no MIME mapping, such as a new font or .webmanifest |
404.7 or 404.8 | Request filtering: file extension or hidden segment denied | Probes for .config, bin or App_Data. Expected on any public site |
404.13 or 404.15 | Request filtering: content too long or query string too long | Uploads above the limit, or bloated query strings from a client bug |
500.19 | Configuration data is invalid | A broken web.config or a missing IIS module, often straight after a deployment |
500.30 | ASP.NET Core app failed to start (in-process) | The app threw at start-up. The Application event log has the exception |
502.3 | Bad gateway from a forwarding module | The ASP.NET Core module or ARR timed out or couldn't reach its backend |
502.5 | ASP.NET Core process failure (out-of-process), or ARR WebSocket failure | For ASP.NET Core out-of-process hosting, the app process wouldn't start or failed to listen; on ARR, this sub-status is a WebSocket failure |
503.2 | Concurrent request limit exceeded | The server's appConcurrentRequestLimit was reached |
The Win32 status adds detail that HTTP never would. 0 is success. 2 is file not found and 5 is access denied, usually a file permission problem behind a 401.3 or 500. 64 and 1236 mean the connection went away before IIS finished sending, so a large time-taken on those lines is a slow or departed client, not a slow server. Filter them out before you look at response times.
The pack's status donut shows only the HTTP code, so this query gives you the full breakdown of the last 24 hours of errors. Save it as iis-codes.json and send it the same way as the detection query:
{
"size": 0,
"query": { "bool": { "filter": [
{ "range": { "@timestamp": { "gte": "now-24h" } } },
{ "term": { "event.dataset": "iis.access" } },
{ "range": { "http.response.status_code": { "gte": 400 } } }
] } },
"aggs": {
"status": {
"terms": { "field": "http.response.status_code", "size": 10 },
"aggs": {
"sub_status": { "terms": { "field": "iis.access.sub_status", "size": 5 } },
"win32": { "terms": { "field": "iis.access.win32_status", "size": 5 } },
"top_paths": { "terms": { "field": "url.original", "size": 5 } }
}
}
}
}
In Discover, the same idea is a Lucene query. This finds configuration errors on any server, newest first:
event.dataset:iis.access AND http.response.status_code:500 AND iis.access.sub_status:19
And for response times, the access pipeline converts time-taken from milliseconds to nanoseconds in event.duration, so 1 second is 1000000000. This ranks paths by their 95th percentile over the last hour, leaving out aborted connections:
{
"size": 0,
"query": { "bool": {
"filter": [
{ "range": { "@timestamp": { "gte": "now-1h" } } },
{ "term": { "event.dataset": "iis.access" } }
],
"must_not": [ { "terms": { "iis.access.win32_status": [64, 1236] } } ]
} },
"aggs": {
"paths": {
"terms": { "field": "url.original", "size": 10, "order": { "latency.95": "desc" } },
"aggs": {
"latency": { "percentiles": { "field": "event.duration", "percents": [50, 95] } }
}
}
}
}
The latency.95 order sorts the paths by their own 95th percentile, slowest first. Divide the values by 1,000,000 for milliseconds. time-taken runs until the last byte is sent, so for large downloads it includes the client's network.
Read the HTTP.sys reasons in HTTPERR
Every HTTPERR line has a reason phrase, stored as iis.error.reason_phrase, and for queued requests an application pool name in iis.error.queue_name. The HTTP API errors by reason donut shows the top five. These are the ones you'll actually see:
| Reason | What happened | Worry? |
|---|---|---|
Timer_ConnectionIdle | A kept-alive connection sat idle until HTTP.sys closed it | No. This is usually most of HTTPERR on a busy site |
Timer_MinBytesPerSecond | The client was not receiving the response fast enough (default 150 bytes/sec) | Rarely. Lots of them from a few addresses can be a slow-request attack |
Hostname, BadRequest, Version_N/S | Malformed request, invalid Host header or unsupported HTTP version | Usually scanners. Worth a look if one of your own clients appears |
Connection_Abandoned_By_AppPool | A worker process quit or crashed with requests in flight | Yes. Users got errors. Check the Application and System event logs for the crash |
AppOffline | 503: the application was taken offline, usually by rapid-fail protection after repeated crashes. A manual stop is more often logged as Disabled | Yes. The site is down for everyone in that pool |
QueueFull | 503: the application pool's request queue is full | Yes. The app is too slow or too busy to keep up |
AppPoolTimer | 503: the worker process was too busy to take the request | Yes, same family as QueueFull |
ConnLimit | 503: the site's connection limit was reached | Yes, unless someone set a deliberately low limit |
Because the idle and slow timers dominate on most servers, the donut's top five can hide the important reasons. Pin a negative filter on the dashboard copy, or search for the reasons that mean users were turned away:
event.dataset:iis.error AND iis.error.reason_phrase:(AppOffline OR QueueFull OR AppPoolTimer OR ConnLimit OR Connection_Abandoned_By_AppPool)
The iis.error.queue_name column then tells you which application pool to look at, and host.name which server. That's the stopped-pool pattern: HTTP requests falls for one server, HTTPERR rises and the reasons are AppOffline with your pool's name.
Alert on 5xx share and turned-away requests
Logs stacks include alerting with ElastAlert 2 rules, which you enable from Alerting & Notifications in your stack settings. The guide to creating alerting rules walks through it. Three rules cover most IIS estates. First, the share of requests that failed, using the percentage match rule type, so a busy morning doesn't page anyone just for being busy:
name: IIS 5xx share above 5%
type: percentage_match
index: "filebeat-*"
buffer_time:
minutes: 15
filter:
- query:
query_string:
query: "event.module:iis AND event.dataset:iis.access"
match_bucket_filter:
- range:
http.response.status_code:
gte: 500
max_percentage: 5
min_denominator: 200
query_key: host.name
realert:
minutes: 30
alert:
- "email"
email:
- "[email protected]"
That fires per server when more than 5% of the last 15 minutes of requests were 5xx, as long as there were at least 200 requests. Second, any request turned away because an application pool was stopped, crashed or full. A frequency rule with a low threshold suits this, because these should be rare:
name: IIS app pool turning requests away
type: frequency
index: "filebeat-*"
num_events: 5
timeframe:
minutes: 5
query_key: host.name
filter:
- query:
query_string:
query: "event.dataset:iis.error AND iis.error.reason_phrase:(AppOffline OR QueueFull OR AppPoolTimer OR Connection_Abandoned_By_AppPool)"
alert:
- "email"
email:
- "[email protected]"
Third, a flatline rule on the access log per server. It catches a stopped Filebeat service and a moved log folder, as well as a site that has stopped serving:
name: IIS access log has gone quiet
type: flatline
index: "filebeat-*"
threshold: 1
timeframe:
minutes: 15
query_key: host.name
filter:
- query:
query_string:
query: "event.module:iis AND event.dataset:iis.access"
alert:
- "email"
email:
- "[email protected]"
Raise the threshold to suit the traffic, and leave out servers that are quiet overnight or use a longer window for them. Run each rule with Test only before enabling it, to see how often it would have fired over recent data. A frequency rule on iis.access.sub_status:19 is a cheap extra: one 500.19 is enough to know a configuration change went wrong.
When the IIS panels look wrong
- The pack isn't detected. No document in the last 24 hours of
filebeat-*hasevent.module:iis. Either the filesets are still disabled iniis.yml, Filebeat uses a plain input instead of the module, the logs are in a folder the paths don't cover or the index has been renamed. The detection query above tells you which. - Filebeat is running and nothing arrives. Check the
#Fieldsline forsc-bytes. If it's there without the full standard set, every line is being rejected at indexing. Turn on the Logstash dead letter queue and look indlq-*: thedead_letter_queue_reasonwill mentionnetwork.forwarded_ip. Then change the field selection. New lines index straight away. - HTTP requests is higher than the status donut adds up to. Some lines are stored but unparsed. Search
event.dataset:iis.access AND error.message:*and look atevent.original, then compare that server's#Fieldsline with the table above. One site with Host or Site Name ticked is the usual cause. - The HTTP API error panels are empty. The
errorfileset is disabled, or the server is genuinely quiet. Run the bad Host header test to prove the path. - Every client is the same few addresses. You're logging the load balancer. Add the
X-Forwarded-Forcustom field and usenetwork.forwarded_ip. - Times look an hour out. IIS writes UTC in the W3C
dateandtimefields regardless of the server's zone, and the pipeline reads them as UTC. OpenSearch Dashboards then shows them in your browser's zone. The setting called Use local time for file naming and rollover changes only file names, not the times inside. - A
u_exfile from yesterday is still growing. That's the local time rollover setting at work on a server far from UTC. It's harmless for the dashboards. url.pathis empty in Discover. On current stacks the IIS access path is stored inurl.original. Search and group on that.- Health checks dominate the request counts. Load balancer probes every few seconds can outnumber real users. Pin a negative filter such as
NOT url.original:"/health"on your copy, rather than turning logging off for the probe. - Last event received is empty. No IIS events in the selected range. On a site with traffic, check
Get-Service filebeatand Filebeat's own log in itslogsfolder for TLS or permission errors.
Limits and trade-offs
- The parser is fixed. Ingest pipelines on Logit.io Logs stacks are managed, so you can't add a pattern for your own field order. Changing the W3C field selection to one of the supported sets is the fix, and it takes effect on the next line written.
- Some HTTP.sys detail is missing. HTTPERR records don't carry a user, a duration or a log level, and lines from idle timeouts have no method, path or status. That's what HTTP.sys writes, not something the pipeline drops.
- No geolocation by default. The pipeline tries to geolocate
source.ip, but behind a load balancer that's a private address with no location.network.forwarded_ipisn't geolocated, which is why the pack has no map. - Response time is approximate.
time-takenincludes sending the response, so slow clients and large downloads inflate it. For server-side latency per endpoint, application traces are more precise. - Logs only arrive when IIS flushes them. Expect up to about a minute of delay from IIS log buffering on top of shipping, so alert windows shorter than five minutes will be noisy.
- Access logs contain personal data. Client addresses, Windows user names, query strings and, with all fields, cookies. Check who can view
filebeat-*on your stack, keep retention to what you need and don't put secrets in query strings. - Other ways of shipping IIS logs won't fill the pack. Logs sent by NXLog, Winlogbeat or the OpenTelemetry Collector reach your stack, but without
event.module:iisand the IIS pipelines. If you collect Windows event logs too, the guide to Windows event logs on Logit.io covers that side.
Unlock complete visibility with hosted ELK, Grafana, and Prometheus-backed Observability
FAQ
Which IIS versions does the pack work with?
Any IIS that writes W3C logs with a supported field set. IIS 8.5 and 10, on Windows Server 2012 R2 to 2025, are the usual case and support the X-Forwarded-For custom field. IIS 7 and 7.5 logs parse with their own default field set, which doesn't include the referrer, but those versions can't log custom fields.
We host many sites on one server. Can I see them separately?
Yes, two ways. With the default fields, each site logs to its own W3SVC<id> folder and the path is in log.file.path, so a filter such as log.file.path:*W3SVC2* picks one site. With all 22 standard fields, iis.access.site_name carries the same W3SVC<id> value and url.domain carries the host header, which is easier to read.
Does it work for Azure App Service or IIS in containers?
Not directly. The pack expects Filebeat's iis module reading log files on a Windows host. App Service and Windows containers write IIS logs in other places and usually ship them another way, so those records won't carry event.module:iis. For IIS on Windows virtual machines in Azure or AWS, the setup in this guide applies unchanged.
We also run Apache or Nginx. Do the packs overlap?
No. Each pack counts only its own module's events, so the numbers never double up. The Apache access and error log dashboards and the Nginx pack follow the same pattern for Linux web servers, and you can install all of them on one stack.
Can I add panels or change the dashboard?
Yes. The installed dashboard is an ordinary OpenSearch Dashboards saved object. Save a copy under a new name before you edit it. Common first changes are a sub-status donut, url.original and network.forwarded_ip columns in the requests table and a negative filter for health checks and idle timeouts.
Can I try it on the free trial?
Yes. The 14-day free trial includes every feature, including the Dashboard Library, alerting and Hosted OpenSearch MCP. Point Filebeat at one IIS server, run the two test requests and install the pack.
Next steps
Check the #Fields line on every web server and settle on either the default fields or all 22 standard fields, plus X-Forwarded-For if you're behind a load balancer. Enable both filesets in the Filebeat iis module, run the 404 and bad Host header tests and find both in Discover. Then install the Microsoft IIS pack, make a copy with the path and sub-status columns and add the 5xx share and app pool alerts. If you're running your own OpenSearch cluster for Windows web logs, a Logit.io Logs stack gives you managed Logstash, OpenSearch and OpenSearch Dashboards with the Dashboard Library built in. For a dedicated cluster, see Hosted OpenSearch and OpenSearch pricing. To ask questions of the same IIS logs from any MCP-compatible AI assistant, such as which application pool turned requests away during last night's deployment, see Hosted OpenSearch MCP.
