internal/processmetrics/sapcontrol/sapcontrol.go (215 lines of code) (raw):

/* Copyright 2022 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 https://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 sapcontrol implements generic sapcontrol functions. package sapcontrol import ( "context" "fmt" "regexp" "strconv" "strings" "github.com/GoogleCloudPlatform/sapagent/internal/sapcontrolclient" "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/commandlineexecutor" "github.com/GoogleCloudPlatform/workloadagentplatform/sharedlibraries/log" sapb "github.com/GoogleCloudPlatform/sapagent/protos/sapapp" ) var ( // Expected format: "(Process ID) name: (Process Name)" processNameRegex = regexp.MustCompile(`([0-9]+) name: ([a-z|A-Z|_|\+]+)`) // Expected format: "(Process ID) dispstatus: (Display Status)" processDisplayStatusRegex = regexp.MustCompile(`([0-9]+) dispstatus: ([a-z|A-Z|_|\+]+)`) // Expected format: "(Process ID) pid: (PID)" processPIDRegex = regexp.MustCompile(`([0-9]+) pid: ([0-9]+)`) sapcontrolStatus = map[int]string{ 0: "Last webmethod call successful.", 1: "Last webmethod call failed, invalid parameter.", 2: "StartWait, StopWait, WaitforStarted, WaitforStopped RestartServiceWait timed out. CheckSystemCertificates detected warnings", 3: "GetProcessList succeeded, all processes running correctly. CheckSystemCertificates detected errors.", 4: "GetProcessList succeeded, all processes stopped.", } emptyChars = regexp.MustCompile(`[\s\t\n\r]`) ) type ( // Properties is a receiver for sapcontrol functions. Properties struct { Instance *sapb.SAPInstance } // ClientInterface contains API methods and has been created for easy testing of the SAPControl APIs ClientInterface interface { GetProcessList() ([]sapcontrolclient.OSProcess, error) ABAPGetWPTable() ([]sapcontrolclient.WorkProcess, error) GetQueueStatistic() ([]sapcontrolclient.TaskHandlerQueue, error) GetEnqLockTable() ([]sapcontrolclient.EnqLock, error) } // ProcessStatus has the sap process status. ProcessStatus struct { Name string DisplayStatus string IsGreen bool PID string } // EnqLock has the attributes returned by sapcontrol's EnqGetLockTable function. EnqLock struct { LockName, LockArg, LockMode, Owner, OwnerVB string UserCountOwner, UserCountOwnerVB int64 Client, User, Transaction, Object, Backup string } ) // ExecProcessList uses the SAPControl command to obtain the process list result. // Parameters are a commandlineexecutor.Execute and commandlineexecutor.Params // Example Usage: // // params := commandlineexecutor.Params{ // User: "hdbadm", // Executable: "/usr/sap/HDB/HDB00/exe/sapcontrol", // ArgsToSplit: "-nr 00 -function GetProcessList -format script", // Env: []string{"LD_LIBRARY_PATH=/usr/sap/HDB/HDB00/exe/ld_library"}, // } // sc := &sapcontrol.Properties{&sapb.SAPInstance{}} // result, exitStatus, err := sc.ExecProcessList(commandlineexecutor.ExecuteCommand, params) // // Returns: // - A commandlineexecutor.Result struct containing the result of the SAPControl command execution. // - The exit status returned by sapcontrol command as int. // - Error if process detection fails, nil otherwise. func ExecProcessList(ctx context.Context, exec commandlineexecutor.Execute, params commandlineexecutor.Params) (commandlineexecutor.Result, int, error) { result := exec(ctx, params) if result.Error != nil && !result.ExitStatusParsed { log.CtxLogger(ctx).Debugw("Failed to get SAP Process Status", log.Error(result.Error)) return result, 0, result.Error } message, ok := sapcontrolStatus[result.ExitCode] if !ok { return result, result.ExitCode, fmt.Errorf("invalid sapcontrol return code: %d", result.ExitCode) } log.CtxLogger(ctx).Debugw("Sapcontrol ExecStatusProcessList", "status", result.ExitCode, "message", message, "stdout", result.StdOut) return result, result.ExitCode, nil } // GetProcessList uses the SapControl web API to build a map describing the statuses // of all SAP processes. // Parameter is a ClientInterface // Example Usage: // // scdc := sapcontrolclient.New("02") // returns a Client that implements ClientInterface // scp := &sapcontrol.Properties{&sapb.SAPInstance{}} // processes, err := scp.GetProcessList(scdc) // // Returns: // - A map[int]*ProcessStatus where key is the process index according to the process position // in the returned process list, and the value is a ProcessStatus struct containing process // status details. // - Error if sapcontrolclient.GetProcessList fails, nil otherwise. func (p *Properties) GetProcessList(ctx context.Context, c ClientInterface) (map[int]*ProcessStatus, error) { processes, err := c.GetProcessList() if err != nil { log.CtxLogger(ctx).Debugw("Failed to get SAP Process Status via API call", log.Error(err), "SAPSID:", p.Instance.GetSapsid(), "Instance Number:", p.Instance.GetInstanceNumber()) return nil, err } log.CtxLogger(ctx).Debugw("Sapcontrol GetProcessList", "API response", processes, "SAPSID:", p.Instance.GetSapsid(), "Instance Number:", p.Instance.GetInstanceNumber()) return createProcessMapFromAPIResp(ctx, processes), nil } func createProcessMapFromAPIResp(ctx context.Context, resp []sapcontrolclient.OSProcess) map[int]*ProcessStatus { processes := make(map[int]*ProcessStatus) for i, p := range resp { if p.Name == "" || p.Dispstatus == "" || p.Pid == 0 { continue } // Sample p.Dispstatus := "SAPControl-GREEN". We want to extract only the status "GREEN". splitDs := strings.Split(p.Dispstatus, "-") if len(splitDs) != 2 { continue } processes[i] = &ProcessStatus{ Name: p.Name, DisplayStatus: splitDs[1], PID: fmt.Sprintf("%d", p.Pid), IsGreen: strings.ToUpper(splitDs[1]) == "GREEN", } } log.CtxLogger(ctx).Debugw("Process statuses", "statuses", processes) return processes } // WorkProcessDetails contains the maps that will be used by the consumers to derive metrics. // - processes - A map with key->worker_process_type and value->total_process_count. // - busyProcesses - A map with key->worker_process_type and value->busy_process_count. // - busyProcessPercentage - A map with key->worker_process_type and value->busy_process_percentage. // - processNameToPID - A map with key->pid and value->worker_process_type. type WorkProcessDetails struct { Processes map[string]int BusyProcesses map[string]int BusyProcessPercentage map[string]int ProcessNameToPID map[string]string } // ABAPGetWPTable uses the sapcontrolclient package to run the ABAPGetWPTable SAPControl function. // Returns: WorkProcessDetails struct func (p *Properties) ABAPGetWPTable(ctx context.Context, c ClientInterface) (WorkProcessDetails, error) { wp, err := c.ABAPGetWPTable() if err != nil { log.CtxLogger(ctx).Debugw("Failed to run ABAPGetWPTable API call", log.Error(err), "SAPSID:", p.Instance.GetSapsid(), "Instance Number:", p.Instance.GetInstanceNumber()) return WorkProcessDetails{}, err } log.CtxLogger(ctx).Debugw("Sapcontrol ABAPGetWPTable", "API Response", wp, "SAPSID:", p.Instance.GetSapsid(), "Instance Number:", p.Instance.GetInstanceNumber()) return processABAPGetWPTableResponse(ctx, wp), nil } // processABAPGetWPTableResponse processes the WorkProcess list returned by the ABAPGetWPTable SAPControl function. func processABAPGetWPTableResponse(ctx context.Context, wp []sapcontrolclient.WorkProcess) (wpDetails WorkProcessDetails) { wpDetails.Processes = make(map[string]int) wpDetails.BusyProcesses = make(map[string]int) wpDetails.BusyProcessPercentage = make(map[string]int) wpDetails.ProcessNameToPID = make(map[string]string) for _, p := range wp { workProcessType := p.Type wpDetails.Processes[workProcessType]++ wpDetails.Processes["Total"]++ wpDetails.ProcessNameToPID[strconv.FormatInt(p.Pid, 10)] = workProcessType if p.Status != "Wait" { wpDetails.BusyProcesses[workProcessType]++ wpDetails.BusyProcesses["Total"]++ } } for workProcessType, processCount := range wpDetails.Processes { if processCount == 0 { log.CtxLogger(ctx).Debugw("Process count zero", "type", workProcessType) continue } busyProcessCount, _ := wpDetails.BusyProcesses[workProcessType] wpDetails.BusyProcessPercentage[workProcessType] = (busyProcessCount * 100) / processCount } log.CtxLogger(ctx).Debugw("Found ABAP Processes", "count", wpDetails.Processes, "busy", wpDetails.BusyProcesses, "percentage", wpDetails.BusyProcessPercentage, "pidMap", wpDetails.ProcessNameToPID) return wpDetails } // ParseQueueStats runs and parses the output of sapcontrol function GetQueueStatistic. // Returns: // - currentQueueUsage - A map with key->queue_type and value->current_queue_usage. // - peakQueueUsage - A map with key->queue_type and value->peak_queue_usage. func (p *Properties) ParseQueueStats(ctx context.Context, exec commandlineexecutor.Execute, params commandlineexecutor.Params) (currentQueueUsage, peakQueueUsage map[string]int, err error) { const ( numberOfColumns = 6 typeColumn = 0 currentQueueUsageColumn = 1 peakQueueUsageColumn = 2 ) result := exec(ctx, params) if result.Error != nil && !result.ExitStatusParsed { log.CtxLogger(ctx).Debugw("Failed to run GetQueueStatistic", log.Error(result.Error)) return nil, nil, result.Error } currentQueueUsage = make(map[string]int) peakQueueUsage = make(map[string]int) lines := strings.Split(result.StdOut, "\n") for _, line := range lines { line = emptyChars.ReplaceAllString(line, "") row := strings.Split(line, ",") if len(row) != numberOfColumns || row[typeColumn] == "Typ" { continue } queue, current, peak := row[typeColumn], row[currentQueueUsageColumn], row[peakQueueUsageColumn] currentVal, err := strconv.Atoi(current) if err != nil { log.CtxLogger(ctx).Debugw("Could not parse current queue usage", log.Error(err)) continue } currentQueueUsage[queue] = currentVal peakVal, err := strconv.Atoi(peak) if err != nil { log.CtxLogger(ctx).Debugw("Could not parse peak queue usage", log.Error(err)) continue } peakQueueUsage[queue] = peakVal } log.CtxLogger(ctx).Debugw("Found Queue stats", "currentqueueusage", currentQueueUsage, "peakqueueusage", peakQueueUsage) return currentQueueUsage, peakQueueUsage, nil } // GetQueueStatistic performs GetQueueStatistic soap request. // Returns: // - currentQueueUsage - A map with key->queue_type and value->current_queue_usage. // - peakQueueUsage - A map with key->queue_type and value->peak_queue_usage. func (p *Properties) GetQueueStatistic(ctx context.Context, c ClientInterface) (map[string]int64, map[string]int64, error) { tq, err := c.GetQueueStatistic() if err != nil { log.CtxLogger(ctx).Debugw("Failed to run GetQueueStatistic API call", log.Error(err)) return nil, nil, err } currentQueueUsage, peakQueueUsage := processGetQueueStatisticResponse(ctx, tq) return currentQueueUsage, peakQueueUsage, nil } // processGetQueueStatisticResponse processes the TaskHandlerQueue list returned by the GetQueueStatistic SAPControl function. func processGetQueueStatisticResponse(ctx context.Context, taskQueues []sapcontrolclient.TaskHandlerQueue) (map[string]int64, map[string]int64) { currentQueueUsage := make(map[string]int64) peakQueueUsage := make(map[string]int64) for _, q := range taskQueues { queue, current, peak := q.Type, q.Now, q.High currentQueueUsage[queue] = current peakQueueUsage[queue] = peak } log.CtxLogger(ctx).Debugw("Found Queue stats", "currentqueueusage", currentQueueUsage, "peakqueueusage", peakQueueUsage) return currentQueueUsage, peakQueueUsage } // EnqGetLockTable performs the SOAP API request // returns // - A slice of EnqLock structs containing lock details // - error if API call fails func (p *Properties) EnqGetLockTable(ctx context.Context, c ClientInterface) ([]*EnqLock, error) { resp, err := c.GetEnqLockTable() log.CtxLogger(ctx).Info("EnqGetLockTable API response", resp, err) if err != nil { log.CtxLogger(ctx).Debugw("EnqGetLockTable API call failed", log.Error(err)) return nil, err } enqLocks := []*EnqLock{} for _, lock := range resp { item := &EnqLock{ LockName: lock.LockName, LockArg: lock.LockArg, LockMode: lock.LockMode, Owner: lock.Owner, OwnerVB: lock.OwnerVB, UserCountOwner: lock.UseCountOwner, UserCountOwnerVB: lock.UseCountOwnerVB, Client: lock.Client, User: lock.User, Transaction: lock.Transaction, Object: lock.Object, Backup: lock.Backup, } enqLocks = append(enqLocks, item) } return enqLocks, nil }