diff --git a/go.mod b/go.mod index 15dba69f98..c5e0c4bf4f 100644 --- a/go.mod +++ b/go.mod @@ -69,7 +69,7 @@ replace ( github.com/open-telemetry/opentelemetry-collector-contrib/receiver/awsekshyperpodreceiver => github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/awsekshyperpodreceiver v0.0.0-20260717191209-ee093fbfffbd github.com/open-telemetry/opentelemetry-collector-contrib/receiver/awsxrayreceiver => github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/awsxrayreceiver v0.0.0-20260717191209-ee093fbfffbd github.com/open-telemetry/opentelemetry-collector-contrib/receiver/jmxreceiver => github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/jmxreceiver v0.0.0-20260717191209-ee093fbfffbd - github.com/open-telemetry/opentelemetry-collector-contrib/receiver/mysqlreceiver => github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/mysqlreceiver v0.0.0-20260728131824-bed4560687ee + github.com/open-telemetry/opentelemetry-collector-contrib/receiver/mysqlreceiver => github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/mysqlreceiver v0.0.0-20260730161852-83b5027c386b github.com/open-telemetry/opentelemetry-collector-contrib/receiver/postgresqlreceiver => github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/postgresqlreceiver v0.0.0-20260717191209-ee093fbfffbd github.com/open-telemetry/opentelemetry-collector-contrib/receiver/prometheusreceiver => github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/prometheusreceiver v0.0.0-20260717191209-ee093fbfffbd github.com/open-telemetry/opentelemetry-collector-contrib/receiver/sqlserverreceiver => github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/sqlserverreceiver v0.0.0-20260729175257-2c50db1bb14e diff --git a/go.sum b/go.sum index f2e32764af..5a7ddaec4c 100644 --- a/go.sum +++ b/go.sum @@ -278,8 +278,8 @@ github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/awsxrayr github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/awsxrayreceiver v0.0.0-20260717191209-ee093fbfffbd/go.mod h1:gNDpUc9eYHKy9YqGYK8toPIo3QZKdYQFLnaVQDA32A0= github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/jmxreceiver v0.0.0-20260717191209-ee093fbfffbd h1:AF4G2iJwaLeBivwjWMppWYbmfObc+foQws5fetLUWxk= github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/jmxreceiver v0.0.0-20260717191209-ee093fbfffbd/go.mod h1:wQV77sHJJdUMIvbJna8LjlNb0omEdiVSrwQnoVAlTMM= -github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/mysqlreceiver v0.0.0-20260728131824-bed4560687ee h1:XELS9NH+v8xvMQS49SckY5HWDfqrYNzUPQT62NT4QgY= -github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/mysqlreceiver v0.0.0-20260728131824-bed4560687ee/go.mod h1:NAyuB/xYR3FsPuegcRvVeStWW83y5oUQhekzT856urI= +github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/mysqlreceiver v0.0.0-20260730161852-83b5027c386b h1:AHEwT0RHOaEDz0t5WrH7OkQH93I2zoFaM90qR01oLII= +github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/mysqlreceiver v0.0.0-20260730161852-83b5027c386b/go.mod h1:NAyuB/xYR3FsPuegcRvVeStWW83y5oUQhekzT856urI= github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/postgresqlreceiver v0.0.0-20260717191209-ee093fbfffbd h1:Q0XNkIPZEiIlknoP0X0PxoKVmlKR0itxyHQjZUkFTsg= github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/postgresqlreceiver v0.0.0-20260717191209-ee093fbfffbd/go.mod h1:DdKZOg4y/ac/f/ENvYWWg6e6r+INsAkx6DqRXzps1TU= github.com/amazon-contributing/opentelemetry-collector-contrib/receiver/prometheusreceiver v0.0.0-20260717191209-ee093fbfffbd h1:gguRr06gAfXYB1kxUZH7VaZobUauXRX+uNWGA0hsGx4= diff --git a/internal/detector/sqlserver/extract/port.go b/internal/detector/sqlserver/extract/port.go index 975ba1fb0f..9faa745482 100644 --- a/internal/detector/sqlserver/extract/port.go +++ b/internal/detector/sqlserver/extract/port.go @@ -22,15 +22,14 @@ type portExtractor struct { subExtractors []detector.PortExtractor } -// NewPortExtractor creates a port extractor that attempts to find the SQL Server port -// from command line arguments (-p flag) or environment variables (MSSQL_TCP_PORT). -// Falls back to the default SQL Server port 1433. +// NewPortExtractor creates a port extractor that attempts to find the SQL Server port. +// On Windows, it first checks the registry to resolve named instance ports. +// Then it tries command line arguments (-p flag) and environment variables (MSSQL_TCP_PORT). +// Falls back to the default SQL Server port 1433 if all sources fail. func NewPortExtractor() detector.PortExtractor { + extractors := append(platformPortExtractors(), &cmdlinePortExtractor{}, &envPortExtractor{}) return &portExtractor{ - subExtractors: []detector.PortExtractor{ - &cmdlinePortExtractor{}, - &envPortExtractor{}, - }, + subExtractors: extractors, } } diff --git a/internal/detector/sqlserver/extract/port_other.go b/internal/detector/sqlserver/extract/port_other.go new file mode 100644 index 0000000000..24a1e88061 --- /dev/null +++ b/internal/detector/sqlserver/extract/port_other.go @@ -0,0 +1,14 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +//go:build !windows + +package extract + +import "github.com/aws/amazon-cloudwatch-agent/internal/detector" + +// platformPortExtractors returns an empty slice on non-Windows platforms. +// Linux SQL Server uses -p flag or MSSQL_TCP_PORT which are handled by the common extractors. +func platformPortExtractors() []detector.PortExtractor { + return nil +} diff --git a/internal/detector/sqlserver/extract/port_windows.go b/internal/detector/sqlserver/extract/port_windows.go new file mode 100644 index 0000000000..e4a5e3c75c --- /dev/null +++ b/internal/detector/sqlserver/extract/port_windows.go @@ -0,0 +1,126 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +//go:build windows + +package extract + +import ( + "context" + "fmt" + "strconv" + "strings" + + "golang.org/x/sys/windows/registry" + + "github.com/aws/amazon-cloudwatch-agent/internal/detector" + "github.com/aws/amazon-cloudwatch-agent/internal/detector/util" +) + +const ( + instanceNameFlag = "-s" + defaultInstanceName = "MSSQLSERVER" + sqlServerRegBasePath = `SOFTWARE\Microsoft\Microsoft SQL Server` +) + +// platformPortExtractors returns Windows-specific port extractors. +// The registry extractor correctly resolves ports for named instances. +func platformPortExtractors() []detector.PortExtractor { + return []detector.PortExtractor{®istryPortExtractor{}} +} + +// registryPortExtractor reads the TCP port from the Windows registry for a SQL Server instance. +// Each instance stores its port in: +// HKLM\SOFTWARE\Microsoft\Microsoft SQL Server\\MSSQLServer\SuperSocketNetLib\Tcp\IPAll +type registryPortExtractor struct{} + +func (e *registryPortExtractor) Extract(ctx context.Context, process detector.Process) (int, error) { + instanceName := extractInstanceName(ctx, process) + + instanceID, err := resolveInstanceID(instanceName) + if err != nil { + return 0, err + } + + return readPortFromRegistry(instanceID) +} + +// extractInstanceName gets the SQL Server instance name from the process command line. +// Named instances are started with -s INSTANCENAME flag. Default instance uses MSSQLSERVER. +func extractInstanceName(ctx context.Context, process detector.Process) string { + args, err := process.CmdlineSliceWithContext(ctx) + if err != nil { + return defaultInstanceName + } + + for i, arg := range args { + lower := strings.ToLower(arg) + if lower == instanceNameFlag && i+1 < len(args) { + return strings.ToUpper(args[i+1]) + } + if strings.HasPrefix(lower, instanceNameFlag) && len(arg) > len(instanceNameFlag) { + return strings.ToUpper(arg[len(instanceNameFlag):]) + } + } + + return defaultInstanceName +} + +// resolveInstanceID maps an instance name (e.g., "MSSQLSERVER" or "YOURDBINSTANCE2") +// to its registry ID (e.g., "MSSQL17.MSSQLSERVER") by reading +// HKLM\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL +func resolveInstanceID(instanceName string) (string, error) { + keyPath := sqlServerRegBasePath + `\Instance Names\SQL` + key, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.QUERY_VALUE) + if err != nil { + return "", fmt.Errorf("failed to open registry key %s: %w", keyPath, err) + } + defer key.Close() + + instanceID, _, err := key.GetStringValue(instanceName) + if err != nil { + return "", fmt.Errorf("failed to read instance %s from registry: %w", instanceName, err) + } + + return instanceID, nil +} + +// readPortFromRegistry reads the TCP port for a given instance ID. +// Checks TcpPort (static) first, then TcpDynamicPorts (dynamic). +func readPortFromRegistry(instanceID string) (int, error) { + keyPath := fmt.Sprintf(`%s\%s\MSSQLServer\SuperSocketNetLib\Tcp\IPAll`, sqlServerRegBasePath, instanceID) + key, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.QUERY_VALUE) + if err != nil { + return 0, fmt.Errorf("failed to open registry key %s: %w", keyPath, err) + } + defer key.Close() + + if port, err := readPortValue(key, "TcpPort"); err == nil { + return port, nil + } + + if port, err := readPortValue(key, "TcpDynamicPorts"); err == nil { + return port, nil + } + + return 0, detector.ErrExtractPort +} + +func readPortValue(key registry.Key, valueName string) (int, error) { + val, _, err := key.GetStringValue(valueName) + if err != nil { + return 0, err + } + val = strings.TrimSpace(val) + if val == "" { + return 0, detector.ErrExtractPort + } + port, err := strconv.Atoi(val) + if err != nil { + return 0, err + } + if !util.IsValidPort(port) { + return 0, detector.ErrInvalidPort + } + return port, nil +} diff --git a/internal/detector/sqlserver/extract/port_windows_test.go b/internal/detector/sqlserver/extract/port_windows_test.go new file mode 100644 index 0000000000..ea01d8f5e2 --- /dev/null +++ b/internal/detector/sqlserver/extract/port_windows_test.go @@ -0,0 +1,65 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +//go:build windows + +package extract + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aws/amazon-cloudwatch-agent/internal/detector/detectortest" +) + +func TestExtractInstanceName(t *testing.T) { + ctx := context.Background() + + tests := map[string]struct { + cmdline []string + cmdlineErr error + wantInstance string + }{ + "DefaultInstance/NoCmdlineFlag": { + cmdline: []string{"sqlservr"}, + wantInstance: "MSSQLSERVER", + }, + "DefaultInstance/CmdlineError": { + cmdlineErr: assert.AnError, + wantInstance: "MSSQLSERVER", + }, + "NamedInstance/SeparateArg": { + cmdline: []string{"sqlservr", "-s", "YOURDBINSTANCE2"}, + wantInstance: "YOURDBINSTANCE2", + }, + "NamedInstance/AttachedArg": { + cmdline: []string{"sqlservr", "-sYOURDBINSTANCE2"}, + wantInstance: "YOURDBINSTANCE2", + }, + "NamedInstance/LowercaseFlag": { + cmdline: []string{"sqlservr", "-s", "myinstance"}, + wantInstance: "MYINSTANCE", + }, + "NamedInstance/WithOtherFlags": { + cmdline: []string{"sqlservr", "-f", "-s", "INST2", "-T", "3608"}, + wantInstance: "INST2", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + mp := new(detectortest.MockProcess) + if tt.cmdlineErr != nil { + mp.On("CmdlineSliceWithContext", ctx).Return(nil, tt.cmdlineErr) + } else { + mp.On("CmdlineSliceWithContext", ctx).Return(tt.cmdline, nil) + } + + got := extractInstanceName(ctx, mp) + assert.Equal(t, tt.wantInstance, got) + mp.AssertExpectations(t) + }) + } +} diff --git a/translator/translate/otel/pipeline/opentelemetry/databaseinsights/translator.go b/translator/translate/otel/pipeline/opentelemetry/databaseinsights/translator.go index e6156c05a7..c1e211c40d 100644 --- a/translator/translate/otel/pipeline/opentelemetry/databaseinsights/translator.go +++ b/translator/translate/otel/pipeline/opentelemetry/databaseinsights/translator.go @@ -5,6 +5,7 @@ package databaseinsights import ( "fmt" + "runtime" "strconv" "strings" "time" @@ -283,6 +284,10 @@ func (t *dbiTranslator) serverLogReceiver() common.ComponentTranslator { filelog.WithSeverityPattern(buildSqlServerSeverityPattern()), filelog.WithSeverityMapping(sqlserverLogSeverityMapping), ) + // Windows SQL Server writes ERRORLOG in UTF-16LE encoding. + if runtime.GOOS == "windows" { + opts = append(opts, filelog.WithEncoding("utf-16le")) + } } return filelog.NewTranslator(opts...) }