├── .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 | --------------------------------------------------------------------------------