Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/workload-discovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/aws/amazon-cloudwatch-agent/internal/detector/filter"
"github.com/aws/amazon-cloudwatch-agent/internal/detector/java"
"github.com/aws/amazon-cloudwatch-agent/internal/detector/nvidia"
"github.com/aws/amazon-cloudwatch-agent/internal/detector/postgresql"
"github.com/aws/amazon-cloudwatch-agent/internal/detector/util"
"github.com/aws/amazon-cloudwatch-agent/tool/paths"
)
Expand All @@ -47,6 +48,7 @@ func NewDiscoverer(cfg Config, logger *slog.Logger) *Discoverer {
logger: logger,
processDetectors: []detector.ProcessDetector{
java.NewDetector(logger, filters.Process.Name),
postgresql.NewDetector(logger),
},
deviceDetectors: []detector.DeviceDetector{
nvidia.NewDetector(logger),
Expand Down
1 change: 1 addition & 0 deletions internal/detector/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
CategoryKafkaBroker Category = "KAFKA/BROKER"
CategoryKafkaClient Category = "KAFKA/CLIENT"
CategoryNvidiaGPU Category = "NVIDIA_GPU"
CategoryPostgreSQL Category = "POSTGRESQL"
)

// Status represents whether the resource requires more actions before telemetry is available.
Expand Down
24 changes: 24 additions & 0 deletions internal/detector/postgresql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# PostgreSQL Process Detector

Detects PostgreSQL database server processes running on the system.

## Overview

The PostgreSQL detector identifies PostgreSQL server instances by checking for the `postgres` executable name. It is used by the `workload-discovery` command to automatically discover PostgreSQL workloads.

## Detection Method

The detector examines the executable path of each process and checks if the base name is `postgres`.

## Status Results
- `READY`: PostgreSQL process detected with a port (explicit via `-p` flag or `PGPORT`, otherwise defaults to 5432).

## Sample Metadata Result
```json
{
"categories": ["POSTGRESQL"],
"name": "postgresql",
"status": "READY",
"telemetryPort": 5432
}
```
94 changes: 94 additions & 0 deletions internal/detector/postgresql/extract/port.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT

package extract

import (
"context"
"strconv"
"strings"

"github.com/aws/amazon-cloudwatch-agent/internal/detector"
"github.com/aws/amazon-cloudwatch-agent/internal/detector/util"
)

const (
portFlag = "-p"
portEnvVar = "PGPORT"
defaultPostgresPort = 5432
)

type portExtractor struct {
subExtractors []detector.PortExtractor
}

// NewPortExtractor creates a port extractor that attempts to find the PostgreSQL port
// from command line arguments (-p flag) or environment variables (PGPORT).
// Falls back to the default PostgreSQL port 5432.
func NewPortExtractor() detector.PortExtractor {
return &portExtractor{
subExtractors: []detector.PortExtractor{
&cmdlinePortExtractor{},
&envPortExtractor{},
},
}
}

func (e *portExtractor) Extract(ctx context.Context, process detector.Process) (int, error) {
for _, sub := range e.subExtractors {
port, err := sub.Extract(ctx, process)
if err == nil {
return port, nil
}
}
return defaultPostgresPort, nil
}

// cmdlinePortExtractor extracts port from -p flag
type cmdlinePortExtractor struct{}

func (e *cmdlinePortExtractor) Extract(ctx context.Context, process detector.Process) (int, error) {
args, err := process.CmdlineSliceWithContext(ctx)
if err != nil {
return 0, err
}

for i, arg := range args {
if arg == portFlag && i+1 < len(args) {
port, err := strconv.Atoi(args[i+1])
if err == nil && util.IsValidPort(port) {
return port, nil
}
}
if strings.HasPrefix(arg, portFlag) && len(arg) > len(portFlag) {
port, err := strconv.Atoi(arg[len(portFlag):])
if err == nil && util.IsValidPort(port) {
return port, nil
}
}
}

return 0, detector.ErrExtractPort
}

// envPortExtractor extracts port from PGPORT environment variable
type envPortExtractor struct{}

func (e *envPortExtractor) Extract(ctx context.Context, process detector.Process) (int, error) {
env, err := process.EnvironWithContext(ctx)
if err != nil {
return 0, err
}

for _, entry := range env {
parts := strings.SplitN(entry, "=", 2)
if len(parts) == 2 && parts[0] == portEnvVar {
port, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err == nil && util.IsValidPort(port) {
return port, nil
}
}
}

return 0, detector.ErrExtractPort
}
85 changes: 85 additions & 0 deletions internal/detector/postgresql/extract/port_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT

package extract

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/aws/amazon-cloudwatch-agent/internal/detector/detectortest"
)

func TestPortExtractor(t *testing.T) {
ctx := context.Background()
extractor := NewPortExtractor()

tests := map[string]struct {
setup func(*detectortest.MockProcess)
wantPort int
}{
"Success/PortFromFlag": {
setup: func(mp *detectortest.MockProcess) {
mp.On("CmdlineSliceWithContext", ctx).Return([]string{"postgres", "-p", "5433"}, nil)
},
wantPort: 5433,
},
"Success/PortFromFlagNoSpace": {
setup: func(mp *detectortest.MockProcess) {
mp.On("CmdlineSliceWithContext", ctx).Return([]string{"postgres", "-p5434"}, nil)
},
wantPort: 5434,
},
"Success/PortFromEnv": {
setup: func(mp *detectortest.MockProcess) {
mp.On("CmdlineSliceWithContext", ctx).Return([]string{"postgres"}, nil)
mp.On("EnvironWithContext", ctx).Return([]string{"PATH=/usr/bin", "PGPORT=5435"}, nil)
},
wantPort: 5435,
},
// cmdline is tried first; when it finds a port, env is never called
"Success/CmdlineTakesPrecedence": {
setup: func(mp *detectortest.MockProcess) {
mp.On("CmdlineSliceWithContext", ctx).Return([]string{"postgres", "-p", "5433"}, nil)
},
wantPort: 5433,
},
"Success/DefaultPort": {
setup: func(mp *detectortest.MockProcess) {
mp.On("CmdlineSliceWithContext", ctx).Return([]string{"postgres"}, nil)
mp.On("EnvironWithContext", ctx).Return([]string{"PATH=/usr/bin"}, nil)
},
wantPort: 5432,
},
"Success/DefaultPortWithOtherFlags": {
setup: func(mp *detectortest.MockProcess) {
mp.On("CmdlineSliceWithContext", ctx).Return([]string{"postgres", "-D", "/var/lib/postgresql/data"}, nil)
mp.On("EnvironWithContext", ctx).Return([]string{}, nil)
},
wantPort: 5432,
},
"Success/DefaultOnAllSourcesFail": {
setup: func(mp *detectortest.MockProcess) {
mp.On("CmdlineSliceWithContext", ctx).Return(nil, assert.AnError)
mp.On("EnvironWithContext", ctx).Return(nil, assert.AnError)
},
wantPort: 5432,
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
mp := new(detectortest.MockProcess)
tt.setup(mp)

port, err := extractor.Extract(ctx, mp)

require.NoError(t, err)
assert.Equal(t, tt.wantPort, port)
mp.AssertExpectations(t)
})
}
}
71 changes: 71 additions & 0 deletions internal/detector/postgresql/postgresql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT

package postgresql

import (
"context"
"log/slog"
"strings"

"github.com/aws/amazon-cloudwatch-agent/internal/detector"
"github.com/aws/amazon-cloudwatch-agent/internal/detector/postgresql/extract"
"github.com/aws/amazon-cloudwatch-agent/internal/detector/util"
)

const (
exeName = "postgres"
)

type postgresqlDetector struct {
logger *slog.Logger
portExtractor detector.PortExtractor
}

var _ detector.ProcessDetector = (*postgresqlDetector)(nil)

// NewDetector creates a new process detector that identifies PostgreSQL processes.
func NewDetector(logger *slog.Logger) detector.ProcessDetector {
return &postgresqlDetector{
logger: logger,
portExtractor: extract.NewPortExtractor(),
}
}

// Detect identifies PostgreSQL processes and returns metadata.
// Only detects the main postgres process, not worker processes.
func (d *postgresqlDetector) Detect(ctx context.Context, process detector.Process) (*detector.Metadata, error) {
exe, err := process.ExeWithContext(ctx)
if err != nil {
return nil, err
}

base := util.BaseExe(exe)
if base != exeName {
return nil, detector.ErrIncompatibleDetector
}

// Check if this is the main postgres process or a worker
args, err := process.CmdlineSliceWithContext(ctx)
if err != nil {
return nil, err
}

if len(args) > 0 && strings.HasPrefix(strings.TrimSpace(args[0]), exeName+":") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if cmdline is unreadable here (race with process exit, permission denied), this propagates the raw error even though we already confirmed the exe is postgres at L47. Could return ErrIncompatibleDetector instead so a transient /proc read failure doesn't surface as a detection error for a process we can't confirm is the main server. Low risk since the discoverer just continues past non-nil errors anyway.

return nil, detector.ErrIncompatibleDetector
}

d.logger.Debug("PostgreSQL process detected", "pid", process.PID())

md := &detector.Metadata{
Name: "postgresql",
Categories: []detector.Category{detector.CategoryPostgreSQL},
}

port, _ := d.portExtractor.Extract(ctx, process)

md.Status = detector.StatusReady
md.TelemetryPort = port

return md, nil
}
Loading
Loading