├── .github └── workflows │ ├── linux-test.yml │ ├── static-analysis.yml │ └── unsupported.yml ├── LICENSE.md ├── README.md ├── client.go ├── client_linux.go ├── client_linux_integration_test.go ├── client_linux_test.go ├── client_others.go ├── go.mod ├── go.sum ├── stats.go └── stats_linux.go /.github/workflows/linux-test.yml: -------------------------------------------------------------------------------- 1 | name: Linux Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | go-version: ["1.22"] 17 | os: [ubuntu-latest, macos-latest] 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - name: Set up Go 22 | uses: actions/setup-go@v2 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | id: go 26 | 27 | - name: Check out code into the Go module directory 28 | uses: actions/checkout@v2 29 | 30 | # Run basic tests, we just want to make sure there is parity on Linux and 31 | # macOS. 32 | - name: Run tests 33 | run: go test ./... 34 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | go-version: ["1.22"] 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | id: go 24 | 25 | - name: Check out code into the Go module directory 26 | uses: actions/checkout@v2 27 | 28 | - name: Install staticcheck 29 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 30 | 31 | - name: Print staticcheck version 32 | run: staticcheck -version 33 | 34 | - name: Run staticcheck 35 | run: staticcheck ./... 36 | 37 | - name: Run go vet 38 | run: go vet ./... 39 | -------------------------------------------------------------------------------- /.github/workflows/unsupported.yml: -------------------------------------------------------------------------------- 1 | name: Unsupported 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | go-version: ["1.22"] 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Set up Go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: ${{ matrix.go-version }} 24 | id: go 25 | 26 | - name: Check out code into the Go module directory 27 | uses: actions/checkout@v2 28 | 29 | # Although this package doesn't support Windows, we want to verify that 30 | # everything builds properly. 31 | - name: Verify build for non-UNIX platforms 32 | run: go build 33 | env: 34 | GOOS: windows 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (C) 2016-2022 Matt Layher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # taskstats [![Test Status](https://github.com/mdlayher/taskstats/workflows/Test/badge.svg)](https://github.com/mdlayher/taskstats/actions) [![Go Reference](https://pkg.go.dev/badge/github.com/mdlayher/taskstats.svg)](https://pkg.go.dev/github.com/mdlayher/taskstats) [![Go Report Card](https://goreportcard.com/badge/github.com/mdlayher/taskstats)](https://goreportcard.com/report/github.com/mdlayher/taskstats) 2 | 3 | Package `taskstats` provides access to Linux's taskstats interface, for sending 4 | per-task, per-process, and cgroup statistics from the kernel to userspace. MIT 5 | Licensed. 6 | 7 | For more information on taskstats, please see: 8 | - https://www.kernel.org/doc/Documentation/accounting/cgroupstats.txt 9 | - https://www.kernel.org/doc/Documentation/accounting/taskstats.txt 10 | - https://www.kernel.org/doc/Documentation/accounting/taskstats-struct.txt 11 | - https://andrestc.com/post/linux-delay-accounting/ 12 | 13 | ## Notes 14 | 15 | * When instrumenting Go programs, use either the `taskstats.Self()` or 16 | `taskstats.TGID()` method. Using the `PID()` method on multithreaded 17 | programs, including Go programs, will produce inaccurate results. 18 | 19 | * Access to taskstats requires that the application have at least `CAP_NET_RAW` 20 | capability (see 21 | [capabilities(7)](http://man7.org/linux/man-pages/man7/capabilities.7.html)). 22 | Otherwise, the application must be run as root. 23 | 24 | * If running the application in a container (e.g. via Docker), it cannot be run 25 | in a network namespace -- usually this means that host networking must be 26 | used. 27 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Package taskstats provides access to Linux's taskstats interface, for sending 2 | // per-task, per-process, and cgroup statistics from the kernel to userspace. 3 | // 4 | // For more information on taskstats, please see: 5 | // - https://www.kernel.org/doc/Documentation/accounting/cgroupstats.txt 6 | // - https://www.kernel.org/doc/Documentation/accounting/taskstats.txt 7 | // - https://www.kernel.org/doc/Documentation/accounting/taskstats-struct.txt 8 | // - https://andrestc.com/post/linux-delay-accounting/ 9 | package taskstats 10 | 11 | import ( 12 | "io" 13 | "os" 14 | ) 15 | 16 | // A Client provides access to Linux taskstats information. 17 | // 18 | // Some Client operations require elevated privileges. 19 | type Client struct { 20 | c osClient 21 | } 22 | 23 | // New creates a new Client. 24 | func New() (*Client, error) { 25 | c, err := newClient() 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return &Client{ 31 | c: c, 32 | }, nil 33 | } 34 | 35 | // CGroupStats retrieves cgroup statistics for the cgroup specified by path. 36 | // Path should be a CPU cgroup path found in sysfs, such as: 37 | // - /sys/fs/cgroup/cpu 38 | // - /sys/fs/cgroup/cpu/docker 39 | // - /sys/fs/cgroup/cpu/docker/(hexadecimal identifier) 40 | func (c *Client) CGroupStats(path string) (*CGroupStats, error) { 41 | return c.c.CGroupStats(path) 42 | } 43 | 44 | // Self is a convenience method for retrieving statistics about the current 45 | // process. 46 | func (c *Client) Self() (*Stats, error) { 47 | return c.c.TGID(os.Getpid()) 48 | } 49 | 50 | // PID retrieves statistics about a process, identified by its PID. 51 | func (c *Client) PID(pid int) (*Stats, error) { 52 | return c.c.PID(pid) 53 | } 54 | 55 | // TGID retrieves statistics about a thread group, identified by its TGID. 56 | func (c *Client) TGID(tgid int) (*Stats, error) { 57 | return c.c.TGID(tgid) 58 | } 59 | 60 | // Close releases resources used by a Client. 61 | func (c *Client) Close() error { 62 | return c.c.Close() 63 | } 64 | 65 | // An osClient is the operating system-specific implementation of Client. 66 | type osClient interface { 67 | io.Closer 68 | CGroupStats(path string) (*CGroupStats, error) 69 | PID(pid int) (*Stats, error) 70 | TGID(tgid int) (*Stats, error) 71 | } 72 | -------------------------------------------------------------------------------- /client_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package taskstats 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "unsafe" 10 | 11 | "github.com/mdlayher/genetlink" 12 | "github.com/mdlayher/netlink" 13 | "github.com/mdlayher/netlink/nlenc" 14 | "golang.org/x/sys/unix" 15 | ) 16 | 17 | // Fixed structure sizes. 18 | const sizeofCGroupStats = int(unsafe.Sizeof(unix.CGroupStats{})) 19 | 20 | var _ osClient = &client{} 21 | 22 | // A client is a Linux-specific taskstats client. 23 | type client struct { 24 | c *genetlink.Conn 25 | family genetlink.Family 26 | } 27 | 28 | // newClient opens a connection to the taskstats family using 29 | // generic netlink. 30 | func newClient() (*client, error) { 31 | c, err := genetlink.Dial(nil) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | // Best effort. 37 | _ = c.SetOption(netlink.ExtendedAcknowledge, true) 38 | 39 | return initClient(c) 40 | } 41 | 42 | // initClient is the internal client constructor used in some tests. 43 | func initClient(c *genetlink.Conn) (*client, error) { 44 | f, err := c.GetFamily(unix.TASKSTATS_GENL_NAME) 45 | if err != nil { 46 | _ = c.Close() 47 | return nil, err 48 | } 49 | 50 | return &client{ 51 | c: c, 52 | family: f, 53 | }, nil 54 | } 55 | 56 | // Close implements osClient. 57 | func (c *client) Close() error { 58 | return c.c.Close() 59 | } 60 | 61 | // PID implements osClient. 62 | func (c *client) PID(pid int) (*Stats, error) { 63 | return c.getStats(pid, unix.TASKSTATS_CMD_ATTR_PID, unix.TASKSTATS_TYPE_AGGR_PID) 64 | } 65 | 66 | // TGID implements osClient. 67 | func (c *client) TGID(tgid int) (*Stats, error) { 68 | return c.getStats(tgid, unix.TASKSTATS_CMD_ATTR_TGID, unix.TASKSTATS_TYPE_AGGR_TGID) 69 | } 70 | 71 | func (c *client) getStats(id int, cmdAttr, typeAggr uint16) (*Stats, error) { 72 | // Query taskstats for information using a specific ID. 73 | attrs := []netlink.Attribute{{ 74 | Type: cmdAttr, 75 | Data: nlenc.Uint32Bytes(uint32(id)), 76 | }} 77 | 78 | msg, err := c.execute(unix.TASKSTATS_CMD_GET, attrs) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | return parseMessage(*msg, typeAggr) 84 | } 85 | 86 | // CGroupStats implements osClient. 87 | func (c *client) CGroupStats(path string) (*CGroupStats, error) { 88 | // Open cgroup path so its file descriptor can be passed to taskstats. 89 | f, err := os.Open(path) 90 | if err != nil { 91 | return nil, err 92 | } 93 | defer f.Close() 94 | 95 | // Query taskstats for cgroup information using the file descriptor. 96 | attrs := []netlink.Attribute{{ 97 | Type: unix.CGROUPSTATS_CMD_ATTR_FD, 98 | Data: nlenc.Uint32Bytes(uint32(f.Fd())), 99 | }} 100 | 101 | msg, err := c.execute(unix.CGROUPSTATS_CMD_GET, attrs) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | return parseCGroupMessage(*msg) 107 | } 108 | 109 | // execute executes a single generic netlink command and returns its response. 110 | func (c *client) execute(cmd uint8, attrs []netlink.Attribute) (*genetlink.Message, error) { 111 | b, err := netlink.MarshalAttributes(attrs) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | msg := genetlink.Message{ 117 | Header: genetlink.Header{ 118 | Command: cmd, 119 | Version: unix.TASKSTATS_VERSION, 120 | }, 121 | Data: b, 122 | } 123 | 124 | msgs, err := c.c.Execute(msg, c.family.ID, netlink.Request) 125 | if err != nil { 126 | // We don't want to expose netlink errors directly to callers, so unpack 127 | // the error for use with os.IsPermission and similar. 128 | oerr, ok := err.(*netlink.OpError) 129 | if !ok { 130 | // Expect all errors to conform to netlink.OpError. 131 | return nil, fmt.Errorf("taskstats: netlink operation returned non-netlink error (please file a bug: https://github.com/mdlayher/taskstats): %v", err) 132 | } 133 | 134 | return nil, oerr.Err 135 | } 136 | 137 | if l := len(msgs); l != 1 { 138 | return nil, fmt.Errorf("taskstats: unexpected number of response messages: %d", l) 139 | } 140 | 141 | return &msgs[0], nil 142 | } 143 | 144 | // parseCGroupMessage attempts to parse a CGroupStats structure from a generic netlink message. 145 | func parseCGroupMessage(m genetlink.Message) (*CGroupStats, error) { 146 | attrs, err := netlink.UnmarshalAttributes(m.Data) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | for _, a := range attrs { 152 | // Only parse cgroupstats structure. 153 | if a.Type != unix.CGROUPSTATS_TYPE_CGROUP_STATS { 154 | continue 155 | } 156 | 157 | // Verify that the byte slice containing a unix.CGroupStats is the 158 | // size expected by this package, so we don't blindly cast the 159 | // byte slice into a structure of the wrong size. 160 | if want, got := sizeofCGroupStats, len(a.Data); want != got { 161 | return nil, fmt.Errorf("unexpected cgroupstats structure size, want %d, got %d", want, got) 162 | } 163 | 164 | cs := *(*unix.CGroupStats)(unsafe.Pointer(&a.Data[0])) 165 | return parseCGroupStats(cs) 166 | } 167 | 168 | // No taskstats response found. 169 | return nil, os.ErrNotExist 170 | } 171 | 172 | // parseMessage attempts to parse a Stats structure from a generic netlink message. 173 | func parseMessage(m genetlink.Message, typeAggr uint16) (*Stats, error) { 174 | attrs, err := netlink.UnmarshalAttributes(m.Data) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | for _, a := range attrs { 180 | // Only parse ID+stats structure. 181 | if a.Type != typeAggr { 182 | continue 183 | } 184 | 185 | nattrs, err := netlink.UnmarshalAttributes(a.Data) 186 | if err != nil { 187 | return nil, err 188 | } 189 | 190 | for _, na := range nattrs { 191 | // Only parse Stats element since caller would already have ID. 192 | if na.Type != unix.TASKSTATS_TYPE_STATS { 193 | continue 194 | } 195 | 196 | // Assume the kernel returns a structure compatible with the 197 | // taskstats struct and cast directly. 198 | return parseStats(*(*unix.Taskstats)(unsafe.Pointer(&na.Data[0]))) 199 | } 200 | } 201 | 202 | // No taskstats response found. 203 | return nil, os.ErrNotExist 204 | } 205 | -------------------------------------------------------------------------------- /client_linux_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package taskstats_test 5 | 6 | import ( 7 | "os" 8 | "testing" 9 | 10 | "github.com/mdlayher/taskstats" 11 | ) 12 | 13 | func TestLinuxClientIntegration(t *testing.T) { 14 | c, err := taskstats.New() 15 | if err != nil { 16 | t.Fatalf("failed to open client: %v", err) 17 | } 18 | defer c.Close() 19 | 20 | t.Run("self", func(t *testing.T) { 21 | testSelfStats(t, c) 22 | }) 23 | 24 | t.Run("cgroup", func(t *testing.T) { 25 | testCGroupStats(t, c) 26 | }) 27 | } 28 | 29 | func testSelfStats(t *testing.T, c *taskstats.Client) { 30 | stats, err := c.Self() 31 | if err != nil { 32 | if os.IsPermission(err) { 33 | t.Skipf("taskstats requires elevated permission: %v", err) 34 | } 35 | 36 | t.Fatalf("failed to retrieve self stats: %v", err) 37 | } 38 | 39 | if stats.BeginTime.IsZero() { 40 | t.Fatalf("unexpected zero begin time") 41 | } 42 | 43 | // TODO(mdlayher): verify more fields? 44 | } 45 | 46 | func testCGroupStats(t *testing.T, c *taskstats.Client) { 47 | // TODO(mdlayher): try to verify these in some meaningful way, but for now, 48 | // no error means the structure is valid, which works. 49 | _, err := c.CGroupStats("/sys/fs/cgroup/cpu") 50 | if err == nil { 51 | return 52 | } 53 | 54 | if os.IsNotExist(err) { 55 | t.Skipf("did not find cgroup CPU stats: %v", err) 56 | } 57 | 58 | t.Fatalf("failed to retrieve cgroup stats: %v", err) 59 | } 60 | -------------------------------------------------------------------------------- /client_linux_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package taskstats 5 | 6 | import ( 7 | "os" 8 | "testing" 9 | "time" 10 | "unsafe" 11 | 12 | "github.com/google/go-cmp/cmp" 13 | "github.com/google/go-cmp/cmp/cmpopts" 14 | "github.com/mdlayher/genetlink" 15 | "github.com/mdlayher/genetlink/genltest" 16 | "github.com/mdlayher/netlink" 17 | "github.com/mdlayher/netlink/nltest" 18 | "golang.org/x/sys/unix" 19 | ) 20 | 21 | const sizeofV8 = int(unsafe.Offsetof(unix.Taskstats{}.Thrashing_count)) 22 | 23 | func TestLinuxClientCGroupStatsBadMessages(t *testing.T) { 24 | f, done := tempFile(t) 25 | defer done() 26 | 27 | tests := []struct { 28 | name string 29 | msgs []genetlink.Message 30 | }{ 31 | { 32 | name: "no messages", 33 | msgs: []genetlink.Message{}, 34 | }, 35 | { 36 | name: "two messages", 37 | msgs: []genetlink.Message{{}, {}}, 38 | }, 39 | { 40 | name: "incorrect cgroupstats size", 41 | msgs: []genetlink.Message{{ 42 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 43 | Type: unix.CGROUPSTATS_TYPE_CGROUP_STATS, 44 | Data: []byte{0xff}, 45 | }}), 46 | }}, 47 | }, 48 | } 49 | 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 53 | return tt.msgs, nil 54 | }) 55 | defer c.Close() 56 | 57 | _, err := c.CGroupStats(f) 58 | if err == nil { 59 | t.Fatal("an error was expected, but none occurred") 60 | } 61 | }) 62 | } 63 | } 64 | 65 | func TestLinuxClientCGroupStatsIsNotExist(t *testing.T) { 66 | tests := []struct { 67 | name string 68 | msg genetlink.Message 69 | createFile bool 70 | }{ 71 | { 72 | name: "no file", 73 | }, 74 | { 75 | name: "no attributes", 76 | msg: genetlink.Message{}, 77 | createFile: true, 78 | }, 79 | { 80 | name: "no aggr+pid", 81 | msg: genetlink.Message{ 82 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 83 | Type: unix.TASKSTATS_TYPE_NULL, 84 | }}), 85 | }, 86 | createFile: true, 87 | }, 88 | { 89 | name: "no stats", 90 | msg: genetlink.Message{ 91 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 92 | // Wrong type for cgroup stats. 93 | Type: unix.TASKSTATS_TYPE_AGGR_PID, 94 | }}), 95 | }, 96 | createFile: true, 97 | }, 98 | } 99 | 100 | for _, tt := range tests { 101 | t.Run(tt.name, func(t *testing.T) { 102 | // Only create the file when requested, so we can also exercise the 103 | // case where the file doesn't exist. 104 | var f string 105 | if tt.createFile { 106 | file, done := tempFile(t) 107 | defer done() 108 | f = file 109 | } 110 | 111 | c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 112 | return []genetlink.Message{tt.msg}, nil 113 | }) 114 | defer c.Close() 115 | 116 | _, err := c.CGroupStats(f) 117 | if !os.IsNotExist(err) { 118 | t.Fatalf("expected is not exist, but got: %v", err) 119 | } 120 | }) 121 | } 122 | } 123 | 124 | func TestLinuxClientCGroupStatsOK(t *testing.T) { 125 | f, done := tempFile(t) 126 | defer done() 127 | 128 | stats := unix.CGroupStats{ 129 | Sleeping: 1, 130 | Running: 2, 131 | Stopped: 3, 132 | Uninterruptible: 4, 133 | Io_wait: 5, 134 | } 135 | 136 | fn := func(gm genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 137 | attrs, err := netlink.UnmarshalAttributes(gm.Data) 138 | if err != nil { 139 | t.Fatalf("failed to unmarshal netlink attributes: %v", err) 140 | } 141 | 142 | if l := len(attrs); l != 1 { 143 | t.Fatalf("unexpected number of attributes: %d", l) 144 | } 145 | 146 | if diff := cmp.Diff(unix.CGROUPSTATS_CMD_ATTR_FD, int(attrs[0].Type)); diff != "" { 147 | t.Fatalf("unexpected netlink attribute type (-want +got):\n%s", diff) 148 | } 149 | 150 | // Cast unix.CGroupStats structure into a byte array with the correct size. 151 | b := *(*[sizeofCGroupStats]byte)(unsafe.Pointer(&stats)) 152 | 153 | return []genetlink.Message{{ 154 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 155 | Type: unix.CGROUPSTATS_TYPE_CGROUP_STATS, 156 | Data: b[:], 157 | }}), 158 | }}, nil 159 | } 160 | 161 | c := testClient(t, genltest.CheckRequest( 162 | familyID, 163 | unix.CGROUPSTATS_CMD_GET, 164 | netlink.Request, 165 | fn, 166 | )) 167 | defer c.Close() 168 | 169 | newStats, err := c.CGroupStats(f) 170 | if err != nil { 171 | t.Fatalf("failed to get stats: %v", err) 172 | } 173 | 174 | cstats, err := parseCGroupStats(stats) 175 | if err != nil { 176 | t.Fatalf("failed to parse cgroup stats: %v", err) 177 | } 178 | 179 | if diff := cmp.Diff(cstats, newStats); diff != "" { 180 | t.Fatalf("unexpected cgroupstats structure (-want +got):\n%s", diff) 181 | } 182 | } 183 | 184 | func TestLinuxClientPIDBadMessages(t *testing.T) { 185 | tests := []struct { 186 | name string 187 | msgs []genetlink.Message 188 | }{ 189 | { 190 | name: "no messages", 191 | msgs: []genetlink.Message{}, 192 | }, 193 | { 194 | name: "two messages", 195 | msgs: []genetlink.Message{{}, {}}, 196 | }, 197 | } 198 | 199 | for _, tt := range tests { 200 | t.Run(tt.name, func(t *testing.T) { 201 | c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 202 | return tt.msgs, nil 203 | }) 204 | defer c.Close() 205 | 206 | _, err := c.PID(1) 207 | if err == nil { 208 | t.Fatal("an error was expected, but none occurred") 209 | } 210 | }) 211 | } 212 | } 213 | 214 | func TestLinuxClientTGIDBadMessages(t *testing.T) { 215 | tests := []struct { 216 | name string 217 | msgs []genetlink.Message 218 | }{ 219 | { 220 | name: "no messages", 221 | msgs: []genetlink.Message{}, 222 | }, 223 | { 224 | name: "two messages", 225 | msgs: []genetlink.Message{{}, {}}, 226 | }, 227 | } 228 | 229 | for _, tt := range tests { 230 | t.Run(tt.name, func(t *testing.T) { 231 | c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 232 | return tt.msgs, nil 233 | }) 234 | defer c.Close() 235 | 236 | _, err := c.TGID(1) 237 | if err == nil { 238 | t.Fatal("an error was expected, but none occurred") 239 | } 240 | }) 241 | } 242 | } 243 | 244 | func TestLinuxClientPIDIsNotExist(t *testing.T) { 245 | tests := []struct { 246 | name string 247 | msg genetlink.Message 248 | }{ 249 | { 250 | name: "no attributes", 251 | msg: genetlink.Message{}, 252 | }, 253 | { 254 | name: "no aggr+pid", 255 | msg: genetlink.Message{ 256 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 257 | Type: unix.TASKSTATS_TYPE_NULL, 258 | }}), 259 | }, 260 | }, 261 | { 262 | name: "no stats", 263 | msg: genetlink.Message{ 264 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 265 | Type: unix.TASKSTATS_TYPE_AGGR_PID, 266 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 267 | Type: unix.TASKSTATS_TYPE_NULL, 268 | }}), 269 | }}), 270 | }, 271 | }, 272 | } 273 | 274 | for _, tt := range tests { 275 | t.Run(tt.name, func(t *testing.T) { 276 | c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 277 | return []genetlink.Message{tt.msg}, nil 278 | }) 279 | defer c.Close() 280 | 281 | _, err := c.PID(1) 282 | if !os.IsNotExist(err) { 283 | t.Fatalf("expected is not exist, but got: %v", err) 284 | } 285 | }) 286 | } 287 | } 288 | 289 | func TestLinuxClientTGIDIsNotExist(t *testing.T) { 290 | tests := []struct { 291 | name string 292 | msg genetlink.Message 293 | }{ 294 | { 295 | name: "no attributes", 296 | msg: genetlink.Message{}, 297 | }, 298 | { 299 | name: "no aggr+tgid", 300 | msg: genetlink.Message{ 301 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 302 | Type: unix.TASKSTATS_TYPE_NULL, 303 | }}), 304 | }, 305 | }, 306 | { 307 | name: "no stats", 308 | msg: genetlink.Message{ 309 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 310 | Type: unix.TASKSTATS_TYPE_AGGR_TGID, 311 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 312 | Type: unix.TASKSTATS_TYPE_NULL, 313 | }}), 314 | }}), 315 | }, 316 | }, 317 | } 318 | 319 | for _, tt := range tests { 320 | t.Run(tt.name, func(t *testing.T) { 321 | c := testClient(t, func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 322 | return []genetlink.Message{tt.msg}, nil 323 | }) 324 | defer c.Close() 325 | 326 | _, err := c.TGID(1) 327 | if !os.IsNotExist(err) { 328 | t.Fatalf("expected is not exist, but got: %v", err) 329 | } 330 | }) 331 | } 332 | } 333 | 334 | func TestLinuxClientPIDOK(t *testing.T) { 335 | pid := os.Getpid() 336 | 337 | stats := unix.Taskstats{ 338 | Version: unix.TASKSTATS_VERSION, 339 | Ac_pid: uint32(pid), 340 | Ac_etime: 0, 341 | Ac_utime: 1, 342 | Ac_stime: 2, 343 | Ac_btime: 3, 344 | Ac_minflt: 4, 345 | Ac_majflt: 5, 346 | Cpu_count: 6, 347 | Cpu_delay_total: 7, 348 | Blkio_count: 8, 349 | Blkio_delay_total: 9, 350 | Swapin_count: 10, 351 | Swapin_delay_total: 11, 352 | Freepages_count: 12, 353 | Freepages_delay_total: 13, 354 | } 355 | 356 | fn := func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 357 | // Cast unix.Taskstats structure into a byte array with the correct size. 358 | b := *(*[sizeofV8]byte)(unsafe.Pointer(&stats)) 359 | 360 | return []genetlink.Message{{ 361 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 362 | Type: unix.TASKSTATS_TYPE_AGGR_PID, 363 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 364 | Type: unix.TASKSTATS_TYPE_STATS, 365 | Data: b[:], 366 | }}), 367 | }}), 368 | }}, nil 369 | } 370 | 371 | c := testClient(t, genltest.CheckRequest( 372 | familyID, 373 | unix.TASKSTATS_CMD_GET, 374 | netlink.Request, 375 | fn, 376 | )) 377 | defer c.Close() 378 | 379 | newStats, err := c.PID(pid) 380 | if err != nil { 381 | t.Fatalf("failed to get stats: %v", err) 382 | } 383 | 384 | tstats := Stats{ 385 | ElapsedTime: time.Duration(0), 386 | UserCPUTime: time.Microsecond * 1, 387 | SystemCPUTime: time.Microsecond * 2, 388 | BeginTime: time.Unix(3, 0), 389 | MinorPageFaults: 4, 390 | MajorPageFaults: 5, 391 | CPUDelayCount: 6, 392 | CPUDelay: time.Nanosecond * 7, 393 | BlockIODelayCount: 8, 394 | BlockIODelay: time.Nanosecond * 9, 395 | SwapInDelayCount: 10, 396 | SwapInDelay: time.Nanosecond * 11, 397 | FreePagesDelayCount: 12, 398 | FreePagesDelay: time.Nanosecond * 13, 399 | } 400 | 401 | opts := []cmp.Option{ 402 | cmpopts.IgnoreUnexported(tstats), 403 | } 404 | 405 | if diff := cmp.Diff(&tstats, newStats, opts...); diff != "" { 406 | t.Fatalf("unexpected taskstats structure (-want +got):\n%s", diff) 407 | } 408 | } 409 | 410 | func TestLinuxClientTGIDOK(t *testing.T) { 411 | tgid := os.Getpid() 412 | 413 | stats := unix.Taskstats{ 414 | Version: unix.TASKSTATS_VERSION, 415 | Ac_pid: uint32(tgid), 416 | Ac_etime: 0, 417 | Ac_utime: 1, 418 | Ac_stime: 2, 419 | Ac_btime: 3, 420 | Ac_minflt: 4, 421 | Ac_majflt: 5, 422 | Cpu_count: 6, 423 | Cpu_delay_total: 7, 424 | Blkio_count: 8, 425 | Blkio_delay_total: 9, 426 | Swapin_count: 10, 427 | Swapin_delay_total: 11, 428 | Freepages_count: 12, 429 | Freepages_delay_total: 13, 430 | } 431 | 432 | fn := func(_ genetlink.Message, _ netlink.Message) ([]genetlink.Message, error) { 433 | // Cast unix.Taskstats structure into a byte array with the correct size. 434 | b := *(*[sizeofV8]byte)(unsafe.Pointer(&stats)) 435 | 436 | return []genetlink.Message{{ 437 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 438 | Type: unix.TASKSTATS_TYPE_AGGR_TGID, 439 | Data: nltest.MustMarshalAttributes([]netlink.Attribute{{ 440 | Type: unix.TASKSTATS_TYPE_STATS, 441 | Data: b[:], 442 | }}), 443 | }}), 444 | }}, nil 445 | } 446 | 447 | c := testClient(t, genltest.CheckRequest( 448 | familyID, 449 | unix.TASKSTATS_CMD_GET, 450 | netlink.Request, 451 | fn, 452 | )) 453 | defer c.Close() 454 | 455 | newStats, err := c.TGID(tgid) 456 | if err != nil { 457 | t.Fatalf("failed to get stats: %v", err) 458 | } 459 | 460 | tstats := Stats{ 461 | ElapsedTime: time.Duration(0), 462 | UserCPUTime: time.Microsecond * 1, 463 | SystemCPUTime: time.Microsecond * 2, 464 | BeginTime: time.Unix(3, 0), 465 | MinorPageFaults: 4, 466 | MajorPageFaults: 5, 467 | CPUDelayCount: 6, 468 | CPUDelay: time.Nanosecond * 7, 469 | BlockIODelayCount: 8, 470 | BlockIODelay: time.Nanosecond * 9, 471 | SwapInDelayCount: 10, 472 | SwapInDelay: time.Nanosecond * 11, 473 | FreePagesDelayCount: 12, 474 | FreePagesDelay: time.Nanosecond * 13, 475 | } 476 | 477 | opts := []cmp.Option{ 478 | cmpopts.IgnoreUnexported(tstats), 479 | } 480 | 481 | if diff := cmp.Diff(&tstats, newStats, opts...); diff != "" { 482 | t.Fatalf("unexpected taskstats structure (-want +got):\n%s", diff) 483 | } 484 | } 485 | 486 | const familyID = 20 487 | 488 | func testClient(t *testing.T, fn genltest.Func) *client { 489 | family := genetlink.Family{ 490 | ID: familyID, 491 | Version: unix.TASKSTATS_GENL_VERSION, 492 | Name: unix.TASKSTATS_GENL_NAME, 493 | } 494 | 495 | conn := genltest.Dial(genltest.ServeFamily(family, fn)) 496 | 497 | c, err := initClient(conn) 498 | if err != nil { 499 | t.Fatalf("failed to open client: %v", err) 500 | } 501 | 502 | return c 503 | } 504 | 505 | func tempFile(t *testing.T) (string, func()) { 506 | f, err := os.CreateTemp("", "taskstats-test") 507 | if err != nil { 508 | t.Fatalf("failed to create temporary file: %v", err) 509 | } 510 | _ = f.Close() 511 | 512 | return f.Name(), func() { 513 | if err := os.Remove(f.Name()); err != nil { 514 | t.Fatalf("failed to remove temporary file: %v", err) 515 | } 516 | } 517 | } 518 | -------------------------------------------------------------------------------- /client_others.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package taskstats 5 | 6 | import ( 7 | "fmt" 8 | "runtime" 9 | ) 10 | 11 | // errUnimplemented is returned by all functions on platforms that 12 | // cannot make use of taskstats. 13 | var errUnimplemented = fmt.Errorf("taskstats not implemented on %s/%s", 14 | runtime.GOOS, runtime.GOARCH) 15 | 16 | var _ osClient = &client{} 17 | 18 | // A client is an unimplemented taskstats client. 19 | type client struct{} 20 | 21 | // newClient always returns an error. 22 | func newClient() (*client, error) { 23 | return nil, errUnimplemented 24 | } 25 | 26 | // Close implements osClient. 27 | func (c *client) Close() error { 28 | return errUnimplemented 29 | } 30 | 31 | // CGroupStats implements osClient. 32 | func (c *client) CGroupStats(path string) (*CGroupStats, error) { 33 | return nil, errUnimplemented 34 | } 35 | 36 | // PID implements osClient. 37 | func (c *client) PID(pid int) (*Stats, error) { 38 | return nil, errUnimplemented 39 | } 40 | 41 | // TGID implements osClient. 42 | func (c *client) TGID(tgid int) (*Stats, error) { 43 | return nil, errUnimplemented 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mdlayher/taskstats 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/google/go-cmp v0.6.0 7 | github.com/mdlayher/genetlink v1.3.2 8 | github.com/mdlayher/netlink v1.7.2 9 | golang.org/x/sys v0.28.0 10 | ) 11 | 12 | require ( 13 | github.com/josharian/native v1.1.0 // indirect 14 | github.com/mdlayher/socket v0.5.1 // indirect 15 | golang.org/x/net v0.32.0 // indirect 16 | golang.org/x/sync v0.10.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 4 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 5 | github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= 6 | github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= 7 | github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= 8 | github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= 9 | github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= 10 | github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= 11 | golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= 12 | golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= 13 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 14 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 15 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 16 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 17 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | package taskstats 2 | 3 | import "time" 4 | 5 | // CGroupStats contains statistics for tasks of an individual cgroup. 6 | type CGroupStats struct { 7 | Sleeping uint64 8 | Running uint64 9 | Stopped uint64 10 | Uninterruptible uint64 11 | IOWait uint64 12 | } 13 | 14 | // Stats contains statistics for an individual task. 15 | type Stats struct { 16 | BeginTime time.Time 17 | ElapsedTime time.Duration 18 | UserCPUTime time.Duration 19 | SystemCPUTime time.Duration 20 | MinorPageFaults uint64 21 | MajorPageFaults uint64 22 | CPUDelayCount uint64 23 | CPUDelay time.Duration 24 | BlockIODelayCount uint64 25 | BlockIODelay time.Duration 26 | SwapInDelayCount uint64 27 | SwapInDelay time.Duration 28 | FreePagesDelayCount uint64 29 | FreePagesDelay time.Duration 30 | ThrashingDelayCount uint64 31 | ThrashingDelay time.Duration 32 | } 33 | -------------------------------------------------------------------------------- /stats_linux.go: -------------------------------------------------------------------------------- 1 | package taskstats 2 | 3 | import ( 4 | "time" 5 | 6 | "golang.org/x/sys/unix" 7 | ) 8 | 9 | // parseCGroupStats parses a raw cgroupstats structure into a cleaner form. 10 | func parseCGroupStats(cs unix.CGroupStats) (*CGroupStats, error) { 11 | // This conversion isn't really necessary for this type, but it allows us 12 | // to export a structure that isn't defined in a platform-specific way. 13 | stats := &CGroupStats{ 14 | Sleeping: cs.Sleeping, 15 | Running: cs.Running, 16 | Stopped: cs.Stopped, 17 | Uninterruptible: cs.Uninterruptible, 18 | IOWait: cs.Io_wait, 19 | } 20 | 21 | return stats, nil 22 | } 23 | 24 | // parseStats parses a raw taskstats structure into a cleaner form. 25 | func parseStats(ts unix.Taskstats) (*Stats, error) { 26 | stats := &Stats{ 27 | BeginTime: time.Unix(int64(ts.Ac_btime), 0), 28 | ElapsedTime: microseconds(ts.Ac_etime), 29 | UserCPUTime: microseconds(ts.Ac_utime), 30 | SystemCPUTime: microseconds(ts.Ac_stime), 31 | MinorPageFaults: ts.Ac_minflt, 32 | MajorPageFaults: ts.Ac_majflt, 33 | CPUDelayCount: ts.Cpu_count, 34 | CPUDelay: nanoseconds(ts.Cpu_delay_total), 35 | BlockIODelayCount: ts.Blkio_count, 36 | BlockIODelay: nanoseconds(ts.Blkio_delay_total), 37 | SwapInDelayCount: ts.Swapin_count, 38 | SwapInDelay: nanoseconds(ts.Swapin_delay_total), 39 | FreePagesDelayCount: ts.Freepages_count, 40 | FreePagesDelay: nanoseconds(ts.Freepages_delay_total), 41 | ThrashingDelayCount: ts.Thrashing_count, 42 | ThrashingDelay: nanoseconds(ts.Thrashing_delay_total), 43 | } 44 | 45 | return stats, nil 46 | } 47 | 48 | // nanoseconds converts a raw number of nanoseconds into a time.Duration. 49 | func nanoseconds(t uint64) time.Duration { 50 | return time.Duration(t) * time.Nanosecond 51 | } 52 | 53 | // microseconds converts a raw number of microseconds into a time.Duration. 54 | func microseconds(t uint64) time.Duration { 55 | return time.Duration(t) * time.Microsecond 56 | } 57 | --------------------------------------------------------------------------------