apps/iis.go (210 lines of code) (raw):
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package apps
import (
"context"
"fmt"
"github.com/GoogleCloudPlatform/ops-agent/confgenerator"
"github.com/GoogleCloudPlatform/ops-agent/confgenerator/fluentbit"
"github.com/GoogleCloudPlatform/ops-agent/confgenerator/otel"
"github.com/GoogleCloudPlatform/ops-agent/internal/platform"
)
type MetricsReceiverIis struct {
confgenerator.ConfigComponent `yaml:",inline"`
confgenerator.MetricsReceiverShared `yaml:",inline"`
confgenerator.VersionedReceivers `yaml:",inline"`
}
func (r MetricsReceiverIis) Type() string {
return "iis"
}
func (r MetricsReceiverIis) Pipelines(_ context.Context) ([]otel.ReceiverPipeline, error) {
if r.ReceiverVersion == "2" {
return []otel.ReceiverPipeline{{
Receiver: otel.Component{
Type: "iis",
Config: map[string]interface{}{
"collection_interval": r.CollectionIntervalString(),
},
},
Processors: map[string][]otel.Component{"metrics": {
otel.TransformationMetrics(
otel.FlattenResourceAttribute("iis.site", "site"),
otel.FlattenResourceAttribute("iis.application_pool", "app_pool"),
),
// Drop all resource keys; Must be done in a separate transform,
// otherwise the above flatten resource attribute queries will only
// work for the first datapoint
otel.TransformationMetrics(
otel.RetainResource(),
),
otel.CondenseResourceMetrics(),
otel.MetricsTransform(
otel.UpdateMetricRegexp("^iis",
otel.AggregateLabels(
"sum",
"direction",
"request",
),
),
otel.AddPrefix("workload.googleapis.com"),
),
otel.NormalizeSums(),
otel.ModifyInstrumentationScope(r.Type(), "2.0"),
}},
}}, nil
}
// Return version 1 if version is anything other than 2
return []otel.ReceiverPipeline{{
Receiver: otel.Component{
Type: "windowsperfcounters",
Config: map[string]interface{}{
"collection_interval": r.CollectionIntervalString(),
"perfcounters": []map[string]interface{}{
{
"object": "Web Service",
"instances": []string{"_Total"},
"counters": []map[string]string{
{"name": "Current Connections"},
{"name": "Total Bytes Received"},
{"name": "Total Bytes Sent"},
{"name": "Total Connection Attempts (all instances)"},
{"name": "Total Delete Requests"},
{"name": "Total Get Requests"},
{"name": "Total Head Requests"},
{"name": "Total Options Requests"},
{"name": "Total Post Requests"},
{"name": "Total Put Requests"},
{"name": "Total Trace Requests"},
},
},
},
},
},
ExporterTypes: map[string]otel.ExporterType{
"metrics": otel.System,
},
Processors: map[string][]otel.Component{"metrics": {
otel.MetricsTransform(
otel.RenameMetric(
`\Web Service(_Total)\Current Connections`,
"iis/current_connections",
),
// $ needs to be escaped because reasons.
// https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/metricstransformprocessor#rename-multiple-metrics-using-substitution
otel.CombineMetrics(
`^\\Web Service\(_Total\)\\Total Bytes (?P<direction>.*)$$`,
"iis/network/transferred_bytes_count",
// change data type from double -> int64
otel.ToggleScalarDataType,
),
otel.RenameMetric(
`\Web Service(_Total)\Total Connection Attempts (all instances)`,
"iis/new_connection_count",
// change data type from double -> int64
otel.ToggleScalarDataType,
),
otel.CombineMetrics(
`^\\Web Service\(_Total\)\\Total (?P<http_method>.*) Requests$$`,
"iis/request_count",
// change data type from double -> int64
otel.ToggleScalarDataType,
),
otel.AddPrefix("agent.googleapis.com"),
),
otel.CastToSum(
"agent.googleapis.com/iis/network/transferred_bytes_count",
"agent.googleapis.com/iis/new_connection_count",
"agent.googleapis.com/iis/request_count",
),
otel.NormalizeSums(),
otel.ModifyInstrumentationScope(r.Type(), "1.0"),
}},
}}, nil
}
func init() {
confgenerator.MetricsReceiverTypes.RegisterType(func() confgenerator.MetricsReceiver { return &MetricsReceiverIis{} }, platform.Windows)
}
type LoggingProcessorIisAccess struct {
confgenerator.ConfigComponent `yaml:",inline"`
}
func (*LoggingProcessorIisAccess) Type() string {
return "iis_access"
}
const (
iisMergeRecordFieldsLuaFunction = `iis_merge_fields`
iisMergeRecordFieldsLuaScriptContents = `
function iis_merge_fields(tag, timestamp, record)
if (record["cs_uri_query"] == "-") then
record["cs_uri_query"] = nil
end
if (record["http_request_referer"] == "-") then
record["http_request_referer"] = nil
end
if (record["user"] == "-") then
record["user"] = nil
end
record["http_request_serverIp"] = table.concat({record["http_request_serverIp"], ":", record["s_port"]})
if (record["cs_uri_query"] == nil or record["cs_uri_query"] == '') then
record["http_request_requestUrl"] = record["cs_uri_stem"]
else
record["http_request_requestUrl"] = table.concat({record["cs_uri_stem"], "?", record["cs_uri_query"]})
end
record["cs_uri_query"] = nil
record["cs_uri_stem"] = nil
record["s_port"] = nil
return 2, timestamp, record
end
`
)
func (p *LoggingProcessorIisAccess) Components(ctx context.Context, tag, uid string) []fluentbit.Component {
c := confgenerator.LoggingProcessorParseRegex{
// Documentation:
// https://docs.microsoft.com/en-us/windows/win32/http/w3c-logging
// sample line: 2022-03-10 17:26:30 ::1 GET /iisstart.png - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+WOW64;+Trident/7.0;+rv:11.0)+like+Gecko http://localhost/ 200 0 0 18
// sample line: 2022-03-10 17:26:30 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+WOW64;+Trident/7.0;+rv:11.0)+like+Gecko - 200 0 0 352
// sample line: 2022-03-10 17:26:32 ::1 GET /favicon.ico - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+WOW64;+Trident/7.0;+rv:11.0)+like+Gecko - 404 0 2 49
Regex: `^(?<timestamp>\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})\s(?<http_request_serverIp>[^\s]+)\s(?<http_request_requestMethod>[^\s]+)\s(?<cs_uri_stem>\/[^\s]*)\s(?<cs_uri_query>[^\s]*)\s(?<s_port>\d*)\s(?<user>[^\s]+)\s(?<http_request_remoteIp>[^\s]+)\s(?<http_request_userAgent>[^\s]+)\s(?<http_request_referer>[^\s]+)\s(?<http_request_status>\d{3})\s(?<sc_substatus>\d+)\s(?<sc_win32_status>\d+)\s(?<time_taken>\d+)$`,
ParserShared: confgenerator.ParserShared{
TimeKey: "timestamp",
TimeFormat: " %Y-%m-%d %H:%M:%S",
Types: map[string]string{
"http_request_status": "integer",
},
},
}.Components(ctx, tag, uid)
// iis logs "-" when a field does not have a value. Remove the field entirely when this happens.
c = append(c, fluentbit.LuaFilterComponents(tag, iisMergeRecordFieldsLuaFunction, iisMergeRecordFieldsLuaScriptContents)...)
c = append(c, []fluentbit.Component{
// This is used to exclude the header lines above the logs
// EXAMPLE LINES:
// #Software: Microsoft Internet Information Services 10.0
// #Version: 1.0
// #Date: 2022-04-11 12:53:50
{
Kind: "FILTER",
Config: map[string]string{
"Name": "grep",
"Match": tag,
"Exclude": "message ^#(?:Fields|Date|Version|Software):",
},
},
}...)
fields := map[string]*confgenerator.ModifyField{
InstrumentationSourceLabel: instrumentationSourceValue(p.Type()),
}
// Generate the httpRequest structure.
for _, field := range []string{
"serverIp",
"remoteIp",
"requestMethod",
"requestUrl",
"status",
"referer",
"userAgent",
} {
fields[fmt.Sprintf("httpRequest.%s", field)] = &confgenerator.ModifyField{
MoveFrom: fmt.Sprintf("jsonPayload.http_request_%s", field),
}
}
c = append(c,
confgenerator.LoggingProcessorModifyFields{
Fields: fields,
}.Components(ctx, tag, uid)...,
)
return c
}
type LoggingReceiverIisAccess struct {
LoggingProcessorIisAccess `yaml:",inline"`
ReceiverMixin confgenerator.LoggingReceiverFilesMixin `yaml:",inline" validate:"structonly"`
}
func (r LoggingReceiverIisAccess) Components(ctx context.Context, tag string) []fluentbit.Component {
if len(r.ReceiverMixin.IncludePaths) == 0 {
r.ReceiverMixin.IncludePaths = []string{
`C:\inetpub\logs\LogFiles\W3SVC1\u_ex*`,
}
}
c := r.ReceiverMixin.Components(ctx, tag)
c = append(c, r.LoggingProcessorIisAccess.Components(ctx, tag, "iis_access")...)
return c
}
func init() {
confgenerator.LoggingReceiverTypes.RegisterType(func() confgenerator.LoggingReceiver { return &LoggingReceiverIisAccess{} }, platform.Windows)
confgenerator.LoggingProcessorTypes.RegisterType(func() confgenerator.LoggingProcessor { return &LoggingProcessorIisAccess{} }, platform.Windows)
}