From 7044b556c27d6ac2b97a08bea9620d5642d337ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Wed, 24 Jul 2024 11:18:08 +0200 Subject: [PATCH] Add terminal service session info (#1525) --- .gitignore | 1 + docs/collector.terminal_services.md | 137 +++++++++--- pkg/collector/collector.go | 3 + pkg/collector/config.go | 2 + pkg/collector/remote_fx/remote_fx.go | 53 ++--- .../terminal_services/terminal_services.go | 133 ++++++------ .../terminal_services_test.go | 2 + pkg/headers/wtsapi32/wtsapi32.go | 198 ++++++++++++++++++ 8 files changed, 409 insertions(+), 120 deletions(-) create mode 100644 pkg/headers/wtsapi32/wtsapi32.go diff --git a/.gitignore b/.gitignore index 2c199092..49cf5ecf 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ output/ *.syso installer/*.msi installer/*.wixpdb +local/ \ No newline at end of file diff --git a/docs/collector.terminal_services.md b/docs/collector.terminal_services.md index bf71f9db..0a826941 100644 --- a/docs/collector.terminal_services.md +++ b/docs/collector.terminal_services.md @@ -2,12 +2,13 @@ The terminal_services collector exposes terminal services (Remote Desktop Services) performance metrics. -||| --|- -Metric name prefix | `terminal_services` -Data source | Perflib/WMI -Classes | [`Win32_PerfRawData_LocalSessionManager_TerminalServices`](https://wutils.com/wmi/root/cimv2/win32_perfrawdata_localsessionmanager_terminalservices/), [`Win32_PerfRawData_TermService_TerminalServicesSession`](https://docs.microsoft.com/en-us/previous-versions/aa394344(v%3Dvs.85)), [`Win32_PerfRawData_RemoteDesktopConnectionBrokerPerformanceCounterProvider_RemoteDesktopConnectionBrokerCounterset`](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/mt729067(v%3Dws.11)) -Enabled by default? | No +| | | +|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| __Metric name prefix__ | `terminal_services` | +| __Data source__ | Perflib/WMI, Win32 | +| __Classes__ | [`Win32_PerfRawData_LocalSessionManager_TerminalServices`](https://wutils.com/wmi/root/cimv2/win32_perfrawdata_localsessionmanager_terminalservices/), [`Win32_PerfRawData_TermService_TerminalServicesSession`](https://docs.microsoft.com/en-us/previous-versions/aa394344(v%3Dvs.85)), [`Win32_PerfRawData_RemoteDesktopConnectionBrokerPerformanceCounterProvider_RemoteDesktopConnectionBrokerCounterset`](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/mt729067(v%3Dws.11)) | +| __Win32 API__ | [WTSEnumerateSessionsEx](https://learn.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsenumeratesessionsexw) | +| __Enabled by default?__ | No | ## Flags @@ -15,34 +16,116 @@ None ## Metrics -Name | Description | Type | Labels ------|-------------|------|------- -`windows_terminal_services_local_session_count` | Number of local Terminal Services sessions. | gauge | `session` -`windows_terminal_services_connection_broker_performance_total`* | The total number of connections handled by the Connection Brokers since the service started. | counter | `connection` -`windows_terminal_services_handles` | Total number of handles currently opened by this process. This number is the sum of the handles currently opened by each thread in this process. | gauge | `session_name` -`windows_terminal_services_page_fault_total` | Rate at which page faults occur in the threads executing in this process. A page fault occurs when a thread refers to a virtual memory page that is not in its working set in main memory. The page may not be retrieved from disk if it is on the standby list and therefore already in main memory. The page also may not be retrieved if it is in use by another process which shares the page. | counter | `session_name` -`windows_terminal_services_page_file_bytes` | Current number of bytes this process has used in the paging file(s). Paging files are used to store pages of memory used by the process that are not contained in other files. Paging files are shared by all processes, and lack of space in paging files can prevent other processes from allocating memory. | gauge | `session_name` -`windows_terminal_services_page_file_bytes_peak` | Maximum number of bytes this process has used in the paging file(s). Paging files are used to store pages of memory used by the process that are not contained in other files. Paging files are shared by all processes, and lack of space in paging files can prevent other processes from allocating memory. | gauge | `session_name` -`windows_terminal_services_privileged_time_seconds_total` | total elapsed time that the threads of the process have spent executing code in privileged mode. | Counter | `session_name` -`windows_terminal_services_processor_time_seconds_total` | total elapsed time that all of the threads of this process used the processor to execute instructions. | Counter | `session_name` -`windows_terminal_services_user_time_seconds_total` | total elapsed time that this process's threads have spent executing code in user mode. Applications, environment subsystems, and integral subsystems execute in user mode. | Counter | `session_name` -`windows_terminal_services_pool_non_paged_bytes` | Number of bytes in the non-paged pool, an area of system memory (physical memory used by the operating system) for objects that cannot be written to disk, but must remain in physical memory as long as they are allocated. This property displays the last observed value only; it is not an average. | gauge | `session_name` -`windows_terminal_services_pool_paged_bytes` | Number of bytes in the paged pool, an area of system memory (physical memory used by the operating system) for objects that can be written to disk when they are not being used. This property displays the last observed value only; it is not an average. | gauge | `session_name` -`windows_terminal_services_private_bytes` | Current number of bytes this process has allocated that cannot be shared with other processes. | gauge | `session_name` -`windows_terminal_services_threads` | Number of threads currently active in this process. An instruction is the basic unit of execution in a processor, and a thread is the object that executes instructions. Every running process has at least one thread. | gauge | `session_name` -`windows_terminal_services_virtual_bytes` | Current size, in bytes, of the virtual address space the process is using. Use of virtual address space does not necessarily imply corresponding use of either disk or main memory pages. Virtual space is finite and, by using too much, the process can limit its ability to load libraries. | gauge | `session_name` -`windows_terminal_services_virtual_bytes_peak` | Maximum number of bytes of virtual address space the process has used at any one time. Use of virtual address space does not necessarily imply corresponding use of either disk or main memory pages. Virtual space is finite and, by using too much, the process might limit its ability to load libraries. | gauge | `session_name` -`windows_terminal_services_working_set_bytes` | Current number of bytes in the working set of this process. The working set is the set of memory pages touched recently by the threads in the process. If free memory in the computer is above a threshold, pages are left in the working set of a process even if they are not in use. When free memory falls below a threshold, pages are trimmed from working sets. If they are needed, they are then soft-faulted back into the working set before they leave main memory. | gauge | `session_name` -`windows_terminal_services_working_set_bytes_peak` | Maximum number of bytes in the working set of this process at any point in time. The working set is the set of memory pages touched recently by the threads in the process. If free memory in the computer is above a threshold, pages are left in the working set of a process even if they are not in use. When free memory falls below a threshold, pages are trimmed from working sets. If they are needed, they are then soft-faulted back into the working set before they leave main memory. | gauge | `session_name` +| Name | Description | Type | Labels | +|------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|-----------------| +| `windows_terminal_services_session_info` | Info about active WTS sessions | gauge | host,user,state | +| `windows_terminal_services_local_session_count` | Number of local Terminal Services sessions. | gauge | `session` | +| `windows_terminal_services_connection_broker_performance_total`* | The total number of connections handled by the Connection Brokers since the service started. | counter | `connection` | +| `windows_terminal_services_handles` | Total number of handles currently opened by this process. This number is the sum of the handles currently opened by each thread in this process. | gauge | `session_name` | +| `windows_terminal_services_page_fault_total` | Rate at which page faults occur in the threads executing in this process. A page fault occurs when a thread refers to a virtual memory page that is not in its working set in main memory. The page may not be retrieved from disk if it is on the standby list and therefore already in main memory. The page also may not be retrieved if it is in use by another process which shares the page. | counter | `session_name` | +| `windows_terminal_services_page_file_bytes` | Current number of bytes this process has used in the paging file(s). Paging files are used to store pages of memory used by the process that are not contained in other files. Paging files are shared by all processes, and lack of space in paging files can prevent other processes from allocating memory. | gauge | `session_name` | +| `windows_terminal_services_page_file_bytes_peak` | Maximum number of bytes this process has used in the paging file(s). Paging files are used to store pages of memory used by the process that are not contained in other files. Paging files are shared by all processes, and lack of space in paging files can prevent other processes from allocating memory. | gauge | `session_name` | +| `windows_terminal_services_privileged_time_seconds_total` | total elapsed time that the threads of the process have spent executing code in privileged mode. | Counter | `session_name` | +| `windows_terminal_services_processor_time_seconds_total` | total elapsed time that all of the threads of this process used the processor to execute instructions. | Counter | `session_name` | +| `windows_terminal_services_user_time_seconds_total` | total elapsed time that this process's threads have spent executing code in user mode. Applications, environment subsystems, and integral subsystems execute in user mode. | Counter | `session_name` | +| `windows_terminal_services_pool_non_paged_bytes` | Number of bytes in the non-paged pool, an area of system memory (physical memory used by the operating system) for objects that cannot be written to disk, but must remain in physical memory as long as they are allocated. This property displays the last observed value only; it is not an average. | gauge | `session_name` | +| `windows_terminal_services_pool_paged_bytes` | Number of bytes in the paged pool, an area of system memory (physical memory used by the operating system) for objects that can be written to disk when they are not being used. This property displays the last observed value only; it is not an average. | gauge | `session_name` | +| `windows_terminal_services_private_bytes` | Current number of bytes this process has allocated that cannot be shared with other processes. | gauge | `session_name` | +| `windows_terminal_services_threads` | Number of threads currently active in this process. An instruction is the basic unit of execution in a processor, and a thread is the object that executes instructions. Every running process has at least one thread. | gauge | `session_name` | +| `windows_terminal_services_virtual_bytes` | Current size, in bytes, of the virtual address space the process is using. Use of virtual address space does not necessarily imply corresponding use of either disk or main memory pages. Virtual space is finite and, by using too much, the process can limit its ability to load libraries. | gauge | `session_name` | +| `windows_terminal_services_virtual_bytes_peak` | Maximum number of bytes of virtual address space the process has used at any one time. Use of virtual address space does not necessarily imply corresponding use of either disk or main memory pages. Virtual space is finite and, by using too much, the process might limit its ability to load libraries. | gauge | `session_name` | +| `windows_terminal_services_working_set_bytes` | Current number of bytes in the working set of this process. The working set is the set of memory pages touched recently by the threads in the process. If free memory in the computer is above a threshold, pages are left in the working set of a process even if they are not in use. When free memory falls below a threshold, pages are trimmed from working sets. If they are needed, they are then soft-faulted back into the working set before they leave main memory. | gauge | `session_name` | +| `windows_terminal_services_working_set_bytes_peak` | Maximum number of bytes in the working set of this process at any point in time. The working set is the set of memory pages touched recently by the threads in the process. If free memory in the computer is above a threshold, pages are left in the working set of a process even if they are not in use. When free memory falls below a threshold, pages are trimmed from working sets. If they are needed, they are then soft-faulted back into the working set before they leave main memory. | gauge | `session_name` | `* windows_terminal_services_connection_broker_performance_total` only collected if server has `Remote Desktop Connection Broker` role. ### Example metric -_This collector does not yet have explained examples, we would appreciate your help adding them!_ + +``` +windows_remote_fx_net_udp_packets_sent_total{session_name="RDP-Tcp 0"} 0 +# HELP windows_terminal_services_cpu_time_seconds_total Total elapsed time that this process's threads have spent executing code. +# TYPE windows_terminal_services_cpu_time_seconds_total counter +windows_terminal_services_cpu_time_seconds_total{mode="RDP-Tcp 0",session_name="privileged"} 98.4843739 +windows_terminal_services_cpu_time_seconds_total{mode="RDP-Tcp 0",session_name="processor"} 620.4687488999999 +windows_terminal_services_cpu_time_seconds_total{mode="RDP-Tcp 0",session_name="user"} 521.9843741 +# HELP windows_terminal_services_handles Total number of handles currently opened by this process. This number is the sum of the handles currently opened by each thread in this process. +# TYPE windows_terminal_services_handles gauge +windows_terminal_services_handles{session_name="RDP-Tcp 0"} 20999 +# HELP windows_terminal_services_page_fault_total Rate at which page faults occur in the threads executing in this process. A page fault occurs when a thread refers to a virtual memory page that is not in its working set in main memory. The page may not be retrieved from disk if it is on the standby list and therefore already in main memory. The page also may not be retrieved if it is in use by another process which shares the page. +# TYPE windows_terminal_services_page_fault_total counter +windows_terminal_services_page_fault_total{session_name="RDP-Tcp 0"} 1.0436271e+07 +# HELP windows_terminal_services_page_file_bytes Current number of bytes this process has used in the paging file(s). Paging files are used to store pages of memory used by the process that are not contained in other files. Paging files are shared by all processes, and lack of space in paging files can prevent other processes from allocating memory. +# TYPE windows_terminal_services_page_file_bytes gauge +windows_terminal_services_page_file_bytes{session_name="RDP-Tcp 0"} 4.310188032e+09 +# HELP windows_terminal_services_page_file_bytes_peak Maximum number of bytes this process has used in the paging file(s). Paging files are used to store pages of memory used by the process that are not contained in other files. Paging files are shared by all processes, and lack of space in paging files can prevent other processes from allocating memory. +# TYPE windows_terminal_services_page_file_bytes_peak gauge +windows_terminal_services_page_file_bytes_peak{session_name="RDP-Tcp 0"} 4.817412096e+09 +# HELP windows_terminal_services_pool_non_paged_bytes Number of bytes in the non-paged pool, an area of system memory (physical memory used by the operating system) for objects that cannot be written to disk, but must remain in physical memory as long as they are allocated. This property displays the last observed value only; it is not an average. +# TYPE windows_terminal_services_pool_non_paged_bytes gauge +windows_terminal_services_pool_non_paged_bytes{session_name="RDP-Tcp 0"} 1.325456e+06 +# HELP windows_terminal_services_pool_paged_bytes Number of bytes in the paged pool, an area of system memory (physical memory used by the operating system) for objects that can be written to disk when they are not being used. This property displays the last observed value only; it is not an average. +# TYPE windows_terminal_services_pool_paged_bytes gauge +windows_terminal_services_pool_paged_bytes{session_name="RDP-Tcp 0"} 2.4651264e+07 +# HELP windows_terminal_services_private_bytes Current number of bytes this process has allocated that cannot be shared with other processes. +# TYPE windows_terminal_services_private_bytes gauge +windows_terminal_services_private_bytes{session_name="RDP-Tcp 0"} 4.310188032e+09 +# HELP windows_terminal_services_session_info Terminal Services sessions info +# TYPE windows_terminal_services_session_info gauge +windows_terminal_services_session_info{host="",session_name="RDP-Tcp 0",state="active",user="domain\\user"} 1 +windows_terminal_services_session_info{host="",session_name="RDP-Tcp 0",state="connect_query",user="domain\\user"} 0 +windows_terminal_services_session_info{host="",session_name="RDP-Tcp 0",state="connected",user="domain\\user"} 0 +windows_terminal_services_session_info{host="",session_name="RDP-Tcp 0",state="disconnected",user="domain\\user"} 0 +windows_terminal_services_session_info{host="",session_name="RDP-Tcp 0",state="down",user="domain\\user"} 0 +windows_terminal_services_session_info{host="",session_name="RDP-Tcp 0",state="idle",user="domain\\user"} 0 +windows_terminal_services_session_info{host="",session_name="RDP-Tcp 0",state="init",user="domain\\user"} 0 +windows_terminal_services_session_info{host="",session_name="RDP-Tcp 0",state="listen",user="domain\\user"} 0 +windows_terminal_services_session_info{host="",session_name="RDP-Tcp 0",state="reset",user="domain\\user"} 0 +windows_terminal_services_session_info{host="",session_name="RDP-Tcp 0",state="shadow",user="domain\\user"} 0 +windows_terminal_services_session_info{host="",session_name="console",state="active",user=""} 0 +windows_terminal_services_session_info{host="",session_name="console",state="connect_query",user=""} 0 +windows_terminal_services_session_info{host="",session_name="console",state="connected",user=""} 1 +windows_terminal_services_session_info{host="",session_name="console",state="disconnected",user=""} 0 +windows_terminal_services_session_info{host="",session_name="console",state="down",user=""} 0 +windows_terminal_services_session_info{host="",session_name="console",state="idle",user=""} 0 +windows_terminal_services_session_info{host="",session_name="console",state="init",user=""} 0 +windows_terminal_services_session_info{host="",session_name="console",state="listen",user=""} 0 +windows_terminal_services_session_info{host="",session_name="console",state="reset",user=""} 0 +windows_terminal_services_session_info{host="",session_name="console",state="shadow",user=""} 0 +windows_terminal_services_session_info{host="",session_name="services",state="active",user=""} 0 +windows_terminal_services_session_info{host="",session_name="services",state="connect_query",user=""} 0 +windows_terminal_services_session_info{host="",session_name="services",state="connected",user=""} 0 +windows_terminal_services_session_info{host="",session_name="services",state="disconnected",user=""} 1 +windows_terminal_services_session_info{host="",session_name="services",state="down",user=""} 0 +windows_terminal_services_session_info{host="",session_name="services",state="idle",user=""} 0 +windows_terminal_services_session_info{host="",session_name="services",state="init",user=""} 0 +windows_terminal_services_session_info{host="",session_name="services",state="listen",user=""} 0 +windows_terminal_services_session_info{host="",session_name="services",state="reset",user=""} 0 +windows_terminal_services_session_info{host="",session_name="services",state="shadow",user=""} 0 +# HELP windows_terminal_services_threads Number of threads currently active in this process. An instruction is the basic unit of execution in a processor, and a thread is the object that executes instructions. Every running process has at least one thread. +# TYPE windows_terminal_services_threads gauge +windows_terminal_services_threads{session_name="RDP-Tcp 0"} 676 +# HELP windows_terminal_services_virtual_bytes Current size, in bytes, of the virtual address space the process is using. Use of virtual address space does not necessarily imply corresponding use of either disk or main memory pages. Virtual space is finite and, by using too much, the process can limit its ability to load libraries. +# TYPE windows_terminal_services_virtual_bytes gauge +windows_terminal_services_virtual_bytes{session_name="RDP-Tcp 0"} 9.3228347629568e+13 +# HELP windows_terminal_services_virtual_bytes_peak Maximum number of bytes of virtual address space the process has used at any one time. Use of virtual address space does not necessarily imply corresponding use of either disk or main memory pages. Virtual space is finite and, by using too much, the process might limit its ability to load libraries. +# TYPE windows_terminal_services_virtual_bytes_peak gauge +windows_terminal_services_virtual_bytes_peak{session_name="RDP-Tcp 0"} 9.323192164352e+13 +# HELP windows_terminal_services_working_set_bytes Current number of bytes in the working set of this process. The working set is the set of memory pages touched recently by the threads in the process. If free memory in the computer is above a threshold, pages are left in the working set of a process even if they are not in use. When free memory falls below a threshold, pages are trimmed from working sets. If they are needed, they are then soft-faulted back into the working set before they leave main memory. +# TYPE windows_terminal_services_working_set_bytes gauge +windows_terminal_services_working_set_bytes{session_name="RDP-Tcp 0"} 6.0632064e+09 +# HELP windows_terminal_services_working_set_bytes_peak Maximum number of bytes in the working set of this process at any point in time. The working set is the set of memory pages touched recently by the threads in the process. If free memory in the computer is above a threshold, pages are left in the working set of a process even if they are not in use. When free memory falls below a threshold, pages are trimmed from working sets. If they are needed, they are then soft-faulted back into the working set before they leave main memory. +# TYPE windows_terminal_services_working_set_bytes_peak gauge +windows_terminal_services_working_set_bytes_peak{session_name="RDP-Tcp 0"} 6.74854912e+09 +``` ## Useful queries -_This collector does not yet have any useful queries added, we would appreciate your help adding them!_ + +Use metrics can be combined with other metrics to create useful queries. For example, with remote_fx metrics: + +``` +windows_remote_fx_net_loss_rate * on(session_name) group_left(user) (windows_terminal_services_session_info == 1) +``` ## Alerting examples _This collector does not yet have alerting examples, we would appreciate your help adding them!_ diff --git a/pkg/collector/collector.go b/pkg/collector/collector.go index ed9dd7c3..67f315ba 100644 --- a/pkg/collector/collector.go +++ b/pkg/collector/collector.go @@ -87,6 +87,8 @@ func NewWithFlags(app *kingpin.Application) Collectors { } // NewWithConfig To be called by the external libraries for collector initialization without running kingpin.Parse +// +//goland:noinspection GoUnusedExportedFunction func NewWithConfig(logger log.Logger, config Config) Collectors { collectors := map[string]types.Collector{} collectors[ad.Name] = ad.New(logger, &config.Ad) @@ -145,6 +147,7 @@ func NewWithConfig(logger log.Logger, config Config) Collectors { collectors[time.Name] = time.New(logger, &config.Time) collectors[vmware.Name] = vmware.New(logger, &config.Vmware) collectors[vmware_blast.Name] = vmware_blast.New(logger, &config.VmwareBlast) + return New(collectors) } diff --git a/pkg/collector/config.go b/pkg/collector/config.go index 5d51841b..cb4e3804 100644 --- a/pkg/collector/config.go +++ b/pkg/collector/config.go @@ -118,6 +118,8 @@ type Config struct { } // ConfigDefaults Is an interface to be used by the external libraries. It holds all ConfigDefaults form all collectors +// +//goland:noinspection GoUnusedGlobalVariable var ConfigDefaults = Config{ Ad: ad.ConfigDefaults, Adcs: adcs.ConfigDefaults, diff --git a/pkg/collector/remote_fx/remote_fx.go b/pkg/collector/remote_fx/remote_fx.go index 1c9901ba..26bf5be5 100644 --- a/pkg/collector/remote_fx/remote_fx.go +++ b/pkg/collector/remote_fx/remote_fx.go @@ -242,7 +242,7 @@ func (c *collector) collectRemoteFXNetworkCount(ctx *types.ScrapeContext, ch cha for _, d := range dst { // only connect metrics for remote named sessions - n := strings.ToLower(d.Name) + n := strings.ToLower(normalizeSessionName(d.Name)) if n == "" || n == "services" || n == "console" { continue } @@ -250,81 +250,81 @@ func (c *collector) collectRemoteFXNetworkCount(ctx *types.ScrapeContext, ch cha c.BaseTCPRTT, prometheus.GaugeValue, utils.MilliSecToSec(d.BaseTCPRTT), - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.BaseUDPRTT, prometheus.GaugeValue, utils.MilliSecToSec(d.BaseUDPRTT), - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.CurrentTCPBandwidth, prometheus.GaugeValue, (d.CurrentTCPBandwidth*1000)/8, - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.CurrentTCPRTT, prometheus.GaugeValue, utils.MilliSecToSec(d.CurrentTCPRTT), - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.CurrentUDPBandwidth, prometheus.GaugeValue, (d.CurrentUDPBandwidth*1000)/8, - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.CurrentUDPRTT, prometheus.GaugeValue, utils.MilliSecToSec(d.CurrentUDPRTT), - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.TotalReceivedBytes, prometheus.CounterValue, d.TotalReceivedBytes, - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.TotalSentBytes, prometheus.CounterValue, d.TotalSentBytes, - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.UDPPacketsReceivedPersec, prometheus.CounterValue, d.UDPPacketsReceivedPersec, - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.UDPPacketsSentPersec, prometheus.CounterValue, d.UDPPacketsSentPersec, - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.FECRate, prometheus.GaugeValue, d.FECRate, - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.LossRate, prometheus.GaugeValue, d.LossRate, - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.RetransmissionRate, prometheus.GaugeValue, d.RetransmissionRate, - d.Name, + normalizeSessionName(d.Name), ) } return nil @@ -352,7 +352,7 @@ func (c *collector) collectRemoteFXGraphicsCounters(ctx *types.ScrapeContext, ch for _, d := range dst { // only connect metrics for remote named sessions - n := strings.ToLower(d.Name) + n := strings.ToLower(normalizeSessionName(d.Name)) if n == "" || n == "services" || n == "console" { continue } @@ -360,60 +360,65 @@ func (c *collector) collectRemoteFXGraphicsCounters(ctx *types.ScrapeContext, ch c.AverageEncodingTime, prometheus.GaugeValue, utils.MilliSecToSec(d.AverageEncodingTime), - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.FrameQuality, prometheus.GaugeValue, d.FrameQuality, - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.FramesSkippedPerSecondInsufficientResources, prometheus.CounterValue, d.FramesSkippedPerSecondInsufficientClientResources, - d.Name, + normalizeSessionName(d.Name), "client", ) ch <- prometheus.MustNewConstMetric( c.FramesSkippedPerSecondInsufficientResources, prometheus.CounterValue, d.FramesSkippedPerSecondInsufficientNetworkResources, - d.Name, + normalizeSessionName(d.Name), "network", ) ch <- prometheus.MustNewConstMetric( c.FramesSkippedPerSecondInsufficientResources, prometheus.CounterValue, d.FramesSkippedPerSecondInsufficientServerResources, - d.Name, + normalizeSessionName(d.Name), "server", ) ch <- prometheus.MustNewConstMetric( c.GraphicsCompressionratio, prometheus.GaugeValue, d.GraphicsCompressionratio, - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.InputFramesPerSecond, prometheus.CounterValue, d.InputFramesPerSecond, - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.OutputFramesPerSecond, prometheus.CounterValue, d.OutputFramesPerSecond, - d.Name, + normalizeSessionName(d.Name), ) ch <- prometheus.MustNewConstMetric( c.SourceFramesPerSecond, prometheus.CounterValue, d.SourceFramesPerSecond, - d.Name, + normalizeSessionName(d.Name), ) } return nil } + +// normalizeSessionName ensure that the session is the same between WTS API and performance counters +func normalizeSessionName(sessionName string) string { + return strings.Replace(sessionName, "RDP-tcp", "RDP-Tcp", 1) +} diff --git a/pkg/collector/terminal_services/terminal_services.go b/pkg/collector/terminal_services/terminal_services.go index 55f49884..dbfc528f 100644 --- a/pkg/collector/terminal_services/terminal_services.go +++ b/pkg/collector/terminal_services/terminal_services.go @@ -4,11 +4,14 @@ package terminal_services import ( "errors" + "fmt" "strings" + "syscall" "github.com/alecthomas/kingpin/v2" "github.com/go-kit/log" "github.com/go-kit/log/level" + "github.com/prometheus-community/windows_exporter/pkg/headers/wtsapi32" "github.com/prometheus-community/windows_exporter/pkg/perflib" "github.com/prometheus-community/windows_exporter/pkg/types" "github.com/prometheus-community/windows_exporter/pkg/wmi" @@ -52,15 +55,16 @@ type collector struct { connectionBrokerEnabled bool + hServer syscall.Handle + + SessionInfo *prometheus.Desc LocalSessionCount *prometheus.Desc ConnectionBrokerPerformance *prometheus.Desc HandleCount *prometheus.Desc PageFaultsPersec *prometheus.Desc PageFileBytes *prometheus.Desc PageFileBytesPeak *prometheus.Desc - PercentPrivilegedTime *prometheus.Desc - PercentProcessorTime *prometheus.Desc - PercentUserTime *prometheus.Desc + PercentCPUTime *prometheus.Desc PoolNonpagedBytes *prometheus.Desc PoolPagedBytes *prometheus.Desc PrivateBytes *prometheus.Desc @@ -91,7 +95,6 @@ func (c *collector) SetLogger(logger log.Logger) { func (c *collector) GetPerfCounter() ([]string, error) { return []string{ - "Terminal Services", "Terminal Services Session", "Remote Desktop Connection Broker Counterset", }, nil @@ -100,10 +103,10 @@ func (c *collector) GetPerfCounter() ([]string, error) { func (c *collector) Build() error { c.connectionBrokerEnabled = isConnectionBrokerServer(c.logger) - c.LocalSessionCount = prometheus.NewDesc( - prometheus.BuildFQName(types.Namespace, Name, "local_session_count"), - "Number of Terminal Services sessions", - []string{"session"}, + c.SessionInfo = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "session_info"), + "Terminal Services sessions info", + []string{"session_name", "user", "host", "state"}, nil, ) c.ConnectionBrokerPerformance = prometheus.NewDesc( @@ -136,22 +139,10 @@ func (c *collector) Build() error { []string{"session_name"}, nil, ) - c.PercentPrivilegedTime = prometheus.NewDesc( - prometheus.BuildFQName(types.Namespace, Name, "privileged_time_seconds_total"), - "Total elapsed time that the threads of the process have spent executing code in privileged mode.", - []string{"session_name"}, - nil, - ) - c.PercentProcessorTime = prometheus.NewDesc( - prometheus.BuildFQName(types.Namespace, Name, "processor_time_seconds_total"), - "Total elapsed time that all of the threads of this process used the processor to execute instructions.", - []string{"session_name"}, - nil, - ) - c.PercentUserTime = prometheus.NewDesc( - prometheus.BuildFQName(types.Namespace, Name, "user_time_seconds_total"), - "Total elapsed time that this process's threads have spent executing code in user mode. Applications, environment Names, and integral Names execute in user mode.", - []string{"session_name"}, + c.PercentCPUTime = prometheus.NewDesc( + prometheus.BuildFQName(types.Namespace, Name, "cpu_time_seconds_total"), + "Total elapsed time that this process's threads have spent executing code.", + []string{"mode", "session_name"}, nil, ) c.PoolNonpagedBytes = prometheus.NewDesc( @@ -202,14 +193,22 @@ func (c *collector) Build() error { []string{"session_name"}, nil, ) + + var err error + + c.hServer, err = wtsapi32.WTSOpenServer("") + if err != nil { + return fmt.Errorf("failed to open WTS server: %w", err) + } + return nil } // Collect sends the metric values for each metric // to the provided prometheus Metric channel. func (c *collector) Collect(ctx *types.ScrapeContext, ch chan<- prometheus.Metric) error { - if err := c.collectTSSessionCount(ctx, ch); err != nil { - _ = level.Error(c.logger).Log("msg", "failed collecting terminal services session count metrics", "err", err) + if err := c.collectWTSSessions(ch); err != nil { + _ = level.Error(c.logger).Log("msg", "failed collecting terminal services session infos", "err", err) return err } if err := c.collectTSSessionCounters(ctx, ch); err != nil { @@ -227,46 +226,6 @@ func (c *collector) Collect(ctx *types.ScrapeContext, ch chan<- prometheus.Metri return nil } -type perflibTerminalServices struct { - ActiveSessions float64 `perflib:"Active Sessions"` - InactiveSessions float64 `perflib:"Inactive Sessions"` - TotalSessions float64 `perflib:"Total Sessions"` -} - -func (c *collector) collectTSSessionCount(ctx *types.ScrapeContext, ch chan<- prometheus.Metric) error { - dst := make([]perflibTerminalServices, 0) - err := perflib.UnmarshalObject(ctx.PerfObjects["Terminal Services"], &dst, c.logger) - if err != nil { - return err - } - if len(dst) == 0 { - return errors.New("WMI query returned empty result set") - } - - ch <- prometheus.MustNewConstMetric( - c.LocalSessionCount, - prometheus.GaugeValue, - dst[0].ActiveSessions, - "active", - ) - - ch <- prometheus.MustNewConstMetric( - c.LocalSessionCount, - prometheus.GaugeValue, - dst[0].InactiveSessions, - "inactive", - ) - - ch <- prometheus.MustNewConstMetric( - c.LocalSessionCount, - prometheus.GaugeValue, - dst[0].TotalSessions, - "total", - ) - - return nil -} - type perflibTerminalServicesSession struct { Name string HandleCount float64 `perflib:"Handle Count"` @@ -331,22 +290,25 @@ func (c *collector) collectTSSessionCounters(ctx *types.ScrapeContext, ch chan<- d.Name, ) ch <- prometheus.MustNewConstMetric( - c.PercentPrivilegedTime, + c.PercentCPUTime, prometheus.CounterValue, d.PercentPrivilegedTime, d.Name, + "privileged", ) ch <- prometheus.MustNewConstMetric( - c.PercentProcessorTime, + c.PercentCPUTime, prometheus.CounterValue, d.PercentProcessorTime, d.Name, + "processor", ) ch <- prometheus.MustNewConstMetric( - c.PercentUserTime, + c.PercentCPUTime, prometheus.CounterValue, d.PercentUserTime, d.Name, + "user", ) ch <- prometheus.MustNewConstMetric( c.PoolNonpagedBytes, @@ -439,3 +401,36 @@ func (c *collector) collectCollectionBrokerPerformanceCounter(ctx *types.ScrapeC return nil } + +func (c *collector) collectWTSSessions(ch chan<- prometheus.Metric) error { + sessions, err := wtsapi32.WTSEnumerateSessionsEx(c.hServer, c.logger) + if err != nil { + return fmt.Errorf("failed to enumerate WTS sessions: %w", err) + } + + for _, session := range sessions { + userName := session.UserName + if session.DomainName != "" { + userName = fmt.Sprintf("%s\\%s", session.DomainName, session.UserName) + } + + for stateID, stateName := range wtsapi32.WTSSessionStates { + isState := 0.0 + if session.State == stateID { + isState = 1.0 + } + + ch <- prometheus.MustNewConstMetric( + c.SessionInfo, + prometheus.GaugeValue, + isState, + strings.Replace(session.SessionName, "#", " ", -1), + userName, + session.HostName, + stateName, + ) + } + } + + return nil +} diff --git a/pkg/collector/terminal_services/terminal_services_test.go b/pkg/collector/terminal_services/terminal_services_test.go index dac213a6..d4cad82f 100644 --- a/pkg/collector/terminal_services/terminal_services_test.go +++ b/pkg/collector/terminal_services/terminal_services_test.go @@ -8,5 +8,7 @@ import ( ) func BenchmarkCollector(b *testing.B) { + testutils.FuncBenchmarkCollector(b, terminal_services.Name, terminal_services.NewWithFlags) + } diff --git a/pkg/headers/wtsapi32/wtsapi32.go b/pkg/headers/wtsapi32/wtsapi32.go new file mode 100644 index 00000000..c2151baa --- /dev/null +++ b/pkg/headers/wtsapi32/wtsapi32.go @@ -0,0 +1,198 @@ +package wtsapi32 + +import ( + "fmt" + "syscall" + "unsafe" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "golang.org/x/sys/windows" +) + +type WTSTypeClass int + +// The valid values for the WTSTypeClass enumeration +const ( + WTSTypeProcessInfoLevel0 WTSTypeClass = iota + WTSTypeProcessInfoLevel1 + WTSTypeSessionInfoLevel1 +) + +type WTSConnectState uint32 + +const ( + // wtsActive A user is logged on to the WinStation. This state occurs when a user is signed in and actively connected to the device. + wtsActive WTSConnectState = iota + // wtsConnected The WinStation is connected to the client. + wtsConnected + // wtsConnectQuery The WinStation is in the process of connecting to the client. + wtsConnectQuery + // wtsShadow The WinStation is shadowing another WinStation. + wtsShadow + // wtsDisconnected The WinStation is active but the client is disconnected. + // This state occurs when a user is signed in but not actively connected to the device, such as when the user has chosen to exit to the lock screen. + wtsDisconnected + // wtsIdle The WinStation is waiting for a client to connect. + wtsIdle + // wtsListen The WinStation is listening for a connection. A listener session waits for requests for new client connections. + // No user is logged on a listener session. A listener session cannot be reset, shadowed, or changed to a regular client session. + wtsListen + // wtsReset The WinStation is being reset. + wtsReset + // wtsDown The WinStation is down due to an error. + wtsDown + // wtsInit The WinStation is initializing. + wtsInit +) + +// WTSSessionInfo1w contains information about a session on a Remote Desktop Session Host (RD Session Host) server. +// docs: https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/ns-wtsapi32-wts_session_info_1w +type wtsSessionInfo1 struct { + // ExecEnvID An identifier that uniquely identifies the session within the list of sessions returned by the WTSEnumerateSessionsEx function. + ExecEnvID uint32 + // State A value of the WTSConnectState enumeration type that specifies the connection state of a Remote Desktop Services session. + State uint32 + // SessionID A session identifier assigned by the RD Session Host server, RD Virtualization Host server, or virtual machine. + SessionID uint32 + // pSessionName A pointer to a null-terminated string that contains the name of this session. For example, "services", "console", or "RDP-Tcp#0". + pSessionName *uint16 + // pHostName A pointer to a null-terminated string that contains the name of the computer that the session is running on. + // If the session is running directly on an RD Session Host server or RD Virtualization Host server, the string contains NULL. + // If the session is running on a virtual machine, the string contains the name of the virtual machine. + pHostName *uint16 + // pUserName A pointer to a null-terminated string that contains the name of the user who is logged on to the session. + // If no user is logged on to the session, the string contains NULL. + pUserName *uint16 + // pDomainName A pointer to a null-terminated string that contains the domain name of the user who is logged on to the session. + // If no user is logged on to the session, the string contains NULL. + pDomainName *uint16 + // pFarmName A pointer to a null-terminated string that contains the name of the farm that the virtual machine is joined to. + // If the session is not running on a virtual machine that is joined to a farm, the string contains NULL. + pFarmName *uint16 +} + +type WTSSession struct { + ExecEnvID uint32 + State WTSConnectState + SessionID uint32 + SessionName string + HostName string + UserName string + DomainName string + FarmName string +} + +var ( + wtsapi32 = windows.NewLazySystemDLL("wtsapi32.dll") + + procWTSOpenServerEx = wtsapi32.NewProc("WTSOpenServerExW") + procWTSEnumerateSessionsEx = wtsapi32.NewProc("WTSEnumerateSessionsExW") + procWTSFreeMemoryEx = wtsapi32.NewProc("WTSFreeMemoryExW") + procWTSCloseServer = wtsapi32.NewProc("WTSCloseServer") + + WTSSessionStates = map[WTSConnectState]string{ + wtsActive: "active", + wtsConnected: "connected", + wtsConnectQuery: "connect_query", + wtsShadow: "shadow", + wtsDisconnected: "disconnected", + wtsIdle: "idle", + wtsListen: "listen", + wtsReset: "reset", + wtsDown: "down", + wtsInit: "init", + } +) + +func WTSOpenServer(server string) (syscall.Handle, error) { + var ( + err error + serverName *uint16 + ) + + if server != "" { + serverName, err = syscall.UTF16PtrFromString(server) + if err != nil { + return syscall.InvalidHandle, err + } + } + + r1, _, err := procWTSOpenServerEx.Call(uintptr(unsafe.Pointer(serverName))) + serverHandle := syscall.Handle(r1) + + if serverHandle == syscall.InvalidHandle { + return syscall.InvalidHandle, err + } + + return serverHandle, nil +} + +func WTSCloseServer(server syscall.Handle) error { + _, _, err := procWTSCloseServer.Call(uintptr(server)) + if err != nil { + return fmt.Errorf("failed to close server: %w", err) + } + + return err +} + +func WTSFreeMemoryEx(class WTSTypeClass, pMemory uintptr, NumberOfEntries uint32) error { + _, _, err := procWTSFreeMemoryEx.Call( + uintptr(class), + pMemory, + uintptr(NumberOfEntries), + ) + + return err +} + +func WTSEnumerateSessionsEx(server syscall.Handle, logger log.Logger) ([]WTSSession, error) { + var sessionInfoPointer uintptr + var count uint32 + + pLevel := uint32(1) + r1, _, err := procWTSEnumerateSessionsEx.Call( + uintptr(server), + uintptr(unsafe.Pointer(&pLevel)), + uintptr(0), + uintptr(unsafe.Pointer(&sessionInfoPointer)), + uintptr(unsafe.Pointer(&count)), + ) + + if r1 != 1 { + return nil, err + } + + if sessionInfoPointer != 0 { + defer func(class WTSTypeClass, pMemory uintptr, NumberOfEntries uint32) { + err := WTSFreeMemoryEx(class, pMemory, NumberOfEntries) + if err != nil { + _ = level.Error(logger).Log("msg", "failed to free memory", "err", err) + } + }(WTSTypeSessionInfoLevel1, sessionInfoPointer, count) + } + + var sizeTest wtsSessionInfo1 + sessionSize := unsafe.Sizeof(sizeTest) + + sessions := make([]WTSSession, 0, count) + for i := uint32(0); i < count; i++ { + curPtr := unsafe.Pointer(sessionInfoPointer + (uintptr(i) * sessionSize)) + data := (*wtsSessionInfo1)(curPtr) + + sessionInfo := WTSSession{ + ExecEnvID: data.ExecEnvID, + State: WTSConnectState(data.State), + SessionID: data.SessionID, + SessionName: windows.UTF16PtrToString(data.pSessionName), + HostName: windows.UTF16PtrToString(data.pHostName), + UserName: windows.UTF16PtrToString(data.pUserName), + DomainName: windows.UTF16PtrToString(data.pDomainName), + FarmName: windows.UTF16PtrToString(data.pFarmName), + } + sessions = append(sessions, sessionInfo) + } + + return sessions, nil +}