├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── NOTICE
├── README.md
├── ci
├── Makefile
└── go.mk
├── go.mod
├── go.sum
├── init
└── com.amazon.ec2.monitoring.agents.cpuutilization.plist
├── lib
└── ec2macossystemmonitor
│ ├── cpuutilization.go
│ ├── logging.go
│ ├── relayd.go
│ ├── relayd_test.go
│ ├── serial.go
│ ├── util.go
│ └── util_test.go
├── main.go
└── scripts
└── setup-ec2monitoring
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: gomod
4 | directory: /
5 | schedule:
6 | interval: weekly
7 | day: tuesday
8 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | buildtest:
13 | strategy:
14 | matrix:
15 | os: [ubuntu-latest, macos-latest]
16 | runs-on: ${{ matrix.os }}
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Setup Go
20 | uses: actions/setup-go@v3
21 | with:
22 | go-version: stable
23 | cache: true
24 | - name: Fetch deps
25 | run: make -f ci/Makefile --include-dir ci ci-deps
26 | - name: Do Build
27 | run: make -f ci/Makefile --include-dir ci ci-build
28 | - name: Run Tests
29 | run: make -f ci/Makefile --include-dir ci ci-test
30 | lint:
31 | runs-on: ubuntu-latest
32 | steps:
33 | - uses: actions/checkout@v3
34 | - name: Setup Go
35 | uses: actions/setup-go@v3
36 | with:
37 | go-version: stable
38 | cache: true
39 | - name: golangci-lint
40 | uses: golangci/golangci-lint-action@v3
41 | - name: Run Linter
42 | run: make -f ci/Makefile --include-dir ci ci-lint
43 |
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | *.log
4 | tmp/
5 | /ec2-macos-system-monitor
6 | /cpuutilization_darwin*
7 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4 | documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7 | information to effectively respond to your bug report or contribution.
8 |
9 |
10 | ## Reporting Bugs/Feature Requests
11 |
12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13 |
14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 |
23 | ## Contributing via Pull Requests
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
25 |
26 | 1. You are working against the latest source on the *main* branch.
27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
29 |
30 | To send us a pull request, please:
31 |
32 | 1. Fork the repository.
33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
34 | 3. Ensure local tests pass.
35 | 4. Commit to your fork using clear commit messages.
36 | 5. Send us a pull request, answering any default questions in the pull request interface.
37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
38 |
39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
41 |
42 |
43 | ## Finding contributions to work on
44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
45 |
46 |
47 | ## Code of Conduct
48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
50 | opensource-codeofconduct@amazon.com with any additional questions or comments.
51 |
52 |
53 | ## Security issue notifications
54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
55 |
56 |
57 | ## Licensing
58 |
59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | GOPKGPATH = github.com/aws/ec2-macos-system-monitor
2 |
3 | .PHONY: all
4 | all: build test
5 |
6 | .PHONY: build
7 | # we use go-psutil which builds against headers
8 | build: CGO_ENABLED=1
9 | build: cpuutilization_darwin
10 |
11 | .PHONY: test
12 | test: GO_TEST_FLAGS=-cover
13 | test: gotest
14 |
15 | .PHONY: lint
16 | lint: golint
17 |
18 | .PHONY: format fmt
19 | format fmt: goimports
20 |
21 | cpuutilization_darwin: cpuutilization_darwin_amd64 cpuutilization_darwin_arm64
22 | lipo -create -output $@ $^
23 |
24 | cpuutilization_darwin_%:
25 | GOOS=darwin GOARCH=$* \
26 | go build -o cpuutilization_darwin_$* -ldflags="-s -w" .
27 |
28 | .PHONY: clean
29 | clean:
30 | go clean
31 | -rm -f cpuutilization_darwin*
32 |
33 | include ci/go.mk
34 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Amazon EC2 System Monitor for macOS
2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Amazon EC2 System Monitor for macOS
2 |
3 |
4 | ## Overview
5 | Amazon EC2 System Monitor for macOS is a small agent that runs on every [mac1.metal](https://aws.amazon.com/ec2/instance-types/mac/)
6 | instance to provide on-instance metrics in CloudWatch. Currently the primary use case for this agent is to send CPU utilization
7 | metrics. This uses a serial connection attached via the [AWS Nitro System](https://aws.amazon.com/ec2/nitro/)
8 | and is forwarded to CloudWatch for the instance automatically.
9 |
10 | ## Usage
11 | The agent is installed and enabled by default for all AMIs vended by AWS. It logs to
12 | `/var/log/amazon/ec2/system-monitoring.log` and can be updated via [Homebrew](https://github.com/aws/homebrew-aws).
13 |
14 | ### Managing the monitor with `setup-ec2monitoring`
15 | The package includes a shell script for enabling, disabling, and listing the current status of the agent
16 | according to `launchd`.
17 |
18 | ### Viewing the agent status
19 | To view the status of the agent:
20 | ```bash
21 | sudo setup-ec2monitoring list
22 | ```
23 |
24 | ### Enabling the agent
25 | To enable/install ec2-macos-system-monitor:
26 | ```bash
27 | sudo setup-ec2monitoring enable
28 | ```
29 | This must be run if updating to a new version to ensure it is scheduled to run again.
30 |
31 | ### Disabling the agent
32 | To disable ec2-macos-system-monitor:
33 | ```bash
34 | sudo setup-ec2monitoring disable
35 | ```
36 |
37 | ## Design
38 | The Amazon EC2 System Monitor for macOS uses multiple goroutines to manage two primary mechanisms:
39 | 1. The serial relay takes data from a UNIX domain socket and writes the data in a payload via a basic wire protocol.
40 | 2. Runs a ticker that reads CPU utilization and sends the CPU usage percentage to the UNIX domain socket.
41 |
42 | This design allows for multiple different processes to write to the serial device while allowing one process to
43 | always have the device open for writing.
44 |
45 | ### Wire protocol
46 | The wire protocol's primary purpose is to ensure the payload is complete by wrapping the payload in a checksum.
47 | There is a tag which is used as a namespace to ensure the reader knows what type of data is being written. The data
48 | itself which, in this case is typically the CPU utilization as a percentage of total usage the second field. Finally, a
49 | boolean is set specifying if the data should be compressed before sending. A checksum is then computed on this payload
50 | and included along with the payload. This payload with checksum allows the receiver to ensure that all the data was
51 | correctly received as well as inform if the data should be decompressed before parsing.
52 |
53 | ## Security
54 |
55 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
56 |
57 | ## License
58 |
59 | This project is licensed under the [Apache License, version 2.0](https://www.apache.org/licenses/LICENSE-2.0).
60 |
61 |
--------------------------------------------------------------------------------
/ci/Makefile:
--------------------------------------------------------------------------------
1 | include go.mk
2 |
3 | ci-deps: goget
4 | ci-build: gobuild
5 | ci-test: gotest
6 | ci-lint: golint
7 |
--------------------------------------------------------------------------------
/ci/go.mk:
--------------------------------------------------------------------------------
1 | GO = go
2 | GOIMPORTS = goimports $(if $(GOPKGPATH),-local $(GOPKGPATH))
3 | GOLANGCILINT = golangci-lint
4 |
5 | export GOOS
6 | export GOARCH
7 | export CGO_ENABLED
8 |
9 | goget:
10 | $(GO) get -t ./...
11 |
12 | gobuild:
13 | $(GO) build $(GO_BUILD_FLAGS) $(V) ./...
14 |
15 | gotest:
16 | $(GO) test $(GO_TEST_FLAGS) $(V) ./...
17 |
18 | goimports gofmt:
19 | $(GOIMPORTS) -w .
20 |
21 | golint:
22 | $(GOLANGCILINT) run
23 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/aws/ec2-macos-system-monitor
2 |
3 | go 1.23.0
4 |
5 | toolchain go1.23.3
6 |
7 | require (
8 | github.com/shirou/gopsutil v3.21.11+incompatible
9 | go.bug.st/serial v1.6.3
10 | )
11 |
12 | require (
13 | github.com/creack/goselect v0.1.2 // indirect
14 | github.com/go-ole/go-ole v1.3.0 // indirect
15 | github.com/tklauser/go-sysconf v0.3.15 // indirect
16 | github.com/tklauser/numcpus v0.10.0 // indirect
17 | github.com/yusufpapurcu/wmi v1.2.4 // indirect
18 | golang.org/x/sys v0.31.0 // indirect
19 | )
20 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
2 | github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
6 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
7 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
10 | github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
11 | github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
12 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
13 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
14 | github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
15 | github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
16 | github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
17 | github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
18 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
19 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
20 | go.bug.st/serial v1.6.3 h1:S3OG1bH+IDyokVndKrzwxI9ywiGBd8sWOn08dzSqEQI=
21 | go.bug.st/serial v1.6.3/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
22 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
23 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
24 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
25 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
27 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
28 |
--------------------------------------------------------------------------------
/init/com.amazon.ec2.monitoring.agents.cpuutilization.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | KeepAlive
6 |
7 | SuccessfulExit
8 |
9 |
10 | Label
11 | com.amazon.ec2.monitoring.agents.cpuutilization
12 | ProgramArguments
13 |
14 | /usr/local/libexec/send-cpu-utilization
15 |
16 | StartInterval
17 | 59
18 | StandardErrorPath
19 | /var/log/amazon/ec2/system-monitoring.log
20 | StandardOutPath
21 | /var/log/amazon/ec2/system-monitoring.log
22 |
23 |
24 |
--------------------------------------------------------------------------------
/lib/ec2macossystemmonitor/cpuutilization.go:
--------------------------------------------------------------------------------
1 | package ec2macossystemmonitor
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 |
7 | "github.com/shirou/gopsutil/cpu"
8 | )
9 |
10 | // RunningCpuUsage gathers the value expected for CloudWatch but allows long running measurement. This is intended for
11 | // usage where repeated calls will take place.
12 | func RunningCpuUsage() (s string, err error) {
13 | percent, err := cpu.Percent(0, false)
14 | if err != nil {
15 | return "", fmt.Errorf("ec2macossystemmonitor: error while getting cpu stats: %s", err)
16 | }
17 | return strconv.FormatFloat(percent[0], 'f', -1, 64), nil
18 | }
19 |
--------------------------------------------------------------------------------
/lib/ec2macossystemmonitor/logging.go:
--------------------------------------------------------------------------------
1 | package ec2macossystemmonitor
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "log/syslog"
7 | "os"
8 | )
9 |
10 | // Logger contains booleans for where to log, a tag used in syslog and the syslog Writer itself.
11 | type Logger struct {
12 | LogToStdout bool
13 | LogToSystemLog bool
14 | Tag string
15 | SystemLog *syslog.Writer
16 | }
17 |
18 | // defaultLogInterval is the number of writes before emitting a log entry 10 = once every 10 minutes
19 | const DefaultLogInterval = 10
20 |
21 | // StatusLogBuffer contains a message format string and a written bytes for this format string for flushing the logs
22 | type StatusLogBuffer struct {
23 | Message string
24 | Written int64
25 | }
26 |
27 | // IntervalLogger is a special logger that provides a way to only log at a certain interval.
28 | type IntervalLogger struct {
29 | logger Logger
30 | LogInterval int
31 | Counter int
32 | Message string
33 | }
34 |
35 | // NewLogger creates a new logger. Logger writes using the LOG_LOCAL0 facility by default if system logging is enabled.
36 | func NewLogger(tag string, systemLog bool, stdout bool) (logger *Logger, err error) {
37 | // Set up system logging, if enabled
38 | syslogger := &syslog.Writer{}
39 | if systemLog {
40 | syslogger, err = syslog.New(syslog.LOG_LOCAL0, tag)
41 | if err != nil {
42 | return &Logger{}, fmt.Errorf("ec2macossystemmonitor: unable to create new syslog logger: %s\n", err)
43 | }
44 | }
45 | // Set log to use microseconds, if stdout is enabled
46 | if stdout {
47 | log.SetFlags(log.LstdFlags | log.Lmicroseconds)
48 | }
49 |
50 | return &Logger{
51 | LogToSystemLog: systemLog,
52 | LogToStdout: stdout,
53 | Tag: tag,
54 | SystemLog: syslogger,
55 | }, nil
56 | }
57 |
58 | // Info writes info to stdout and/or the system log.
59 | func (l *Logger) Info(v ...interface{}) {
60 | if l.LogToStdout {
61 | log.Print(v...)
62 | }
63 | if l.LogToSystemLog {
64 | _ = l.SystemLog.Info(fmt.Sprint(v...))
65 | }
66 | }
67 |
68 | // Infof writes formatted info to stdout and/or the system log.
69 | func (l *Logger) Infof(format string, v ...interface{}) {
70 | if l.LogToStdout {
71 | log.Printf(format, v...)
72 | }
73 | if l.LogToSystemLog {
74 | _ = l.SystemLog.Info(fmt.Sprintf(format, v...))
75 | }
76 | }
77 |
78 | // Warn writes a warning to stdout and/or the system log.
79 | func (l *Logger) Warn(v ...interface{}) {
80 | if l.LogToStdout {
81 | log.Print(v...)
82 | }
83 | if l.LogToSystemLog {
84 | _ = l.SystemLog.Warning(fmt.Sprint(v...))
85 | }
86 | }
87 |
88 | // Warnf writes a formatted warning to stdout and/or the system log.
89 | func (l *Logger) Warnf(format string, v ...interface{}) {
90 | if l.LogToStdout {
91 | log.Printf(format, v...)
92 | }
93 | if l.LogToSystemLog {
94 | _ = l.SystemLog.Warning(fmt.Sprintf(format, v...))
95 | }
96 | }
97 |
98 | // Error writes an error to stdout and/or the system log.
99 | func (l *Logger) Error(v ...interface{}) {
100 | if l.LogToStdout {
101 | log.Print(v...)
102 | }
103 | if l.LogToSystemLog {
104 | _ = l.SystemLog.Err(fmt.Sprint(v...))
105 | }
106 | }
107 |
108 | // Errorf writes a formatted error to stdout and/or the system log.
109 | func (l *Logger) Errorf(format string, v ...interface{}) {
110 | if l.LogToStdout {
111 | log.Printf(format, v...)
112 | }
113 | if l.LogToSystemLog {
114 | _ = l.SystemLog.Err(fmt.Sprintf(format, v...))
115 | }
116 | }
117 |
118 | // Fatal writes an error to stdout and/or the system log then exits 1.
119 | func (l *Logger) Fatal(v ...interface{}) {
120 | l.Error(v...)
121 | os.Exit(1)
122 | }
123 |
124 | // Fatalf writes a formatted error to stdout and/or the system log then exits 1.
125 | func (l *Logger) Fatalf(format string, v ...interface{}) {
126 | l.Errorf(format, v...)
127 | os.Exit(1)
128 | }
129 |
130 | // PushToInterval adds to the counter and sets the Message, care should be taken to retrieve the Message before setting since
131 | // its overwritten
132 | func (t *IntervalLogger) PushToInterval(i int, message string) (flushed bool) {
133 | t.Counter = +i
134 | t.Message = message
135 | if t.Counter > t.LogInterval {
136 | t.logger.Info(message)
137 | t.Counter = 0
138 | return true
139 | }
140 | return false
141 | }
142 |
--------------------------------------------------------------------------------
/lib/ec2macossystemmonitor/relayd.go:
--------------------------------------------------------------------------------
1 | package ec2macossystemmonitor
2 |
3 | import (
4 | "bytes"
5 | "compress/zlib"
6 | "encoding/base64"
7 | "encoding/json"
8 | "fmt"
9 | "hash/adler32"
10 | "net"
11 | "os"
12 | "sync/atomic"
13 | "time"
14 | )
15 |
16 | const SocketTimeout = 5 * time.Second
17 |
18 | // DefaultRelaydSocketPath is the default socket for relayd listener.
19 | const DefaultRelaydSocketPath = "/tmp/.ec2monitoring.sock"
20 |
21 | // CheckSocketExists is a helper function to quickly check if the service UDS
22 | // exists.
23 | func CheckSocketExists(socketPath string) (exists bool) {
24 | return fileExists(socketPath)
25 | }
26 |
27 | // BuildMessage takes a tag along with data for the tag and builds a byte slice to be sent to the relay.
28 | //
29 | // The tag is used as a way to namespace various payloads that are supported. Data is the payload and its format is
30 | // specific to each tag. Each payload has the option to be compressed and this flag is part of the envelope created for
31 | // sending data. The slice of bytes is passed back to the caller to allow flexibility to log the bytes if desired before
32 | // passing to the relay via PassToRelayd
33 | func BuildMessage(tag string, data string, compress bool) ([]byte, error) {
34 | payload := SerialPayload{
35 | Tag: tag,
36 | Compress: compress,
37 | Data: data,
38 | }
39 |
40 | // This determines if the data will be passed in as provided or zlib compressed and then base64 encoded
41 | // Some payload will exceed the limit of what can be sent on the serial device, so compression allows more data
42 | // to be sent. base64 encoding allows safe characters only to be passed on the device
43 | if compress {
44 | var b bytes.Buffer
45 | w, err := zlib.NewWriterLevel(&b, 9)
46 | if err != nil {
47 | return nil, fmt.Errorf("ec2macossystemmonitor: couldn't get compression writer: %w", err)
48 | }
49 | _, err = w.Write([]byte(data))
50 | if err != nil {
51 | return nil, fmt.Errorf("ec2macossystemmonitor: couldn't copy compressed data: %w", err)
52 | }
53 | err = w.Close()
54 | if err != nil {
55 | return nil, fmt.Errorf("ec2macossystemmonitor: couldn't close compressor: %w", err)
56 | }
57 |
58 | payload.Data = base64.StdEncoding.EncodeToString(b.Bytes())
59 | }
60 |
61 | // Marshal the payload to wrap in the relay output message.
62 | payloadBytes, err := json.Marshal(payload)
63 | if err != nil {
64 | return nil, fmt.Errorf("ec2macossystemmonitor: %w", err)
65 | }
66 |
67 | messageBytes, err := json.Marshal(SerialMessage{
68 | Checksum: adler32.Checksum(payloadBytes),
69 | Payload: string(payloadBytes),
70 | })
71 | if err != nil {
72 | return nil, fmt.Errorf("ec2macossystemmonitor: marshal: %w", err)
73 | }
74 |
75 | // FIXME: message shouldn't append the newline, that's up to clients to
76 | // decide (ie: flushing data as needed to clients/servers).
77 | messageBytes = append(messageBytes, "\n"...)
78 |
79 | return messageBytes, nil
80 | }
81 |
82 | // PassToRelayd takes a byte slice and writes it to a UNIX socket to send for relaying.
83 | func PassToRelayd(messageBytes []byte) (n int, err error) {
84 | // Make sure we have socket to connect to.
85 | if !fileExists(DefaultRelaydSocketPath) {
86 | return 0, fmt.Errorf("ec2macossystemmonitor: %s does not exist, cannot send message: %s", DefaultRelaydSocketPath, string(messageBytes))
87 | }
88 |
89 | // Connect and relay!
90 | sock, err := net.Dial("unix", DefaultRelaydSocketPath)
91 | if err != nil {
92 | return 0, fmt.Errorf("cec2macossystemmonitor: could not connect to %s: %s", DefaultRelaydSocketPath, err)
93 | }
94 | defer sock.Close()
95 |
96 | n, err = sock.Write(messageBytes)
97 | if err != nil {
98 | return n, fmt.Errorf("ec2macossystemmonitor: error while writing to socket: %s", err)
99 | }
100 |
101 | return n, nil
102 | }
103 |
104 | // SendMessage takes a tag along with data for the tag and writes to a UNIX socket to send for relaying. This is provided
105 | // for convenience to allow quick sending of data to the relay. It calls BuildMessage and then PassToRelayd in order.
106 | func SendMessage(tag string, data string, compress bool) (n int, err error) {
107 | msgBytes, err := BuildMessage(tag, data, compress)
108 | if err != nil {
109 | return 0, fmt.Errorf("ec2macossystemmonitor: error while building message bytes: %w", err)
110 | }
111 |
112 | return PassToRelayd(msgBytes)
113 | }
114 |
115 | // SerialRelay manages client & listener to relay recieved messages to a serial
116 | // connection.
117 | type SerialRelay struct {
118 | // serialConnection is the managed serial device connection for writing
119 | // (ie: relayed output).
120 | serialConnection *SerialConnection
121 | // listener handles connections to relay received messages to the configured
122 | // serialConnection.
123 | listener net.Listener
124 | // ReadyToClose is the channel for communicating the need to close
125 | // connections.
126 | //
127 | // TODO: use context as replacement for cancellation
128 | ReadyToClose chan bool
129 | }
130 |
131 | // NewRelay creates an instance of the relay server and returns a SerialRelay for manual closing.
132 | //
133 | // The SerialRelay returned from NewRelay is designed to be used in a go routine by using StartRelay. This allows the
134 | // caller to handle OS Signals and other events for clean shutdown rather than relying upon defer calls.
135 | func NewRelay(serialDevice string) (relay SerialRelay, err error) {
136 | const socketPath = DefaultRelaydSocketPath
137 |
138 | // Create a serial connection
139 | serCon, err := NewSerialConnection(serialDevice)
140 | if err != nil {
141 | return SerialRelay{}, fmt.Errorf("relayd: failed to build a connection to serial interface: %w", err)
142 | }
143 |
144 | // Remove
145 | if err = os.RemoveAll(socketPath); err != nil {
146 | if _, ok := err.(*os.PathError); ok {
147 | // Help guide that the SocketPath is invalid
148 | return SerialRelay{}, fmt.Errorf("relayd: unable to clean %s: %w", socketPath, err)
149 | } else {
150 | // Unknown issue, return the error directly
151 | return SerialRelay{}, err
152 | }
153 |
154 | }
155 |
156 | // Create the UDS listener.
157 | addr, err := net.ResolveUnixAddr("unix", DefaultRelaydSocketPath)
158 | if err != nil {
159 | return SerialRelay{}, fmt.Errorf("relayd: unable to resolve address: %w", err)
160 | }
161 | listener, err := net.ListenUnix("unix", addr)
162 | if err != nil {
163 | return SerialRelay{}, fmt.Errorf("relayd: unable to listen on socket: %w", err)
164 | }
165 |
166 | return SerialRelay{
167 | listener: listener,
168 | serialConnection: serCon,
169 | ReadyToClose: make(chan bool),
170 | }, nil
171 | }
172 |
173 | // setListenerDeadline will set a deadline on the underlying net.Listener if
174 | // supported, no-op otherwise.
175 | func (relay *SerialRelay) setListenerDeadline(t time.Time) error {
176 | deadliner, ok := relay.listener.(interface{
177 | SetDeadline(time.Time) error
178 | })
179 | if ok {
180 | return deadliner.SetDeadline(t)
181 | }
182 |
183 | return nil
184 | }
185 |
186 | // StartRelay starts the listener ahdn handles connections for the serial relay.
187 | //
188 | // This is a server implementation of the SerialRelay so it logs to a provided
189 | // logger, and empty logger can be provided to stop logging if desired. This
190 | // function is designed to be used in a go routine so logging may be the only
191 | // way to get data about behavior while it is running. The resources can be shut
192 | // down by sending true to the ReadyToClose channel. This invokes CleanUp()
193 | // which is exported in case the caller desires to call it instead.
194 | func (relay *SerialRelay) StartRelay(logger *Logger, relayStatus *StatusLogBuffer) {
195 | // Accept new connections, dispatching them to relayServer in a goroutine.
196 | for {
197 | err := relay.setListenerDeadline(time.Now().Add(SocketTimeout))
198 | if err != nil {
199 | logger.Fatal("Unable to set deadline on socket:", err)
200 | }
201 |
202 | socCon, err := relay.listener.Accept()
203 | // Look for signal to exit, otherwise keep going, check the error only if we aren't supposed to shutdown
204 | select {
205 | case <-relay.ReadyToClose:
206 | logger.Info("[relayd] requested to shutdown")
207 | // Clean up resources manually
208 | relay.CleanUp()
209 | // Return to stop the connections from continuing
210 | return
211 | default:
212 | // If ReadyToClose has not been sent, then check for errors, handle timeouts, otherwise process
213 | if err != nil {
214 | if er, ok := err.(net.Error); ok && er.Timeout() {
215 | // This is just a timeout, break the loop and go to the top to start listening again
216 | continue
217 | } else {
218 | // This is some other error, for Accept(), its a fatal error if we can't Accept()
219 | logger.Fatal("Unable to start accepting on socket:", err)
220 | }
221 | }
222 |
223 | }
224 |
225 | // Write the date to the relay
226 | written, err := relay.serialConnection.RelayData(socCon)
227 | if err != nil {
228 | logger.Errorf("Failed to send data: %s\n", err)
229 | }
230 |
231 | // Increment the counter
232 | atomic.AddInt64(&relayStatus.Written, int64(written))
233 | }
234 | }
235 |
236 | // CleanUp manually closes the connections for a Serial Relay. This is called from StartRelay when true is sent on
237 | // ReadyToClose so it should only be called separately if closing outside of that mechanism.
238 | func (relay *SerialRelay) CleanUp() {
239 | _ = relay.listener.Close()
240 | _ = relay.serialConnection.Close()
241 |
242 | _ = os.RemoveAll(DefaultRelaydSocketPath)
243 | }
244 |
--------------------------------------------------------------------------------
/lib/ec2macossystemmonitor/relayd_test.go:
--------------------------------------------------------------------------------
1 | package ec2macossystemmonitor
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | // TestBuildMessage creates some basic tests to ensure the options result in the correct bytes
9 | func TestBuildMessage(t *testing.T) {
10 | // emptyTestBytes is the byte slice of a payload tag "test" and empty payload
11 | emptyTestBytes := []byte{123, 34, 99, 115, 117, 109, 34, 58, 53, 48, 55, 53, 55, 57, 55, 52, 52, 44, 34, 112, 97, 121, 108, 111, 97, 100, 34, 58, 34, 123, 92, 34, 116, 97, 103, 92, 34, 58, 92, 34, 116, 101, 115, 116, 92, 34, 44, 92, 34, 99, 111, 109, 112, 114, 101, 115, 115, 92, 34, 58, 102, 97, 108, 115, 101, 44, 92, 34, 100, 97, 116, 97, 92, 34, 58, 92, 34, 92, 34, 125, 34, 125, 10}
12 | // emptyTestBytes is the byte slice of a payload tag "test" and empty payload while compressing ""
13 | emptyCompressedTestBytes := []byte{123, 34, 99, 115, 117, 109, 34, 58, 49, 54, 57, 53, 52, 54, 49, 51, 56, 44, 34, 112, 97, 121, 108, 111, 97, 100, 34, 58, 34, 123, 92, 34, 116, 97, 103, 92, 34, 58, 92, 34, 116, 101, 115, 116, 92, 34, 44, 92, 34, 99, 111, 109, 112, 114, 101, 115, 115, 92, 34, 58, 116, 114, 117, 101, 44, 92, 34, 100, 97, 116, 97, 92, 34, 58, 92, 34, 101, 78, 111, 66, 65, 65, 68, 47, 47, 119, 65, 65, 65, 65, 69, 61, 92, 34, 125, 34, 125, 10}
14 | basicCPUTestBytes := []byte{123, 34, 99, 115, 117, 109, 34, 58, 50, 49, 49, 56, 49, 57, 50, 57, 53, 48, 44, 34, 112, 97, 121, 108, 111, 97, 100, 34, 58, 34, 123, 92, 34, 116, 97, 103, 92, 34, 58, 92, 34, 99, 112, 117, 117, 116, 105, 108, 92, 34, 44, 92, 34, 99, 111, 109, 112, 114, 101, 115, 115, 92, 34, 58, 102, 97, 108, 115, 101, 44, 92, 34, 100, 97, 116, 97, 92, 34, 58, 92, 34, 50, 46, 48, 92, 34, 125, 34, 125, 10}
15 |
16 | type args struct {
17 | tag string
18 | data string
19 | compress bool
20 | }
21 | tests := []struct {
22 | name string
23 | args args
24 | want []byte
25 | wantErr bool
26 | }{
27 | {"Empty Message", args{"test", "", false}, emptyTestBytes, false},
28 | {"Empty Message Compressed", args{"test", "", true}, emptyCompressedTestBytes, false},
29 | {"Basic CPU Test", args{"cpuutil", "2.0", false}, basicCPUTestBytes, false},
30 | }
31 | for _, tt := range tests {
32 | t.Run(tt.name, func(t *testing.T) {
33 | got, err := BuildMessage(tt.args.tag, tt.args.data, tt.args.compress)
34 | if (err != nil) != tt.wantErr {
35 | t.Errorf("BuildMessage() error = %v, wantErr %v", err, tt.wantErr)
36 | return
37 | }
38 | if !reflect.DeepEqual(got, tt.want) {
39 | t.Errorf("BuildMessage() got = %v, want %v", got, tt.want)
40 | }
41 | })
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/ec2macossystemmonitor/serial.go:
--------------------------------------------------------------------------------
1 | package ec2macossystemmonitor
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "net"
8 |
9 | "go.bug.st/serial"
10 | )
11 |
12 | // SerialConnection is the container for passing the ReadWriteCloser for serial connections.
13 | type SerialConnection struct {
14 | port serial.Port
15 | }
16 |
17 | // SerialPayload is the container for a payload that is written to serial device.
18 | type SerialPayload struct {
19 | // Tag is the namespace that separates different types of data on the device
20 | Tag string `json:"tag"`
21 | // Compress determines if the data is compressed and base64 encoded
22 | Compress bool `json:"compress"`
23 | // Data is the actual data payload to be consumed
24 | Data string `json:"data"`
25 | }
26 |
27 | // SerialMessage is the container to actually send on the serial connection, contains checksum of SerialPayload to
28 | // provide additional assurance the entire payload has been written.
29 | type SerialMessage struct {
30 | // Checksum is the checksum used to ensure all data was received
31 | Checksum uint32 `json:"csum"`
32 | // Payload is the SerialPayload in json format
33 | Payload string `json:"payload"`
34 | }
35 |
36 | // NewSerialConnection creates a serial device connection and returns a reference to the connection.
37 | func NewSerialConnection(device string) (conn *SerialConnection, err error) {
38 | // Set up options for serial device, take defaults for now on everything else
39 | mode := &serial.Mode{
40 | BaudRate: 115200,
41 | }
42 |
43 | // Attempt to avoid opening a non-existent serial connection
44 | if !fileExists(device) {
45 | return nil, fmt.Errorf("ec2macossystemmonitor: serial device does not exist: %s", device)
46 | }
47 | // Open the serial port
48 | port, err := serial.Open(device, mode)
49 | if err != nil {
50 | return nil, fmt.Errorf("ec2macossystemmonitor: unable to get serial connection: %s", err)
51 | }
52 | // Put the port in a SerialConnection for handing it off
53 | s := SerialConnection{port}
54 | return &s, nil
55 | }
56 |
57 | // Close is simply a pass through to close the device so it remains open in the scope needed.
58 | func (s *SerialConnection) Close() (err error) {
59 | err = s.port.Close()
60 | if err != nil {
61 | return err
62 | }
63 | return nil
64 | }
65 |
66 | // RelayData is the primary function for reading data from the socket provided and writing to the serial connection.
67 | func (s *SerialConnection) RelayData(sock net.Conn) (n int, err error) {
68 | defer sock.Close()
69 | // Create a buffer for reading in from the socket, probably want to bound this
70 | var buf bytes.Buffer
71 | // Read in the socket data into the buffer
72 | _, err = io.Copy(&buf, sock)
73 | if err != nil {
74 | return 0, fmt.Errorf("ec2macossystemmonitor: failed to read socket to buffer: %s", err)
75 | }
76 | // Write out the buffer to the serial device
77 | written, err := s.port.Write(buf.Bytes())
78 | if err != nil {
79 | return 0, fmt.Errorf("ec2macossystemmonitor: failed to write buffer to serial: %s", err)
80 | }
81 | return written, nil
82 | }
83 |
--------------------------------------------------------------------------------
/lib/ec2macossystemmonitor/util.go:
--------------------------------------------------------------------------------
1 | package ec2macossystemmonitor
2 |
3 | import (
4 | "os"
5 | )
6 |
7 | // fileExists returns true if a file exists and is not a directory.
8 | func fileExists(filename string) bool {
9 | info, err := os.Stat(filename)
10 | if os.IsNotExist(err) {
11 | return false
12 | }
13 | return !info.IsDir()
14 | }
15 |
--------------------------------------------------------------------------------
/lib/ec2macossystemmonitor/util_test.go:
--------------------------------------------------------------------------------
1 | package ec2macossystemmonitor
2 |
3 | import (
4 | "runtime"
5 | "testing"
6 | )
7 |
8 | // Test_fileExists tests the fileExists helper function with basic coverage
9 | func Test_fileExists(t *testing.T) {
10 | _, testFile, _, _ := runtime.Caller(0)
11 | type args struct {
12 | filename string
13 | }
14 | tests := []struct {
15 | name string
16 | args args
17 | want bool
18 | }{
19 | {"Not Real File", args{"notafile"}, false},
20 | {"Known File", args{testFile}, true},
21 | }
22 | for _, tt := range tests {
23 | t.Run(tt.name, func(t *testing.T) {
24 | if got := fileExists(tt.args.filename); got != tt.want {
25 | t.Errorf("fileExists() = %v, want %v", got, tt.want)
26 | }
27 | })
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "io/fs"
6 | "log"
7 | "os"
8 | "os/signal"
9 | "strconv"
10 | "sync/atomic"
11 | "syscall"
12 | "time"
13 |
14 | ec2sm "github.com/aws/ec2-macos-system-monitor/lib/ec2macossystemmonitor"
15 | )
16 |
17 | // pollInterval is the duration in between gathering of CPU metrics.
18 | const pollInterval = 60 * time.Second
19 |
20 | // defaultSerialDevices lists the preferred order and supported set of serial
21 | // devices attached to the instance for monitor communication. The serial device
22 | // is able to receive monitor payloads encapsulated in json.
23 | var defaultSerialDevices = []string{
24 | "/dev/cu.pci-0000:4c:00.0,@00",
25 | "/dev/cu.pci-serial0",
26 | }
27 |
28 | func main() {
29 | disableSyslog := flag.Bool("disable-syslog", false, "Prevent log output to syslog")
30 | flag.Parse()
31 |
32 | logger, err := ec2sm.NewLogger("ec2monitoring-cpuutilization", !*disableSyslog, true)
33 | if err != nil {
34 | log.Fatalf("Failed to create logger: %s", err)
35 | }
36 |
37 | serialDevice := firstSerialDevice(defaultSerialDevices)
38 | if serialDevice == "" {
39 | log.Fatal("No serial devices found for relay")
40 | }
41 | logger.Infof("Found serial device for relay %q\n", serialDevice)
42 |
43 | logger.Infof("Starting up relayd for monitoring\n")
44 | relay, err := ec2sm.NewRelay(serialDevice)
45 | if err != nil {
46 | log.Fatalf("Failed to create relay: %s", err)
47 | }
48 | intervalString := strconv.Itoa(ec2sm.DefaultLogInterval)
49 | cpuStatus := ec2sm.StatusLogBuffer{Message: "Sent CPU Utilization (%d bytes) over " + intervalString + " minute(s)", Written: 0}
50 | relayStatus := ec2sm.StatusLogBuffer{Message: "[relayd] Received data and sent %d bytes to serial device over " + intervalString + " minutes", Written: 0}
51 |
52 | // Kick off Relay in a go routine
53 | go relay.StartRelay(logger, &relayStatus)
54 |
55 | // Setup signal handling into a channel, catch SIGINT and SIGTERM for now which should suffice for launchd
56 | signals := make(chan os.Signal, 1)
57 | signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
58 |
59 | // Setup the polling channel for kicking off CPU metrics gathering
60 | pollingCh := time.Tick(pollInterval)
61 |
62 | // Setup logging interval timer for flushing logs
63 | LoggingTimer := time.Tick(ec2sm.DefaultLogInterval * time.Minute)
64 |
65 | // Check if the socket is there, if not, warn that this might fail
66 | if !ec2sm.CheckSocketExists(ec2sm.DefaultRelaydSocketPath) {
67 | logger.Fatal("Socket does not exist, relayd may not be running")
68 | }
69 | // Main for loop that polls for signals and CPU ticks
70 | for {
71 | select {
72 | case sig := <-signals:
73 | if cpuStatus.Written > 0 {
74 | logger.Infof(cpuStatus.Message, cpuStatus.Written)
75 | }
76 | if relayStatus.Written > 0 {
77 | logger.Infof(relayStatus.Message, relayStatus.Written)
78 | }
79 | log.Println("exiting due to signal:", sig)
80 | // Send signal to relay server through channel to shutdown
81 | relay.ReadyToClose <- true
82 | // Exit cleanly
83 | os.Exit(0)
84 | case <-pollingCh:
85 | // Fetch the current CPU Utilization
86 | cpuUtilization, err := ec2sm.RunningCpuUsage()
87 | if err != nil {
88 | logger.Fatalf("Unable to get CPU Utilization: %s\n", err)
89 | }
90 |
91 | // Send the data to the relay
92 | written, err := ec2sm.SendMessage("cpuutil", cpuUtilization, false)
93 | if err != nil {
94 | logger.Fatalf("Unable to write message to relay: %s", err)
95 | }
96 | // Add current written values to running total cpuStatus.Written
97 | cpuStatus.Written += int64(written)
98 | case <-LoggingTimer:
99 | // flush the logs since the timer fired. The cpuStatus info is local to this routine but relayStatus is not,
100 | // so use atomic for the non-local one to ensure its safe
101 | logger.Infof(cpuStatus.Message, cpuStatus.Written)
102 | // Since we logged the total, reset to zero for continued tracking
103 | cpuStatus.Written = 0
104 | logger.Infof(relayStatus.Message, relayStatus.Written)
105 | // Since we logged the total, reset to zero, do this via atomic since its modified in another goroutine
106 | atomic.StoreInt64(&relayStatus.Written, 0)
107 | }
108 |
109 | }
110 | }
111 |
112 | // firstSerialDevice returns the first path found in the list of serial device
113 | // paths given.
114 | func firstSerialDevice(devPaths []string) string {
115 | serialDeviceNode := func(p string) bool {
116 | if stat, err := os.Stat(p); err == nil {
117 | return stat.Mode()&fs.ModeCharDevice == fs.ModeCharDevice
118 | }
119 | return false
120 | }
121 |
122 | for _, devPath := range devPaths {
123 | if serialDeviceNode(devPath) {
124 | return devPath
125 | }
126 | }
127 |
128 | // no suitable device found
129 | return ""
130 | }
131 |
--------------------------------------------------------------------------------
/scripts/setup-ec2monitoring:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Deprecated: this is the orignial plist location that was updated by Homebrew on install
4 | HOMEBREW_PREFIX="/usr/local/Cellar"
5 | # System location for desired services
6 | LAUNCHD_PLIST_DIR="/Library/LaunchDaemons"
7 | # Deprecated: This is the original relayd plist
8 | RELAYD_PLIST="com.amazon.ec2.monitoring.relayd.plist"
9 | # Deprecated: search path for all agents in the Homebrew directory
10 | AGENT_PLISTS_PATTERN="${HOMEBREW_PREFIX}/com.amazon.ec2.monitoring.agents."
11 | # Search path for all agents in the Launchd directory, its a glob for future agents
12 | LAUNCHD_AGENT_PLISTS_PATTERN="${LAUNCHD_PLIST_DIR}/com.amazon.ec2.monitoring.agents."
13 |
14 | # Print usage
15 | usage() {
16 | echo "Usage: setup-ec2monitoring "
17 | echo "Operations are:"
18 | echo " enable - Enable the monitoring launchd services"
19 | echo " disable - Disable the monitoring launchd services"
20 | echo " list - List the current services"
21 | }
22 |
23 | # Helper function to exit
24 | die() {
25 | echo "$@" >&2
26 | exit 1
27 | }
28 |
29 | # Helper function for getting the label for working with launchd
30 | # Takes the name of the plist as an argument"
31 | get_label() {
32 | local plist_name=${1:?}
33 |
34 | /usr/libexec/PlistBuddy -c "Print Label" "${LAUNCHD_PLIST_DIR}"/"${plist_name}"
35 | }
36 |
37 | # Helper function for enabling a service
38 | # Takes the name of the plist as an argument"
39 | enable_service() {
40 | local plist_name=${1:?}
41 | local label
42 | local homebrew_plist
43 | local system_plist
44 | homebrew_plist="${HOMEBREW_PREFIX}/${plist_name}"
45 | system_plist="${LAUNCHD_PLIST_DIR}/${plist_name}"
46 |
47 | # Homebrew doesn't install into /Library/LaunchDaemons so this manages those files, first check this plist
48 | # is desired for this version of monitoring
49 | if [ -f "${homebrew_plist}" ]; then
50 |
51 | # If the file is missing, its a fresh install, just copy
52 | if [ ! -f "${system_plist}" ]; then
53 | echo -e "Adding ${plist_name} to ${LAUNCHD_PLIST_DIR}"
54 | cp "${homebrew_plist}" "${LAUNCHD_PLIST_DIR}"
55 | # If the file differs, replace it
56 | elif ! cmp -s "${system_plist}" "${homebrew_plist}"; then
57 | echo -e "Updating ${plist_name} at ${LAUNCHD_PLIST_DIR}"
58 | cp "${homebrew_plist}" "${LAUNCHD_PLIST_DIR}"
59 | fi
60 |
61 | fi
62 |
63 | # Casks install into /Library/LaunchDaemons so this is for directly enabling them
64 | # as well as the plists in the deprecated location
65 | if [ -f "${system_plist}" ]; then
66 |
67 | label="$(get_label "${plist_name}")"
68 | test -z "${label}" && echo "possibly invalid plist: ${plist_name}" >&2 && return 1
69 | launchctl enable system/"${label}" && launchctl bootstrap system "${system_plist}"
70 |
71 | fi
72 | }
73 |
74 | # Helper function for disabling a service
75 | # Takes the name of the plist as an argument"
76 | disable_service() {
77 | local plist_name=${1:?}
78 | local label
79 |
80 | if [ -f "${plist}" ]; then
81 | label="$(get_label "${plist_name}")"
82 | test -z "${label}" && echo "possibly invalid plist: ${plist_name}" >&2 && return 1
83 | launchctl bootout system "${LAUNCHD_PLIST_DIR}/${plist_name}"
84 | launchctl disable system/"${label}"
85 | fi
86 | }
87 |
88 | # Ensure this is run as root
89 | test "${EUID}" -ne 0 && die "must run as root"
90 |
91 | # Get the desired operation
92 | operation=${1}
93 |
94 | # Get the plist files from both the Homebrew location and Launchd and combine them into one list
95 | agent_plists=($(ls ${AGENT_PLISTS_PATTERN}*.plist 2> /dev/null))
96 | launchd_plists=($(ls ${LAUNCHD_AGENT_PLISTS_PATTERN}*.plist 2> /dev/null))
97 | tmp=("${agent_plists[@]}" "${launchd_plists[@]}")
98 | plists=()
99 | for plist in "${tmp[@]}"; do
100 | if [ "$plist" != "" ]; then
101 | plists+=("$plist")
102 | fi
103 | done
104 |
105 | if [ "${operation}" == "enable" ]; then
106 | enable_service ${RELAYD_PLIST}
107 | for plist in "${plists[@]}"; do
108 | enable_service "$(basename "${plist}")"
109 | done
110 | elif [ "${operation}" == "disable" ]; then
111 | for plist in "${plists[@]}"; do
112 | disable_service "$(basename "${plist}")"
113 | done
114 | if [ -f "${LAUNCHD_PLIST_DIR}/${RELAYD_PLIST}" ]; then
115 | disable_service ${RELAYD_PLIST}
116 | fi
117 | elif [ "${operation}" == "list" ]; then
118 | if [ -f "${LAUNCHD_PLIST_DIR}/${RELAYD_PLIST}" ]; then
119 | get_label ${RELAYD_PLIST}
120 | fi
121 | for plist in "${plists[@]}"; do
122 | get_label "$(basename "${plist}")"
123 | done
124 | else
125 | usage
126 | die "unknown operation: ${operation}"
127 | fi
128 |
--------------------------------------------------------------------------------