├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Docs ├── Examples │ ├── DebugDump │ │ ├── README.md │ │ └── config.yml │ ├── ExportToAzureMonitor │ │ ├── README.md │ │ └── config.yml │ └── Filters │ │ ├── 00-Simple │ │ └── filter.yml │ │ ├── 10-Uninteresting │ │ ├── filter.yml │ │ └── rs-interesting.yml │ │ └── 20-Advanced │ │ ├── filter.yml │ │ ├── rs-monorepo.yml │ │ └── rs-other.yml ├── README.md ├── config-filter-settings.md ├── config-pii-settings.md ├── config-ruleset-definition.md ├── configure-custom-collector.md └── generate-custom-collector.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── config.go ├── evt_apply.go ├── evt_apply_test.go ├── evt_parse.go ├── evt_parse_test.go ├── factory.go ├── filter_settings.go ├── filter_settings_test.go ├── fsdetaillevel.go ├── go.mod ├── go.sum ├── internal └── go-winio │ ├── LICENSE │ ├── README.md │ ├── file.go │ ├── internal │ ├── fs │ │ ├── doc.go │ │ ├── fs.go │ │ ├── fs_test.go │ │ ├── security.go │ │ └── zsyscall_windows.go │ └── stringbuffer │ │ ├── wstring.go │ │ └── wstring_test.go │ ├── pipe.go │ ├── pipe_test.go │ ├── sd.go │ ├── sd_test.go │ └── zsyscall_windows.go ├── jmap_get.go ├── jmap_get_test.go ├── parse_yml.go ├── pii.go ├── platform_unix.go ├── platform_windows.go ├── rcvr_base.go ├── rcvr_namedpipe.go ├── rcvr_unixsocket.go ├── reject_client.go ├── ruleset_definition.go ├── trace2dataset.go ├── trace2emitotlp.go ├── trace2ruleset.go ├── trace2semconv.go ├── trace2sids.go ├── unixsocket_darwin.go ├── unixsocket_linux.go └── version.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ macos-latest, ubuntu-latest, windows-2019 ] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Setup Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version-file: 'go.mod' 20 | 21 | - name: Build 22 | run: go build ./... 23 | 24 | - name: Run Unit Tests 25 | run: go test ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This repository is maintained by: 2 | * @dscho @mjcheetham @mpysson 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource@github.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | 4 | Hi there! We're thrilled that you'd like to contribute to this 5 | project. Your help is essential for keeping it great. 6 | 7 | Contributions to this project are 8 | [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) 9 | to the public under the 10 | [project's open source license](./LICENSE). 11 | 12 | Please note that this project is released with a 13 | [Contributor Code of Conduct](./CODE_OF_CONDUCT.md). 14 | By participating in this project you agree to abide by its terms. 15 | 16 | 17 | ## Prerequisites for running and testing code 18 | 19 | These are one time installations required to be able to test your 20 | changes locally as part of the pull request (PR) submission process. 21 | 22 | 1. Install Go [through download](https://go.dev/doc/install) | [through Homebrew](https://formulae.brew.sh/formula/go) 23 | 1. Clone this repository. 24 | 1. Build and test your changes in isolation within your `trace2receiver` component. 25 | 1. Submit your PR. 26 | 27 | Of course the whole point of this is to run your `trace2receiver` 28 | component within a custom collector and generate some data, so you 29 | should also test it in that context. 30 | See 31 | [Building and Configuration](./Docs/README.md). 32 | 33 | 1. Create an 34 | [OpenTelemetry Custom Collector](https://opentelemetry.io/docs/collector/) 35 | using the builder tool in a new peer repository that references the 36 | `trace2receiver` component. 37 | 1. The `go.mod` file in your collector should reference the public version 38 | of the `trace2receiver` component. You may need to use a 39 | [`replace`](https://go.dev/doc/modules/gomod-ref#replace) 40 | to redirect GO to your development version for testing. 41 | 1. Build and test your component changes running under a collector. 42 | 43 | Your custom collector should not be included in your PR; just changes 44 | to the `trace2receiver` component. 45 | 46 | 47 | ## Submitting a pull request 48 | 49 | 1. Clone the repository 50 | 1. Make sure the tests pass on your machine: `go test -v ./...` 51 | 1. Create a new branch: `git checkout -b my-branch-name` 52 | 1. Make your change, add tests, and make sure the tests still pass 53 | 1. Push to your fork and submit a pull request. 54 | 1. Pat yourself on the back and wait for your pull request to be reviewed and merged. 55 | 56 | Here are a few things you can do that will increase the likelihood of 57 | your pull request being accepted: 58 | 59 | - Write tests. 60 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 61 | - Write a [good commit message](https://github.blog/2022-06-30-write-better-commits-build-better-projects/). 62 | 63 | 64 | ## Resources 65 | 66 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 67 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 68 | - [GitHub Help](https://help.github.com) 69 | -------------------------------------------------------------------------------- /Docs/Examples/DebugDump/README.md: -------------------------------------------------------------------------------- 1 | # Debug Dump Example 2 | 3 | This `config.yml` can be used to locally convert the Trace2 4 | event data to OTLP and dump it to the configured logfile 5 | without sending it to any data stores. 6 | -------------------------------------------------------------------------------- /Docs/Examples/DebugDump/config.yml: -------------------------------------------------------------------------------- 1 | # This example dumps Trace2 data to the standard logging 2 | # stream when the service daemon runs. This is usually 3 | # stderr when running it interactively. It may be the 4 | # Windows Event Viewer on Windows when running as a service. 5 | # 6 | # TODO: 7 | # [1] Update the pathnames to point to your installation and/or 8 | # ProgramData directory. 9 | # 10 | # You can enable `exporters.logging.verbosity` and/or 11 | # `service.telemetry.logs.level` to see debug logging. 12 | # THIS WILL GENERATE A LOT OF DATA, so use it with care. 13 | # 14 | # If you want to enable filtering and/or PII data, uncomment the 15 | # correpsonding lines and create the additional .yml files. 16 | 17 | receivers: 18 | trace2receiver: 19 | socket: "/usr/local//trace2.socket" 20 | pipe: "//./pipe/" 21 | 22 | # filter: "/usr/local//filter.yml" 23 | # pii: "/usr/local//pii.yml" 24 | 25 | # filter: "C:/ProgramData//filter.yml" 26 | # pii: "C:/ProgramData//pii.yml" 27 | 28 | processors: 29 | 30 | exporters: 31 | logging: 32 | verbosity: normal # basic, normal, detailed 33 | 34 | service: 35 | telemetry: 36 | metrics: 37 | level: none # disable default prometheus metrics on http://localhost:8888/metrics 38 | logs: 39 | level: "INFO" # "INFO", "DEBUG" 40 | pipelines: 41 | traces: 42 | receivers: [trace2receiver] 43 | processors: [] 44 | exporters: [logging] 45 | -------------------------------------------------------------------------------- /Docs/Examples/ExportToAzureMonitor/README.md: -------------------------------------------------------------------------------- 1 | # Exporting Trace2 Data to Azure Monitor Application Insights 2 | 3 | You can send Trace2 telemetry data from Git to Azure Monitor 4 | Application Insights[^1] using the `azuremonitor` component[^2][^3]. 5 | 6 | Use the Azure Portal to create an Application Insights database and 7 | enter the "instrumentation key" in your `config.yml` file. 8 | A sample `config.yml` file is provided here to help you get started. 9 | 10 | You can use the portal to visualize your telemetry data (both 11 | individual span records or distributed traces using the end-to-end 12 | transaction page). 13 | 14 | You can also configure Azure Data Explorer to remotely access your 15 | Application Insights database and view the span records.[^4] 16 | 17 | Telemetry for Git commands (aka process spans) will appear in the 18 | `requests` table. Data for events internal to a command, such as 19 | thread and region spans, will appear in the `dependencies` table. 20 | (This separation is a feature of the `azuremonitor` exporter.) So you 21 | may need to do `union` or `join` Kusto queries to see all of the data 22 | for an individual Git command. However, it does make the Azure portal 23 | `Application Map` feature more useful. 24 | 25 | By default, Azure tries to strip out PII / GDPR data from incoming 26 | telemetry and in some cases replaces it with less-specific data.[^5] 27 | For example, the `azuremonitor` exporter adds many AppIns-level fields 28 | to the telemetry data that is sent to Azure, such as `client_IP`. In 29 | the cloud, Azure may overwrite that field with `0.0.0.0` and add 30 | `client_City`, `client_StateOrProvice`, and `client_CountryOrRegion` 31 | fields. See `DisableIpMasking` in [^5]. 32 | 33 | See also 34 | [Config PII Settings](../../config-pii-settings.md). 35 | 36 | 37 | [^1]: https://learn.microsoft.com/en-us/azure/azure-monitor/overview 38 | [^2]: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/azuremonitorexporter 39 | [^3]: https://pkg.go.dev/github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azuremonitorexporter 40 | [^4]: https://learn.microsoft.com/en-us/azure/data-explorer/query-monitor-data 41 | [^5]: https://learn.microsoft.com/en-us/azure/azure-monitor/app/ip-collection?tabs=framework%2Cnodejs 42 | -------------------------------------------------------------------------------- /Docs/Examples/ExportToAzureMonitor/config.yml: -------------------------------------------------------------------------------- 1 | # This example builds a pipeline to collect Trace2 data, transform 2 | # it into OTLP and export it to Azure Monitor Application Insight. 3 | # 4 | # TODO: 5 | # [1] Update the pathnames to point to your installation and/or 6 | # ProgramData directory. 7 | # [2] Replace the placeholder with your instrumentation key. 8 | # 9 | # You can enable `exporters.logging.verbosity` and/or 10 | # `service.telemetry.logs.level` to see debug logging. 11 | # THIS WILL GENERATE A LOT OF DATA, so use it with care. 12 | # 13 | # If you want to enable filtering and/or PII data, uncomment the 14 | # correpsonding lines and create the additional .yml files. 15 | 16 | receivers: 17 | trace2receiver: 18 | socket: "/usr/local//trace2.socket" 19 | pipe: "//./pipe/" 20 | 21 | # filter: "/usr/local//filter.yml" 22 | # pii: "/usr/local//pii.yml" 23 | 24 | # filter: "C:/ProgramData//filter.yml" 25 | # pii: "C:/ProgramData//pii.yml" 26 | 27 | processors: 28 | 29 | exporters: 30 | logging: 31 | verbosity: normal # basic, normal, detailed 32 | azuremonitor: 33 | instrumentation_key: <> 34 | spaneventsenabled: true 35 | 36 | service: 37 | telemetry: 38 | metrics: 39 | level: none # disable default prometheus metrics on http://localhost:8888/metrics 40 | logs: 41 | level: "INFO" # "INFO", "DEBUG" 42 | pipelines: 43 | traces: 44 | receivers: [trace2receiver] 45 | processors: [] 46 | exporters: [azuremonitor, logging] 47 | -------------------------------------------------------------------------------- /Docs/Examples/Filters/00-Simple/filter.yml: -------------------------------------------------------------------------------- 1 | # Filter the Trace2 data stream before generating OTLP. 2 | # Use a single detail level for all commands on all repos. 3 | # 4 | # TODO: 5 | # [1] Uncomment the one you want. The builtin default of 6 | # "dl:summary" is used otherwise. 7 | 8 | defaults: 9 | # ruleset: "dl:drop" # An expensive way to do nothing. 10 | # ruleset: "dl:summary" # The builtin default. 11 | # ruleset: "dl:process" 12 | # ruleset: "dl:verbose" 13 | -------------------------------------------------------------------------------- /Docs/Examples/Filters/10-Uninteresting/filter.yml: -------------------------------------------------------------------------------- 1 | # Filter the Trace2 data stream before generating OTLP. Use a single 2 | # ruleset to filter out "uninteresting" commands and only generate 3 | # data for "interesting" ones. 4 | # 5 | # Since no `nickname_key` or `ruleset_key` is defined, commands 6 | # will inherit the default ruleset. And we don't need to define 7 | # `git config` values for them. 8 | # 9 | # TODO: 10 | # [1] Update the absolute pathname to the yml file. 11 | 12 | rulesets: 13 | "rs:interesting": "/rs-interesting.yml" 14 | 15 | defaults: 16 | ruleset: "rs:interesting" 17 | -------------------------------------------------------------------------------- /Docs/Examples/Filters/10-Uninteresting/rs-interesting.yml: -------------------------------------------------------------------------------- 1 | # Generate verbose data for interesting commands, summary data for status, 2 | # and discard data from all others commands. 3 | 4 | commands: 5 | "git:fetch": "dl:verbose" 6 | "git:pull": "dl:verbose" 7 | "git:push": "dl:verbose" 8 | "git:status": "dl:summary" 9 | 10 | defaults: 11 | detail: "dl:drop" 12 | -------------------------------------------------------------------------------- /Docs/Examples/Filters/20-Advanced/filter.yml: -------------------------------------------------------------------------------- 1 | # Filter the Trace2 data stream before generating OTLP. 2 | # Define nickname to allow repo instances to identify themselves. 3 | # Create different rulesets for different nicknames. 4 | # Filter out uninteresting commands from monorepos. 5 | # Drop all data for personal repos. 6 | # Generate summary data for any other repos. 7 | # 8 | # To use this example, you must add `otel.trace2.*` to the system 9 | # or global Git config: 10 | # 11 | # `git config --system trace2.configparams 'otel.trace2.*'` 12 | # 13 | # in order to cause Git to send the repo instance's nickname 14 | # in the telemetry stream. 15 | 16 | keynames: 17 | nickname_key: "otel.trace2.nickname" 18 | 19 | nicknames: 20 | "monorepo": "rs:monorepo" 21 | "private": "dl:drop" 22 | 23 | rulesets: 24 | "rs:monorepo": "/rs-monorepo.yml" 25 | "rs:other": "/rs-other.yml" 26 | 27 | defaults: 28 | ruleset: "rs:other" 29 | -------------------------------------------------------------------------------- /Docs/Examples/Filters/20-Advanced/rs-monorepo.yml: -------------------------------------------------------------------------------- 1 | # Generate verbose data for push, pull, and status and process-level 2 | # data for other commands. 3 | # 4 | # You might use this to investigate problematic push and pull times, 5 | # and then extend it to include more details on sub-commands like 6 | # index-pack, remote-https, gc, and switching branches. 7 | 8 | commands: 9 | "git:fetch": "dl:verbose" 10 | "git:pull": "dl:verbose" 11 | "git:push": "dl:verbose" 12 | "git:status": "dl:verbose" 13 | 14 | defaults: 15 | detail: "dl:process" 16 | -------------------------------------------------------------------------------- /Docs/Examples/Filters/20-Advanced/rs-other.yml: -------------------------------------------------------------------------------- 1 | # Generate summary data for the commands usually seen in dashboards 2 | # and drop data for trivial housekeeping commands. 3 | 4 | commands: 5 | "git:fetch": "dl:summary" 6 | "git:pull": "dl:summary" 7 | "git:push": "dl:summary" 8 | "git:status": "dl:summary" 9 | 10 | defaults: 11 | detail: "dl:drop" 12 | -------------------------------------------------------------------------------- /Docs/README.md: -------------------------------------------------------------------------------- 1 | # Building and Configuration 2 | 3 | 4 | ## Generating a new Custom Collector 5 | 6 | If you don't have an OTEL Custom Collector service daemon, you can use 7 | the OpenTelemetry Custom Collector Builder tool to 8 | [generate a new custom collector](./generate-custom-collector.md) 9 | to contain the `trace2receiver` component. 10 | 11 | 12 | ## Configure Your Custom Collector 13 | 14 | After building your custom collector and statically linking all of the 15 | required components, you can run your custom collector. It is 16 | intended to be a long-running service daemon managed by the OS, such 17 | as `launchctl(1)` on macOS, `systemd(1)` on Linux, or the Control 18 | Panel Service Manager on Windows. However, it is helpful to run it 19 | interactively while you work on your configuration. 20 | 21 | The collector requires a 22 | [`config.yml` configuration file](./configure-custom-collector.md) 23 | to specify which (of the linked) components you actually want to use 24 | and how they should be connected and configured. 25 | 26 | This `config.yml` file will be read in by the collector when it starts up, 27 | so you should plan to distribute it with the executable. 28 | 29 | If you want to change your `config.yml` or any of the filter or 30 | privacy files that it references, you'll need to stop and restart your 31 | collector service daemon, since these files are only read during startup. 32 | 33 | _All pathnames in the `config.yml` file should be absolute paths rather 34 | than relative paths to avoid startup working directory confusion when 35 | run by the OS service manager._ 36 | 37 | 38 | ## Appendix: Caveats 39 | 40 | ### Unmonitored Git Commands 41 | 42 | Long-running Git commands like `git fsmonitor--daemon run` that 43 | operate background are incompatible with the `trace2receiver` because 44 | they are designed to run for days and the OTEL telemetry is only 45 | generated when the process exits. The receiver automatically drops 46 | the pipe/socket connection from such daemon commands as quickly as 47 | possible to avoid wasting resources. 48 | 49 | 50 | 51 | ### Updating Filter Specifications 52 | 53 | There have been requests to have the receiver periodically poll 54 | some web endpoint for updated filter specifications. This is 55 | outside of the scope of the `trace2receiver` component, since it 56 | operates as a component within an unknown OTEL Custom Collector. 57 | 58 | This functionality can be easily provided by an Administrator 59 | cron script to poll a web service that they own and restart the 60 | collector as necessary. 61 | -------------------------------------------------------------------------------- /Docs/config-filter-settings.md: -------------------------------------------------------------------------------- 1 | # Config Filter Settings 2 | 3 | The `filter.yml` file controls how the `trace2receiver` component 4 | translates the Trace2 data stream from Git commands into OTEL data 5 | structures. This filtering is content- and context-aware and is 6 | independent of any statistical filtering performed by later stages in 7 | the OTEL Collector pipeline. 8 | 9 | The filter settings pathname is set in the 10 | `receivers.trace2receiver.filter` 11 | parameter in the main `config.yml` file. 12 | 13 | 14 | 15 | ## Smart Filtering using Detail Levels, Rulesets, and Repo Nicknames 16 | 17 | The `trace2receiver` does "smart filtering" rather than just 18 | "percentage filtering". This allows it to control the verbosity of 19 | the generated telemetry from the Trace2 data stream from Git commands. 20 | For example, you might want very verbose output for your monorepo 21 | while doing a performance study and only minimal output otherwise. Or 22 | you might want no telemetry at all for insignificant or personal 23 | repos. 24 | 25 | 1. *Detail Levels:* There are four builtin detail levels. These vary 26 | from no telemetry to very verbose telemetry. These form the 27 | foundation of the filtering system. 28 | 29 | 2. *Rulesets:* Rulesets build upon detail levels. They let you define 30 | a meaningful name for a set of filtering patterns, such as dropping 31 | telemetry for "uninteresting" commands and requesting verbose 32 | telemetry for "interesting" ones. Rulesets can only refer to detail 33 | levels. They cannot refer to other rulesets. 34 | 35 | 3. *Repo Nicknames:* Repo Nicknames are an aliasing technique built on 36 | top of rulesets. They serve two roles: (1) they select a detail level 37 | or ruleset for content filtering, and (2) they help with data 38 | aggregation from different repo instances across different machines. 39 | 40 | As we'll see later, a Git command can refer to a detail level, a 41 | ruleset, or a repo nickname to override the default filtering and 42 | telemetry verbosity. 43 | 44 | The following sections explain each of these concepts in more detail. 45 | And later in this document we'll see how they can be used by Git 46 | commands. 47 | 48 | 49 | 50 | ### Builtin Detail Levels 51 | 52 | All detail level names begin with a `dl:` prefix to distinguish 53 | them from ruleset names and repo nicknames. 54 | 55 | ``` 56 | ::= "dl:drop" 57 | | "dl:summary" 58 | | "dl:process" 59 | | "dl:verbose" 60 | ``` 61 | 62 | 1. `dl:drop` -- Drop or omit all telemetry for the command. 63 | 64 | 2. `dl:summary` -- Generate basic telemetry for the command; the 65 | primary focus is the lifespan of the command, the arguments, and exit 66 | code. 67 | 68 | 3. `dl:process` -- Adds process-level data events, process-level timer 69 | and counter values, and child process (and hook) events to the 70 | summary-level data. 71 | 72 | 4. `dl:verbose` -- Adds thread-level and region-level details to the 73 | process-level data. 74 | 75 | 76 | 77 | ### User-defined Rulesets 78 | 79 | All ruleset names have a `rs:` prefix to distinguish them from detail 80 | levels and repo nicknames. 81 | 82 | ``` 83 | ::= "rs:" 84 | ``` 85 | 86 | The content of a ruleset is defined in a 87 | [ruleset file](./config-ruleset-definition.md). 88 | 89 | A ruleset name is essentially an alias for the underlying ruleset 90 | file. Using a ruleset name avoids requiring users know how and where 91 | the telemetry service is installed. 92 | 93 | The `filter.yml` file contains a dictionary to map ruleset names to 94 | pathnames: 95 | 96 | ``` 97 | rulesets: 98 | : 99 | : 100 | ... 101 | ``` 102 | 103 | Ruleset files will be loaded when the receiver starts up. 104 | 105 | > [!NOTE] 106 | > If you want to modify the list of rulesets or edit one of the 107 | > ruleset files, you'll need to restart the telemetry service 108 | > when you're finished. 109 | 110 | 111 | 112 | ### Repo Nicknames 113 | 114 | A repo nickname is another level aliasing on top of rulesets. 115 | Conceptually, this is a way to say that this repo is an instance 116 | of project "foo" and that telemetry data from it can be aggregated 117 | with Git command data from other instances of project "foo". 118 | 119 | This avoids the need for the telemetry service or data store to try to 120 | _guess_ how to aggregate data by parsing the `remote.origin.url` or 121 | the basename of the repo root directory. Users can simple say that 122 | this repo is an instance of repo "foo" and aggregate or partition 123 | data as they want. 124 | 125 | Nicknames also let us say that all instances of repo "foo" should use 126 | the ruleset "rs:bar". 127 | 128 | A repo nickname is a simple string without either `dl:` or `rs:` prefix. 129 | 130 | The `filter.yml` file contains a dictionary to map nicknames to detail 131 | levels or rulesets: 132 | 133 | ``` 134 | nicknames: 135 | : | 136 | : | 137 | ... 138 | ``` 139 | 140 | 141 | 142 | ## Telemetry Meta Data 143 | 144 | Git can be told to send additional Git config key/value pairs in the 145 | Trace2 telemetry string using the 146 | [`trace2.configparams`](https://git-scm.com/docs/api-trace2#Documentation/technical/api-trace2.txt-ConfigdefparamEvents) 147 | config setting. We can use that mechanism to have Git send extra meta 148 | data to help `trace2receiver` decide how to generate or filter OTEL 149 | data. 150 | 151 | _In the examples here we have chosen to use the `otel.trace2.*` 152 | namespace for all of these special config settings, but you can use 153 | any prefix you want._ 154 | 155 | To tell Git to always send these config settings, we must add this 156 | namespace to the `trace2.configparams` config setting at the `global` 157 | or `system` level. 158 | 159 | ``` 160 | $ git config --system trace2.configparams "otel.trace2.*" 161 | ``` 162 | 163 | The `filter.yml` contains a dictionary to define the spelling of 164 | these keys: 165 | 166 | ``` 167 | keynames: 168 | nickname_key: "otel.trace2.nickname" 169 | ruleset_key: "otel.trace2.ruleset" 170 | ``` 171 | 172 | 173 | 174 | ### Using the Repo Nickname Config Setting 175 | 176 | We can set repo nicknames on our repos using the Git config 177 | setting named in the `nickname_key` parameter. Thereafter, Git will 178 | silently send the nickname on every Git command in those repos. 179 | 180 | The nickname should be local to the individual repo. 181 | 182 | 183 | ``` 184 | $ cd /path/to/my/repo1 185 | $ git config --local otel.trace2.nickname "monorepo" 186 | $ 187 | $ cd /path/to/my/repo2 188 | $ git config --local otel.trace2.nickname "monorepo" 189 | $ 190 | $ cd /path/to/my/repo3 191 | $ git config --local otel.trace2.nickname "personal" 192 | ``` 193 | 194 | Or you can set it for a single command: 195 | 196 | ``` 197 | $ cd /path/to/my/repo4 198 | $ git -c otel.trace2.nickname=personal status 199 | ``` 200 | 201 | If no nickname is defined or the given repo nickname is not defined in 202 | the `filter.yml` file, the receiver will fall back to the default 203 | filter settings. 204 | 205 | _In the above example, I've suggested "monorepo" and "personal" as 206 | nicknames, but you might use the base name of the repo, such as 207 | `git.git` or `chromium.git` or just `chromium`. Or you might use a 208 | project codename (and further hide the origin URL)._ 209 | 210 | _You might use different nicknames for desktop users versus build 211 | servers on instances of the same repo to help partition the data in 212 | the data store by use cases or machine classes. For example, you 213 | might want to see the P80 fetch times for interactive users and not 214 | have to sift thru fetches from build machines._ 215 | 216 | 217 | 218 | ### Using the Ruleset Config Setting 219 | 220 | The repo nickname helps identify/classify the data and lets you set an 221 | expected ruleset. However, there are times when you might want to 222 | maintain the above classification, but use different verbosity for 223 | some commands or for some repo instances. 224 | 225 | The `ruleset_key` parameter lets you explicitly select a ruleset and 226 | override the ruleset associated with the nickname. 227 | 228 | 229 | ``` 230 | $ cd /path/to/my/repo1 231 | $ git config --local otel.trace2.ruleset "rs:production" 232 | $ 233 | $ cd /path/to/my/repo2 234 | $ git config --local otel.trace2.ruleset "rs:test" 235 | $ 236 | $ cd /path/to/my/repo3 237 | $ git config --local otel.trace2.ruleset "dl:drop" 238 | ``` 239 | 240 | Or set it for a single command: 241 | 242 | ``` 243 | $ cd /path/to/my/repo4 244 | $ git -c otel.trace2.ruleset="dl:summary" status 245 | ``` 246 | 247 | If the named ruleset or detail level is not defined in the `filter.yml` 248 | file, the receiver will fall back to the default filter settings. 249 | 250 | If a Git command sends both a `ruleset_key` and `nickname_key`, the 251 | `ruleset_key` wins. (Both key values will be included in the OTEL 252 | telemetry, but the telemetry data will be filtered using the value of 253 | the `ruleset_key`.) 254 | 255 | 256 | 257 | ## Filter Settings Syntax 258 | 259 | Now that all of the concepts have been introduced, we can describe 260 | the complete syntax of the `filter.yml` file. All sections and rows 261 | are optional. 262 | 263 | ``` 264 | keynames: 265 | nickname_key: 266 | ruleset_key: 267 | 268 | nicknames: 269 | : | 270 | : | 271 | ... 272 | 273 | rulesets: 274 | : 275 | : 276 | ... 277 | 278 | defaults: 279 | ruleset: | 280 | ``` 281 | 282 | The value of the `defaults.ruleset` parameter will be used when a Git 283 | command does not specify a repo nickname or ruleset. 284 | 285 | If there is no default, the builtin default of `dl:summary` will be 286 | used. 287 | 288 | 289 | 290 | ## Example 291 | 292 | In this filter: 293 | 294 | ``` 295 | keynames: 296 | nickname_key: "otel.trace2.nickname" 297 | ruleset_key: "otel.trace2.ruleset" 298 | 299 | nicknames: 300 | monorepo: "dl:verbose" 301 | personal: "dl:drop" 302 | 303 | rulesets: 304 | "rs:status": "./rulesets/rs-status.yml" 305 | 306 | defaults: 307 | ruleset: "dl:summary" 308 | ``` 309 | 310 | The receiver will watch for the `otel.trace2.nickname` and 311 | `otel.trace2.ruleset` Git config key/values pairs in the Trace2 312 | telemetry stream to override the builtin filtering defaults. 313 | 314 | Commands that send `otel.trace2.ruleset = rs:status` will 315 | use the command-level filtering described in the `rs-status.yml` 316 | ruleset file. 317 | 318 | Commands that send `otel.trace2.nickname = monorepo` will 319 | use `dl:verbose` and emit very verbose telemetry. 320 | 321 | Commands that send `otel.trace2.nickname = personal` will 322 | use `dl:drop` and not emit any telemetry. 323 | 324 | All other commands will use the default `dl:summary` and 325 | emit command overview telemetry. 326 | 327 | 328 | -------------------------------------------------------------------------------- /Docs/config-pii-settings.md: -------------------------------------------------------------------------------- 1 | # Config PII Settings 2 | 3 | The PII Settings file contains privacy-related feature flags for the 4 | `trace2receiver` component. Currently, this includes flags to add 5 | user and hostname data that may not be present in the original Trace2 6 | data stream. Later, it may include other flags to redact or not 7 | redact sensitive data found within the Trace2 data stream. 8 | 9 | NOTE: These flags may add GDPR-sensitive data to the OTEL telemetry 10 | data stream. Use them at your own risk. 11 | 12 | The PII settings pathname is set in the 13 | `receivers.trace2receiver.pii` 14 | parameter in the main `config.yml` file. 15 | 16 | ## `pii.yml` Syntax 17 | 18 | The PII settings file has the following syntax: 19 | 20 | ``` 21 | include: 22 | hostname: 23 | username: 24 | ``` 25 | 26 | ### `include.hostname` 27 | 28 | Add the system hostname using the `trace2.pii.hostname` attribute. 29 | 30 | ### `include.username` 31 | 32 | Add the username associated with the Git command using the `trace2.pii.username` 33 | attribute. 34 | -------------------------------------------------------------------------------- /Docs/config-ruleset-definition.md: -------------------------------------------------------------------------------- 1 | # Config Ruleset Definition 2 | 3 | The `filter.yml` file controls how the `trace2receiver` component 4 | translates and/or filters the Git Trace2 telemetry stream into OTEL 5 | telemetry data. 6 | 7 | Conceptually, the `filter.yml` layer says that telemetry for all Git 8 | commands from a repo that is an instance of project "foo" should use 9 | ruleset "rs:bar". Ruleset "rs:bar" points to a pathname containing 10 | the ruleset file. 11 | 12 | A ruleset definition allows you to specify which commands on that repo 13 | are "interesting" and which a not. For example, you may only care 14 | about `git status`, `git fetch`, and `git push`, but not `git 15 | rev-list` or `git rev-parse`. 16 | 17 | 18 | 19 | ## Git Command Qualifiers 20 | 21 | The Trace2 telemetry stream contains the name of the executable 22 | and, when present, the name and mode. We combine these (when 23 | present) to fully describe the command. 24 | 25 | 1. ``: This is usally the basename of `argv[0]`. Usually this is 26 | `git`, but other tools may also emit Trace2 telemetry, such the 27 | [GCM](https://github.com/git-ecosystem/git-credential-manager), 28 | so we do not assume it is `git`. 29 | 30 | 2. ``: The 31 | [name (or verb)](https://git-scm.com/docs/api-trace2#Documentation/technical/api-trace2.txt-codecmdnamecode) 32 | of the command, such a `status` or `fetch`. This value is is 33 | optional, since some ``'s might not have a name/verb. 34 | 35 | 3. ``: The 36 | [mode](https://git-scm.com/docs/api-trace2#Documentation/technical/api-trace2.txt-codecmdmodecode) 37 | of the command. Commands like `git checkout` have different modes, 38 | such as switching branches vs restoring an individual file. This 39 | value is also optional. 40 | 41 | 42 | 43 | ## A Fully Qualified Name 44 | 45 | The `trace2receiver` combines the above command part names into a 46 | single fully qualified token: 47 | 48 | ``` 49 | ::= ":#" # if both name and mode are present 50 | ::= ":" # if mode is not present 51 | ::= "" # if name is not present 52 | 53 | ::= | | 54 | ``` 55 | 56 | For example, `git:checkout#branch` or `git:status`. 57 | 58 | 59 | 60 | ## Ruleset Command Pattern Matching 61 | 62 | A ruleset lets you select a different detail levels for different 63 | commands. The ruleset `.yml` file contains an optional dictionary 64 | to map command patterns to detail levels. 65 | 66 | The receiver will first try to find an entry for the `` 67 | in the dictionary. If not present, it will try `` and 68 | then `` until it finds a match. 69 | 70 | If no match is found, the ruleset default (if present) will be used. 71 | If the ruleset does not have a default value, the containing 72 | `filter.yml` default or the receiver builtin default will be used. 73 | 74 | 75 | 76 | ## Ruleset Definition Syntax 77 | 78 | ``` 79 | commands: 80 | : 81 | : 82 | ... 83 | 84 | defaults: 85 | detail: 86 | ``` 87 | 88 | 89 | 90 | ## Example 91 | 92 | In this ruleset: 93 | 94 | ``` 95 | commands: 96 | "git:rev-list": "dl:drop" # (1) 97 | "git:config": "dl:drop" # (1) 98 | "git:checkout#path": "dl:drop" # (1) 99 | 100 | "git:checkout#branch": "dl:verbose" # (2) 101 | 102 | "git:checkout": "dl:process" # (3) 103 | 104 | "git": "dl:summary" # (4) 105 | 106 | defaults: 107 | detail: "dl:drop" # (5) 108 | ``` 109 | 110 | The receiver will: 111 | * (1) drop telemetry for `git rev-list`, `git config`, and individual 112 | file (path) checkouts, 113 | * (2) emit verbose telemetry for branch changes, 114 | * (3) emit process level telemetry other types of `git checkout` commands, 115 | * (4) emit summary telemetry for all other `git` commands, and 116 | * (5) drop telemetry from any non-git command. 117 | 118 | Note that the `commands` array is a dictionary rather than a list, so 119 | order does not matter. Lookups will try `` then `` and 120 | then `` until a match is found. 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /Docs/configure-custom-collector.md: -------------------------------------------------------------------------------- 1 | # Configure a Custom Collector 2 | 3 | The custom collector requires a `config.yml` file to specify 4 | which (of the linked-in) components should be initialized, 5 | how data should flow between them, and how they should be 6 | individually configured. 7 | 8 | [Collector Configuration](https://opentelemetry.io/docs/collector/configuration/) 9 | describes the concepts and `config.yml` file format. 10 | 11 | ## Adding `trace2receiver` to `config.yml` 12 | 13 | To use the `trace2receiver` component the following sections must be 14 | present in the `config.yml` file. The first section tells the custom 15 | collector framework to initialize the component. This is conceptually 16 | like causing the component's constructor function to be called when 17 | the collector boots up. The second section puts the receiver 18 | component into a 19 | [pipeline](https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/design.md#pipelines) 20 | with one or more other components so that the OTEL telemetry data 21 | generated by the `trace2receiver` component actually goes somewhere. 22 | 23 | ``` 24 | receivers: 25 | trace2receiver: 26 | 27 | 28 | ... 29 | 30 | service: 31 | pipelines: 32 | traces: 33 | receivers: [trace2receiver] 34 | processors: [] 35 | exporters: [, logging] 36 | ``` 37 | 38 | ## `trace2receiver` Specific Config Values 39 | 40 | The `trace2receiver` component has the following config values: 41 | 42 | ``` 43 | receivers: 44 | trace2receiver: 45 | socket: 46 | pipe: 47 | pii: 48 | filter: 49 | ``` 50 | 51 | For example: 52 | 53 | ``` 54 | receivers: 55 | trace2receiver: 56 | socket: "/usr/local/my-collector/trace2.socket" 57 | pipe: "//./pipe/my-collector.pipe" 58 | pii: "/usr/local/my-collector/pii.yml" 59 | filter: "/usr/local/my-collector/filter.yml" 60 | ``` 61 | 62 | ### `` (Required on Unix) 63 | 64 | The pathname will be used on Linux and macOS hosts to create a Unix 65 | Domain Socket where the receiver will listen for telemetry from Git 66 | commands. The socket will be created when the collector starts up. 67 | 68 | To tell Git to send Trace2 telemetry to the receiver, you must set 69 | the Git `trace2.eventtarget` config setting at the `global` or 70 | `system` level to this socket pathname. See the Git Trace2 API 71 | documentation on the 72 | [event format target](https://git-scm.com/docs/api-trace2#_the_event_format_target) 73 | and 74 | [enabling a target](https://git-scm.com/docs/api-trace2#_enabling_a_target) 75 | for details. 76 | 77 | ``` 78 | $ git config --system trace2.eventtarget "af_unix:/usr/local/my-collector/trace2.socket" 79 | ``` 80 | 81 | _The `af_unix:` prefix is required to tell Git that it should expect a 82 | Unix Domain Socket rather than a plain file._ 83 | 84 | ### `` (Required on Windows) 85 | 86 | The pathname will be used on Windows hosts to create a Windows Named 87 | Pipe where the receiver will listen for telemetry from Git commands. 88 | This named pipe will be created when the collector starts up. This 89 | pathname must refer to a local named pipe `//./pipe/...` because named 90 | pipe servers can only create and listen on local pipes. You may use 91 | forward or backslashes. 92 | 93 | To tell Git to send Trace2 telemetry to the receiver, you must set 94 | the Git `trace2.eventtarget` config setting at the `global` or 95 | `system` level to this named pipe pathname. See the Git Trace2 API 96 | documentation on the 97 | [event format target](https://git-scm.com/docs/api-trace2#_the_event_format_target) 98 | and 99 | [enabling a target](https://git-scm.com/docs/api-trace2#_enabling_a_target) 100 | for details. 101 | 102 | ``` 103 | $ git config --system trace2.eventtarget "//./pipe/my-collector.pipe" 104 | ``` 105 | 106 | ### `` (Optional) 107 | 108 | The pathname to a `pii.yml` file containing privacy-related feature flags. 109 | This is optional. These features are disabled by default. 110 | 111 | See [config PII settings](./config-pii-settings.md) for details. 112 | 113 | ### `` (Optional) 114 | 115 | The pathname to a `filter.yml` file controlling the verbosity of the 116 | generated OTEL telemetry data. This is optional. If omitted, 117 | summary-level telemetry will be emitted. 118 | 119 | See [config filter settings](./config-filter-settings.md) for details. 120 | -------------------------------------------------------------------------------- /Docs/generate-custom-collector.md: -------------------------------------------------------------------------------- 1 | # Generating a Custom Collector 2 | 3 | An 4 | [OTEL Custom Collector](https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/design.md#opentelemetry-collector-architecture) 5 | is an instance of a stock table-driven service daemon that can receive 6 | telemetry data from a variety of sources, transform or process the 7 | data in some way, and export/relay the data to a variety of cloud 8 | services. 9 | 10 | 11 | 12 | ## Generating Source Code for a Customer Collector 13 | 14 | Source code for a custom collector is generated using the 15 | [OTEL Collector Builder (OCB)](https://github.com/open-telemetry/opentelemetry-collector/tree/main/cmd/builder) 16 | tool. A `builder-config.yml` configuration file specifies the set 17 | of supported 18 | ["receiver"](https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/design.md#receivers), 19 | ["processor"](https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/design.md#processors), 20 | and 21 | ["exporter"](https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/design.md#exporters) 22 | components that will be statically linked into the resulting custom collector executable. 23 | 24 | The above link shows how to install the OCB tool, create a 25 | `builder-config.yml` file, and run the tool generate your custom 26 | collector source code. 27 | 28 | 29 | 30 | ## Available Exporter Components 31 | 32 | The modular nature of the OTEL Collector allows us to bundle 33 | many different exporters into the collector executable and then 34 | simply refer to them in the `config.yml` file. 35 | 36 | There are too many exporters in the catalog to include them all 37 | in a generated collector, so just select the ones that you need. 38 | Here are a few popular ones: 39 | 40 | 1. [OTLP](https://pkg.go.dev/go.opentelemetry.io/collector/exporter/otlpexporter#section-readme) 41 | 2. [Azure Monitor Application Insights](https://pkg.go.dev/github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azuremonitorexporter) 42 | 3. [Local Logging](https://pkg.go.dev/go.opentelemetry.io/collector/exporter/loggingexporter) 43 | 44 | Others can be found here: 45 | 46 | * [Primary Exporters](https://github.com/open-telemetry/opentelemetry-collector/tree/main/exporter) 47 | * [Contrib Exporters](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter) 48 | 49 | 50 | 51 | ## Example `builder-config.yml` 52 | 53 | Your `builder-config.yml` file should list all of the components that 54 | you want to use. These will be statically linked into your collector's 55 | executable. For example: 56 | 57 | ``` 58 | dist: 59 | module: 60 | name: 61 | output_path: 62 | ... 63 | 64 | exporters: 65 | - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/exporter/azuremonitorexporter v0.76.1 66 | - import: go.opentelemetry.io/collector/exporter/loggingexporter 67 | gomod: go.opentelemetry.io/collector v0.76.1 68 | - import: go.opentelemetry.io/collector/exporter/otlpexporter 69 | gomod: go.opentelemetry.io/collector v0.76.1 70 | 71 | receivers: 72 | - import: go.opentelemetry.io/collector/receiver/otlpreceiver 73 | gomod: go.opentelemetry.io/collector v0.76.1 74 | - gomod: github.com/git-ecosystem/trace2receiver v0.0.0 75 | 76 | processors: 77 | - import: go.opentelemetry.io/collector/processor/batchprocessor 78 | gomod: go.opentelemetry.io/collector v0.76.1 79 | ``` 80 | 81 | Here we reference stock (core) OTLP and Logging components, 82 | the Azure Monitor component from the 83 | [OTEL Collector Contrib](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main) 84 | collection, 85 | and the `trace2receiver` component from this repository. 86 | 87 | All of these component libraries will be statically linked into your 88 | custom collector. 89 | 90 | 91 | 92 | ## Running the Builder Tool 93 | 94 | 95 | ``` 96 | $ ~/go/bin/builder --config ./builder-config.yml --skip-compilation --skip-get-modules 97 | $ cd 98 | $ go build 99 | ``` 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright GitHub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Trace2 Receiver 2 | 3 | The `trace2receiver` project is a 4 | [trace receiver](https://opentelemetry.io/docs/collector/trace-receiver/) 5 | component library for an 6 | [OpenTelemetry Custom Collector](https://opentelemetry.io/docs/collector/) 7 | daemon. It receives 8 | [Git Trace2](https://git-scm.com/docs/api-trace2#_the_event_format_target) 9 | telemetry from local Git commands, translates it into an OpenTelemetry 10 | format, and forwards it to other OpenTelemetry components. 11 | 12 | This component is useful it you want to collect performance data for 13 | Git commands, aggregate data from multiple users to create performance 14 | dashboards, build distributed traces of nested Git commands, or 15 | understand how the size and shape of your Git repositories affect 16 | command performance. 17 | 18 | 19 | ## Background 20 | 21 | This project is a GOLANG static library component that must be linked 22 | into an OpenTelemetry Custom Collector along with other pipeline and 23 | exporter components to process and forward the telemetry data to a 24 | data store, such as Azure Monitor or another 25 | [OTLP](https://opentelemetry.io/docs/specs/otel/protocol/) 26 | aware cloud provider. 27 | 28 | Setup and configuration details are provided in the 29 | [Docs](./Docs/README.md). 30 | 31 | 32 | The [sample-trace2-otel-collector](https://github.com/git-ecosystem/sample-trace2-otel-collector) 33 | peer repository contains a pre-built open source sample collector to help you get started. See the 34 | README for more details. 35 | 36 | 37 | ## Contributions 38 | 39 | This project is under active development, and loves contributions from the community. 40 | Check out the 41 | [CONTRIBUTING](./CONTRIBUTING.md) 42 | guide for details on getting started. 43 | 44 | 45 | ## Requirements 46 | 47 | This project is written in GOLANG and uses 48 | [OpenTelemetry](https://opentelemetry.io/docs/getting-started/dev/) 49 | libraries and tools. See the OpenTelemetry documentation for more 50 | information. 51 | 52 | This project runs on Linux, macOS, and Windows. 53 | 54 | 55 | 56 | ## License 57 | 58 | This project is licensed under the terms of the MIT open source license. 59 | Please refer to [LICENSE](./LICENSE) for the full terms. 60 | 61 | 62 | ## Maintainers 63 | 64 | See [CODEOWNERS](./CODEOWNERS) for a list of current project maintainers. 65 | 66 | 67 | ## Support 68 | 69 | See [SUPPORT](./SUPPORT.md) for instructions on how to file bugs, make feature 70 | requests, or seek help. 71 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Thanks for helping make GitHub safe for everyone. 2 | 3 | # Security 4 | 5 | GitHub takes the security of our software products and services seriously, including all of the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). 6 | 7 | Even though [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) and therefore not eligible for bounty rewards, we will ensure that your finding gets passed along to the appropriate maintainers for remediation. 8 | 9 | ## Reporting Security Issues 10 | 11 | If you believe you have found a security vulnerability in any GitHub-owned repository, please report it to us through coordinated disclosure. 12 | 13 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 14 | 15 | Instead, please send an email to opensource-security[@]github.com. 16 | 17 | Please include as much of the information listed below as you can to help us better understand and resolve the issue: 18 | 19 | * The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) 20 | * Full paths of source file(s) related to the manifestation of the issue 21 | * The location of the affected source code (tag/branch/commit or direct URL) 22 | * Any special configuration required to reproduce the issue 23 | * Step-by-step instructions to reproduce the issue 24 | * Proof-of-concept or exploit code (if possible) 25 | * Impact of the issue, including how an attacker might exploit the issue 26 | 27 | This information will help us triage your report more quickly. 28 | 29 | ## Policy 30 | 31 | See [GitHub's Safe Harbor Policy](https://docs.github.com/en/github/site-policy/github-bug-bounty-program-legal-safe-harbor#1-safe-harbor-terms) 32 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new issue. 6 | 7 | For help or questions about using this project, please create an issue or start a discussion. 8 | 9 | - `trace2receiver` is under active development and maintained by GitHub staff **AND THE COMMUNITY**. We will do our best to respond to support, feature requests, and community questions in a timely manner. 10 | 11 | ## GitHub Support Policy 12 | 13 | Support for this project is limited to the resources listed above. 14 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | ) 9 | 10 | // `Config` represents the complete configuration settings for 11 | // an individual receiver declaration from the `config.yaml`. 12 | // 13 | // These fields must be public (start with capital letter) so 14 | // that the generic code in the collector can find them. 15 | // 16 | // We have different types of OS-specific paths where we listen 17 | // for Trace2 telemetry. We allow both types in a single config 18 | // file, so that we can share it between clients; only the 19 | // correct one for the platform will actually be used. 20 | type Config struct { 21 | // On Windows, this is a named pipe. The canonical form is 22 | // (the backslash spelling of) `//./pipe/`. 23 | // 24 | // `CreateNamedPipeW()` documents that named pipes can only be 25 | // created on the local NPFS and must use `.` rather than a 26 | // general UNC hostname. (Subsequent clients can connect to 27 | // a remote pipe, but a server can only CREATE a local one. 28 | // 29 | // Therefore, we allow the pipename to be abbreviated in the 30 | // `config.yaml` as just `` and assume the prefix. 31 | // 32 | // This config file field is ignored on non-Windows platforms. 33 | NamedPipePath string `mapstructure:"pipe"` 34 | 35 | // On Unix, this is a Unix domain socket. This is an absolute 36 | // or relative pathname on the local file system. To avoid 37 | // confusion with the existing Git Trace2 setup, we allow this 38 | // to be of the form `af_unix:[:]` and strip 39 | // off the prefix. 40 | // 41 | // This config file field is ignored on Windows platforms. 42 | UnixSocketPath string `mapstructure:"socket"` 43 | 44 | // Allow command and control verbs to be embedded in the Trace2 45 | // data stream. 46 | AllowCommandControlVerbs bool `mapstructure:"enable_commands"` 47 | 48 | // Pathname to YML file containing PII settings. 49 | PiiSettingsPath string `mapstructure:"pii"` 50 | piiSettings *PiiSettings 51 | 52 | // Pathname to YML file containing our filter settings. 53 | FilterSettingsPath string `mapstructure:"filter"` 54 | filterSettings *FilterSettings 55 | } 56 | 57 | // `Validate()` checks if the receiver configuration is valid. 58 | // 59 | // This function is called once for each `trace2receiver[/]:` 60 | // declaration (in the top-level `receivers:` section). 61 | // 62 | // The file format and the customer collector framework 63 | // allows more than one instance of a `trace2receiver` to be 64 | // defined (presumably with different source types, pathnames, 65 | // or verbosity) and run concurrently within this process. 66 | // See: https://opentelemetry.io/docs/collector/configuration/ 67 | // 68 | // A receiver declaration does not imply that it will actually 69 | // be instantiated (realized) in the factory. The receiver 70 | // declaration causes a `cfg *Config` to be instantiated and 71 | // that's it. (The instantiation in the factory is controlled 72 | // by the `service.pipelines.traces.receivers:` array.) 73 | func (cfg *Config) Validate() error { 74 | 75 | var path string 76 | var err error 77 | 78 | if runtime.GOOS == "windows" { 79 | if len(cfg.NamedPipePath) == 0 { 80 | return fmt.Errorf("receivers.trace2receiver.pipe not defined") 81 | } 82 | path, err = normalize_named_pipe_path(cfg.NamedPipePath) 83 | if err != nil { 84 | return fmt.Errorf("receivers.trace2receiver.pipe invalid: '%s'", 85 | err.Error()) 86 | } 87 | cfg.NamedPipePath = path 88 | } else { 89 | if len(cfg.UnixSocketPath) == 0 { 90 | return fmt.Errorf("receivers.trace2receiver.socket not defined") 91 | } 92 | path, err = normalize_uds_path(cfg.UnixSocketPath) 93 | if err != nil { 94 | return fmt.Errorf("receivers.trace2receiver.socket invalid: '%s'", 95 | err.Error()) 96 | } 97 | cfg.UnixSocketPath = path 98 | } 99 | 100 | if len(cfg.PiiSettingsPath) > 0 { 101 | cfg.piiSettings, err = parsePiiFile(cfg.PiiSettingsPath) 102 | if err != nil { 103 | return err 104 | } 105 | } 106 | 107 | if len(cfg.FilterSettingsPath) > 0 { 108 | cfg.filterSettings, err = parseFilterSettings(cfg.FilterSettingsPath) 109 | if err != nil { 110 | return err 111 | } 112 | } 113 | 114 | return nil 115 | } 116 | 117 | // Require (the backslash spelling of) `//./pipe/` but allow 118 | // `` as an alias for the full spelling. Complain if given a 119 | // regular UNC or drive letter pathname. 120 | func normalize_named_pipe_path(in string) (string, error) { 121 | 122 | in_lower := strings.ToLower(in) // normalize to lowercase 123 | in_slash := filepath.Clean(in_lower) // normalize to backslashes 124 | if strings.HasPrefix(in_slash, `\\.\pipe\`) { 125 | // We were given a NPFS path. Use the original as is. 126 | return in, nil 127 | } 128 | 129 | if strings.HasPrefix(in_slash, `\\`) { 130 | // We were given a general UNC path. Reject it. 131 | return "", fmt.Errorf(`expect '[\\.\pipe\]'`) 132 | } 133 | 134 | if len(in) > 2 && in[1] == ':' { 135 | // We have a drive letter. Reject it. 136 | return "", fmt.Errorf(`expect '[\\.\pipe\]'`) 137 | } 138 | 139 | // We cannot use `filepath.VolumeName()` or `filepath.Abs()` 140 | // because they will be interpreted relative to the CWD 141 | // which is not on the NPFS. 142 | // 143 | // So assume that this relative path is a shortcut and join it 144 | // with our required prefix. 145 | 146 | out := filepath.Join(`\\.\pipe`, in) 147 | return out, nil 148 | } 149 | 150 | // Pathnames for Unix domain sockets are just normal Unix 151 | // pathnames. However, we do allow an optional `af_unix:` 152 | // or `af_unix:stream:` prefix. (This helps if they set it 153 | // to the value of the GIT_TRACE2_EVENT string, which does 154 | // require the prefix.) 155 | func normalize_uds_path(in string) (string, error) { 156 | 157 | p, found := strings.CutPrefix(in, "af_unix:stream:") 158 | if found { 159 | return p, nil 160 | } 161 | 162 | _, found = strings.CutPrefix(in, "af_unix:dgram:") 163 | if found { 164 | return "", fmt.Errorf("SOCK_DGRAM sockets are not supported") 165 | } 166 | 167 | p, found = strings.CutPrefix(in, "af_unix:") 168 | if found { 169 | return p, nil 170 | } 171 | 172 | return in, nil 173 | } 174 | -------------------------------------------------------------------------------- /factory.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import ( 4 | "go.opentelemetry.io/collector/component" 5 | "go.opentelemetry.io/collector/receiver" 6 | ) 7 | 8 | var ( 9 | typeStr = component.MustNewType("trace2receiver") 10 | ) 11 | 12 | const ( 13 | stability = component.StabilityLevelStable 14 | ) 15 | 16 | func createDefaultConfig() component.Config { 17 | return &Config{ 18 | NamedPipePath: "", 19 | UnixSocketPath: "", 20 | AllowCommandControlVerbs: false, 21 | PiiSettingsPath: "", 22 | piiSettings: nil, 23 | FilterSettingsPath: "", 24 | filterSettings: nil, 25 | } 26 | } 27 | 28 | //func createMetrics(_ context.Context, params receiver.CreateSettings, baseCfg component.Config, consumer consumer.Metrics) (receiver.Metrics, error) { 29 | // return nil, nil 30 | //} 31 | 32 | //func createLogs(_ context.Context, params receiver.CreateSettings, baseCfg component.Config, consumer consumer.Logs) (receiver.Logs, error) { 33 | // return nil, nil 34 | //} 35 | 36 | // NewFactory creates a factory for trace2 receiver. 37 | func NewFactory() receiver.Factory { 38 | return receiver.NewFactory( 39 | typeStr, 40 | createDefaultConfig, 41 | receiver.WithTraces(createTraces, stability), 42 | //receiver.WithMetrics(createMetrics, stability), 43 | //receiver.WithLogs(createLogs, stability), 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /filter_settings.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // FilterSettings describes how we should filter the OTLP output 9 | // that we generate. It also describes the special keys that we 10 | // look for in the Trace2 event stream to help us decide how to 11 | // filter data for a particular command. 12 | type FilterSettings struct { 13 | Keynames FilterKeynames `mapstructure:"keynames"` 14 | Nicknames FilterNicknames `mapstructure:"nicknames"` 15 | Rulesets FilterRulesets `mapstructure:"rulesets"` 16 | Defaults FilterDefaults `mapstructure:"defaults"` 17 | 18 | // The set of custom rulesets defined in YML are each parsed 19 | // and loaded into definitions so that we can use them. 20 | rulesetDefs map[string]*RulesetDefinition 21 | } 22 | 23 | // FilterKeynames defines the names of the Git config settings that 24 | // will be used in `def_param` events to send repository/worktree 25 | // data to us. This lets a site have their own namespace for 26 | // these keys. Some of these keys will also be sent to the cloud. 27 | type FilterKeynames struct { 28 | 29 | // NicknameKey defines the Git config setting that can be used 30 | // to send an optional user-friendly id or nickname for a repo 31 | // or worktree. 32 | // 33 | // We can use the nickname to decide how to filter data 34 | // for the repo and to identify the repo in the cloud (and 35 | // possibly without exposing any PII or the actualy identity 36 | // of the repo/worktree). 37 | // 38 | // This can eliminate the need to rely on `remote.origin.url` 39 | // or the worktree root directory to identify (or guess at 40 | // the identity of) the repo. 41 | NicknameKey string `mapstructure:"nickname_key"` 42 | 43 | // RuleSetKey defines the Git config setting that can be used 44 | // to optionally send the name of the desired filter ruleset. 45 | // This value overrides any implied ruleset associated with 46 | // the RepoIdKey. 47 | RulesetKey string `mapstructure:"ruleset_key"` 48 | } 49 | 50 | // FilterDefaults defines default filtering values. 51 | type FilterDefaults struct { 52 | 53 | // Ruleset defines the default ruleset or detail level to be 54 | // used when we receive data from a repo/worktree that does 55 | // not explicitly name one or does not have a nickname mapping. 56 | // 57 | // If not set, we default to the absolute default. 58 | RulesetName string `mapstructure:"ruleset"` 59 | } 60 | 61 | // FilterNicknames is used to map a repo nickname to the name of the 62 | // ruleset or detail-level that should be used. 63 | // 64 | // This table is optional. 65 | type FilterNicknames map[string]string 66 | 67 | // FilterRulesets is used to map a custom ruleset name to the pathname 68 | // of the associated YML file. This form is used when parsing the 69 | // filter settings YML file. We use this to create the real ruleset 70 | // table (possibly with lazy loading). 71 | type FilterRulesets map[string]string 72 | 73 | // Parse `filter.yml` in decode. 74 | func parseFilterSettings(path string) (*FilterSettings, error) { 75 | return parseYmlFile[FilterSettings](path, parseFilterSettingsFromBuffer) 76 | } 77 | 78 | // Parse a buffer containing the contents of a `filter.yml` and decode. 79 | func parseFilterSettingsFromBuffer(data []byte, path string) (*FilterSettings, error) { 80 | fs, err := parseYmlBuffer[FilterSettings](data, path) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | // After parsing the YML and populating the `mapstructure` fields, we need 86 | // to validate them and/or build internal structures from them. 87 | 88 | // For each custom ruleset [ -> ] in the table (the map[string]string), 89 | // create a peer entry in the internal [ -> ] table and preload 90 | // the various `ruleset.yml` files. 91 | fs.rulesetDefs = make(map[string]*RulesetDefinition) 92 | for k_rs_name, v_rs_path := range fs.Rulesets { 93 | if !strings.HasPrefix(k_rs_name, "rs:") || len(k_rs_name) < 4 || len(v_rs_path) == 0 { 94 | return nil, fmt.Errorf("ruleset has invalid name or pathname'%s':'%s'", k_rs_name, v_rs_path) 95 | } 96 | 97 | fs.rulesetDefs[k_rs_name], err = parseRulesetFile(v_rs_path) 98 | if err != nil { 99 | return nil, err 100 | } 101 | } 102 | 103 | return fs, nil 104 | } 105 | 106 | // Add a ruleset to the filter settings. This is primarily for writing test code. 107 | func (fs *FilterSettings) addRuleset(rs_name string, path string, rsdef *RulesetDefinition) { 108 | if fs.Rulesets == nil { 109 | fs.Rulesets = make(FilterRulesets) 110 | } 111 | fs.Rulesets[rs_name] = path 112 | 113 | if fs.rulesetDefs == nil { 114 | fs.rulesetDefs = make(map[string]*RulesetDefinition) 115 | } 116 | fs.rulesetDefs[rs_name] = rsdef 117 | } 118 | 119 | // For example: 120 | // 121 | // Tell Git to send a `def_param` for all config settings with 122 | // the `otel.trace2.*` namespace. 123 | // 124 | // $ git config --system trace2.configparams "otel.trace2.*" 125 | // 126 | // Tell Git that my workrepo worktree is an instance of "monorepo" 127 | // (regardless what the origin URL or worktree root directory 128 | // names are). 129 | // 130 | // $ cd /path/to/my/workrepo/ 131 | // $ git config --local otel.trace2.nickname "monorepo" 132 | // 133 | // Tell Git that my duplicate workrepo worktree is another 134 | // instance of the same "monorepo" (so data from both repos 135 | // can be aggregated in the cloud). 136 | // 137 | // $ cd /path/to/my/workrepo-copy/ 138 | // $ git config --local otel.trace2.nickname "monorepo" 139 | // 140 | // 141 | // 142 | // Tell Git that my privaterepo is an instance of "private" 143 | // (or is a member of a group distinct from my other repos). 144 | // 145 | // $ cd /path/to/my/privaterepo 146 | // $ git config --local otel.trace2.nickname "private" 147 | // 148 | // Tell Git that my other worktree should be filtered using 149 | // the "rs:xyz" ruleset (regardless of whether there is a nickname 150 | // defined for the worktree). 151 | // 152 | // $ cd /path/to/my/otherrepo 153 | // $ git config --local otel.trace2.ruleset "rs:xyz" 154 | // 155 | // 156 | // filter.yml 157 | // ========== 158 | // keynames: 159 | // nickname_key: "otel.trace2.nickname" 160 | // ruleset_key: "otel.trace2.ruleset" 161 | // 162 | // nicknames: 163 | // "monorepo": "dl:verbose" 164 | // "private": "dl:drop" 165 | // 166 | // rulesets: 167 | // "rs:status": "./rulesets/rs-status.yml" 168 | // "rs:xyz": "./rulesets/rs-xyz.yml" 169 | // 170 | // defaults: 171 | // ruleset: "dl:summary" 172 | // 173 | // 174 | // rulesets/rs-status.yml 175 | // ====================== 176 | // commands: 177 | // "git:status": "dl:verbose" 178 | // 179 | // defaults: 180 | // detail: "dl:drop" 181 | // 182 | // 183 | // rulesets/rs-xyz.yml 184 | // =================== 185 | // commands: 186 | // "git:fetch": "dl:verbose" 187 | // "git:pull": "dl:verbose" 188 | // "git:status": "dl:summary" 189 | // 190 | // defaults: 191 | // detail: "dl:drop" 192 | -------------------------------------------------------------------------------- /filter_settings_test.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | // Each of the "TEST/*" pathnames are a fake placeholder to make 10 | // the `FilterSettings` data structure happy. We do everything 11 | // in memory here. 12 | var x_fs_path string = "TEST/fs.yml" 13 | var x_rs_path string = "TEST/rs.yml" 14 | 15 | var x_qn = QualifiedNames{ 16 | exe: "c", 17 | exeVerb: "c:v", 18 | exeVerbMode: "c:v#m", 19 | } 20 | 21 | // ////////////////////////////////////////////////////////////// 22 | 23 | var x_fs_empty_yml string = ` 24 | ` 25 | 26 | // If filter settings is empty, we always get the global 27 | // builtin default detail level. 28 | func Test_Empty_FilterSettings(t *testing.T) { 29 | params := make(map[string]string) 30 | 31 | fs := x_TryLoadFilterSettings(t, x_fs_empty_yml, x_fs_path) 32 | 33 | dl, dl_debug := computeDetailLevel(fs, params, x_qn) 34 | 35 | assert.Equal(t, DetailLevelSummary, dl) // the inherited global default 36 | assert.Equal(t, "[builtin-default -> dl:summary]", dl_debug) 37 | } 38 | 39 | // ////////////////////////////////////////////////////////////// 40 | 41 | var x_fs_default_yml string = ` 42 | defaults: 43 | ruleset: "dl:verbose" 44 | ` 45 | 46 | // The filter settings overrides the global builtin default detail 47 | // level. 48 | func Test_Default_FilterSettings(t *testing.T) { 49 | params := make(map[string]string) 50 | 51 | fs := x_TryLoadFilterSettings(t, x_fs_default_yml, x_fs_path) 52 | 53 | dl, dl_debug := computeDetailLevel(fs, params, x_qn) 54 | 55 | assert.Equal(t, DetailLevelVerbose, dl) 56 | assert.Equal(t, "[default-ruleset -> dl:verbose]", dl_debug) 57 | } 58 | 59 | // ////////////////////////////////////////////////////////////// 60 | 61 | var x_fs_rsdef0_yml string = ` 62 | rulesets: 63 | # "rs:rsdef0": "TEST/rs.yml" (use addRuleset()) 64 | 65 | defaults: 66 | ruleset: "rs:rsdef0" 67 | ` 68 | 69 | var x_rs_rsdef0_name string = "rs:rsdef0" 70 | 71 | var x_rs_rsdef0_yml string = ` 72 | defaults: 73 | detail: "dl:process" 74 | ` 75 | 76 | // The filter settings overrides the global builtin default detail 77 | // level, but uses a ruleset indirection. Since the ruleset does 78 | // not define any command mappings, it falls thru to the ruleset default. 79 | func Test_RSDef0_FilterSettings(t *testing.T) { 80 | params := make(map[string]string) 81 | 82 | fs := x_TryLoadFilterSettings(t, x_fs_rsdef0_yml, x_fs_path) 83 | x_TryLoadRuleset(t, fs, x_rs_rsdef0_name, x_rs_path, x_rs_rsdef0_yml) 84 | 85 | dl, dl_debug := computeDetailLevel(fs, params, x_qn) 86 | 87 | assert.Equal(t, DetailLevelProcess, dl) 88 | assert.Equal(t, "[default-ruleset -> rs:rsdef0]/[command -> c:v#m]/[ruleset-default -> dl:process]", dl_debug) 89 | } 90 | 91 | // ////////////////////////////////////////////////////////////// 92 | 93 | var x_rkey string = "otel.trace2.ruleset" // must match ruleset_key in the following 94 | 95 | var x_fs_key_yml string = ` 96 | keynames: 97 | ruleset_key: "otel.trace2.ruleset" 98 | 99 | rulesets: 100 | # "rs:rsdef0": "TEST/rs.yml" (use addRuleset()) 101 | # "rs:rsdef1": "TEST/rs.yml" (use addRuleset()) 102 | 103 | defaults: 104 | ruleset: "rs:rsdef0" 105 | ` 106 | 107 | var x_rs_rsdef1_name string = "rs:rsdef1" 108 | 109 | var x_rs_rsdef1_yml string = ` 110 | defaults: 111 | detail: "dl:summary" 112 | ` 113 | 114 | // The filter settings defines a `ruleset_key` as a way to use a Git 115 | // config value to request a specific ruleset. The filter settings 116 | // defines two rulesets by name. 117 | // 118 | // Verify that lookups default to rsdef0 when no key is provided 119 | // and then that we get rsdef1 when requested. 120 | // 121 | // If the requested ruleset does not exist, fall back to the global 122 | // builtin default detail level. 123 | func Test_RulesetKey_FilterSettings(t *testing.T) { 124 | params := make(map[string]string) 125 | 126 | fs := x_TryLoadFilterSettings(t, x_fs_key_yml, x_fs_path) 127 | x_TryLoadRuleset(t, fs, x_rs_rsdef0_name, x_rs_path, x_rs_rsdef0_yml) 128 | x_TryLoadRuleset(t, fs, x_rs_rsdef1_name, x_rs_path, x_rs_rsdef1_yml) 129 | 130 | dl, dl_debug := computeDetailLevel(fs, params, x_qn) 131 | 132 | assert.Equal(t, dl, DetailLevelProcess) 133 | assert.Equal(t, dl_debug, "[default-ruleset -> rs:rsdef0]/[command -> c:v#m]/[ruleset-default -> dl:process]") 134 | 135 | params[x_rkey] = x_rs_rsdef1_name // set the Git config key 136 | 137 | dl, dl_debug = computeDetailLevel(fs, params, x_qn) 138 | 139 | assert.Equal(t, DetailLevelSummary, dl) 140 | assert.Equal(t, "[rskey -> rs:rsdef1]/[command -> c:v#m]/[ruleset-default -> dl:summary]", dl_debug) 141 | 142 | params[x_rkey] += "-bogus" // set the Git config key to an unknown ruleset 143 | 144 | dl, dl_debug = computeDetailLevel(fs, params, x_qn) 145 | 146 | assert.Equal(t, DetailLevelSummary, dl) 147 | assert.Equal(t, "[rskey -> rs:rsdef1-bogus]/[rs:rsdef1-bogus -> INVALID]/[builtin-default -> dl:summary]", dl_debug) 148 | } 149 | 150 | // ////////////////////////////////////////////////////////////// 151 | 152 | var x_nnkey string = "otel.trace2.nickname" 153 | var x_nn string = "monorepo" 154 | 155 | var x_fs_nnkey_yml string = ` 156 | keynames: 157 | nickname_key: "otel.trace2.nickname" 158 | 159 | nicknames: 160 | "monorepo": "rs:rsdef1" 161 | 162 | rulesets: 163 | # "rs:rsdef0": "TEST/rs.yml" (use addRuleset()) 164 | # "rs:rsdef1": "TEST/rs.yml" (use addRuleset()) 165 | 166 | defaults: 167 | ruleset: "rs:rsdef0" 168 | ` 169 | 170 | // The filter settings defines a `nickname_key` as a way to use a Git 171 | // config value to declare that a worktree is an instance of some repo. 172 | // The filter settings defines a table to map nicknames to rulesets. 173 | // And it defines two rulesets. 174 | // 175 | // Verify that lookups default to rsdef0 when no nickname is provided 176 | // and then that we get the rsdef1 ruleset when the nickname is used. 177 | func Test_NicknameKey_FilterSettings(t *testing.T) { 178 | params := make(map[string]string) 179 | 180 | fs := x_TryLoadFilterSettings(t, x_fs_nnkey_yml, x_fs_path) 181 | x_TryLoadRuleset(t, fs, x_rs_rsdef0_name, x_rs_path, x_rs_rsdef0_yml) 182 | x_TryLoadRuleset(t, fs, x_rs_rsdef1_name, x_rs_path, x_rs_rsdef1_yml) 183 | 184 | dl, dl_debug := computeDetailLevel(fs, params, x_qn) 185 | 186 | assert.Equal(t, dl, DetailLevelProcess) 187 | assert.Equal(t, dl_debug, "[default-ruleset -> rs:rsdef0]/[command -> c:v#m]/[ruleset-default -> dl:process]") 188 | 189 | params[x_nnkey] = x_nn // set the Git config key 190 | 191 | dl, dl_debug = computeDetailLevel(fs, params, x_qn) 192 | 193 | assert.Equal(t, DetailLevelSummary, dl) 194 | assert.Equal(t, "[nickname -> monorepo]/[monorepo -> rs:rsdef1]/[command -> c:v#m]/[ruleset-default -> dl:summary]", dl_debug) 195 | 196 | params[x_nnkey] += "-bogus" // set the Git config key to an unknown nickname 197 | 198 | dl, dl_debug = computeDetailLevel(fs, params, x_qn) 199 | 200 | assert.Equal(t, DetailLevelProcess, dl) 201 | assert.Equal(t, "[nickname -> monorepo-bogus]/[monorepo-bogus -> UNKNOWN]/[default-ruleset -> rs:rsdef0]/[command -> c:v#m]/[ruleset-default -> dl:process]", dl_debug) 202 | } 203 | 204 | // ////////////////////////////////////////////////////////////// 205 | 206 | var x_fs_rscmd0_yml string = ` 207 | rulesets: 208 | # "rs:rscmd0": "TEST/rs.yml" (use addRuleset()) 209 | 210 | defaults: 211 | ruleset: "rs:rscmd0" 212 | ` 213 | 214 | var x_rs_rscmd0_name string = "rs:rscmd0" 215 | 216 | var x_rs_rscmd0_yml string = ` 217 | commands: 218 | "c:v#m": "dl:drop" 219 | "c:v": "dl:summary" 220 | "c": "dl:process" 221 | 222 | defaults: 223 | detail: "dl:verbose" 224 | ` 225 | 226 | // The filter settings overrides the global builtin default detail 227 | // level, but uses a ruleset indirection. The rscmd0 ruleset defines 228 | // command mappings. Verify that each command variation gets mapped 229 | // correctly. 230 | func Test_RSCmd0_FilterSettings(t *testing.T) { 231 | params := make(map[string]string) 232 | 233 | fs := x_TryLoadFilterSettings(t, x_fs_rscmd0_yml, x_fs_path) 234 | x_TryLoadRuleset(t, fs, x_rs_rscmd0_name, x_rs_path, x_rs_rscmd0_yml) 235 | 236 | var qn1 = QualifiedNames{ 237 | exe: "c", 238 | exeVerb: "c:v", 239 | exeVerbMode: "c:v#m", 240 | } 241 | 242 | dl, dl_debug := computeDetailLevel(fs, params, qn1) 243 | 244 | assert.Equal(t, DetailLevelDrop, dl) 245 | assert.Equal(t, "[default-ruleset -> rs:rscmd0]/[command -> c:v#m]/[c:v#m -> dl:drop]", dl_debug) 246 | 247 | qn1.exeVerbMode = "c:v#ZZ" // change the mode to get verb fallback 248 | 249 | dl, dl_debug = computeDetailLevel(fs, params, qn1) 250 | 251 | assert.Equal(t, DetailLevelSummary, dl) 252 | assert.Equal(t, "[default-ruleset -> rs:rscmd0]/[command -> c:v#ZZ]/[c:v -> dl:summary]", dl_debug) 253 | 254 | qn1.exeVerb = "c:YY" // change the verb to get exe fallback 255 | qn1.exeVerbMode = "c:YY#ZZ" 256 | 257 | dl, dl_debug = computeDetailLevel(fs, params, qn1) 258 | 259 | assert.Equal(t, DetailLevelProcess, dl) 260 | assert.Equal(t, "[default-ruleset -> rs:rscmd0]/[command -> c:YY#ZZ]/[c -> dl:process]", dl_debug) 261 | 262 | qn1.exe = "XX" // change the exe to get ruleset default fallback 263 | qn1.exeVerb = "XX:YY" 264 | qn1.exeVerbMode = "XX:YY#ZZ" 265 | 266 | dl, dl_debug = computeDetailLevel(fs, params, qn1) 267 | 268 | assert.Equal(t, DetailLevelVerbose, dl) 269 | assert.Equal(t, "[default-ruleset -> rs:rscmd0]/[command -> XX:YY#ZZ]/[ruleset-default -> dl:verbose]", dl_debug) 270 | } 271 | 272 | // ////////////////////////////////////////////////////////////// 273 | 274 | func x_TryLoadFilterSettings(t *testing.T, yml string, path string) *FilterSettings { 275 | fs, err := parseFilterSettingsFromBuffer([]byte(yml), path) 276 | if err != nil { 277 | t.Fatalf("parseFilterSettings(%s): %s", path, err.Error()) 278 | } 279 | return fs 280 | } 281 | 282 | func x_TryLoadRuleset(t *testing.T, fs *FilterSettings, name string, path string, yml string) { 283 | rs, err := parseRulesetFromBuffer([]byte(yml), path) 284 | if err != nil { 285 | t.Fatalf("parseRuleset(%s): %s", path, err.Error()) 286 | } 287 | 288 | fs.addRuleset(name, path, rs) 289 | } 290 | 291 | // ////////////////////////////////////////////////////////////// 292 | 293 | func Test_Nil_Nil_FilterSettings(t *testing.T) { 294 | 295 | dl, dl_debug := computeDetailLevel(nil, nil, x_qn) 296 | 297 | assert.Equal(t, DetailLevelSummary, dl) 298 | assert.Equal(t, "[builtin-default -> dl:summary]", dl_debug) 299 | } 300 | 301 | func Test_FSEmpty_Nil_FilterSettings(t *testing.T) { 302 | 303 | fs := x_TryLoadFilterSettings(t, x_fs_empty_yml, x_fs_path) 304 | 305 | dl, dl_debug := computeDetailLevel(fs, nil, x_qn) 306 | 307 | assert.Equal(t, DetailLevelSummary, dl) 308 | assert.Equal(t, "[builtin-default -> dl:summary]", dl_debug) 309 | } 310 | 311 | func Test_FSNNKey_Nil_FilterSettings(t *testing.T) { 312 | 313 | fs := x_TryLoadFilterSettings(t, x_fs_nnkey_yml, x_fs_path) 314 | x_TryLoadRuleset(t, fs, x_rs_rsdef0_name, x_rs_path, x_rs_rsdef0_yml) 315 | x_TryLoadRuleset(t, fs, x_rs_rsdef1_name, x_rs_path, x_rs_rsdef1_yml) 316 | 317 | dl, dl_debug := computeDetailLevel(fs, nil, x_qn) 318 | 319 | assert.Equal(t, DetailLevelProcess, dl) 320 | assert.Equal(t, "[default-ruleset -> rs:rsdef0]/[command -> c:v#m]/[ruleset-default -> dl:process]", dl_debug) 321 | } 322 | -------------------------------------------------------------------------------- /fsdetaillevel.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // FilterDetailLevel describes the amount of detail in the output 8 | // OTLP that we will generate for a Git command. 9 | type FilterDetailLevel int 10 | 11 | const ( 12 | DetailLevelUnset FilterDetailLevel = iota 13 | DetailLevelDrop 14 | DetailLevelSummary 15 | DetailLevelProcess 16 | DetailLevelVerbose 17 | ) 18 | 19 | // All detail level names have leading "dl:" to help avoid 20 | // cycles when resolving a custom ruleset name. 21 | const ( 22 | DetailLevelDropName string = "dl:drop" 23 | DetailLevelSummaryName string = "dl:summary" 24 | DetailLevelProcessName string = "dl:process" 25 | DetailLevelVerboseName string = "dl:verbose" 26 | 27 | DetailLevelDefaultName string = DetailLevelSummaryName 28 | ) 29 | 30 | // Convert a detail level name into a detail level id. 31 | func getDetailLevel(dl_name string) (FilterDetailLevel, error) { 32 | switch dl_name { 33 | case DetailLevelDropName: 34 | return DetailLevelDrop, nil 35 | case DetailLevelSummaryName: 36 | return DetailLevelSummary, nil 37 | case DetailLevelProcessName: 38 | return DetailLevelProcess, nil 39 | case DetailLevelVerboseName: 40 | return DetailLevelVerbose, nil 41 | default: 42 | return DetailLevelUnset, errors.New("invalid detail level") 43 | } 44 | } 45 | 46 | func WantRegionAndThreadSpans(dl FilterDetailLevel) bool { 47 | return dl == DetailLevelVerbose 48 | } 49 | 50 | func WantChildSpans(dl FilterDetailLevel) bool { 51 | return dl == DetailLevelProcess || dl == DetailLevelVerbose 52 | } 53 | 54 | func WantProcessAncestry(dl FilterDetailLevel) bool { 55 | return dl == DetailLevelProcess || dl == DetailLevelVerbose 56 | } 57 | 58 | func WantProcessAliases(dl FilterDetailLevel) bool { 59 | return dl == DetailLevelProcess || dl == DetailLevelVerbose 60 | } 61 | 62 | func WantProcessTimersCountersAndData(dl FilterDetailLevel) bool { 63 | return dl == DetailLevelProcess || dl == DetailLevelVerbose 64 | } 65 | 66 | func WantMainThreadTimersAndCounters(dl FilterDetailLevel) bool { 67 | return WantRegionAndThreadSpans(dl) 68 | } 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/git-ecosystem/trace2receiver 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.1 6 | 7 | require ( 8 | github.com/stretchr/testify v1.9.0 9 | go.opentelemetry.io/collector/component v0.109.0 10 | go.opentelemetry.io/collector/component/componentstatus v0.109.0 11 | go.opentelemetry.io/collector/consumer v0.109.0 12 | go.opentelemetry.io/collector/pdata v1.15.0 13 | go.opentelemetry.io/collector/receiver v0.109.0 14 | go.opentelemetry.io/otel v1.30.0 15 | go.uber.org/zap v1.27.0 16 | ) 17 | 18 | require ( 19 | go.opentelemetry.io/collector/config/configtelemetry v0.109.0 // indirect 20 | go.opentelemetry.io/collector/consumer/consumerprofiles v0.109.0 // indirect 21 | go.opentelemetry.io/collector/pdata/pprofile v0.109.0 // indirect 22 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect 23 | ) 24 | 25 | require ( 26 | github.com/davecgh/go-spew v1.1.1 // indirect 27 | github.com/go-logr/logr v1.4.2 // indirect 28 | github.com/go-logr/stdr v1.2.2 // indirect 29 | github.com/gogo/protobuf v1.3.2 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c 32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 | github.com/modern-go/reflect2 v1.0.2 // indirect 34 | github.com/pmezard/go-difflib v1.0.0 // indirect 35 | go.opentelemetry.io/otel/metric v1.30.0 // indirect 36 | go.opentelemetry.io/otel/trace v1.30.0 // indirect 37 | go.uber.org/multierr v1.11.0 // indirect 38 | golang.org/x/net v0.33.0 // indirect 39 | golang.org/x/sys v0.28.0 40 | golang.org/x/text v0.21.0 // indirect 41 | google.golang.org/grpc v1.66.2 // indirect 42 | google.golang.org/protobuf v1.34.2 // indirect 43 | gopkg.in/yaml.v2 v2.4.0 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 5 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 6 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 7 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 8 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 9 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 10 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 11 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 12 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 13 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 14 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 15 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 16 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 17 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 18 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 19 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 20 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 21 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 22 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= 23 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 24 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 25 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 27 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 28 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 32 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 33 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 34 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 35 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 36 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 37 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 38 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 39 | go.opentelemetry.io/collector v0.109.0 h1:ULnMWuwcy4ix1oP5RFFRcmpEbaU5YabW6nWcLMQQRo0= 40 | go.opentelemetry.io/collector/component v0.109.0 h1:AU6eubP1htO8Fvm86uWn66Kw0DMSFhgcRM2cZZTYfII= 41 | go.opentelemetry.io/collector/component v0.109.0/go.mod h1:jRVFY86GY6JZ61SXvUN69n7CZoTjDTqWyNC+wJJvzOw= 42 | go.opentelemetry.io/collector/component/componentstatus v0.109.0 h1:LiyJOvkv1lVUqBECvolifM2lsXFEgVXHcIw0MWRf/1I= 43 | go.opentelemetry.io/collector/component/componentstatus v0.109.0/go.mod h1:TBx2Leggcw1c1tM+Gt/rDYbqN9Unr3fMxHh2TbxLizI= 44 | go.opentelemetry.io/collector/config/configtelemetry v0.109.0 h1:ItbYw3tgFMU+TqGcDVEOqJLKbbOpfQg3AHD8b22ygl8= 45 | go.opentelemetry.io/collector/config/configtelemetry v0.109.0/go.mod h1:R0MBUxjSMVMIhljuDHWIygzzJWQyZHXXWIgQNxcFwhc= 46 | go.opentelemetry.io/collector/consumer v0.109.0 h1:fdXlJi5Rat/poHPiznM2mLiXjcv1gPy3fyqqeirri58= 47 | go.opentelemetry.io/collector/consumer v0.109.0/go.mod h1:E7PZHnVe1DY9hYy37toNxr9/hnsO7+LmnsixW8akLQI= 48 | go.opentelemetry.io/collector/consumer/consumerprofiles v0.109.0 h1:+WZ6MEWQRC6so3IRrW916XK58rI9NnrFHKW/P19jQvc= 49 | go.opentelemetry.io/collector/consumer/consumerprofiles v0.109.0/go.mod h1:spZ9Dn1MRMPDHHThdXZA5TrFhdOL1wsl0Dw45EBVoVo= 50 | go.opentelemetry.io/collector/consumer/consumertest v0.109.0 h1:v4w9G2MXGJ/eabCmX1DvQYmxzdysC8UqIxa/BWz7ACo= 51 | go.opentelemetry.io/collector/consumer/consumertest v0.109.0/go.mod h1:lECt0qOrx118wLJbGijtqNz855XfvJv0xx9GSoJ8qSE= 52 | go.opentelemetry.io/collector/pdata v1.15.0 h1:q/T1sFpRKJnjDrUsHdJ6mq4uSqViR/f92yvGwDby/gY= 53 | go.opentelemetry.io/collector/pdata v1.15.0/go.mod h1:2wcsTIiLAJSbqBq/XUUYbi+cP+N87d0jEJzmb9nT19U= 54 | go.opentelemetry.io/collector/pdata/pprofile v0.109.0 h1:5lobQKeHk8p4WC7KYbzL6ZqqX3eSizsdmp5vM8pQFBs= 55 | go.opentelemetry.io/collector/pdata/pprofile v0.109.0/go.mod h1:lXIifCdtR5ewO17JAYTUsclMqRp6h6dCowoXHhGyw8Y= 56 | go.opentelemetry.io/collector/receiver v0.109.0 h1:DTOM7xaDl7FUGQIjvjmWZn03JUE+aG4mJzWWfb7S8zw= 57 | go.opentelemetry.io/collector/receiver v0.109.0/go.mod h1:jeiCHaf3PE6aXoZfHF5Uexg7aztu+Vkn9LVw0YDKm6g= 58 | go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= 59 | go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= 60 | go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= 61 | go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= 62 | go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= 63 | go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= 64 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 65 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 66 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 67 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 68 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 69 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 70 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 71 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 72 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 73 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 74 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 75 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 76 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 77 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 78 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 79 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 80 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 81 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 82 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 83 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 84 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 85 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 86 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 87 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 88 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 89 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 90 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 91 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 92 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 93 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 94 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 95 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 96 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 97 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 98 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 99 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 100 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 101 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= 102 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 103 | google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= 104 | google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= 105 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 106 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 107 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 108 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 109 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 110 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 111 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 112 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 113 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 114 | -------------------------------------------------------------------------------- /internal/go-winio/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Microsoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /internal/go-winio/README.md: -------------------------------------------------------------------------------- 1 | # trace2receiver/internal/go-winio 2 | 3 | This directory contains a stripped down fork of 4 | [Microsoft/go-winio](https://github.com/microsoft/go-winio.git). 5 | It only includes support for Windows named pipes; everything else 6 | has been removed. 7 | 8 | The `go-winio` repository was forked so that I could use the changes 9 | proposed in my 10 | [pipe: add server backlog for concurrent `Accept()` PR](https://github.com/microsoft/go-winio/pull/291) 11 | without the complications of `replace` statements in the `go.mod` files 12 | of generated custom collectors. 13 | 14 | The `go-winio` repository was also forked to allow me maintain 15 | reponsibility for the maintenance and support of the named pipe 16 | routines. It avoids adding a support load from the OTEL community 17 | onto the team that nicely developed and donated this code. 18 | 19 | I've included a brief summary of the code. Please refer to the original 20 | [Microsoft/go-winio/README](https://github.com/microsoft/go-winio/README.md) 21 | for complete information on this code. 22 | 23 | ## `trace2receiver/internal/go-winio` Code Summary 24 | 25 | This repository contains utilities for efficiently performing Win32 IO operations in 26 | Go. Currently, this is focused on accessing named pipes and other file handles, and 27 | for using named pipes as a net transport. 28 | 29 | This code relies on IO completion ports to avoid blocking IO on system threads, allowing Go 30 | to reuse the thread to schedule another goroutine. This limits support to Windows Vista and 31 | newer operating systems. This is similar to the implementation of network sockets in Go's net 32 | package. 33 | 34 | ## `trace2receiver/internal/go-winio` License 35 | 36 | Please see the [LICENSE](./LICENSE) file for licensing information. 37 | 38 | ## `trace2receiver/internal/go-winio` Contributing 39 | 40 | This project welcomes contributions and suggestions. 41 | You can contribute to the forked version of `go-winio` using the instructions 42 | in the `trace2receiver` root directory. To contribute to the upstream version 43 | of `go-winio` please see their contributing guidelines. 44 | 45 | ## Special Thanks 46 | 47 | Thanks to [natefinch][natefinch] for the inspiration for this library. 48 | See [npipe](https://github.com/natefinch/npipe) for another named pipe implementation. 49 | 50 | [natefinch]: https://github.com/natefinch 51 | -------------------------------------------------------------------------------- /internal/go-winio/file.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package winio 5 | 6 | import ( 7 | "errors" 8 | "io" 9 | "runtime" 10 | "sync" 11 | "sync/atomic" 12 | "syscall" 13 | "time" 14 | 15 | "golang.org/x/sys/windows" 16 | ) 17 | 18 | //sys cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) = CancelIoEx 19 | //sys createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) = CreateIoCompletionPort 20 | //sys getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus 21 | //sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes 22 | //sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult 23 | 24 | //todo (go1.19): switch to [atomic.Bool] 25 | 26 | type atomicBool int32 27 | 28 | func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 } 29 | func (b *atomicBool) setFalse() { atomic.StoreInt32((*int32)(b), 0) } 30 | func (b *atomicBool) setTrue() { atomic.StoreInt32((*int32)(b), 1) } 31 | 32 | //revive:disable-next-line:predeclared Keep "new" to maintain consistency with "atomic" pkg 33 | func (b *atomicBool) swap(new bool) bool { 34 | var newInt int32 35 | if new { 36 | newInt = 1 37 | } 38 | return atomic.SwapInt32((*int32)(b), newInt) == 1 39 | } 40 | 41 | var ( 42 | ErrFileClosed = errors.New("file has already been closed") 43 | ErrTimeout = &timeoutError{} 44 | ) 45 | 46 | type timeoutError struct{} 47 | 48 | func (*timeoutError) Error() string { return "i/o timeout" } 49 | func (*timeoutError) Timeout() bool { return true } 50 | func (*timeoutError) Temporary() bool { return true } 51 | 52 | type timeoutChan chan struct{} 53 | 54 | var ioInitOnce sync.Once 55 | var ioCompletionPort windows.Handle 56 | 57 | // ioResult contains the result of an asynchronous IO operation. 58 | type ioResult struct { 59 | bytes uint32 60 | err error 61 | } 62 | 63 | // ioOperation represents an outstanding asynchronous Win32 IO. 64 | type ioOperation struct { 65 | o windows.Overlapped 66 | ch chan ioResult 67 | } 68 | 69 | func initIO() { 70 | h, err := createIoCompletionPort(windows.InvalidHandle, 0, 0, 0xffffffff) 71 | if err != nil { 72 | panic(err) 73 | } 74 | ioCompletionPort = h 75 | go ioCompletionProcessor(h) 76 | } 77 | 78 | // win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall. 79 | // It takes ownership of this handle and will close it if it is garbage collected. 80 | type win32File struct { 81 | handle windows.Handle 82 | wg sync.WaitGroup 83 | wgLock sync.RWMutex 84 | closing atomicBool 85 | socket bool 86 | readDeadline deadlineHandler 87 | writeDeadline deadlineHandler 88 | } 89 | 90 | type deadlineHandler struct { 91 | setLock sync.Mutex 92 | channel timeoutChan 93 | channelLock sync.RWMutex 94 | timer *time.Timer 95 | timedout atomicBool 96 | } 97 | 98 | // makeWin32File makes a new win32File from an existing file handle. 99 | func makeWin32File(h windows.Handle) (*win32File, error) { 100 | f := &win32File{handle: h} 101 | ioInitOnce.Do(initIO) 102 | _, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff) 103 | if err != nil { 104 | return nil, err 105 | } 106 | err = setFileCompletionNotificationModes(h, windows.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS|windows.FILE_SKIP_SET_EVENT_ON_HANDLE) 107 | if err != nil { 108 | return nil, err 109 | } 110 | f.readDeadline.channel = make(timeoutChan) 111 | f.writeDeadline.channel = make(timeoutChan) 112 | return f, nil 113 | } 114 | 115 | // Deprecated: use NewOpenFile instead. 116 | func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) { 117 | return NewOpenFile(windows.Handle(h)) 118 | } 119 | 120 | func NewOpenFile(h windows.Handle) (io.ReadWriteCloser, error) { 121 | // If we return the result of makeWin32File directly, it can result in an 122 | // interface-wrapped nil, rather than a nil interface value. 123 | f, err := makeWin32File(h) 124 | if err != nil { 125 | return nil, err 126 | } 127 | return f, nil 128 | } 129 | 130 | // closeHandle closes the resources associated with a Win32 handle. 131 | func (f *win32File) closeHandle() { 132 | f.wgLock.Lock() 133 | // Atomically set that we are closing, releasing the resources only once. 134 | if !f.closing.swap(true) { 135 | f.wgLock.Unlock() 136 | // cancel all IO and wait for it to complete 137 | _ = cancelIoEx(f.handle, nil) 138 | f.wg.Wait() 139 | // at this point, no new IO can start 140 | windows.Close(f.handle) 141 | f.handle = 0 142 | } else { 143 | f.wgLock.Unlock() 144 | } 145 | } 146 | 147 | // Close closes a win32File. 148 | func (f *win32File) Close() error { 149 | f.closeHandle() 150 | return nil 151 | } 152 | 153 | // IsClosed checks if the file has been closed. 154 | func (f *win32File) IsClosed() bool { 155 | return f.closing.isSet() 156 | } 157 | 158 | // prepareIO prepares for a new IO operation. 159 | // The caller must call f.wg.Done() when the IO is finished, prior to Close() returning. 160 | func (f *win32File) prepareIO() (*ioOperation, error) { 161 | f.wgLock.RLock() 162 | if f.closing.isSet() { 163 | f.wgLock.RUnlock() 164 | return nil, ErrFileClosed 165 | } 166 | f.wg.Add(1) 167 | f.wgLock.RUnlock() 168 | c := &ioOperation{} 169 | c.ch = make(chan ioResult) 170 | return c, nil 171 | } 172 | 173 | // ioCompletionProcessor processes completed async IOs forever. 174 | func ioCompletionProcessor(h windows.Handle) { 175 | for { 176 | var bytes uint32 177 | var key uintptr 178 | var op *ioOperation 179 | err := getQueuedCompletionStatus(h, &bytes, &key, &op, windows.INFINITE) 180 | if op == nil { 181 | panic(err) 182 | } 183 | op.ch <- ioResult{bytes, err} 184 | } 185 | } 186 | 187 | // todo: helsaawy - create an asyncIO version that takes a context 188 | 189 | // asyncIO processes the return value from ReadFile or WriteFile, blocking until 190 | // the operation has actually completed. 191 | func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) { 192 | if err != windows.ERROR_IO_PENDING { //nolint:errorlint // err is Errno 193 | return int(bytes), err 194 | } 195 | 196 | if f.closing.isSet() { 197 | _ = cancelIoEx(f.handle, &c.o) 198 | } 199 | 200 | var timeout timeoutChan 201 | if d != nil { 202 | d.channelLock.Lock() 203 | timeout = d.channel 204 | d.channelLock.Unlock() 205 | } 206 | 207 | var r ioResult 208 | select { 209 | case r = <-c.ch: 210 | err = r.err 211 | if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno 212 | if f.closing.isSet() { 213 | err = ErrFileClosed 214 | } 215 | } else if err != nil && f.socket { 216 | // err is from Win32. Query the overlapped structure to get the winsock error. 217 | var bytes, flags uint32 218 | err = wsaGetOverlappedResult(f.handle, &c.o, &bytes, false, &flags) 219 | } 220 | case <-timeout: 221 | _ = cancelIoEx(f.handle, &c.o) 222 | r = <-c.ch 223 | err = r.err 224 | if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno 225 | err = ErrTimeout 226 | } 227 | } 228 | 229 | // runtime.KeepAlive is needed, as c is passed via native 230 | // code to ioCompletionProcessor, c must remain alive 231 | // until the channel read is complete. 232 | // todo: (de)allocate *ioOperation via win32 heap functions, instead of needing to KeepAlive? 233 | runtime.KeepAlive(c) 234 | return int(r.bytes), err 235 | } 236 | 237 | // Read reads from a file handle. 238 | func (f *win32File) Read(b []byte) (int, error) { 239 | c, err := f.prepareIO() 240 | if err != nil { 241 | return 0, err 242 | } 243 | defer f.wg.Done() 244 | 245 | if f.readDeadline.timedout.isSet() { 246 | return 0, ErrTimeout 247 | } 248 | 249 | var bytes uint32 250 | err = windows.ReadFile(f.handle, b, &bytes, &c.o) 251 | n, err := f.asyncIO(c, &f.readDeadline, bytes, err) 252 | runtime.KeepAlive(b) 253 | 254 | // Handle EOF conditions. 255 | if err == nil && n == 0 && len(b) != 0 { 256 | return 0, io.EOF 257 | } else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno 258 | return 0, io.EOF 259 | } else { 260 | return n, err 261 | } 262 | } 263 | 264 | // Write writes to a file handle. 265 | func (f *win32File) Write(b []byte) (int, error) { 266 | c, err := f.prepareIO() 267 | if err != nil { 268 | return 0, err 269 | } 270 | defer f.wg.Done() 271 | 272 | if f.writeDeadline.timedout.isSet() { 273 | return 0, ErrTimeout 274 | } 275 | 276 | var bytes uint32 277 | err = windows.WriteFile(f.handle, b, &bytes, &c.o) 278 | n, err := f.asyncIO(c, &f.writeDeadline, bytes, err) 279 | runtime.KeepAlive(b) 280 | return n, err 281 | } 282 | 283 | func (f *win32File) SetReadDeadline(deadline time.Time) error { 284 | return f.readDeadline.set(deadline) 285 | } 286 | 287 | func (f *win32File) SetWriteDeadline(deadline time.Time) error { 288 | return f.writeDeadline.set(deadline) 289 | } 290 | 291 | func (f *win32File) Flush() error { 292 | return windows.FlushFileBuffers(f.handle) 293 | } 294 | 295 | func (f *win32File) Fd() uintptr { 296 | return uintptr(f.handle) 297 | } 298 | 299 | func (d *deadlineHandler) set(deadline time.Time) error { 300 | d.setLock.Lock() 301 | defer d.setLock.Unlock() 302 | 303 | if d.timer != nil { 304 | if !d.timer.Stop() { 305 | <-d.channel 306 | } 307 | d.timer = nil 308 | } 309 | d.timedout.setFalse() 310 | 311 | select { 312 | case <-d.channel: 313 | d.channelLock.Lock() 314 | d.channel = make(chan struct{}) 315 | d.channelLock.Unlock() 316 | default: 317 | } 318 | 319 | if deadline.IsZero() { 320 | return nil 321 | } 322 | 323 | timeoutIO := func() { 324 | d.timedout.setTrue() 325 | close(d.channel) 326 | } 327 | 328 | now := time.Now() 329 | duration := deadline.Sub(now) 330 | if deadline.After(now) { 331 | // Deadline is in the future, set a timer to wait 332 | d.timer = time.AfterFunc(duration, timeoutIO) 333 | } else { 334 | // Deadline is in the past. Cancel all pending IO now. 335 | timeoutIO() 336 | } 337 | return nil 338 | } 339 | -------------------------------------------------------------------------------- /internal/go-winio/internal/fs/doc.go: -------------------------------------------------------------------------------- 1 | // This package contains Win32 filesystem functionality. 2 | package fs 3 | -------------------------------------------------------------------------------- /internal/go-winio/internal/fs/fs.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package fs 4 | 5 | import ( 6 | "golang.org/x/sys/windows" 7 | 8 | "github.com/git-ecosystem/trace2receiver/internal/go-winio/internal/stringbuffer" 9 | ) 10 | 11 | //go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go fs.go 12 | 13 | // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew 14 | //sys CreateFile(name string, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) [failretval==windows.InvalidHandle] = CreateFileW 15 | 16 | const NullHandle windows.Handle = 0 17 | 18 | // AccessMask defines standard, specific, and generic rights. 19 | // 20 | // Used with CreateFile and NtCreateFile (and co.). 21 | // 22 | // Bitmask: 23 | // 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 24 | // 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 25 | // +---------------+---------------+-------------------------------+ 26 | // |G|G|G|G|Resvd|A| StandardRights| SpecificRights | 27 | // |R|W|E|A| |S| | | 28 | // +-+-------------+---------------+-------------------------------+ 29 | // 30 | // GR Generic Read 31 | // GW Generic Write 32 | // GE Generic Exectue 33 | // GA Generic All 34 | // Resvd Reserved 35 | // AS Access Security System 36 | // 37 | // https://learn.microsoft.com/en-us/windows/win32/secauthz/access-mask 38 | // 39 | // https://learn.microsoft.com/en-us/windows/win32/secauthz/generic-access-rights 40 | // 41 | // https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants 42 | type AccessMask = windows.ACCESS_MASK 43 | 44 | //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. 45 | const ( 46 | // Not actually any. 47 | // 48 | // For CreateFile: "query certain metadata such as file, directory, or device attributes without accessing that file or device" 49 | // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#parameters 50 | FILE_ANY_ACCESS AccessMask = 0 51 | 52 | GENERIC_READ AccessMask = 0x8000_0000 53 | GENERIC_WRITE AccessMask = 0x4000_0000 54 | GENERIC_EXECUTE AccessMask = 0x2000_0000 55 | GENERIC_ALL AccessMask = 0x1000_0000 56 | ACCESS_SYSTEM_SECURITY AccessMask = 0x0100_0000 57 | 58 | // Specific Object Access 59 | // from ntioapi.h 60 | 61 | FILE_READ_DATA AccessMask = (0x0001) // file & pipe 62 | FILE_LIST_DIRECTORY AccessMask = (0x0001) // directory 63 | 64 | FILE_WRITE_DATA AccessMask = (0x0002) // file & pipe 65 | FILE_ADD_FILE AccessMask = (0x0002) // directory 66 | 67 | FILE_APPEND_DATA AccessMask = (0x0004) // file 68 | FILE_ADD_SUBDIRECTORY AccessMask = (0x0004) // directory 69 | FILE_CREATE_PIPE_INSTANCE AccessMask = (0x0004) // named pipe 70 | 71 | FILE_READ_EA AccessMask = (0x0008) // file & directory 72 | FILE_READ_PROPERTIES AccessMask = FILE_READ_EA 73 | 74 | FILE_WRITE_EA AccessMask = (0x0010) // file & directory 75 | FILE_WRITE_PROPERTIES AccessMask = FILE_WRITE_EA 76 | 77 | FILE_EXECUTE AccessMask = (0x0020) // file 78 | FILE_TRAVERSE AccessMask = (0x0020) // directory 79 | 80 | FILE_DELETE_CHILD AccessMask = (0x0040) // directory 81 | 82 | FILE_READ_ATTRIBUTES AccessMask = (0x0080) // all 83 | 84 | FILE_WRITE_ATTRIBUTES AccessMask = (0x0100) // all 85 | 86 | FILE_ALL_ACCESS AccessMask = (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x1FF) 87 | FILE_GENERIC_READ AccessMask = (STANDARD_RIGHTS_READ | FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | SYNCHRONIZE) 88 | FILE_GENERIC_WRITE AccessMask = (STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE) 89 | FILE_GENERIC_EXECUTE AccessMask = (STANDARD_RIGHTS_EXECUTE | FILE_READ_ATTRIBUTES | FILE_EXECUTE | SYNCHRONIZE) 90 | 91 | SPECIFIC_RIGHTS_ALL AccessMask = 0x0000FFFF 92 | 93 | // Standard Access 94 | // from ntseapi.h 95 | 96 | DELETE AccessMask = 0x0001_0000 97 | READ_CONTROL AccessMask = 0x0002_0000 98 | WRITE_DAC AccessMask = 0x0004_0000 99 | WRITE_OWNER AccessMask = 0x0008_0000 100 | SYNCHRONIZE AccessMask = 0x0010_0000 101 | 102 | STANDARD_RIGHTS_REQUIRED AccessMask = 0x000F_0000 103 | 104 | STANDARD_RIGHTS_READ AccessMask = READ_CONTROL 105 | STANDARD_RIGHTS_WRITE AccessMask = READ_CONTROL 106 | STANDARD_RIGHTS_EXECUTE AccessMask = READ_CONTROL 107 | 108 | STANDARD_RIGHTS_ALL AccessMask = 0x001F_0000 109 | ) 110 | 111 | type FileShareMode uint32 112 | 113 | //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. 114 | const ( 115 | FILE_SHARE_NONE FileShareMode = 0x00 116 | FILE_SHARE_READ FileShareMode = 0x01 117 | FILE_SHARE_WRITE FileShareMode = 0x02 118 | FILE_SHARE_DELETE FileShareMode = 0x04 119 | FILE_SHARE_VALID_FLAGS FileShareMode = 0x07 120 | ) 121 | 122 | type FileCreationDisposition uint32 123 | 124 | //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. 125 | const ( 126 | // from winbase.h 127 | 128 | CREATE_NEW FileCreationDisposition = 0x01 129 | CREATE_ALWAYS FileCreationDisposition = 0x02 130 | OPEN_EXISTING FileCreationDisposition = 0x03 131 | OPEN_ALWAYS FileCreationDisposition = 0x04 132 | TRUNCATE_EXISTING FileCreationDisposition = 0x05 133 | ) 134 | 135 | // Create disposition values for NtCreate* 136 | type NTFileCreationDisposition uint32 137 | 138 | //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. 139 | const ( 140 | // From ntioapi.h 141 | 142 | FILE_SUPERSEDE NTFileCreationDisposition = 0x00 143 | FILE_OPEN NTFileCreationDisposition = 0x01 144 | FILE_CREATE NTFileCreationDisposition = 0x02 145 | FILE_OPEN_IF NTFileCreationDisposition = 0x03 146 | FILE_OVERWRITE NTFileCreationDisposition = 0x04 147 | FILE_OVERWRITE_IF NTFileCreationDisposition = 0x05 148 | FILE_MAXIMUM_DISPOSITION NTFileCreationDisposition = 0x05 149 | ) 150 | 151 | // CreateFile and co. take flags or attributes together as one parameter. 152 | // Define alias until we can use generics to allow both 153 | // 154 | // https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants 155 | type FileFlagOrAttribute uint32 156 | 157 | //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. 158 | const ( 159 | // from winnt.h 160 | 161 | FILE_FLAG_WRITE_THROUGH FileFlagOrAttribute = 0x8000_0000 162 | FILE_FLAG_OVERLAPPED FileFlagOrAttribute = 0x4000_0000 163 | FILE_FLAG_NO_BUFFERING FileFlagOrAttribute = 0x2000_0000 164 | FILE_FLAG_RANDOM_ACCESS FileFlagOrAttribute = 0x1000_0000 165 | FILE_FLAG_SEQUENTIAL_SCAN FileFlagOrAttribute = 0x0800_0000 166 | FILE_FLAG_DELETE_ON_CLOSE FileFlagOrAttribute = 0x0400_0000 167 | FILE_FLAG_BACKUP_SEMANTICS FileFlagOrAttribute = 0x0200_0000 168 | FILE_FLAG_POSIX_SEMANTICS FileFlagOrAttribute = 0x0100_0000 169 | FILE_FLAG_OPEN_REPARSE_POINT FileFlagOrAttribute = 0x0020_0000 170 | FILE_FLAG_OPEN_NO_RECALL FileFlagOrAttribute = 0x0010_0000 171 | FILE_FLAG_FIRST_PIPE_INSTANCE FileFlagOrAttribute = 0x0008_0000 172 | ) 173 | 174 | // NtCreate* functions take a dedicated CreateOptions parameter. 175 | // 176 | // https://learn.microsoft.com/en-us/windows/win32/api/Winternl/nf-winternl-ntcreatefile 177 | // 178 | // https://learn.microsoft.com/en-us/windows/win32/devnotes/nt-create-named-pipe-file 179 | type NTCreateOptions uint32 180 | 181 | //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. 182 | const ( 183 | // From ntioapi.h 184 | 185 | FILE_DIRECTORY_FILE NTCreateOptions = 0x0000_0001 186 | FILE_WRITE_THROUGH NTCreateOptions = 0x0000_0002 187 | FILE_SEQUENTIAL_ONLY NTCreateOptions = 0x0000_0004 188 | FILE_NO_INTERMEDIATE_BUFFERING NTCreateOptions = 0x0000_0008 189 | 190 | FILE_SYNCHRONOUS_IO_ALERT NTCreateOptions = 0x0000_0010 191 | FILE_SYNCHRONOUS_IO_NONALERT NTCreateOptions = 0x0000_0020 192 | FILE_NON_DIRECTORY_FILE NTCreateOptions = 0x0000_0040 193 | FILE_CREATE_TREE_CONNECTION NTCreateOptions = 0x0000_0080 194 | 195 | FILE_COMPLETE_IF_OPLOCKED NTCreateOptions = 0x0000_0100 196 | FILE_NO_EA_KNOWLEDGE NTCreateOptions = 0x0000_0200 197 | FILE_DISABLE_TUNNELING NTCreateOptions = 0x0000_0400 198 | FILE_RANDOM_ACCESS NTCreateOptions = 0x0000_0800 199 | 200 | FILE_DELETE_ON_CLOSE NTCreateOptions = 0x0000_1000 201 | FILE_OPEN_BY_FILE_ID NTCreateOptions = 0x0000_2000 202 | FILE_OPEN_FOR_BACKUP_INTENT NTCreateOptions = 0x0000_4000 203 | FILE_NO_COMPRESSION NTCreateOptions = 0x0000_8000 204 | ) 205 | 206 | type FileSQSFlag = FileFlagOrAttribute 207 | 208 | //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. 209 | const ( 210 | // from winbase.h 211 | 212 | SECURITY_ANONYMOUS FileSQSFlag = FileSQSFlag(SecurityAnonymous << 16) 213 | SECURITY_IDENTIFICATION FileSQSFlag = FileSQSFlag(SecurityIdentification << 16) 214 | SECURITY_IMPERSONATION FileSQSFlag = FileSQSFlag(SecurityImpersonation << 16) 215 | SECURITY_DELEGATION FileSQSFlag = FileSQSFlag(SecurityDelegation << 16) 216 | 217 | SECURITY_SQOS_PRESENT FileSQSFlag = 0x0010_0000 218 | SECURITY_VALID_SQOS_FLAGS FileSQSFlag = 0x001F_0000 219 | ) 220 | 221 | // GetFinalPathNameByHandle flags 222 | // 223 | // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew#parameters 224 | type GetFinalPathFlag uint32 225 | 226 | //nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. 227 | const ( 228 | GetFinalPathDefaultFlag GetFinalPathFlag = 0x0 229 | 230 | FILE_NAME_NORMALIZED GetFinalPathFlag = 0x0 231 | FILE_NAME_OPENED GetFinalPathFlag = 0x8 232 | 233 | VOLUME_NAME_DOS GetFinalPathFlag = 0x0 234 | VOLUME_NAME_GUID GetFinalPathFlag = 0x1 235 | VOLUME_NAME_NT GetFinalPathFlag = 0x2 236 | VOLUME_NAME_NONE GetFinalPathFlag = 0x4 237 | ) 238 | 239 | // getFinalPathNameByHandle facilitates calling the Windows API GetFinalPathNameByHandle 240 | // with the given handle and flags. It transparently takes care of creating a buffer of the 241 | // correct size for the call. 242 | // 243 | // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew 244 | func GetFinalPathNameByHandle(h windows.Handle, flags GetFinalPathFlag) (string, error) { 245 | b := stringbuffer.NewWString() 246 | //TODO: can loop infinitely if Win32 keeps returning the same (or a larger) n? 247 | for { 248 | n, err := windows.GetFinalPathNameByHandle(h, b.Pointer(), b.Cap(), uint32(flags)) 249 | if err != nil { 250 | return "", err 251 | } 252 | // If the buffer wasn't large enough, n will be the total size needed (including null terminator). 253 | // Resize and try again. 254 | if n > b.Cap() { 255 | b.ResizeTo(n) 256 | continue 257 | } 258 | // If the buffer is large enough, n will be the size not including the null terminator. 259 | // Convert to a Go string and return. 260 | return b.String(), nil 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /internal/go-winio/internal/fs/fs_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package fs 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | func Test_GetFinalPathNameByHandle(t *testing.T) { 15 | d := t.TempDir() 16 | // open f via a relative path 17 | name := t.Name() + ".txt" 18 | fullPath := filepath.Join(d, name) 19 | 20 | w, err := os.Getwd() 21 | if err != nil { 22 | t.Fatalf("could not get working directory: %v", err) 23 | } 24 | if err := os.Chdir(d); err != nil { 25 | t.Fatalf("could not chdir to %s: %v", d, err) 26 | } 27 | defer os.Chdir(w) //nolint:errcheck 28 | 29 | f, err := os.Create(name) 30 | if err != nil { 31 | t.Fatalf("could not open %s: %v", fullPath, err) 32 | } 33 | defer f.Close() 34 | 35 | path, err := GetFinalPathNameByHandle(windows.Handle(f.Fd()), GetFinalPathDefaultFlag) 36 | if err != nil { 37 | t.Fatalf("could not get final path for %s: %v", fullPath, err) 38 | } 39 | if strings.EqualFold(fullPath, path) { 40 | t.Fatalf("expected %s, got %s", fullPath, path) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/go-winio/internal/fs/security.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-security_impersonation_level 4 | type SecurityImpersonationLevel int32 // C default enums underlying type is `int`, which is Go `int32` 5 | 6 | // Impersonation levels 7 | const ( 8 | SecurityAnonymous SecurityImpersonationLevel = 0 9 | SecurityIdentification SecurityImpersonationLevel = 1 10 | SecurityImpersonation SecurityImpersonationLevel = 2 11 | SecurityDelegation SecurityImpersonationLevel = 3 12 | ) 13 | -------------------------------------------------------------------------------- /internal/go-winio/internal/fs/zsyscall_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | // Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. 4 | 5 | package fs 6 | 7 | import ( 8 | "syscall" 9 | "unsafe" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | var _ unsafe.Pointer 15 | 16 | // Do the interface allocations only once for common 17 | // Errno values. 18 | const ( 19 | errnoERROR_IO_PENDING = 997 20 | ) 21 | 22 | var ( 23 | errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) 24 | errERROR_EINVAL error = syscall.EINVAL 25 | ) 26 | 27 | // errnoErr returns common boxed Errno values, to prevent 28 | // allocations at runtime. 29 | func errnoErr(e syscall.Errno) error { 30 | switch e { 31 | case 0: 32 | return errERROR_EINVAL 33 | case errnoERROR_IO_PENDING: 34 | return errERROR_IO_PENDING 35 | } 36 | // TODO: add more here, after collecting data on the common 37 | // error values see on Windows. (perhaps when running 38 | // all.bat?) 39 | return e 40 | } 41 | 42 | var ( 43 | modkernel32 = windows.NewLazySystemDLL("kernel32.dll") 44 | 45 | procCreateFileW = modkernel32.NewProc("CreateFileW") 46 | ) 47 | 48 | func CreateFile(name string, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) { 49 | var _p0 *uint16 50 | _p0, err = syscall.UTF16PtrFromString(name) 51 | if err != nil { 52 | return 53 | } 54 | return _CreateFile(_p0, access, mode, sa, createmode, attrs, templatefile) 55 | } 56 | 57 | func _CreateFile(name *uint16, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) { 58 | r0, _, e1 := syscall.Syscall9(procCreateFileW.Addr(), 7, uintptr(unsafe.Pointer(name)), uintptr(access), uintptr(mode), uintptr(unsafe.Pointer(sa)), uintptr(createmode), uintptr(attrs), uintptr(templatefile), 0, 0) 59 | handle = windows.Handle(r0) 60 | if handle == windows.InvalidHandle { 61 | err = errnoErr(e1) 62 | } 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /internal/go-winio/internal/stringbuffer/wstring.go: -------------------------------------------------------------------------------- 1 | package stringbuffer 2 | 3 | import ( 4 | "sync" 5 | "unicode/utf16" 6 | ) 7 | 8 | // TODO: worth exporting and using in mkwinsyscall? 9 | 10 | // Uint16BufferSize is the buffer size in the pool, chosen somewhat arbitrarily to accommodate 11 | // large path strings: 12 | // MAX_PATH (260) + size of volume GUID prefix (49) + null terminator = 310. 13 | const MinWStringCap = 310 14 | 15 | // use *[]uint16 since []uint16 creates an extra allocation where the slice header 16 | // is copied to heap and then referenced via pointer in the interface header that sync.Pool 17 | // stores. 18 | var pathPool = sync.Pool{ // if go1.18+ adds Pool[T], use that to store []uint16 directly 19 | New: func() interface{} { 20 | b := make([]uint16, MinWStringCap) 21 | return &b 22 | }, 23 | } 24 | 25 | func newBuffer() []uint16 { return *(pathPool.Get().(*[]uint16)) } 26 | 27 | // freeBuffer copies the slice header data, and puts a pointer to that in the pool. 28 | // This avoids taking a pointer to the slice header in WString, which can be set to nil. 29 | func freeBuffer(b []uint16) { pathPool.Put(&b) } 30 | 31 | // WString is a wide string buffer ([]uint16) meant for storing UTF-16 encoded strings 32 | // for interacting with Win32 APIs. 33 | // Sizes are specified as uint32 and not int. 34 | // 35 | // It is not thread safe. 36 | type WString struct { 37 | // type-def allows casting to []uint16 directly, use struct to prevent that and allow adding fields in the future. 38 | 39 | // raw buffer 40 | b []uint16 41 | } 42 | 43 | // NewWString returns a [WString] allocated from a shared pool with an 44 | // initial capacity of at least [MinWStringCap]. 45 | // Since the buffer may have been previously used, its contents are not guaranteed to be empty. 46 | // 47 | // The buffer should be freed via [WString.Free] 48 | func NewWString() *WString { 49 | return &WString{ 50 | b: newBuffer(), 51 | } 52 | } 53 | 54 | func (b *WString) Free() { 55 | if b.empty() { 56 | return 57 | } 58 | freeBuffer(b.b) 59 | b.b = nil 60 | } 61 | 62 | // ResizeTo grows the buffer to at least c and returns the new capacity, freeing the 63 | // previous buffer back into pool. 64 | func (b *WString) ResizeTo(c uint32) uint32 { 65 | // allready sufficient (or n is 0) 66 | if c <= b.Cap() { 67 | return b.Cap() 68 | } 69 | 70 | if c <= MinWStringCap { 71 | c = MinWStringCap 72 | } 73 | // allocate at-least double buffer size, as is done in [bytes.Buffer] and other places 74 | if c <= 2*b.Cap() { 75 | c = 2 * b.Cap() 76 | } 77 | 78 | b2 := make([]uint16, c) 79 | if !b.empty() { 80 | copy(b2, b.b) 81 | freeBuffer(b.b) 82 | } 83 | b.b = b2 84 | return c 85 | } 86 | 87 | // Buffer returns the underlying []uint16 buffer. 88 | func (b *WString) Buffer() []uint16 { 89 | if b.empty() { 90 | return nil 91 | } 92 | return b.b 93 | } 94 | 95 | // Pointer returns a pointer to the first uint16 in the buffer. 96 | // If the [WString.Free] has already been called, the pointer will be nil. 97 | func (b *WString) Pointer() *uint16 { 98 | if b.empty() { 99 | return nil 100 | } 101 | return &b.b[0] 102 | } 103 | 104 | // String returns the returns the UTF-8 encoding of the UTF-16 string in the buffer. 105 | // 106 | // It assumes that the data is null-terminated. 107 | func (b *WString) String() string { 108 | // Using [windows.UTF16ToString] would require importing "golang.org/x/sys/windows" 109 | // and would make this code Windows-only, which makes no sense. 110 | // So copy UTF16ToString code into here. 111 | // If other windows-specific code is added, switch to [windows.UTF16ToString] 112 | 113 | s := b.b 114 | for i, v := range s { 115 | if v == 0 { 116 | s = s[:i] 117 | break 118 | } 119 | } 120 | return string(utf16.Decode(s)) 121 | } 122 | 123 | // Cap returns the underlying buffer capacity. 124 | func (b *WString) Cap() uint32 { 125 | if b.empty() { 126 | return 0 127 | } 128 | return b.cap() 129 | } 130 | 131 | func (b *WString) cap() uint32 { return uint32(cap(b.b)) } 132 | func (b *WString) empty() bool { return b == nil || b.cap() == 0 } 133 | -------------------------------------------------------------------------------- /internal/go-winio/internal/stringbuffer/wstring_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package stringbuffer 4 | 5 | import "testing" 6 | 7 | func Test_BufferCapacity(t *testing.T) { 8 | b := NewWString() 9 | 10 | c := b.Cap() 11 | if c < MinWStringCap { 12 | t.Fatalf("expected capacity >= %d, got %d", MinWStringCap, c) 13 | } 14 | 15 | if l := len(b.b); l != int(c) { 16 | t.Fatalf("buffer length (%d) and capacity (%d) mismatch", l, c) 17 | } 18 | 19 | n := uint32(1.5 * MinWStringCap) 20 | nn := b.ResizeTo(n) 21 | if len(b.b) != int(nn) { 22 | t.Fatalf("resized buffer should be %d, was %d", nn, len(b.b)) 23 | } 24 | if n > nn { 25 | t.Fatalf("resized to a value smaller than requested") 26 | } 27 | } 28 | 29 | func Test_BufferFree(t *testing.T) { 30 | // make sure free-ing doesn't set pooled buffer to nil as well 31 | for i := 0; i < 256; i++ { 32 | // try allocating and freeing repeatedly since pool does not guarantee item reuse 33 | b := NewWString() 34 | b.Free() 35 | if b.b != nil { 36 | t.Fatalf("freed buffer is not nil") 37 | } 38 | 39 | b = NewWString() 40 | c := b.Cap() 41 | if c < MinWStringCap { 42 | t.Fatalf("expected capacity >= %d, got %d", MinWStringCap, c) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/go-winio/sd.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package winio 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "unsafe" 10 | 11 | "golang.org/x/sys/windows" 12 | ) 13 | 14 | //sys lookupAccountName(systemName *uint16, accountName string, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) = advapi32.LookupAccountNameW 15 | //sys lookupAccountSid(systemName *uint16, sid *byte, name *uint16, nameSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) = advapi32.LookupAccountSidW 16 | //sys convertSidToStringSid(sid *byte, str **uint16) (err error) = advapi32.ConvertSidToStringSidW 17 | //sys convertStringSidToSid(str *uint16, sid **byte) (err error) = advapi32.ConvertStringSidToSidW 18 | //sys getSecurityDescriptorLength(sd uintptr) (len uint32) = advapi32.GetSecurityDescriptorLength 19 | 20 | type AccountLookupError struct { 21 | Name string 22 | Err error 23 | } 24 | 25 | func (e *AccountLookupError) Error() string { 26 | if e.Name == "" { 27 | return "lookup account: empty account name specified" 28 | } 29 | var s string 30 | switch { 31 | case errors.Is(e.Err, windows.ERROR_INVALID_SID): 32 | s = "the security ID structure is invalid" 33 | case errors.Is(e.Err, windows.ERROR_NONE_MAPPED): 34 | s = "not found" 35 | default: 36 | s = e.Err.Error() 37 | } 38 | return "lookup account " + e.Name + ": " + s 39 | } 40 | 41 | func (e *AccountLookupError) Unwrap() error { return e.Err } 42 | 43 | type SddlConversionError struct { 44 | Sddl string 45 | Err error 46 | } 47 | 48 | func (e *SddlConversionError) Error() string { 49 | return "convert " + e.Sddl + ": " + e.Err.Error() 50 | } 51 | 52 | func (e *SddlConversionError) Unwrap() error { return e.Err } 53 | 54 | // LookupSidByName looks up the SID of an account by name 55 | // 56 | //revive:disable-next-line:var-naming SID, not Sid 57 | func LookupSidByName(name string) (sid string, err error) { 58 | if name == "" { 59 | return "", &AccountLookupError{name, windows.ERROR_NONE_MAPPED} 60 | } 61 | 62 | var sidSize, sidNameUse, refDomainSize uint32 63 | err = lookupAccountName(nil, name, nil, &sidSize, nil, &refDomainSize, &sidNameUse) 64 | if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { //nolint:errorlint // err is Errno 65 | return "", &AccountLookupError{name, err} 66 | } 67 | sidBuffer := make([]byte, sidSize) 68 | refDomainBuffer := make([]uint16, refDomainSize) 69 | err = lookupAccountName(nil, name, &sidBuffer[0], &sidSize, &refDomainBuffer[0], &refDomainSize, &sidNameUse) 70 | if err != nil { 71 | return "", &AccountLookupError{name, err} 72 | } 73 | var strBuffer *uint16 74 | err = convertSidToStringSid(&sidBuffer[0], &strBuffer) 75 | if err != nil { 76 | return "", &AccountLookupError{name, err} 77 | } 78 | sid = windows.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(strBuffer))[:]) 79 | _, _ = windows.LocalFree(windows.Handle(unsafe.Pointer(strBuffer))) 80 | return sid, nil 81 | } 82 | 83 | // LookupNameBySid looks up the name of an account by SID 84 | // 85 | //revive:disable-next-line:var-naming SID, not Sid 86 | func LookupNameBySid(sid string) (name string, err error) { 87 | if sid == "" { 88 | return "", &AccountLookupError{sid, windows.ERROR_NONE_MAPPED} 89 | } 90 | 91 | sidBuffer, err := windows.UTF16PtrFromString(sid) 92 | if err != nil { 93 | return "", &AccountLookupError{sid, err} 94 | } 95 | 96 | var sidPtr *byte 97 | if err = convertStringSidToSid(sidBuffer, &sidPtr); err != nil { 98 | return "", &AccountLookupError{sid, err} 99 | } 100 | defer windows.LocalFree(windows.Handle(unsafe.Pointer(sidPtr))) //nolint:errcheck 101 | 102 | var nameSize, refDomainSize, sidNameUse uint32 103 | err = lookupAccountSid(nil, sidPtr, nil, &nameSize, nil, &refDomainSize, &sidNameUse) 104 | if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { //nolint:errorlint // err is Errno 105 | return "", &AccountLookupError{sid, err} 106 | } 107 | 108 | nameBuffer := make([]uint16, nameSize) 109 | refDomainBuffer := make([]uint16, refDomainSize) 110 | err = lookupAccountSid(nil, sidPtr, &nameBuffer[0], &nameSize, &refDomainBuffer[0], &refDomainSize, &sidNameUse) 111 | if err != nil { 112 | return "", &AccountLookupError{sid, err} 113 | } 114 | 115 | name = windows.UTF16ToString(nameBuffer) 116 | return name, nil 117 | } 118 | 119 | func SddlToSecurityDescriptor(sddl string) ([]byte, error) { 120 | sd, err := windows.SecurityDescriptorFromString(sddl) 121 | if err != nil { 122 | return nil, &SddlConversionError{Sddl: sddl, Err: err} 123 | } 124 | b := unsafe.Slice((*byte)(unsafe.Pointer(sd)), unsafe.Sizeof(windows.SECURITY_DESCRIPTOR{})) 125 | return b, nil 126 | } 127 | 128 | func SecurityDescriptorToSddl(sd []byte) (string, error) { 129 | if l := int(unsafe.Sizeof(windows.SECURITY_DESCRIPTOR{})); len(sd) < l { 130 | return "", fmt.Errorf("SecurityDescriptor (%d) smaller than expected (%d): %w", len(sd), l, windows.ERROR_INCORRECT_SIZE) 131 | } 132 | s := (*windows.SECURITY_DESCRIPTOR)(unsafe.Pointer(&sd[0])) 133 | return s.String(), nil 134 | } 135 | -------------------------------------------------------------------------------- /internal/go-winio/sd_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package winio 5 | 6 | import ( 7 | "errors" 8 | "testing" 9 | 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | func TestLookupInvalidSid(t *testing.T) { 14 | _, err := LookupSidByName(".\\weoifjdsklfj") 15 | var aerr *AccountLookupError 16 | if !errors.As(err, &aerr) || !errors.Is(err, windows.ERROR_NONE_MAPPED) { 17 | t.Fatalf("expected AccountLookupError with ERROR_NONE_MAPPED, got %s", err) 18 | } 19 | } 20 | 21 | func TestLookupInvalidName(t *testing.T) { 22 | _, err := LookupNameBySid("notasid") 23 | var aerr *AccountLookupError 24 | if !errors.As(err, &aerr) || !errors.Is(aerr.Err, windows.ERROR_INVALID_SID) { 25 | t.Fatalf("expected AccountLookupError with ERROR_INVALID_SID got %s", err) 26 | } 27 | } 28 | 29 | func TestLookupValidSid(t *testing.T) { 30 | everyone := "S-1-1-0" 31 | name, err := LookupNameBySid(everyone) 32 | if err != nil { 33 | t.Fatalf("expected a valid account name, got %v", err) 34 | } 35 | 36 | sid, err := LookupSidByName(name) 37 | if err != nil || sid != everyone { 38 | t.Fatalf("expected %s, got %s, %s", everyone, sid, err) 39 | } 40 | } 41 | 42 | func TestLookupEmptyNameFails(t *testing.T) { 43 | _, err := LookupSidByName("") 44 | var aerr *AccountLookupError 45 | if !errors.As(err, &aerr) || !errors.Is(aerr.Err, windows.ERROR_NONE_MAPPED) { 46 | t.Fatalf("expected AccountLookupError with ERROR_NONE_MAPPED, got %s", err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /jmap_get.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // Dictionary used to decode a JSON object record containing a 10 | // single Trace2 Event record into a generic map. This typedef 11 | // is mainly to hide the awkward GOLANG syntax. 12 | type jmap map[string]interface{} 13 | 14 | // Optional keys/value pairs return a pointer to the value so 15 | // that a non-present key is returned as a NULL pointer rather 16 | // than an overloaded zero value. If the map value is of a 17 | // different type than requested, we return an error rather than 18 | // trying to convert it (since we are trying to parse and validate 19 | // a known document format.) 20 | // 21 | // All variants follow these rules: 22 | // Returns (p, nil) when successful. 23 | // Returns (nil, nil) if not present. 24 | // Returns (nil, err) if the value in the map is of a different type. 25 | 26 | // Get an optional string value from the map. 27 | func (jm *jmap) getOptionalString(key string) (*string, error) { 28 | var v interface{} 29 | var ok bool 30 | 31 | if v, ok = (*jm)[key]; !ok { 32 | return nil, nil 33 | } 34 | 35 | ps := new(string) 36 | 37 | switch v := v.(type) { 38 | case string: 39 | *ps = v 40 | return ps, nil 41 | default: 42 | return nil, fmt.Errorf("optional key '%s' does not have string value", key) 43 | } 44 | } 45 | 46 | func (jm *jmap) getOptionalInt64(key string) (*int64, error) { 47 | var v interface{} 48 | var ok bool 49 | 50 | if v, ok = (*jm)[key]; !ok { 51 | return nil, nil 52 | } 53 | 54 | pi := new(int64) 55 | 56 | // Allow both int and int64 in case we are unit testing. Allow float64 57 | // because the generic JSON decoder always creates floats (because JavaScript 58 | // does not have integer data types), so we have to convert it back. 59 | switch v := v.(type) { 60 | case int64: 61 | *pi = v 62 | return pi, nil 63 | case int: 64 | *pi = int64(v) 65 | return pi, nil 66 | case float64: 67 | *pi = int64(v) 68 | return pi, nil 69 | default: 70 | return nil, fmt.Errorf("key '%s' does not have an integer value", key) 71 | } 72 | } 73 | 74 | // Required keys/value pairs return the value or an hard error if 75 | // the key is not present or the map value is of a different type 76 | // than requested. 77 | // 78 | // All variants follow these rules: 79 | // Returns (p, nil) when successful. 80 | // Returns (nil, err) if not present. 81 | // Returns (nil, err) if value type wrong. 82 | 83 | func (jm *jmap) getRequired(key string) (interface{}, error) { 84 | var v interface{} 85 | var ok bool 86 | 87 | if v, ok = (*jm)[key]; !ok { 88 | return nil, fmt.Errorf("key '%s' not present in Trace2 event", key) 89 | } 90 | return v, nil 91 | } 92 | 93 | func (jm *jmap) getRequiredBool(key string) (bool, error) { 94 | var v interface{} 95 | var err error 96 | 97 | if v, err = jm.getRequired(key); err != nil { 98 | return false, err 99 | } 100 | 101 | switch v := v.(type) { 102 | case bool: 103 | return v, nil 104 | default: 105 | return false, fmt.Errorf("key '%s' does not have bool value", key) 106 | } 107 | } 108 | 109 | func (jm *jmap) getRequiredString(key string) (string, error) { 110 | var v interface{} 111 | var err error 112 | 113 | if v, err = jm.getRequired(key); err != nil { 114 | return "", err 115 | } 116 | 117 | switch v := v.(type) { 118 | case string: 119 | return v, nil 120 | default: 121 | return "", fmt.Errorf("key '%s' does not have string value", key) 122 | } 123 | } 124 | 125 | func (jm *jmap) getRequiredInt64(key string) (int64, error) { 126 | var v interface{} 127 | var err error 128 | 129 | if v, err = jm.getRequired(key); err != nil { 130 | return 0, err 131 | } 132 | 133 | // Allow both int and int64 in case we are unit testing. Allow float64 134 | // because the generic JSON decoder always creates floats (because JavaScript 135 | // does not have integer data types), so we have to convert it back. 136 | switch v := v.(type) { 137 | case int64: 138 | return v, nil 139 | case int: 140 | return int64(v), nil 141 | case float64: 142 | return int64(v), nil 143 | default: 144 | return 0, fmt.Errorf("key '%s' does not have an integer value", key) 145 | } 146 | } 147 | 148 | func (jm *jmap) getRequiredStringOrInt64(key string) (interface{}, error) { 149 | var v interface{} 150 | var err error 151 | 152 | if v, err = jm.getRequired(key); err != nil { 153 | return 0, err 154 | } 155 | 156 | // Allow both int and int64 in case we are unit testing. Allow float64 157 | // because the generic JSON decoder always creates floats (because JavaScript 158 | // does not have integer data types), so we have to convert it back. 159 | switch v := v.(type) { 160 | case int64: 161 | return v, nil 162 | case int: 163 | return int64(v), nil 164 | case float64: 165 | return int64(v), nil 166 | case string: 167 | return v, nil 168 | default: 169 | return 0, fmt.Errorf("key '%s' does not have an integer or string value", key) 170 | } 171 | 172 | } 173 | 174 | func (jm *jmap) getRequiredFloat64(key string) (float64, error) { 175 | var v interface{} 176 | var err error 177 | 178 | if v, err = jm.getRequired(key); err != nil { 179 | return 0, err 180 | } 181 | 182 | // Allow both int and int64 in case the JSON writer is sloppy and doesn't 183 | // add a trailing .0 for whole numbers. This is primarily for unit testing 184 | // since the generic JSON decoder always creates floats because of JavaScript 185 | // limitations. 186 | switch v := v.(type) { 187 | case float64: 188 | return v, nil 189 | case int64: 190 | return float64(v), nil 191 | case int: 192 | return float64(v), nil 193 | default: 194 | return 0.0, fmt.Errorf("key '%s' does not have an float value", key) 195 | } 196 | } 197 | 198 | func (jm *jmap) getRequiredTime(key string) (time.Time, error) { 199 | var v interface{} 200 | var err error 201 | 202 | if v, err = jm.getRequired(key); err != nil { 203 | return time.Time{}, err 204 | } 205 | 206 | switch v := v.(type) { 207 | case string: 208 | t, err := time.Parse("2006-01-02T15:04:05.999999Z", v) 209 | if err == nil { 210 | return t, err 211 | } 212 | // A version of GCM sends "+00:00" for the TZ rather than a "Z". 213 | t, err = time.Parse("2006-01-02T15:04:05.999999-07:00", v) 214 | return t, err 215 | default: 216 | return time.Time{}, fmt.Errorf("key '%s' does not have string value", key) 217 | } 218 | } 219 | 220 | // Extract required JSON array value. 221 | // 222 | // We usually use this for "argv", but leave it as an []interface{} 223 | // type rather than assuming it is an []string (because we'll probably 224 | // need that later). 225 | func (jm *jmap) getRequiredArray(key string) ([]interface{}, error) { 226 | var v interface{} 227 | var err error 228 | 229 | if v, err = jm.getRequired(key); err != nil { 230 | return nil, err 231 | } 232 | 233 | switch v := v.(type) { 234 | case []interface{}: 235 | return v, nil 236 | case []string: 237 | // Implicitly case []string back to []interface{} for unit tests. 238 | vv := make([]interface{}, len(v)) 239 | for k := range v { 240 | vv[k] = v[k] 241 | } 242 | return vv, nil 243 | default: 244 | return nil, fmt.Errorf("key '%s' is not an array", key) 245 | } 246 | } 247 | 248 | func (jm *jmap) getRequiredJsonValue(key string) (interface{}, error) { 249 | var v interface{} 250 | var err error 251 | 252 | if v, err = jm.getRequired(key); err != nil { 253 | return nil, err 254 | } 255 | 256 | switch v := v.(type) { 257 | case interface{}: 258 | _, err := json.Marshal(v) 259 | if err != nil { 260 | return nil, fmt.Errorf("key '%s' is not a JSON object", key) 261 | } 262 | return v, nil 263 | default: 264 | return nil, fmt.Errorf("key '%s' is not a JSON object", key) 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /jmap_get_test.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | // Tests in this file are concerned with whether the key/value 4 | // map (created by the JSON decoder) contains the necessary 5 | // required keys, optionally contains optional keys, and that 6 | // the key-value is of the expected type (and only casted in 7 | // a few special cases). 8 | // 9 | // `evt_parse.go` will build upon this to decode Trace2 JSON 10 | // event messages into actual structure fields. 11 | 12 | import ( 13 | "encoding/json" 14 | "math" 15 | "strings" 16 | "testing" 17 | ) 18 | 19 | var jm *jmap = &jmap{ 20 | "optional-string": "a", 21 | "optional-int": 42, 22 | "optional-int-as-float": 13.0, 23 | 24 | "required-string": "b", 25 | "required-int": 99, 26 | "required-int-as-float": 7.0, 27 | "required-bool": true, 28 | "required-float": 3.14, 29 | "required-time": "2023-01-14T15:04:05.999999Z", 30 | "required-array-as-string": []string{"a3", "b3", "c3"}, 31 | "required-array-as-interface": append(make([]interface{}, 0), "x2", "y2"), 32 | 33 | "required-data-json": map[string]interface{}{ 34 | "x": 1, 35 | "y": "foo", 36 | }, 37 | 38 | "alternate-time": "2023-01-14T15:04:05.999999+00:00", 39 | } 40 | 41 | // Optional getter functions 42 | 43 | func Test_getOptionalString_Present(t *testing.T) { 44 | ps, err := jm.getOptionalString("optional-string") 45 | if err != nil || ps == nil || *ps != "a" { 46 | t.Fatalf("getOptionalString") 47 | } 48 | } 49 | func Test_getOptionalString_NotPresent(t *testing.T) { 50 | ps, err := jm.getOptionalString("not-present-string") 51 | if err != nil || ps != nil { 52 | t.Fatalf("getOptionalString") 53 | } 54 | } 55 | func Test_getOptionalString_WrongType(t *testing.T) { 56 | _, err := jm.getOptionalString("optional-int") 57 | if err == nil { 58 | t.Fatal("getOptionaString") 59 | } 60 | } 61 | 62 | func Test_getOptionalInt64_Present(t *testing.T) { 63 | pi, err := jm.getOptionalInt64("optional-int") 64 | if err != nil || pi == nil || *pi != 42 { 65 | t.Fatalf("getOptionalInt64") 66 | } 67 | } 68 | func Test_getOptionalInt64_Present_AsFloat(t *testing.T) { 69 | pi, err := jm.getOptionalInt64("optional-int-as-float") 70 | if err != nil || pi == nil || *pi != 13 { 71 | t.Fatalf("getOptionalInt64") 72 | } 73 | } 74 | func Test_getOptionalInt64_NotPresent(t *testing.T) { 75 | pi, err := jm.getOptionalInt64("not-present-int") 76 | if err != nil || pi != nil { 77 | t.Fatalf("getOptionalInt64") 78 | } 79 | } 80 | func Test_getOptionalInt64_WrongType(t *testing.T) { 81 | _, err := jm.getOptionalInt64("optional-string") 82 | if err == nil { 83 | t.Fatalf("getOptionalInt64") 84 | } 85 | } 86 | 87 | // Required getter functions 88 | 89 | func Test_getRequiredString_Present(t *testing.T) { 90 | s, err := jm.getRequiredString("required-string") 91 | if err != nil || s != "b" { 92 | t.Fatalf("getRequiredString") 93 | } 94 | } 95 | func Test_getRequiredString_NotPresent(t *testing.T) { 96 | _, err := jm.getRequiredString("not-present-string") 97 | if err == nil { 98 | t.Fatalf("getRequiredString") 99 | } 100 | } 101 | func Test_getRequiredString_WrongType(t *testing.T) { 102 | _, err := jm.getRequiredString("required-int") 103 | if err == nil { 104 | t.Fatalf("getRequiredString") 105 | } 106 | } 107 | 108 | func Test_getRequiredInt64_Present(t *testing.T) { 109 | i, err := jm.getRequiredInt64("required-int") 110 | if err != nil || i != 99 { 111 | t.Fatalf("getRequiredInt64") 112 | } 113 | } 114 | func Test_getRequiredInt64_Present_AsFloat(t *testing.T) { 115 | // JSON parser interns ints as floats and we need to undo that. 116 | i, err := jm.getRequiredInt64("required-int-as-float") 117 | if err != nil || i != 7 { 118 | t.Fatalf("getRequiredInt64") 119 | } 120 | } 121 | func Test_getRequiredInt64_NotPresent(t *testing.T) { 122 | _, err := jm.getRequiredInt64("not-present-int") 123 | if err == nil { 124 | t.Fatalf("getRequiredInt64") 125 | } 126 | } 127 | func Test_getRequiredInt64_WrongType(t *testing.T) { 128 | _, err := jm.getRequiredInt64("required-string") 129 | if err == nil { 130 | t.Fatalf("getRequiredInt64") 131 | } 132 | } 133 | 134 | func Test_getRequiredBool_Present(t *testing.T) { 135 | b, err := jm.getRequiredBool("required-bool") 136 | if err != nil || b != true { 137 | t.Fatalf("getRequiredBool") 138 | } 139 | } 140 | func Test_getRequiredBool_NotPresent(t *testing.T) { 141 | _, err := jm.getRequiredBool("not-present-bool") 142 | if err == nil { 143 | t.Fatalf("getRequiredBool") 144 | } 145 | } 146 | func Test_getRequiredBool_WrongType(t *testing.T) { 147 | _, err := jm.getRequiredBool("required-string") 148 | if err == nil { 149 | t.Fatalf("getRequiredBool") 150 | } 151 | } 152 | 153 | func Test_getRequiredStringOrInt64_Present(t *testing.T) { 154 | s, err1 := jm.getRequiredStringOrInt64("required-string") 155 | if err1 != nil || s.(string) != "b" { 156 | t.Fatalf("getRequiredStringOrInt64") 157 | } 158 | i, err2 := jm.getRequiredStringOrInt64("required-int") 159 | if err2 != nil || i.(int64) != 99 { 160 | t.Fatalf("getRequiredStringOrInt64") 161 | } 162 | } 163 | 164 | func float_is_near(v float64, v_ref float64) bool { 165 | return math.Abs(v-v_ref) < 0.001 166 | } 167 | 168 | func Test_getRequiredFloat64_Present(t *testing.T) { 169 | f, err := jm.getRequiredFloat64("required-float") 170 | if err != nil || !float_is_near(f, 3.14) { 171 | t.Fatalf("getRequiredFloat64") 172 | } 173 | } 174 | func Test_getRequiredFloat64_Present_AsInt(t *testing.T) { 175 | // Allow sloppy JSON writers to omit trailing .0 on whole numbers 176 | f, err := jm.getRequiredFloat64("required-int") 177 | if err != nil || !float_is_near(f, 99.0) { 178 | t.Fatalf("getRequiredFloat64") 179 | } 180 | } 181 | func Test_getRequiredFloat64_NotPresent(t *testing.T) { 182 | _, err := jm.getRequiredInt64("not-present-float") 183 | if err == nil { 184 | t.Fatalf("getRequiredFloat64") 185 | } 186 | } 187 | func Test_getRequiredFloat64_WrongType(t *testing.T) { 188 | _, err := jm.getRequiredFloat64("required-string") 189 | if err == nil { 190 | t.Fatalf("getRequiredFloat64") 191 | } 192 | } 193 | 194 | func Test_getRequiredTime_Present(t *testing.T) { 195 | tm, err := jm.getRequiredTime("required-time") 196 | if err != nil || tm.Year() != 2023 || tm.Month() != 1 || tm.Day() != 14 { 197 | t.Fatalf("getRequiredTime") 198 | } 199 | } 200 | func Test_tryAlternateTimeFormat(t *testing.T) { 201 | tm, err := jm.getRequiredTime("alternate-time") 202 | if err != nil || tm.Year() != 2023 || tm.Month() != 1 || tm.Day() != 14 { 203 | t.Fatalf("getRequiredTime on alternate format") 204 | } 205 | } 206 | func Test_getRequiredTime_NotPresent(t *testing.T) { 207 | _, err := jm.getRequiredString("not-present-time") 208 | if err == nil { 209 | t.Fatalf("getRequiredTime") 210 | } 211 | } 212 | func Test_getRequiredTime_WrongType_AsInt(t *testing.T) { 213 | _, err := jm.getRequiredTime("required-int") 214 | if err == nil { 215 | t.Fatalf("getRequiredTime") 216 | } 217 | } 218 | func Test_getRequiredTime_WrongType_AsString(t *testing.T) { 219 | // Try a string value and make time.Parse() fail. 220 | _, err := jm.getRequiredTime("required-string") 221 | if err == nil { 222 | t.Fatalf("getRequiredTime") 223 | } 224 | } 225 | 226 | func Test_getRequiredArray_Present_AsString(t *testing.T) { 227 | a, err := jm.getRequiredArray("required-array-as-string") 228 | if err != nil || len(a) != 3 || a[0] != "a3" { 229 | t.Fatalf("getRequiredArray") 230 | } 231 | } 232 | func Test_getRequiredArray_Present_AsInterface(t *testing.T) { 233 | a, err := jm.getRequiredArray("required-array-as-interface") 234 | if err != nil || len(a) != 2 || a[0] != "x2" { 235 | t.Fatalf("getRequiredArray") 236 | } 237 | } 238 | func Test_getRequiredArray_NotPresent(t *testing.T) { 239 | _, err := jm.getRequiredArray("not-present-array") 240 | if err == nil { 241 | t.Fatalf("getRequiredArray") 242 | } 243 | } 244 | func Test_getRequiredArray_WrongType(t *testing.T) { 245 | _, err := jm.getRequiredArray("required-string") 246 | if err == nil { 247 | t.Fatalf("getRequiredArray") 248 | } 249 | } 250 | 251 | func Test_getRequiredSerialized_Present(t *testing.T) { 252 | jv, err := jm.getRequiredJsonValue("required-data-json") 253 | if err != nil { 254 | t.Fatalf("getRequiredSerialized") 255 | } 256 | b, err := json.Marshal(jv) 257 | if err != nil { 258 | t.Fatalf("getRequiredSerialized") 259 | } 260 | s := string(b) 261 | if !strings.Contains(s, "\"x\":1") { 262 | t.Fatalf("getRequiredSerialized") 263 | } 264 | if !strings.Contains(s, "\"y\":\"foo\"") { 265 | t.Fatalf("getRequiredSerialized") 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /parse_yml.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/mitchellh/mapstructure" 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | type MyYmlFileTypes interface { 12 | RulesetDefinition | FilterSettings | PiiSettings 13 | } 14 | 15 | type MyYmlParseBufferFn[T MyYmlFileTypes] func(data []byte, path string) (*T, error) 16 | 17 | func parseYmlFile[T MyYmlFileTypes](path string, fnPB MyYmlParseBufferFn[T]) (*T, error) { 18 | data, err := os.ReadFile(path) 19 | if err != nil { 20 | return nil, fmt.Errorf("could not read YML '%s': '%s'", 21 | path, err.Error()) 22 | } 23 | 24 | return fnPB(data, path) 25 | } 26 | 27 | func parseYmlBuffer[T MyYmlFileTypes](data []byte, path string) (*T, error) { 28 | m := make(map[interface{}]interface{}) 29 | err := yaml.Unmarshal(data, &m) 30 | if err != nil { 31 | return nil, fmt.Errorf("could not parse YAML '%s': '%s'", 32 | path, err.Error()) 33 | } 34 | 35 | p := new(T) 36 | err = mapstructure.Decode(m, p) 37 | if err != nil { 38 | return nil, fmt.Errorf("could not decode '%s': '%s'", 39 | path, err.Error()) 40 | } 41 | 42 | return p, nil 43 | } 44 | -------------------------------------------------------------------------------- /pii.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | // Settings to enable/disable possibly GDPR-sensitive fields 4 | // in the telemetry output. 5 | type PiiSettings struct { 6 | Include PiiInclude `mapstructure:"include"` 7 | } 8 | 9 | type PiiInclude struct { 10 | // Lookup system hostname and add to process span. 11 | Hostname bool `mapstructure:"hostname"` 12 | 13 | // Lookup the client username and add to process span. 14 | Username bool `mapstructure:"username"` 15 | } 16 | 17 | func parsePiiFile(path string) (*PiiSettings, error) { 18 | return parseYmlFile[PiiSettings](path, parsePiiFromBuffer) 19 | } 20 | 21 | func parsePiiFromBuffer(data []byte, path string) (*PiiSettings, error) { 22 | pii, err := parseYmlBuffer[PiiSettings](data, path) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | // TODO insert any post-parse validation or data structure setup here. 28 | 29 | return pii, nil 30 | } 31 | -------------------------------------------------------------------------------- /platform_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package trace2receiver 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "net" 10 | "os" 11 | 12 | "go.opentelemetry.io/collector/component" 13 | "go.opentelemetry.io/collector/consumer" 14 | "go.opentelemetry.io/collector/receiver" 15 | ) 16 | 17 | var ( 18 | errNilNextConsumer = errors.New("nil next Consumer") 19 | ) 20 | 21 | func createTraces(_ context.Context, 22 | params receiver.Settings, 23 | baseCfg component.Config, 24 | consumer consumer.Traces) (receiver.Traces, error) { 25 | 26 | if consumer == nil { 27 | return nil, errNilNextConsumer 28 | } 29 | 30 | trace2Cfg := baseCfg.(*Config) 31 | 32 | rcvr := &Rcvr_UnixSocket{ 33 | Base: &Rcvr_Base{ 34 | Settings: params, 35 | Logger: params.Logger, 36 | TracesConsumer: consumer, 37 | RcvrConfig: trace2Cfg, 38 | }, 39 | SocketPath: trace2Cfg.UnixSocketPath, 40 | } 41 | return rcvr, nil 42 | } 43 | 44 | // Gather up any requested PII from the machine or 45 | // possibly the connection from the client process. 46 | // Add any requested PII data to `tr2.pii[]`. 47 | func (tr2 *trace2Dataset) pii_gather(cfg *Config, conn *net.UnixConn) { 48 | if cfg.piiSettings != nil && cfg.piiSettings.Include.Hostname { 49 | if h, err := os.Hostname(); err == nil { 50 | tr2.pii[string(Trace2PiiHostname)] = h 51 | } 52 | } 53 | 54 | if cfg.piiSettings != nil && cfg.piiSettings.Include.Username { 55 | if u, err := getPeerUsername(conn); err == nil { 56 | tr2.pii[string(Trace2PiiUsername)] = u 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /platform_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package trace2receiver 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "os" 10 | "os/user" 11 | 12 | "go.opentelemetry.io/collector/component" 13 | "go.opentelemetry.io/collector/consumer" 14 | "go.opentelemetry.io/collector/receiver" 15 | ) 16 | 17 | var ( 18 | errNilNextConsumer = errors.New("nil next Consumer") 19 | ) 20 | 21 | func createTraces(_ context.Context, 22 | params receiver.Settings, 23 | baseCfg component.Config, 24 | consumer consumer.Traces) (receiver.Traces, error) { 25 | 26 | if consumer == nil { 27 | return nil, errNilNextConsumer 28 | } 29 | 30 | trace2Cfg := baseCfg.(*Config) 31 | 32 | rcvr := &Rcvr_NamedPipe{ 33 | Base: &Rcvr_Base{ 34 | Settings: params, 35 | Logger: params.Logger, 36 | TracesConsumer: consumer, 37 | RcvrConfig: trace2Cfg, 38 | }, 39 | NamedPipePath: trace2Cfg.NamedPipePath, 40 | } 41 | return rcvr, nil 42 | } 43 | 44 | // Gather up any requested PII from the machine or 45 | // possibly the connection from the client process. 46 | // Add any requested PII data to `tr2.pii[]`. 47 | func (tr2 *trace2Dataset) pii_gather(cfg *Config) { 48 | if cfg.piiSettings != nil && cfg.piiSettings.Include.Hostname { 49 | if h, err := os.Hostname(); err == nil { 50 | tr2.pii[string(Trace2PiiHostname)] = h 51 | } 52 | } 53 | 54 | if cfg.piiSettings != nil && cfg.piiSettings.Include.Username { 55 | // TODO For now, just lookup the current user. This may 56 | // or may not be valid when the service is officially 57 | // installed. Ideally we should get the user-id of the 58 | // client process. Or, since most Windows systems are 59 | // single login, get the name of the owner of the console 60 | // and assume it doesn't change. 61 | 62 | if u, err := user.Current(); err == nil { 63 | tr2.pii[string(Trace2PiiUsername)] = u.Username 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /rcvr_base.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import ( 4 | "context" 5 | 6 | "go.opentelemetry.io/collector/component" 7 | "go.opentelemetry.io/collector/consumer" 8 | "go.opentelemetry.io/collector/receiver" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type Rcvr_Base struct { 13 | // These fields should be set in ctor() in platform_*.go:createTraces() 14 | // when it is called from factory.go:NewFactory(). 15 | Settings receiver.Settings 16 | Logger *zap.Logger 17 | TracesConsumer consumer.Traces 18 | MetricsConsumer consumer.Metrics 19 | LogsConsumer consumer.Logs 20 | RcvrConfig *Config 21 | 22 | // Component properties set in Start() 23 | ctx context.Context 24 | host component.Host 25 | cancel context.CancelFunc 26 | } 27 | 28 | // `Start()` handles base-class portions of receiver initialization. 29 | func (rcvr_base *Rcvr_Base) Start(unused_ctx context.Context, host component.Host) error { 30 | rcvr_base.host = host 31 | rcvr_base.ctx = context.Background() 32 | rcvr_base.ctx, rcvr_base.cancel = context.WithCancel(rcvr_base.ctx) 33 | 34 | if rcvr_base.RcvrConfig.AllowCommandControlVerbs { 35 | rcvr_base.Logger.Info("Command verbs are enabled") 36 | } 37 | 38 | if rcvr_base.RcvrConfig.piiSettings != nil { 39 | if rcvr_base.RcvrConfig.piiSettings.Include.Hostname { 40 | rcvr_base.Logger.Info("PII: Hostname logging is enabled") 41 | } 42 | if rcvr_base.RcvrConfig.piiSettings.Include.Username { 43 | rcvr_base.Logger.Info("PII: Username logging is enabled") 44 | } 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /rcvr_namedpipe.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package trace2receiver 5 | 6 | import ( 7 | "bufio" 8 | "context" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "net" 13 | "os" 14 | "sync" 15 | 16 | "go.opentelemetry.io/collector/component" 17 | "go.opentelemetry.io/collector/component/componentstatus" 18 | "golang.org/x/sys/windows" 19 | 20 | "github.com/git-ecosystem/trace2receiver/internal/go-winio" 21 | ) 22 | 23 | type Rcvr_NamedPipe struct { 24 | // These fields should be set in ctor() 25 | Base *Rcvr_Base 26 | NamedPipePath string 27 | 28 | // Windows named pipe properties 29 | listener net.Listener 30 | } 31 | 32 | // Start receiving connections from Trace2 clients. 33 | // 34 | // This is part of the `component.Component` interface. 35 | func (rcvr *Rcvr_NamedPipe) Start(unused_ctx context.Context, host component.Host) error { 36 | var err error 37 | 38 | var listenQueueSize int = 5 39 | var acceptPoolSize int = listenQueueSize * 2 40 | 41 | err = rcvr.Base.Start(unused_ctx, host) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | err = rcvr.openNamedPipeServer(listenQueueSize) 47 | if err != nil { 48 | componentstatus.ReportStatus(host, componentstatus.NewFatalErrorEvent(err)) 49 | return err 50 | } 51 | 52 | go rcvr.listenLoop(acceptPoolSize) 53 | return nil 54 | } 55 | 56 | // Stop accepting new connections from Trace2 clients. 57 | // 58 | // This is part of the `component.Component` interface. 59 | func (rcvr *Rcvr_NamedPipe) Shutdown(context.Context) error { 60 | rcvr.listener.Close() 61 | os.Remove(rcvr.NamedPipePath) 62 | rcvr.Base.cancel() 63 | return nil 64 | } 65 | 66 | func (rcvr *Rcvr_NamedPipe) makeSDDL() (sddl string, err error) { 67 | 68 | adminSid, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid) 69 | if err != nil { 70 | rcvr.Base.Logger.Error(fmt.Sprintf("could not create adminSid: %v", err)) 71 | return "", err 72 | } 73 | systemSid, err := windows.CreateWellKnownSid(windows.WinLocalSystemSid) 74 | if err != nil { 75 | rcvr.Base.Logger.Error(fmt.Sprintf("could not create LocalSystemSid: %v", err)) 76 | return "", err 77 | } 78 | ownerSid, err := windows.CreateWellKnownSid(windows.WinCreatorOwnerSid) 79 | if err != nil { 80 | rcvr.Base.Logger.Error(fmt.Sprintf("could not create CreatorSid: %v", err)) 81 | return "", err 82 | } 83 | everyoneSid, err := windows.CreateWellKnownSid(windows.WinWorldSid) 84 | if err != nil { 85 | rcvr.Base.Logger.Error(fmt.Sprintf("could not create WorldSid: %v", err)) 86 | return "", err 87 | } 88 | 89 | access := []windows.EXPLICIT_ACCESS{ 90 | { 91 | AccessPermissions: windows.GENERIC_ALL, 92 | AccessMode: windows.GRANT_ACCESS, 93 | Trustee: windows.TRUSTEE{ 94 | TrusteeForm: windows.TRUSTEE_IS_SID, 95 | TrusteeType: windows.TRUSTEE_IS_GROUP, 96 | TrusteeValue: windows.TrusteeValueFromSID(adminSid), 97 | }, 98 | }, 99 | { 100 | AccessPermissions: windows.GENERIC_ALL, 101 | AccessMode: windows.GRANT_ACCESS, 102 | Trustee: windows.TRUSTEE{ 103 | TrusteeForm: windows.TRUSTEE_IS_SID, 104 | TrusteeType: windows.TRUSTEE_IS_GROUP, 105 | TrusteeValue: windows.TrusteeValueFromSID(systemSid), 106 | }, 107 | }, 108 | { 109 | AccessPermissions: windows.GENERIC_ALL, 110 | AccessMode: windows.GRANT_ACCESS, 111 | Trustee: windows.TRUSTEE{ 112 | TrusteeForm: windows.TRUSTEE_IS_SID, 113 | TrusteeType: windows.TRUSTEE_IS_USER, 114 | TrusteeValue: windows.TrusteeValueFromSID(ownerSid), 115 | }, 116 | }, 117 | { 118 | AccessPermissions: windows.GENERIC_WRITE | windows.GENERIC_READ, 119 | AccessMode: windows.GRANT_ACCESS, 120 | Trustee: windows.TRUSTEE{ 121 | TrusteeForm: windows.TRUSTEE_IS_SID, 122 | TrusteeType: windows.TRUSTEE_IS_GROUP, 123 | TrusteeValue: windows.TrusteeValueFromSID(everyoneSid), 124 | }, 125 | }, 126 | } 127 | 128 | sd, err := windows.NewSecurityDescriptor() 129 | if err != nil { 130 | rcvr.Base.Logger.Error(fmt.Sprintf("could not create SD: %v", err)) 131 | return "", err 132 | } 133 | 134 | acl, err := windows.ACLFromEntries(access, nil) 135 | if err != nil { 136 | rcvr.Base.Logger.Error(fmt.Sprintf("could not create ACL: %v", err)) 137 | return "", err 138 | } 139 | 140 | err = sd.SetDACL(acl, true, false) 141 | if err != nil { 142 | rcvr.Base.Logger.Error(fmt.Sprintf("could not set ACL: %v", err)) 143 | return "", err 144 | } 145 | 146 | sddl = sd.String() 147 | rcvr.Base.Logger.Debug(fmt.Sprintf("SDDL is: %v", sddl)) 148 | return sddl, nil 149 | } 150 | 151 | // Open the server-side of a named pipe. 152 | func (rcvr *Rcvr_NamedPipe) openNamedPipeServer(listenQueueSize int) (err error) { 153 | _ = os.Remove(rcvr.NamedPipePath) 154 | 155 | sddl, err := rcvr.makeSDDL() 156 | if err != nil { 157 | rcvr.Base.Logger.Error(fmt.Sprintf("could not create security descriptor for named pipe: %v", err)) 158 | return err 159 | } 160 | 161 | c := winio.PipeConfig{ 162 | SDDL: sddl, 163 | MessageMode: false, 164 | InputBufferSize: 65536, 165 | OutputBufferSize: 65536, 166 | QueueSize: int32(listenQueueSize), 167 | } 168 | 169 | rcvr.listener, err = winio.ListenPipe(rcvr.NamedPipePath, &c) 170 | if err != nil || rcvr.listener == nil { 171 | rcvr.Base.Logger.Error(fmt.Sprintf("could not create named pipe: %v", err)) 172 | return err 173 | } 174 | 175 | rcvr.Base.Logger.Info(fmt.Sprintf("listening on '%s'", rcvr.NamedPipePath)) 176 | return nil 177 | } 178 | 179 | var workerIdMux sync.Mutex 180 | var workerId uint64 181 | 182 | func makeWorkerId() uint64 { 183 | workerIdMux.Lock() 184 | id := workerId 185 | workerId++ 186 | workerIdMux.Unlock() 187 | return id 188 | } 189 | 190 | // Listen for incoming connections from Trace2 clients. 191 | // Dispatch each to a worker thread. 192 | func (rcvr *Rcvr_NamedPipe) listenLoop(acceptPoolSize int) { 193 | acceptWorkerDoneCh := make(chan bool, acceptPoolSize) 194 | var nrFinished int 195 | 196 | for acceptId := 0; acceptId < acceptPoolSize; acceptId++ { 197 | go rcvr.acceptWorker(acceptId, acceptWorkerDoneCh) 198 | } 199 | 200 | // Watch for `context.cancelFunc` being called by another thread 201 | // while we wait for our accept workers to finish. 202 | 203 | for_loop: 204 | for { 205 | select { 206 | case <-rcvr.Base.ctx.Done(): 207 | // Force close the socket so that current and 208 | // future calls to `Accept()` will fail. 209 | rcvr.listener.Close() 210 | case <-acceptWorkerDoneCh: 211 | nrFinished++ 212 | if nrFinished == acceptPoolSize { 213 | break for_loop 214 | } 215 | } 216 | } 217 | 218 | //rcvr.Base.Logger.Debug(fmt.Sprintf("listenLoop: finished[%d]", nrFinished)) 219 | } 220 | 221 | func (rcvr *Rcvr_NamedPipe) acceptWorker(acceptId int, doneCh chan bool) { 222 | //rcvr.Base.Logger.Debug(fmt.Sprintf("acceptWorker[%d] starting", acceptId)) 223 | for { 224 | //rcvr.Base.Logger.Debug(fmt.Sprintf("acceptWorker[%d] calling Accept()", acceptId)) 225 | conn, err := rcvr.listener.Accept() 226 | //rcvr.Base.Logger.Debug(fmt.Sprintf("acceptWorker[%d] result '%v'", acceptId, err)) 227 | if errors.Is(err, net.ErrClosed) { 228 | break 229 | } 230 | if err != nil { 231 | // Perhaps the client hung up before 232 | // we could service this connection. 233 | rcvr.Base.Logger.Error(err.Error()) 234 | } else { 235 | go rcvr.worker(conn, acceptId, makeWorkerId()) 236 | } 237 | } 238 | 239 | doneCh <- true 240 | //rcvr.Base.Logger.Debug(fmt.Sprintf("acceptWorker[%d] finished", acceptId)) 241 | } 242 | 243 | func (rcvr *Rcvr_NamedPipe) worker(conn net.Conn, acceptId int, workerId uint64) { 244 | var haveError = false 245 | var wg sync.WaitGroup 246 | defer conn.Close() 247 | 248 | //rcvr.Base.Logger.Debug(fmt.Sprintf("worker[%d,%d] starting", acceptId, workerId)) 249 | 250 | doneReading := make(chan bool, 1) 251 | 252 | // Create a subordinate thread to watch for `context.cancelFunc` 253 | // being called by another thread. We need to interrupt our 254 | // (blocking) call to `ReadBytes()` in this worker and (maybe) 255 | // let it emit partial results (if it can do so quickly). 256 | // 257 | // However, we don't want to leak this subordinate thread if this 258 | // worker normally finishes reading all the data from the client 259 | // Git command. 260 | wg.Add(1) 261 | go func() { 262 | defer wg.Done() 263 | select { 264 | case <-rcvr.Base.ctx.Done(): 265 | // Force close the connection from the client to 266 | // help keep the Git command from getting stuck. 267 | // That is, let it get a clean write-error rather 268 | // than blocking on a buffer that we'll never 269 | // read. (It might not actually matter, but it 270 | // doesn't hurt.) 271 | // 272 | // This will also cause the worker's `ReadBytes()` 273 | // to return an error, so that the worker can 274 | // terminate the loop. 275 | conn.Close() 276 | case <-doneReading: 277 | } 278 | }() 279 | 280 | // We assume that a `worker` represents the server side of a connection 281 | // from a single Git client. That is, all events that we receive over 282 | // this connection are from the same process (and will therefore have 283 | // the same Trace2 SID). That is, we don't have to maintain a SID to 284 | // Dataset mapping. 285 | tr2 := NewTrace2Dataset(rcvr.Base) 286 | 287 | tr2.pii_gather(rcvr.Base.RcvrConfig) 288 | 289 | var nrBytesRead int = 0 290 | 291 | r := bufio.NewReader(conn) 292 | for { 293 | rawLine, err := r.ReadBytes('\n') 294 | if err == io.EOF { 295 | //if nrBytesRead == 0 { 296 | // rcvr.Base.Logger.Debug(fmt.Sprintf("worker[%d,%d][dsid %06d] EOF after %d bytes", 297 | // acceptId, workerId, tr2.datasetId, nrBytesRead)) 298 | //} 299 | break 300 | } 301 | if errors.Is(err, net.ErrClosed) { 302 | break 303 | } 304 | if err != nil { 305 | rcvr.Base.Logger.Error(err.Error()) 306 | haveError = true 307 | break 308 | } 309 | 310 | nrBytesRead += len(rawLine) 311 | 312 | if processRawLine(rawLine, tr2, rcvr.Base.Logger, 313 | rcvr.Base.RcvrConfig.AllowCommandControlVerbs) != nil { 314 | haveError = true 315 | break 316 | } 317 | } 318 | 319 | // Tell the subordinate thread that we are finished reading from 320 | // the client so it can go away now. This must not block (because 321 | // the subordinate may already be gone (which is the case if the 322 | // `context.cancelFunc` was called)). 323 | doneReading <- true 324 | 325 | conn.Close() 326 | 327 | if !haveError { 328 | tr2.exportTraces() 329 | } 330 | 331 | // Wait for our subordinate thread to exit 332 | wg.Wait() 333 | 334 | //rcvr.Base.Logger.Debug(fmt.Sprintf("worker[%d,%d] finished", acceptId, workerId)) 335 | } 336 | -------------------------------------------------------------------------------- /rcvr_unixsocket.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package trace2receiver 5 | 6 | import ( 7 | "bufio" 8 | "context" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "net" 13 | "os" 14 | "sync" 15 | "time" 16 | 17 | "go.opentelemetry.io/collector/component" 18 | "go.opentelemetry.io/collector/component/componentstatus" 19 | "golang.org/x/sys/unix" 20 | ) 21 | 22 | // `Rcvr_UnixSocket` implements the `component.TracesReceiver` (aka `component.Receiver` 23 | // (aka `component.Component`)) interface. 24 | type Rcvr_UnixSocket struct { 25 | // These fields should be set in ctor() 26 | Base *Rcvr_Base 27 | SocketPath string 28 | 29 | // Unix socket properties 30 | listener *net.UnixListener 31 | inode uint64 32 | mutex sync.Mutex 33 | isShutdown bool 34 | } 35 | 36 | // Start receiving connections from Trace2 clients. 37 | // 38 | // This is part of the `component.Component` interface. 39 | func (rcvr *Rcvr_UnixSocket) Start(unused_ctx context.Context, host component.Host) error { 40 | var err error 41 | 42 | err = rcvr.Base.Start(unused_ctx, host) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | err = rcvr.openSocketForListening() 48 | if err != nil { 49 | componentstatus.ReportStatus(host, componentstatus.NewFatalErrorEvent(err)) 50 | return err 51 | } 52 | 53 | go rcvr.listenLoop(host) 54 | return nil 55 | } 56 | 57 | // Stop accepting new connections from Trace2 clients. 58 | // 59 | // This is part of the `component.Component` interface. 60 | func (rcvr *Rcvr_UnixSocket) Shutdown(context.Context) error { 61 | rcvr.mutex.Lock() 62 | rcvr.isShutdown = true 63 | 64 | if rcvr.inode != 0 { 65 | // Only unlink the socket if we think we still own it. 66 | os.Remove(rcvr.SocketPath) 67 | rcvr.inode = 0 68 | } 69 | 70 | rcvr.listener.Close() 71 | rcvr.Base.cancel() 72 | 73 | rcvr.mutex.Unlock() 74 | return nil 75 | } 76 | 77 | type SocketPathnameStolenError struct { 78 | Pathname string 79 | SubErr error 80 | } 81 | 82 | func NewSocketPathnameStolenError(pathname string, err error) error { 83 | return &SocketPathnameStolenError{ 84 | Pathname: pathname, 85 | SubErr: err, 86 | } 87 | } 88 | 89 | func (e *SocketPathnameStolenError) Error() string { 90 | if e.SubErr != nil { 91 | return fmt.Sprintf("Socket pathname stolen: '%v' error: '%s'", 92 | e.Pathname, e.SubErr.Error()) 93 | } else { 94 | return fmt.Sprintf("Socket pathname stolen: '%v'", e.Pathname) 95 | } 96 | } 97 | 98 | type SocketInodeChangedError struct { 99 | InodeExpected uint64 100 | InodeObserved uint64 101 | } 102 | 103 | func NewSocketInodeChangedError(ie uint64, io uint64) error { 104 | return &SocketInodeChangedError{ 105 | InodeExpected: ie, 106 | InodeObserved: io, 107 | } 108 | } 109 | 110 | func (e *SocketInodeChangedError) Error() string { 111 | return fmt.Sprintf("Inode changed: expected %v observed %v", e.InodeExpected, e.InodeObserved) 112 | } 113 | 114 | func get_inode(path string) (uint64, error) { 115 | var stat unix.Stat_t 116 | err := unix.Lstat(path, &stat) 117 | if err != nil { 118 | return 0, err 119 | } 120 | 121 | return stat.Ino, nil 122 | } 123 | 124 | // Open the server-side of a Unix domain socket. 125 | func (rcvr *Rcvr_UnixSocket) openSocketForListening() error { 126 | var err error 127 | 128 | rcvr.mutex = sync.Mutex{} 129 | 130 | // The `listen(2)` system call must create the unix domain socket 131 | // in the file system. If the pathname already exists on disk, 132 | // the listen() call will fail. 133 | // 134 | // However, we do not know whether it is a dead socket or another 135 | // process is currently servicing it. 136 | // 137 | // Force delete it under the assumption that the socket is dead 138 | // and was not properly cleaned up by the previous daemon. 139 | // 140 | // NOTE On Unix (in addition to deleting a dead socket) we can 141 | // accidentally delete an active socket (currently being serviced 142 | // by another process) by mistake. We cannot tell the difference 143 | // (without a race-prone client-connection). Our unlink() WILL NOT 144 | // notify the other process; it just decrements the link-count in 145 | // the file system, but does not invalidate the fd in the other 146 | // process (just like you can unlink() a file that someone is still 147 | // writing and it won't actually be deleted until they close their 148 | // file descriptor). In this case, the other daemon process will 149 | // be effectively orphaned -- listening on a socket that no one can 150 | // connect to. This is a basic Unix problem and not specific to 151 | // OTEL Collectors or our receiver component. 152 | // 153 | // We will capture the inode of the socket that we create here and 154 | // add periodically verify in the listener loop's that the socket 155 | // still exists and has the same inode. 156 | _ = os.Remove(rcvr.SocketPath) 157 | 158 | // There are 3 types of Unix Domain Sockets: SOCK_STREAM, SOCK_DGRAM, 159 | // and SOCK_SEQPACKET. Git Trace2 supports the first two. However, 160 | // We're only going to support the first. This corresponds to the 161 | // "af_unix:" or "af_unix:stream:" values for `GIT_TRACE2_EVENT` 162 | // environment variable or the `trace2.eventtarget` config value. 163 | // 164 | // Note: In the C# .Net Core class libraries on Unix, the NamedPipe 165 | // classes are implemented using SOCK_STREAM Unix Domain Sockets 166 | // under the hood. 167 | // 168 | // So limiting ourselves here to SOCK_STREAM is fine. 169 | // 170 | rcvr.listener, err = net.ListenUnix("unix", 171 | &net.UnixAddr{Name: rcvr.SocketPath, 172 | Net: "unix"}) 173 | if err != nil { 174 | rcvr.Base.Logger.Error(fmt.Sprintf("could not create socket: %v", err)) 175 | return err 176 | } 177 | 178 | // By default the unixsock_posix code unlinks the socket 179 | // so that we don't have dead sockets in the file system. 180 | // This works when GO completely controls the environment, 181 | // but can cause problems if/when another process steals 182 | // the socket pathname from us -- we don't want our cleanup 183 | // to delete their socket. 184 | rcvr.listener.SetUnlinkOnClose(false) 185 | 186 | rcvr.inode, err = get_inode(rcvr.SocketPath) 187 | if err != nil { 188 | rcvr.Base.Logger.Error(fmt.Sprintf("could not lstat created socket: %v", err)) 189 | return err 190 | } 191 | 192 | // The UserId of the service process might be controlled by 193 | // the installer, /bin/launchctl, or an OS service manager. 194 | // We need the socket to be world writable in case the service 195 | // gets started as a privileged user so that ordinary Git 196 | // commands can write to it. (Git silently fails if it gets 197 | // a permission error and just turns off telemetry in its 198 | // proceess.) 199 | os.Chmod(rcvr.SocketPath, 0666) 200 | 201 | rcvr.Base.Logger.Info(fmt.Sprintf("listening on socket '%s' at '%v'", rcvr.SocketPath, rcvr.inode)) 202 | return nil 203 | } 204 | 205 | // Listen for incoming connections from Trace2 clients. 206 | // Dispatch each to a worker thread. 207 | func (rcvr *Rcvr_UnixSocket) listenLoop(host component.Host) { 208 | var wg sync.WaitGroup 209 | var worker_id uint64 210 | 211 | doneListening := make(chan bool, 1) 212 | 213 | // Create a subordinate thread to watch for `context.cancelFunc` 214 | // being called by another thread. We need to interrupt our 215 | // (blocking) call to `AcceptUnix()` in this thread and start 216 | // shutting down. 217 | // 218 | // However, we don't want to leak this subordinate thread if 219 | // our loop terminates for other reasons. 220 | wg.Add(1) 221 | go func() { 222 | ticker := time.NewTicker(30 * time.Second) 223 | defer ticker.Stop() 224 | defer wg.Done() 225 | LOOP: 226 | for { 227 | select { 228 | case <-rcvr.Base.ctx.Done(): 229 | // The main collector is requesting that we shutdown. 230 | // Force close the socket so that current and 231 | // futuer calls to `AcceptUnix()` will fail. 232 | rcvr.Base.Logger.Info("ctx.Done signalled") 233 | rcvr.listener.Close() 234 | break LOOP 235 | case <-doneListening: 236 | break LOOP 237 | case <-ticker.C: 238 | rcvr.mutex.Lock() 239 | if rcvr.isShutdown || rcvr.inode == 0 { 240 | // If Shutdown has already been called, then we don't 241 | // want our timeout handler to touch anything and 242 | // especially to not signal a new socket-stolen error 243 | // (because the shutdown code just deleted it). We only 244 | // want to throw a socket-stolen error if something 245 | // happened in the filesystem behind our back. 246 | ticker.Stop() 247 | rcvr.mutex.Unlock() 248 | break LOOP 249 | } 250 | // See if the socket inode was changed by external events. 251 | inode, err := get_inode(rcvr.SocketPath) 252 | if err != nil { 253 | // We could not lstat() our socket, assume it 254 | // has been deleted and/or stolen and give up. 255 | // (We could check the error code to be more 256 | // precise, but we'll probably do the same thing 257 | // in all cases anyway.) 258 | errStolen := NewSocketPathnameStolenError(rcvr.SocketPath, err) 259 | rcvr.Base.Logger.Error(errStolen.Error()) 260 | 261 | rcvr.inode = 0 262 | 263 | componentstatus.ReportStatus(host, componentstatus.NewFatalErrorEvent(errStolen)) 264 | rcvr.mutex.Unlock() 265 | break LOOP 266 | } 267 | if inode != rcvr.inode { 268 | // Someone stole the pathname to the socket and 269 | // created a different file/socket on the path. 270 | // So we will never see another connection on our 271 | // (still functional) socket. We should give up 272 | // and shutdown (without deleting the new socket 273 | // instance; ours should magically go away when 274 | // we close our file descriptor). 275 | errChanged := NewSocketInodeChangedError(rcvr.inode, inode) 276 | errStolen := NewSocketPathnameStolenError(rcvr.SocketPath, errChanged) 277 | rcvr.Base.Logger.Error(errStolen.Error()) 278 | 279 | rcvr.inode = 0 280 | 281 | componentstatus.ReportStatus(host, componentstatus.NewFatalErrorEvent(errStolen)) 282 | rcvr.mutex.Unlock() 283 | break LOOP 284 | } 285 | rcvr.mutex.Unlock() 286 | } 287 | } 288 | }() 289 | 290 | for { 291 | conn, err := rcvr.listener.AcceptUnix() 292 | if err == nil { 293 | worker_id++ 294 | go rcvr.worker(conn, worker_id) 295 | continue 296 | } 297 | 298 | rcvr.mutex.Lock() 299 | if rcvr.isShutdown || rcvr.inode == 0 { 300 | // We already know why the accept() failed because we closed 301 | // the socket, so don't bother with any error messages. 302 | rcvr.mutex.Unlock() 303 | break 304 | } 305 | if errors.Is(err, net.ErrClosed) { 306 | // (This may not be possible any now because of refactorings.) 307 | // Another thread closed our socket fd before Shutdown() was 308 | // called. (Or ctx.Done() was signalled and our helper thread 309 | // closed the socket.) Either way, we don't want to throw up 310 | // a socket-stolen error because whatever did the close may 311 | // still be in-progress and we don't want to get a second call 312 | // to ReportComponentStatus() going. 313 | rcvr.Base.Logger.Error(fmt.Sprintf("XXX: %v", err)) 314 | rcvr.mutex.Unlock() 315 | break 316 | } 317 | // Normal accept() errors do happen from time to time. Perhaps 318 | // the client hung up before we could service this connection. 319 | rcvr.Base.Logger.Error(err.Error()) 320 | rcvr.mutex.Unlock() 321 | } 322 | 323 | // Tell the subordinate thread that we are finished accepting 324 | // connections so it can go away now. This must not block 325 | // (because the subordinate may already be one (which is the 326 | // case if the `context.cancelFunc` was called)). 327 | doneListening <- true 328 | 329 | wg.Wait() 330 | } 331 | 332 | func (rcvr *Rcvr_UnixSocket) worker(conn *net.UnixConn, worker_id uint64) { 333 | var haveError = false 334 | var wg sync.WaitGroup 335 | defer conn.Close() 336 | 337 | doneReading := make(chan bool, 1) 338 | 339 | // Create a subordinate thread to watch for `context.cancelFunc` 340 | // being called by another thread. We need to interrupt our 341 | // (blocking) call to `ReadBytes()` in this worker and (maybe) 342 | // let it emit partial results (if it can do so quickly). 343 | // 344 | // However, we don't want to leak this subordinate thread if this 345 | // worker normally finishes reading all the data from the client 346 | // Git command. 347 | wg.Add(1) 348 | go func() { 349 | defer wg.Done() 350 | select { 351 | case <-rcvr.Base.ctx.Done(): 352 | // Force close the connection from the client to 353 | // help keep the Git command from getting stuck. 354 | // That is, let it get a clean write-error rather 355 | // than blocking on a buffer that we'll never 356 | // read. (It might not actually matter, but it 357 | // doesn't hurt.) 358 | // 359 | // This will also cause the worker's `ReadBytes()` 360 | // to return an error, so that the worker can 361 | // terminate the loop. 362 | conn.Close() 363 | case <-doneReading: 364 | } 365 | }() 366 | 367 | // We assume that a `worker` represents the server side of a connection 368 | // from a single Git client. That is, all events that we receive over 369 | // this connection are from the same process (and will therefore have 370 | // the same Trace2 SID). That is, we don't have to maintain a SID to 371 | // Dataset mapping. 372 | tr2 := NewTrace2Dataset(rcvr.Base) 373 | 374 | tr2.pii_gather(rcvr.Base.RcvrConfig, conn) 375 | 376 | r := bufio.NewReader(conn) 377 | for { 378 | rawLine, err := r.ReadBytes('\n') 379 | if err == io.EOF { 380 | break 381 | } 382 | if errors.Is(err, net.ErrClosed) { 383 | break 384 | } 385 | if err != nil { 386 | rcvr.Base.Logger.Error(err.Error()) 387 | haveError = true 388 | break 389 | } 390 | 391 | if processRawLine(rawLine, tr2, rcvr.Base.Logger, 392 | rcvr.Base.RcvrConfig.AllowCommandControlVerbs) != nil { 393 | haveError = true 394 | break 395 | } 396 | } 397 | 398 | // Tell the subordinate thread that we are finished reading from 399 | // the client so it can go away now. This must not block (because 400 | // the subordinate may already be gone (which is the case if the 401 | // `context.cancelFunc` was called)). 402 | doneReading <- true 403 | 404 | conn.Close() 405 | 406 | if !haveError { 407 | tr2.exportTraces() 408 | } 409 | 410 | // Wait for our subordinate thread to exit 411 | wg.Wait() 412 | } 413 | -------------------------------------------------------------------------------- /reject_client.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import "errors" 4 | 5 | // There are some clients that we want to reject as soon as we 6 | // learn their identity. Primarily this is for daemon Git processes 7 | // like `git fsmonitor--daemon run` and `git daemon` (and their dash 8 | // name peers) that run (for days/months) in the background. Since 9 | // we do not generate the OTLP process span until the client drops 10 | // the connection (ideally after the `atexit` event), we would be 11 | // forced to collect massive state for the background daemon and 12 | // bog down the entire telemetry service. So let's reject them as 13 | // soon as we identify them. 14 | // 15 | // There may be other background commands (like the new bundle server), 16 | // so we may have to have more than one detection methods. 17 | // 18 | // At this point I'm just going to hard code the rejection. I don't 19 | // think it is worth adding code to `FilterSettings` to make this 20 | // optional. 21 | 22 | type RejectClientError struct { 23 | Err error 24 | FSMonitor bool 25 | } 26 | 27 | func (rce *RejectClientError) Error() string { 28 | return rce.Err.Error() 29 | } 30 | 31 | // Is this Git command a `git fsmonitor--daemon` command? 32 | // 33 | // Check in `apply__cmd_name()` since we know FSMonitor sends a valid 34 | // `cmd_name` event. We really only need to reject `run` commands, 35 | // but unfortunately, it does not send `cmd_mode` events, so we cannot 36 | // distinguish between `run`, `start` and `stop`. 37 | func IsFSMonitorDaemon(verb string) error { 38 | if verb == "fsmonitor--daemon" { 39 | return &RejectClientError{ 40 | Err: errors.New("rejecting telemetry from fsmonitor--daemon"), 41 | FSMonitor: true, 42 | } 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /ruleset_definition.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // RulesetDefinition captures the content of a custom ruleset YML file. 8 | type RulesetDefinition struct { 9 | Commands RulesetCommands `mapstructure:"commands"` 10 | Defaults RulesetDefaults `mapstructure:"defaults"` 11 | } 12 | 13 | // RulesetCommands is used to map a Git command to a detail level. 14 | // This allows us to have a different verbosity for different commands. 15 | // For example, verbose for `git status` and drop for `git config`. 16 | // 17 | // We DO NOT support mapping to another ruleset because we want 18 | // to avoid circular dependencies. 19 | // 20 | // A command key should be in the format described in 21 | // `trace2Dataset.setQualifiedExeVerbModeName()`. 22 | // 23 | // The value must be one of [`DetailLevelDropName`, ... ]. 24 | type RulesetCommands map[string]string 25 | 26 | // RulesetDefaults defines default values for this custom ruleset. 27 | type RulesetDefaults struct { 28 | 29 | // The default detail level to use when exec+verb+mode 30 | // lookup fails. 31 | DetailLevelName string `mapstructure:"detail"` 32 | } 33 | 34 | // Parse a `ruleset.yml` and decode. 35 | func parseRulesetFile(path string) (*RulesetDefinition, error) { 36 | return parseYmlFile[RulesetDefinition](path, parseRulesetFromBuffer) 37 | } 38 | 39 | // Parse a buffer containing the contents of a `ruleset.yml` and decode. 40 | func parseRulesetFromBuffer(data []byte, path string) (*RulesetDefinition, error) { 41 | rsdef, err := parseYmlBuffer[RulesetDefinition](data, path) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | // After parsing the YML and populating the `mapstructure` fields, we 47 | // need to validate them and/or build internal structures from them. 48 | 49 | for k_cmd, v_dl := range rsdef.Commands { 50 | // Commands must map to detail levels and not to another ruleset (to 51 | // avoid lookup loops). 52 | _, err = getDetailLevel(v_dl) 53 | if len(k_cmd) == 0 || err != nil { 54 | return nil, fmt.Errorf("ruleset '%s' has invalid command '%s':'%s'", 55 | path, k_cmd, v_dl) 56 | } 57 | } 58 | 59 | if len(rsdef.Defaults.DetailLevelName) > 0 { 60 | // The rulset default detail level must be a detail level and not the 61 | // name of another ruleset (to avoid lookup loops). 62 | _, err = getDetailLevel(rsdef.Defaults.DetailLevelName) 63 | if err != nil { 64 | return nil, fmt.Errorf("ruleset '%s' has invalid default detail level", 65 | path) 66 | } 67 | } else { 68 | // If the custom ruleset did not define a ruleset-specific default 69 | // detail level, assume the builtin global default. 70 | rsdef.Defaults.DetailLevelName = DetailLevelDefaultName 71 | } 72 | 73 | return rsdef, nil 74 | } 75 | -------------------------------------------------------------------------------- /trace2emitotlp.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "runtime" 7 | 8 | "go.opentelemetry.io/collector/pdata/pcommon" 9 | "go.opentelemetry.io/collector/pdata/ptrace" 10 | "go.opentelemetry.io/otel" 11 | semconv "go.opentelemetry.io/otel/semconv/v1.14.0" 12 | ) 13 | 14 | func (tr2 *trace2Dataset) insertResourceServiceFields(resourceAttrs pcommon.Map) { 15 | // The SemConv `service.namespace`, `service.name`, `service.version`, 16 | // and `service.instance.id` fields are somewhat ill-defined in our 17 | // case. They describe the application generating the telemetry and 18 | // an organizational/ownership group that multiple services operate 19 | // under. 20 | // 21 | // This `trace2receiver` component is just a relay/proxy for the 22 | // actual telemetry being generated by commands like `git.exe` or 23 | // `gcm.exe`. That is, this component is not generating original 24 | // telemetry about itself. So we adapt the definitions of these 25 | // fields a bit here. 26 | 27 | // [1] Claim a namespace for all of the things that we will proxy 28 | // since the Git commands don't know anything about any of the OTEL 29 | // and OTLP stuff. 30 | // 31 | // TODO Let's make the namespace a constant for now. Later, we can 32 | // TODO consider if we should read it from the `config.yaml` to 33 | // TODO allow us to fit into our deployment's SemConv scheme. 34 | 35 | resourceAttrs.PutStr(string(semconv.ServiceNamespaceKey), Trace2ServiceNamespace) 36 | 37 | // [2] Use the name of the Git command to define the service name. 38 | // The OTEL guidelines suggest that this is just the name of the 39 | // application (a service that may be running on more than one host 40 | // instances). However, since some visualization tools automatically 41 | // group by the service name, just putting `git` or `gcm` in this 42 | // field doesn't do much for us. Using the `:%` 43 | // string is better here. This deviation feels right since an 44 | // actual service application may define multiple end-points and do 45 | // many different things. 46 | // 47 | // Using the `:% 0 { 218 | jargs, _ := json.Marshal(tr2.process.cmdArgv) 219 | sm.PutStr(string(Trace2CmdArgv), string(jargs)) 220 | } 221 | 222 | if WantProcessAncestry(dl) { 223 | if len(tr2.process.cmdAncestry) > 0 { 224 | jargs, _ := json.Marshal(tr2.process.cmdAncestry) 225 | sm.PutStr(string(Trace2CmdAncestry), string(jargs)) 226 | } 227 | } 228 | 229 | if WantProcessAliases(dl) { 230 | if len(tr2.process.cmdAliasKey) > 0 { 231 | sm.PutStr(string(Trace2CmdAliasKey), tr2.process.cmdAliasKey) 232 | 233 | if len(tr2.process.cmdAliasValue) > 0 { 234 | jargs, _ := json.Marshal(tr2.process.cmdAliasValue) 235 | sm.PutStr(string(Trace2CmdAliasValue), string(jargs)) 236 | } 237 | } 238 | } 239 | 240 | if len(tr2.process.exeErrorFmt) > 0 { 241 | sm.PutStr(string(Trace2CmdErrFmt), tr2.process.exeErrorFmt) 242 | } 243 | if len(tr2.process.exeErrorMsg) > 0 { 244 | sm.PutStr(string(Trace2CmdErrMsg), tr2.process.exeErrorMsg) 245 | } 246 | 247 | if tr2.process.repoSet != nil && len(tr2.process.repoSet) > 0 { 248 | jargs, _ := json.Marshal(tr2.process.repoSet) 249 | sm.PutStr(string(Trace2RepoSet), string(jargs)) 250 | } 251 | 252 | if tr2.process.paramSetValues != nil && len(tr2.process.paramSetValues) > 0 { 253 | jargs, _ := json.Marshal(tr2.process.paramSetValues) 254 | sm.PutStr(string(Trace2ParamSet), string(jargs)) 255 | } 256 | 257 | if WantMainThreadTimersAndCounters(dl) { 258 | // Emit per-thread counters and timers for the main thread because 259 | // it is not handled by `emitNonMainThreadSpan()`. 260 | if tr2.process.mainThread.timers != nil { 261 | jargs, _ := json.Marshal(tr2.process.mainThread.timers) 262 | sm.PutStr(string(Trace2ThreadTimers), string(jargs)) 263 | } 264 | if tr2.process.mainThread.counters != nil { 265 | jargs, _ := json.Marshal(tr2.process.mainThread.counters) 266 | sm.PutStr(string(Trace2ThreadCounters), string(jargs)) 267 | } 268 | } 269 | 270 | if WantProcessTimersCountersAndData(dl) { 271 | if tr2.process.dataValues != nil && len(tr2.process.dataValues) > 0 { 272 | jargs, _ := json.Marshal(tr2.process.dataValues) 273 | sm.PutStr(string(Trace2ProcessData), string(jargs)) 274 | } 275 | if tr2.process.timers != nil { 276 | jargs, _ := json.Marshal(tr2.process.timers) 277 | sm.PutStr(string(Trace2ProcessTimers), string(jargs)) 278 | } 279 | if tr2.process.counters != nil { 280 | jargs, _ := json.Marshal(tr2.process.counters) 281 | sm.PutStr(string(Trace2ProcessCounters), string(jargs)) 282 | } 283 | } 284 | } 285 | 286 | func emitNonMainThreadSpan(span *ptrace.Span, th *TrThread, tr2 *trace2Dataset) { 287 | emitSpanEssentials(span, &th.lifetime, tr2) 288 | 289 | sm := span.Attributes() 290 | sm.PutStr(string(Trace2SpanType), "thread") 291 | 292 | if th.timers != nil { 293 | jargs, _ := json.Marshal(th.timers) 294 | sm.PutStr(string(Trace2ThreadTimers), string(jargs)) 295 | } 296 | 297 | if th.counters != nil { 298 | jargs, _ := json.Marshal(th.counters) 299 | sm.PutStr(string(Trace2ThreadCounters), string(jargs)) 300 | } 301 | } 302 | 303 | func emitRegionSpan(span *ptrace.Span, r *TrRegion, tr2 *trace2Dataset) { 304 | emitSpanEssentials(span, &r.lifetime, tr2) 305 | 306 | sm := span.Attributes() 307 | sm.PutStr(string(Trace2SpanType), "region") 308 | 309 | sm.PutStr(string(Trace2RegionRepoId), fmt.Sprintf("%d", r.repoId)) 310 | 311 | sm.PutStr(string(Trace2RegionNesting), fmt.Sprintf("%d", r.nestingLevel)) 312 | if len(r.message) > 0 { 313 | sm.PutStr(string(Trace2RegionMessage), r.message) 314 | } 315 | 316 | if r.dataValues != nil && len(r.dataValues) > 0 { 317 | jargs, _ := json.Marshal(r.dataValues) 318 | sm.PutStr(string(Trace2RegionData), string(jargs)) 319 | } 320 | } 321 | 322 | func emitChildSpan(span *ptrace.Span, child *TrChild, tr2 *trace2Dataset) { 323 | emitSpanEssentials(span, &child.lifetime, tr2) 324 | 325 | sm := span.Attributes() 326 | sm.PutStr(string(Trace2SpanType), "child") 327 | 328 | if len(child.argv) > 0 { 329 | jargs, _ := json.Marshal(child.argv) 330 | sm.PutStr(string(Trace2ChildArgv), string(jargs)) 331 | } 332 | 333 | // Azure automatically treats integer attributes as "customMeasurements" 334 | // rather than grouping them with the other "customDimensions". Or they 335 | // appear in "customDimensions" with value "". The former can lead to 336 | // weird graphs where data is plotted by PID. So force them to be strings. 337 | sm.PutStr(string(Trace2ChildPid), fmt.Sprintf("%d", child.pid)) 338 | sm.PutStr(string(Trace2ChildExitCode), fmt.Sprintf("%d", child.exitcode)) 339 | 340 | if len(child.readystate) > 0 { 341 | // This was an async child sent to background. 342 | sm.PutStr(string(Trace2ChildReadyState), child.readystate) 343 | } 344 | 345 | sm.PutStr(string(Trace2ChildClass), child.class) 346 | if child.class == "hook" { 347 | sm.PutStr(string(Trace2ChildHookName), child.hookname) 348 | } 349 | } 350 | 351 | func emitExecSpan(span *ptrace.Span, e *TrExec, tr2 *trace2Dataset) { 352 | emitSpanEssentials(span, &e.lifetime, tr2) 353 | 354 | sm := span.Attributes() 355 | sm.PutStr(string(Trace2SpanType), "exec") 356 | 357 | if len(e.argv) > 0 { 358 | jargs, _ := json.Marshal(e.argv) 359 | sm.PutStr(string(Trace2ExecArgv), string(jargs)) 360 | } 361 | 362 | sm.PutStr(string(Trace2ExecExe), e.exe) 363 | sm.PutStr(string(Trace2ExecExitCode), fmt.Sprintf("%d", e.exitcode)) 364 | } 365 | -------------------------------------------------------------------------------- /trace2ruleset.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import "fmt" 4 | 5 | func debugDescribe(base string, lval string, rval string) string { 6 | if len(base) == 0 { 7 | return fmt.Sprintf("[%s -> %s]", lval, rval) 8 | } else { 9 | return fmt.Sprintf("%s/[%s -> %s]", base, lval, rval) 10 | } 11 | } 12 | 13 | // Try to lookup the name of the custom ruleset or detail level using 14 | // value passed in the `def_param` for the `Ruleset Key`. 15 | func (fs *FilterSettings) lookupRulesetNameByRulesetKey(params map[string]string, debug_in string) (rs_dl_name string, ok bool, debug_out string) { 16 | debug_out = debug_in 17 | 18 | if len(fs.Keynames.RulesetKey) == 0 { 19 | return "", false, debug_out 20 | } 21 | 22 | rs_dl_name, ok = params[fs.Keynames.RulesetKey] 23 | if !ok || len(rs_dl_name) == 0 { 24 | return "", false, debug_out 25 | } 26 | 27 | // Acknowledge that we saw the ruleset key in the request and will try to use it. 28 | debug_out = debugDescribe(debug_out, "rskey", rs_dl_name) 29 | 30 | return rs_dl_name, true, debug_out 31 | } 32 | 33 | // Lookup ruleset or detail level name based upon the nickname (if the 34 | // key is defined in the filter settings and if the worktree sent 35 | // a def_param for it). 36 | func (fs *FilterSettings) lookupRulesetNameByNickname(params map[string]string, debug_in string) (rs_dl_name string, ok bool, debug_out string) { 37 | debug_out = debug_in 38 | 39 | if len(fs.Keynames.NicknameKey) == 0 { 40 | return "", false, debug_out 41 | } 42 | 43 | nnvalue, ok := params[fs.Keynames.NicknameKey] 44 | if !ok || len(nnvalue) == 0 { 45 | return "", false, debug_out 46 | } 47 | 48 | // Acknowledge that we saw the nickname in the request. 49 | debug_out = debugDescribe(debug_out, "nickname", nnvalue) 50 | 51 | rs_dl_name, ok = fs.Nicknames[nnvalue] 52 | if !ok || len(rs_dl_name) == 0 { 53 | // Acknowledge that the nickname was not valid. 54 | debug_out := debugDescribe(debug_out, nnvalue, "UNKNOWN") 55 | return "", false, debug_out 56 | } 57 | 58 | // Acknowledge that we will try to use the nickname. 59 | debug_out = debugDescribe(debug_out, nnvalue, rs_dl_name) 60 | 61 | return rs_dl_name, true, debug_out 62 | } 63 | 64 | // Lookup the name of the default ruleset or detail level from 65 | // the global defaults section in the filter settings if it has one. 66 | func (fs *FilterSettings) lookupDefaultRulesetName(debug_in string) (rs_dl_name string, ok bool, debug_out string) { 67 | debug_out = debug_in 68 | 69 | if len(fs.Defaults.RulesetName) == 0 { 70 | return "", false, debug_out 71 | } 72 | 73 | // Acknowledge that we will try to use the global default. 74 | debug_out = debugDescribe(debug_out, "default-ruleset", fs.Defaults.RulesetName) 75 | 76 | return fs.Defaults.RulesetName, true, debug_out 77 | } 78 | 79 | // Determine whether a ruleset or detail level was requested. 80 | func (fs *FilterSettings) lookupRulesetName(params map[string]string, debug_in string) (rs_dl_name string, ok bool, debug_out string) { 81 | debug_out = debug_in 82 | 83 | // If the command sent a `def_param` with the "Ruleset Key" that 84 | // is known, use it. 85 | rs_dl_name, ok, debug_out = fs.lookupRulesetNameByRulesetKey(params, debug_out) 86 | if !ok { 87 | // Otherwise, if the command sent a `def_param` with the "Nickname Key" 88 | // that has a known mapping, use it. 89 | rs_dl_name, ok, debug_out = fs.lookupRulesetNameByNickname(params, debug_out) 90 | if !ok { 91 | // Otherwise, if the filter settings defined a global default 92 | // ruleset, use it. 93 | rs_dl_name, ok, debug_out = fs.lookupDefaultRulesetName(debug_out) 94 | } 95 | } 96 | 97 | return rs_dl_name, ok, debug_out 98 | } 99 | 100 | // Use the global builtin default detail level. 101 | func useBuiltinDefaultDetailLevel(debug_in string) (dl FilterDetailLevel, debug_out string) { 102 | dl, _ = getDetailLevel(DetailLevelDefaultName) 103 | // Acknowledge that we will use the builtin default. 104 | debug_out = debugDescribe(debug_in, "builtin-default", DetailLevelDefaultName) 105 | return dl, debug_out 106 | } 107 | 108 | // Use the ruleset default detail level. (This was set to the global 109 | // builtin default detail level if it wasn't set in the ruleset YML.) 110 | func (rsdef *RulesetDefinition) useRulesetDefaultDetailLevel(debug_in string) (dl FilterDetailLevel, debug_out string) { 111 | dl, _ = getDetailLevel(rsdef.Defaults.DetailLevelName) 112 | // Acknowledge that we will use the ruleset default for this command. 113 | debug_out = debugDescribe(debug_in, "ruleset-default", rsdef.Defaults.DetailLevelName) 114 | return dl, debug_out 115 | } 116 | 117 | // Lookup the detail level for a command using the CmdMap in this ruleset. 118 | // 119 | // We try: `:#`, `:`, and `` until we find 120 | // a match. Then fallback to the ruleset default. We assume that the CmdMap 121 | // only has detail level values (and not links to other custom rulesets), so 122 | // we won't get lookup cycles. 123 | func (rsdef *RulesetDefinition) lookupCommandDetailLevelName(qn QualifiedNames, debug_in string) (string, bool, string) { 124 | // See if there is an entry in the CmdMap for this Git command. 125 | dl_name, ok := rsdef.Commands[qn.exeVerbMode] 126 | if ok { 127 | return dl_name, true, debugDescribe(debug_in, qn.exeVerbMode, dl_name) 128 | } 129 | 130 | dl_name, ok = rsdef.Commands[qn.exeVerb] 131 | if ok { 132 | return dl_name, true, debugDescribe(debug_in, qn.exeVerb, dl_name) 133 | } 134 | 135 | dl_name, ok = rsdef.Commands[qn.exe] 136 | if ok { 137 | return dl_name, true, debugDescribe(debug_in, qn.exe, dl_name) 138 | } 139 | 140 | return "", false, debug_in 141 | } 142 | 143 | // Compute the net-net detail level that we should use for this Git command. 144 | func computeDetailLevel(fs *FilterSettings, params map[string]string, 145 | qn QualifiedNames) (FilterDetailLevel, string) { 146 | 147 | if fs == nil { 148 | // No filter-spec, assume global builtin default detail level. 149 | return useBuiltinDefaultDetailLevel("") 150 | } 151 | 152 | rs_dl_name, ok, debug := fs.lookupRulesetName(params, "") 153 | if !ok { 154 | // No ruleset or detail level, assume global builtin default detail level. 155 | return useBuiltinDefaultDetailLevel(debug) 156 | } 157 | 158 | // If the name is a detail level rather than a named ruleset, then we use it 159 | // as is (since we don't do per-command filtering for detail levels). 160 | dl, err := getDetailLevel(rs_dl_name) 161 | if err == nil { 162 | return dl, debug 163 | } 164 | 165 | // Try to look it up as a custom ruleset. 166 | rsdef, ok := fs.rulesetDefs[rs_dl_name] 167 | if !ok { 168 | // Acknowledge that the ruleset name is not valid/unknown. 169 | debug = debugDescribe(debug, rs_dl_name, "INVALID") 170 | 171 | // We do not have a ruleset with that name. Silently assume the builtin 172 | // default detail level. 173 | return useBuiltinDefaultDetailLevel(debug) 174 | } 175 | 176 | // Acknowledge that we are trying command-level filtering starting with 177 | // the full expression. 178 | debug = debugDescribe(debug, "command", qn.exeVerbMode) 179 | 180 | // Use the requested ruleset and see if this command has a 181 | // command-specific filtering. 182 | dl_name, ok, debug := rsdef.lookupCommandDetailLevelName(qn, debug) 183 | if !ok { 184 | return rsdef.useRulesetDefaultDetailLevel(debug) 185 | } 186 | 187 | dl, err = getDetailLevel(dl_name) 188 | if err == nil { 189 | return dl, debug 190 | } 191 | 192 | // We should not get here because we validated the spelling of all 193 | // of the CmdMap values and the default value when we validated the 194 | // `config.yml`. But force a sane backstop. 195 | dl, _ = getDetailLevel(DetailLevelDefaultName) 196 | debug = debugDescribe(debug, "BACKSTOP", DetailLevelDefaultName) 197 | 198 | return dl, debug 199 | } 200 | -------------------------------------------------------------------------------- /trace2semconv.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import "go.opentelemetry.io/otel/attribute" 4 | 5 | // This file contains semantic conventions for Trace2 reporting. 6 | 7 | const ( 8 | // Value of the `service.namespace` key that we inject into 9 | // all resourceAttributes. 10 | Trace2ServiceNamespace = "trace2" 11 | 12 | // Value of the `instrumentation.name` or `instrumentationlibrary.name` 13 | // key that we inject into the resourceAttributes. (The actual spelling 14 | // of this key varies it seems.) 15 | Trace2InstrumentationName = "trace2receiver" 16 | ) 17 | 18 | // TODO Compare this with the stock `semconv` package for some of 19 | // process keys. 20 | 21 | const ( 22 | // The Trace2 SID of the process. This is the complete SID with 23 | // zero or more slashes describing the SIDs of any Trace2-aware 24 | // parent processes. 25 | Trace2CmdSid = attribute.Key("trace2.cmd.sid") 26 | 27 | // The complete command line args of the process. 28 | Trace2CmdArgv = attribute.Key("trace2.cmd.argv") 29 | 30 | // The version string of the process executable as reported in the 31 | // Trace2 "version" event. 32 | Trace2CmdVersion = attribute.Key("trace2.cmd.version") 33 | 34 | // The command's exit code. Zero if it completed without error. 35 | // If this process was signalled, this should be 128+signo. 36 | Trace2CmdExitCode = attribute.Key("trace2.cmd.exit_code") 37 | 38 | // The base filename of the process executable (with the pathname and 39 | // `.exe` suffix stripped off), for example `git` or `git-remote-https`. 40 | Trace2CmdName = attribute.Key("trace2.cmd.name") 41 | 42 | // The executable name and verb isolated from the command line 43 | // with normalized formatting. For example `git checkout` should 44 | // be reported as `git:checkout`. For commands that do not have 45 | // a verb, this should just be the name of the executable. 46 | Trace2CmdNameVerb = attribute.Key("trace2.cmd.name_verb") 47 | 48 | // The executable name, verb and command mode combined with 49 | // normalized formatting. For example, `git checkout -- ` 50 | // is different from `git checkout `. These should be 51 | // reported as `git:checkout#path` or `git:checkout#branch`. For 52 | // commands that do not have a mode, this should just be the verb. 53 | Trace2CmdNameVerbMode = attribute.Key("trace2.cmd.name_verb_mode") 54 | 55 | // The verb hierarchy for the command as reported by Git itself. 56 | // For example when `git index-pack` is launched by `git fetch`, 57 | // the child process will report a verb of `index-pack` and a 58 | // hierarchy of `fetch/index-pack`. 59 | Trace2CmdHierarchy = attribute.Key("trace2.cmd.hierarchy") 60 | 61 | // The format string of one error message from the command. 62 | Trace2CmdErrFmt = attribute.Key("trace2.cmd.error.format") 63 | Trace2CmdErrMsg = attribute.Key("trace2.cmd.error.message") 64 | 65 | Trace2CmdAliasKey = attribute.Key("trace2.cmd.alias.key") 66 | Trace2CmdAliasValue = attribute.Key("trace2.cmd.alias.value") 67 | 68 | // Optional process hierarchy that invoked this Git command. 69 | // Usually contains things like "bash" and "sshd". This data 70 | // is read from "/proc" on Linux, for example. It may be 71 | // truncated, but contain enough entries to give some crude 72 | // context. 73 | // 74 | // Type: array of string 75 | Trace2CmdAncestry = attribute.Key("trace2.cmd.ancestry") 76 | 77 | // Trace2 classification of the span. For example: "process", 78 | // "thread", "child", or "region". 79 | // 80 | // Type: string 81 | Trace2SpanType = attribute.Key("trace2.span.type") 82 | 83 | Trace2ChildPid = attribute.Key("trace2.child.pid") 84 | Trace2ChildExitCode = attribute.Key("trace2.child.exitcode") 85 | Trace2ChildArgv = attribute.Key("trace2.child.argv") 86 | Trace2ChildClass = attribute.Key("trace2.child.class") 87 | Trace2ChildHookName = attribute.Key("trace2.child.hook") 88 | Trace2ChildReadyState = attribute.Key("trace2.child.ready") 89 | 90 | Trace2RegionMessage = attribute.Key("trace2.region.message") 91 | Trace2RegionNesting = attribute.Key("trace2.region.nesting") 92 | Trace2RegionRepoId = attribute.Key("trace2.region.repoid") 93 | Trace2RegionData = attribute.Key("trace2.region.data") 94 | 95 | Trace2ExecExe = attribute.Key("trace2.exec.exe") 96 | Trace2ExecArgv = attribute.Key("trace2.exec.argv") 97 | Trace2ExecExitCode = attribute.Key("trace2.exec.exitcode") 98 | 99 | Trace2RepoSet = attribute.Key("trace2.repo.set") 100 | Trace2ParamSet = attribute.Key("trace2.param.set") 101 | 102 | Trace2ProcessData = attribute.Key("trace2.process.data") 103 | Trace2ProcessTimers = attribute.Key("trace2.process.timers") 104 | Trace2ProcessCounters = attribute.Key("trace2.process.counters") 105 | 106 | Trace2ThreadTimers = attribute.Key("trace2.thread.timers") 107 | Trace2ThreadCounters = attribute.Key("trace2.thread.counters") 108 | 109 | Trace2GoArch = attribute.Key("trace2.machine.arch") 110 | Trace2GoOS = attribute.Key("trace2.machine.os") 111 | 112 | Trace2PiiHostname = attribute.Key("trace2.pii.hostname") 113 | Trace2PiiUsername = attribute.Key("trace2.pii.username") 114 | ) 115 | -------------------------------------------------------------------------------- /trace2sids.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import ( 4 | "crypto/sha256" 5 | "strings" 6 | ) 7 | 8 | // TODO `trace.TraceID` and `trace.SpanID` are defined as fixed-sized 9 | // byte arrays but we are using the underlying [16]byte and [8]byte 10 | // types rather than the defined types. 11 | // 12 | // I'm not sure if we can/should reference those classes from a receiver. 13 | // They are more associated with a traceProvider and generating traces and 14 | // spans. Which is a whole different portion of the overall API from the 15 | // `ptrace` APIs that we use in a receiver. So for now, just hard-code the 16 | // types here. 17 | 18 | var zeroSpanID [8]byte 19 | 20 | // Synthesize OTEL Trace and Span IDs using data in the Trace2 SID string. 21 | // 22 | // A Trace2 SID looks like a "//.../" where a 23 | // top-level Git command has SID "" and an immediate child 24 | // process has SID "/" and so on. 25 | // 26 | // Since each of these commands will independently log telemetry to separate 27 | // receiver workers threads (and child processes will finish before the 28 | // parent process), we need a unique TraceID for the set to tie them together. 29 | // And to synthesize Span IDs to capture the parent/child relationships 30 | // encoded in the SID. 31 | // 32 | // These IDs cannot be constructed using a random number generator, so we 33 | // use SHA256 on parts of the SID and (since each bit in a SHA result is 34 | // uniformly distributed) extract substrings from the hashes in well-defined 35 | // ways (so that other worker threads will compute the same values on the 36 | // SIDs from other processes). 37 | func extractIDsfromSID(rawSid string) (tid [16]byte, spid [8]byte, spidParent [8]byte) { 38 | sidArray := strings.Split(rawSid, "/") 39 | 40 | // Compute the hash on for the TraceID, since all child 41 | // processes will have in their SIDs. 42 | 43 | hash_0 := sha256.Sum256([]byte(sidArray[0])) 44 | copy(tid[:], hash_0[0:16]) 45 | 46 | if len(sidArray) == 1 { 47 | // We are top-level command, so we have no parent Span and 48 | // we extract some bits from the SID for our SpanID. 49 | copy(spidParent[:], zeroSpanID[:]) 50 | copy(spid[:], hash_0[16:24]) 51 | } else { 52 | // We are a child (grandchild*) of a top-level command. 53 | // Compute hashes on the last 2 and extract SpanID 54 | // bits for this process and its parent process. 55 | n := len(sidArray) - 1 56 | 57 | hash_n1 := sha256.Sum256([]byte(sidArray[n-1])) 58 | copy(spidParent[:], hash_n1[16:24]) 59 | 60 | hash_n := sha256.Sum256([]byte(sidArray[n])) 61 | copy(spid[:], hash_n[16:24]) 62 | } 63 | 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /unixsocket_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package trace2receiver 5 | 6 | import ( 7 | "net" 8 | "os/user" 9 | "strconv" 10 | 11 | "golang.org/x/sys/unix" 12 | ) 13 | 14 | // Get the username of the process on the other end of 15 | // the unix domain socket connection. (It is not sufficient 16 | // to just call `user.Current()` because the telemetry 17 | // service will probably be running as root or some other 18 | // pseudo-user.) 19 | func getPeerUsername(conn *net.UnixConn) (string, error) { 20 | raw, err := conn.SyscallConn() 21 | if err != nil { 22 | return "", err 23 | } 24 | 25 | var cred *unix.Xucred 26 | var crederr error 27 | 28 | err = raw.Control( 29 | func(fd uintptr) { 30 | cred, crederr = unix.GetsockoptXucred(int(fd), 31 | unix.SOL_LOCAL, unix.LOCAL_PEERCRED) 32 | err = crederr 33 | }) 34 | 35 | if err != nil { 36 | return "", err 37 | } 38 | 39 | uidString := strconv.FormatUint(uint64(cred.Uid), 10) 40 | 41 | u, err := user.LookupId(uidString) 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | return u.Username, nil 47 | } 48 | -------------------------------------------------------------------------------- /unixsocket_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package trace2receiver 5 | 6 | import ( 7 | "net" 8 | "os/user" 9 | "strconv" 10 | 11 | "golang.org/x/sys/unix" 12 | ) 13 | 14 | // Get the username of the process on the other end of 15 | // the unix domain socket connection. (It is not sufficient 16 | // to just call `user.Current()` because the telemetry 17 | // service will probably be running as root or some other 18 | // pseudo-user.) 19 | func getPeerUsername(conn *net.UnixConn) (string, error) { 20 | raw, err := conn.SyscallConn() 21 | if err != nil { 22 | return "", err 23 | } 24 | 25 | // On Linux we use "Ucred" on Darwin we use "Xucred". 26 | 27 | var cred *unix.Ucred 28 | var crederr error 29 | 30 | err = raw.Control( 31 | func(fd uintptr) { 32 | cred, crederr = unix.GetsockoptUcred(int(fd), 33 | unix.SOL_SOCKET, unix.SO_PEERCRED) 34 | err = crederr 35 | }) 36 | 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | uidString := strconv.FormatUint(uint64(cred.Uid), 10) 42 | 43 | u, err := user.LookupId(uidString) 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | return u.Username, nil 49 | } 50 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package trace2receiver 2 | 3 | import ( 4 | "runtime/debug" 5 | "strings" 6 | ) 7 | 8 | // Automatically set our version string using the (usually canonical) 9 | // semantic version tag specified in a `require` in the `go.mod` of the 10 | // executable into which we are linked. https://go.dev/ref/mod#vcs-find 11 | // 12 | // Note that modules are consumed in source form, rather than as binary 13 | // artifacts, so we cannot force the consumer to build with `-ldflags` 14 | // to set `-X '=` in their build scripts. 15 | // 16 | // Also, we don't want to force a manual update of a hard-coded constant 17 | // when we create a tag; that is too easy to forget. 18 | // 19 | // Since GOLANG bakes this information into the binary, let's extract 20 | // it and use it. See also: `git version -m `. 21 | // 22 | // We should always create a tag of the form `v..` when we 23 | // make a release. And let the consumer ask for it by name. We set 24 | // the default here in case we are not consumed as a module, such as 25 | // in our local unit tests. 26 | 27 | var Trace2ReceiverVersion string = "v0.0.0-unset" 28 | 29 | func init() { 30 | if bi, ok := debug.ReadBuildInfo(); ok { 31 | for k := range bi.Deps { 32 | p := bi.Deps[k].Path 33 | if strings.Contains(p, "trace2receiver") { 34 | Trace2ReceiverVersion = bi.Deps[k].Version 35 | return 36 | } 37 | } 38 | } 39 | } 40 | --------------------------------------------------------------------------------