├── .codecov.yml ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── dependabot-approver.yml │ └── proj-xmidt-team.yml ├── .gitignore ├── .golangci.yaml ├── .whitesource ├── AUTHORS ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── go.mod ├── go.sum ├── span.go ├── span_test.go ├── spanner.go ├── spanner_test.go ├── trace.go ├── trace_test.go └── tracker.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 50..80 3 | round: down 4 | precision: 2 5 | 6 | ignore: 7 | - "*_test.go" 8 | - "vendor" 9 | 10 | fixes: 11 | - "github.com/xmidt-org/golang-money/::" -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Comcast Cable Communications Management, LLC 2 | # SPDX-License-Identifier: Apache-2.0 3 | --- 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | paths-ignore: 11 | - README.md 12 | - CONTRIBUTING.md 13 | - MAINTAINERS.md 14 | - LICENSE 15 | - NOTICE 16 | pull_request: 17 | workflow_dispatch: 18 | 19 | jobs: 20 | ci: 21 | uses: xmidt-org/.github/.github/workflows/go-ci.yml@go-ci-v1 22 | with: 23 | lint-skip: true 24 | secrets: inherit 25 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-approver.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Dependabot auto approval' 3 | 4 | on: 5 | pull_request_target 6 | permissions: 7 | pull-requests: write 8 | contents: write 9 | 10 | jobs: 11 | package: 12 | uses: xmidt-org/.github/.github/workflows/dependabot-approver-template.yml@main 13 | secrets: inherit 14 | -------------------------------------------------------------------------------- /.github/workflows/proj-xmidt-team.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'PROJ: xmidt-team' 3 | 4 | on: 5 | issues: 6 | types: 7 | - opened 8 | pull_request: 9 | types: 10 | - opened 11 | 12 | jobs: 13 | package: 14 | uses: xmidt-org/.github/.github/workflows/proj-template.yml@proj-v1 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | *vendor/ 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | linters-settings: 3 | misspell: 4 | locale: US 5 | 6 | linters: 7 | enable: 8 | - bodyclose 9 | - dupl 10 | - errorlint 11 | - funlen 12 | - goconst 13 | - gosec 14 | - misspell 15 | - unconvert 16 | - prealloc 17 | disable: 18 | - errcheck 19 | - ineffassign 20 | 21 | issues: 22 | exclude-rules: 23 | - path: _test.go 24 | linters: 25 | - dupl 26 | - funlen 27 | 28 | - path: trace\.go 29 | # Accept normal random function for determining a starting node 30 | text: "G404:" 31 | 32 | linters-settings: 33 | errorlint: 34 | # Report non-wrapping error creation using fmt.Errorf 35 | errorf: false 36 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff" 8 | }, 9 | "issueSettings": { 10 | "minSeverityLevel": "LOW" 11 | } 12 | } -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Nicholas Harter: Original Author 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contribution Guidelines 2 | ======================= 3 | 4 | We love to see contributions to the project and have tried to make it easy to 5 | do so. If you would like to contribute code to this project you can do so 6 | through GitHub by [forking the repository and sending a pull request](http://gun.io/blog/how-to-github-fork-branch-and-pull-request/). 7 | 8 | Before Comcast merges your code into the project you must sign the 9 | [Comcast Contributor License Agreement (CLA)](https://gist.github.com/ComcastOSS/a7b8933dd8e368535378cda25c92d19a). 10 | 11 | If you haven't previously signed a Comcast CLA, you'll automatically be asked 12 | to when you open a pull request. Alternatively, we can e-mail you a PDF that 13 | you can sign and scan back to us. Please send us an e-mail or create a new 14 | GitHub issue to request a PDF version of the CLA. 15 | 16 | If you have a trivial fix or improvement, please create a pull request and 17 | request a review from a [maintainer](MAINTAINERS.md) of this repository. 18 | 19 | If you plan to do something more involved, that involves a new feature or 20 | changing functionality, please first create an [issue](#issues) so a discussion of 21 | your idea can happen, avoiding unnecessary work and clarifying implementation. 22 | 23 | A relevant coding style guideline is the [Go Code Review Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments). 24 | 25 | Documentation 26 | ------------- 27 | 28 | If you contribute anything that changes the behavior of the application, 29 | document it in the follow places as applicable: 30 | * the code itself, through clear comments and unit tests 31 | * [README](README.md) 32 | 33 | This includes new features, additional variants of behavior, and breaking 34 | changes. 35 | 36 | Testing 37 | ------- 38 | 39 | Tests are written using golang's standard testing tools, and are run prior to 40 | the PR being accepted. 41 | 42 | Issues 43 | ------ 44 | 45 | For creating an issue: 46 | * **Bugs:** please be as thorough as possible, with steps to recreate the issue 47 | and any other relevant information. 48 | * **Feature Requests:** please include functionality and use cases. If this is 49 | an extension of a current feature, please include whether or not this would 50 | be a breaking change or how to extend the feature with backwards 51 | compatibility. 52 | * **Security Vulnerability:** please report it at 53 | https://my.xfinity.com/vulnerabilityreport and contact the [maintainers](MAINTAINERS.md). 54 | 55 | If you wish to work on an issue, please assign it to yourself. If you have any 56 | questions regarding implementation, feel free to ask clarifying questions on 57 | the issue itself. 58 | 59 | Pull Requests 60 | ------------- 61 | 62 | * should be narrowly focused with no more than 3 or 4 logical commits 63 | * when possible, address no more than one issue 64 | * should be reviewable in the GitHub code review tool 65 | * should be linked to any issues it relates to (i.e. issue number after (#) in commit messages or pull request message) 66 | * should conform to idiomatic golang code formatting 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | golang-money 2 | Copyright 2019 Comcast Cable Communications Management, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | This product includes software developed at Comcast (http://www.comcast.com/). 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | We have adopted OTEL instead. 2 | 3 | # Money 4 | 5 | ## Distributed Tracing using Go 6 | This is the Go implementation of [Money](https://github.com/Comcast/money) 7 | 8 | [![Build Status](https://github.com/xmidt-org/golang-money/actions/workflows/ci.yml/badge.svg)](https://github.com/xmidt-org/golang-money/actions/workflows/ci.yml) 9 | [![codecov.io](http://codecov.io/github/xmidt-org/golang-money/coverage.svg?branch=main)](http://codecov.io/github/xmidt-org/golang-money?branch=main) 10 | [![Go Report Card](https://goreportcard.com/badge/github.com/xmidt-org/golang-money)](https://goreportcard.com/report/github.com/xmidt-org/golang-money) 11 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=xmidt-org_golang-money&metric=alert_status)](https://sonarcloud.io/dashboard?id=xmidt-org_golang-money) 12 | [![Apache V2 License](http://img.shields.io/badge/license-Apache%20V2-blue.svg)](https://github.com/xmidt-org/golang-money/blob/main/LICENSE) 13 | [![GitHub Release](https://img.shields.io/github/release/xmidt-org/golang-money.svg)](CHANGELOG.md) 14 | [![GoDoc](https://pkg.go.dev/badge/github.com/xmidt-org/golang-money)](https://pkg.go.dev/github.com/xmidt-org/golang-money) 15 | 16 | 17 | ### A Money header looks like the following 18 | ``` 19 | Money: trace-id=YourTraceId;parent-id=12345;span-id=12346;span-name=YourSpanName;start-time=2016-02-15T20:30:46.782538292Z;span-duration=3000083865;error-code=200;span-success=true 20 | ``` 21 | 22 | |Span Data |Description | 23 | |------------|--------------------------------| 24 | |spanId |current span's identifier | 25 | |traceId |name for the trace | 26 | |parentId |current span's parent identifier| 27 | |spanName |current span's name | 28 | |startTime |current span's start time | 29 | |spanDuration|current span's duration time | 30 | |errorCode |current span's error code | 31 | |spanSuccess |Was the current span successful | 32 | 33 | ### Functionality to handle the Money header can be added in two ways 34 | #### 1. Decorate you're handlers with the Money handler 35 | ``` 36 | Money.Decorate( [http.Handler], Money.AddToHandler( [spanName] )) 37 | ``` 38 | 39 | #### 2. Use the Money Begin and End functions by adding them to your http.Handler 40 | 41 | ### Start server and make a request that includes a Money header 42 | 43 | The basics to start a Money trace are a trace id name and starting span id number. 44 | ``` 45 | Money:trace-id=YourTraceId;span-id=12345; 46 | ``` 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xmidt-org/golang-money 2 | 3 | go 1.13 4 | 5 | require github.com/stretchr/testify v1.8.0 6 | -------------------------------------------------------------------------------- /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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 9 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 10 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 14 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 15 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | -------------------------------------------------------------------------------- /span.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Comcast Cable Communications Management, LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // End Copyright 16 | 17 | package money 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "fmt" 23 | "strconv" 24 | "time" 25 | ) 26 | 27 | // Span models all the data related to a Span 28 | // It is a superset to what is specified in the 29 | // spec: https://github.com/Comcast/money/wiki#what-is-captured 30 | type Span struct { 31 | //the user gives us these values 32 | Name string 33 | AppName string 34 | TC *TraceContext 35 | Success bool 36 | Code int 37 | Err error 38 | 39 | //we deduce these values 40 | StartTime time.Time 41 | Duration time.Duration 42 | Host string 43 | } 44 | 45 | // Result models the result fields of a span. 46 | type Result struct { 47 | // Name of the Span (i.e HTTPHandler) 48 | Name string 49 | 50 | // Start Time 51 | 52 | // Name of the application/service running the Span (i.e. Scytale in XMiDT) 53 | AppName string 54 | 55 | // StartTime 56 | 57 | // Code is an abstract value which is up to the span code to supply. 58 | // It is not necessary to enforce that this is an HTTP status code. 59 | // The translation into an HTTP status code should take place elsewhere. 60 | Code int 61 | 62 | // Whether or not this span is defined as "successful" 63 | Success bool 64 | 65 | Err error 66 | 67 | StartTime time.Time 68 | 69 | Duration time.Duration 70 | 71 | Host string 72 | } 73 | 74 | // NewSpan returns a new span instance. 75 | func NewSpan(spanName string, tc *TraceContext) Span { 76 | return Span{ 77 | Name: spanName, 78 | TC: tc, 79 | } 80 | } 81 | 82 | type SpanMap map[string]string 83 | 84 | // Changes a maps values to type string. 85 | func mapFieldToString(m map[string]interface{}) SpanMap { 86 | n := make(map[string]string) 87 | 88 | for k, v := range m { 89 | switch v.(type) { 90 | case float64: 91 | if k == "Duration" { 92 | var i = int64(m[k].(float64)) 93 | var d = time.Duration(i).Nanoseconds() 94 | n[k] = fmt.Sprintf("%v"+"ns", d) 95 | } else if k == "Code" { 96 | n[k] = fmt.Sprintf("%.f", m[k].(float64)) 97 | } 98 | case bool: 99 | n[k] = strconv.FormatBool(m[k].(bool)) 100 | case string: 101 | n[k] = m[k].(string) 102 | case map[string]interface{}: 103 | if k == "TC" { 104 | n[k] = typeInferenceTC(m[k]) 105 | } else if k == "Err" { 106 | n[k] = "Error" 107 | } 108 | } 109 | } 110 | 111 | return n 112 | } 113 | 114 | // Map returns a string map representation of the span 115 | func (s *Span) Map() (SpanMap, error) { 116 | var m map[string]interface{} 117 | 118 | // Receive a map of string to objects 119 | r, err := json.Marshal(s) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | json.Unmarshal(r, &m) 125 | 126 | return mapFieldToString(m), nil 127 | } 128 | 129 | // String returns the string representation of the span 130 | func (s *Span) String() string { 131 | var o = new(bytes.Buffer) 132 | 133 | o.WriteString("span-name=" + s.Name) 134 | o.WriteString(";app-name=" + s.AppName) 135 | o.WriteString(";span-duration=" + fmt.Sprintf("%v"+"ns", s.Duration.Nanoseconds())) 136 | o.WriteString(";span-success=" + strconv.FormatBool(s.Success)) 137 | o.WriteString(";" + encodeTraceContext(s.TC)) 138 | o.WriteString(";start-time=" + s.StartTime.Format("2006-01-02T15:04:05.999999999Z07:00")) 139 | 140 | if s.Host != "" { 141 | o.WriteString(";host=" + s.Host) 142 | } 143 | 144 | if s.Code != 0 { 145 | o.WriteString(fmt.Sprintf(";response-code=%v", s.Code)) 146 | } 147 | 148 | if s.Err != nil { 149 | o.WriteString(fmt.Sprintf(";err=%v", s.Err)) 150 | } 151 | 152 | return o.String() 153 | } 154 | -------------------------------------------------------------------------------- /span_test.go: -------------------------------------------------------------------------------- 1 | package money 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | type stubClock struct{} 15 | 16 | func (stubClock) Start() time.Time { return time.Now() } 17 | 18 | func (stubClock) End(t time.Time) time.Duration { return time.Since(t) } 19 | 20 | func createMockTC() *TraceContext { 21 | return &TraceContext{ 22 | TID: "test-trace", 23 | SID: 1, 24 | PID: 1, 25 | } 26 | } 27 | 28 | func createMockSpan() *Span { 29 | var tc = createMockTC() 30 | var s time.Time 31 | var d time.Duration 32 | var err = fmt.Errorf("err") 33 | 34 | return &Span{ 35 | Name: "test-span", 36 | AppName: "test-app", 37 | TC: tc, 38 | Success: true, 39 | Code: 1, 40 | StartTime: s, 41 | Duration: d, 42 | Host: "localhost", 43 | Err: err, 44 | } 45 | } 46 | 47 | func TestNewSpan(t *testing.T) { 48 | var tc = createMockTC() 49 | var expected = Span{ 50 | Name: "test-span", 51 | TC: tc, 52 | } 53 | 54 | assert.Equal(t, expected, NewSpan("test-span", tc)) 55 | } 56 | 57 | func TestMapFieldToString(t *testing.T) { 58 | var s = createMockSpan() 59 | var m map[string]interface{} 60 | 61 | var ourClock stubClock 62 | var startTime = ourClock.Start() 63 | time.Sleep(200000000) 64 | var duration = ourClock.End(startTime) 65 | 66 | s.StartTime, s.Duration = startTime, duration 67 | 68 | r, err := json.Marshal(s) 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | 73 | json.Unmarshal(r, &m) 74 | 75 | var actual = mapFieldToString(m) 76 | 77 | var expected = SpanMap{ 78 | "Name": "test-span", 79 | "AppName": "test-app", 80 | "TC": "parent-id=1;span-id=1;trace-id=test-trace", 81 | "Success": "true", 82 | "Code": "1", 83 | "Duration": fmt.Sprintf("%v"+"ns", duration.Nanoseconds()), 84 | "StartTime": startTime.Format("2006-01-02T15:04:05.999999999Z07:00"), 85 | "Err": "Error", 86 | "Host": "localhost", 87 | } 88 | 89 | // assert.Equal(t, expected, n) 90 | ok := reflect.DeepEqual(expected, actual) 91 | if !ok { 92 | t.Errorf("Expected:\n%v\n\nActual:\n%v", expected, actual) 93 | } 94 | } 95 | 96 | func TestMap(t *testing.T) { 97 | s := createMockSpan() 98 | 99 | var ourClock stubClock 100 | var startTime = ourClock.Start() 101 | time.Sleep(200000000) 102 | var duration = ourClock.End(startTime) 103 | 104 | s.StartTime, s.Duration = startTime, duration 105 | 106 | sm, err := s.Map() 107 | if err != nil { 108 | log.Fatal(err) 109 | } 110 | 111 | var expected = SpanMap{ 112 | "Name": "test-span", 113 | "AppName": "test-app", 114 | "TC": "parent-id=1;span-id=1;trace-id=test-trace", 115 | "Success": "true", 116 | "Code": "1", 117 | "Duration": fmt.Sprintf("%v"+"ns", duration.Nanoseconds()), 118 | "StartTime": startTime.Format("2006-01-02T15:04:05.999999999Z07:00"), 119 | "Err": "Error", 120 | "Host": "localhost", 121 | } 122 | 123 | assert.Equal(t, expected, sm) 124 | } 125 | 126 | func TestString(t *testing.T) { 127 | 128 | var ourClock stubClock 129 | var startTime = ourClock.Start() 130 | time.Sleep(200000000) 131 | var duration = ourClock.End(startTime) 132 | 133 | var startTimeString = startTime.Format("2006-01-02T15:04:05.999999999Z07:00") 134 | var durationString = fmt.Sprintf("%v"+"ns", duration.Nanoseconds()) 135 | 136 | var s = &Span{ 137 | Name: "test-span", 138 | AppName: "test-app", 139 | TC: createMockTC(), 140 | Success: true, 141 | Code: 1, 142 | StartTime: startTime, 143 | Duration: duration, 144 | Host: "localhost", 145 | } 146 | 147 | var expected = "span-name=test-span" + 148 | ";app-name=test-app" + 149 | ";span-duration=" + durationString + 150 | ";span-success=true" + 151 | ";parent-id=1" + 152 | ";span-id=1" + 153 | ";trace-id=test-trace" + 154 | ";start-time=" + startTimeString + 155 | ";host=localhost" + 156 | ";response-code=1" 157 | 158 | assert.Equal(t, s.String(), expected) 159 | } 160 | -------------------------------------------------------------------------------- /spanner.go: -------------------------------------------------------------------------------- 1 | package money 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | // Spanner acts as the factory for spans for all downstream code. 10 | type Spanner interface { 11 | Start(context.Context, Span) Tracker 12 | } 13 | 14 | //SpanDecoder decodes an X-Money span off a request 15 | type SpanDecoder func(*http.Request) (Span, error) 16 | 17 | // HTTPSpanner implements Spanner and is the root factory 18 | // for HTTP spans 19 | type HTTPSpanner struct { 20 | SD SpanDecoder 21 | } 22 | 23 | //Start defines the start time of the input span s and returns 24 | //a tracker object which can both start a child span for s as 25 | //well as mark the end of span s 26 | func (hs *HTTPSpanner) Start(ctx context.Context, s Span) Tracker { 27 | s.StartTime = time.Now() 28 | 29 | return &HTTPTracker{ 30 | span: s, 31 | Spanner: hs, 32 | } 33 | } 34 | 35 | //Decorate provides an Alice-style decorator for handlers 36 | //that wish to use money 37 | func (hs *HTTPSpanner) Decorate(appName string, next http.Handler) http.Handler { 38 | if hs == nil { 39 | // allow DI of nil values to shut off money trace 40 | return next 41 | } 42 | 43 | return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { 44 | if span, err := hs.SD(request); err == nil { 45 | span.AppName, span.Name = appName, "ServeHTTP" 46 | tracker := hs.Start(request.Context(), span) 47 | 48 | ctx := context.WithValue(request.Context(), contextKeyTracker, tracker) 49 | 50 | s := simpleResponseWriter{ 51 | code: http.StatusOK, 52 | ResponseWriter: response, 53 | } 54 | 55 | next.ServeHTTP(s, request.WithContext(ctx)) 56 | 57 | //TODO: application and not library code should finish the above tracker 58 | //such that information on it could be forwarded 59 | //once confirmed, delete the below 60 | 61 | // tracker.Finish(Result{ 62 | // Name: "ServeHTTP", 63 | // AppName: appName, 64 | // Code: s.code, 65 | // Success: s.code < 400, 66 | // }) 67 | 68 | } else { 69 | next.ServeHTTP(response, request) 70 | } 71 | }) 72 | } 73 | 74 | type HTTPSpannerOptions func(*HTTPSpanner) 75 | 76 | func NewHTTPSpanner(options ...HTTPSpannerOptions) (spanner *HTTPSpanner) { 77 | spanner = new(HTTPSpanner) 78 | 79 | //define the default behavior which is a simple 80 | //extraction of money trace context off the headers 81 | //it is overwritten if the options change it 82 | spanner.SD = func(r *http.Request) (s Span, err error) { 83 | var tc *TraceContext 84 | if tc, err = decodeTraceContext(r.Header.Get(MoneyHeader)); err == nil { 85 | s = Span{ 86 | TC: tc, 87 | } 88 | } 89 | return 90 | } 91 | 92 | for _, o := range options { 93 | o(spanner) 94 | } 95 | return 96 | } 97 | 98 | // simpleResponseWriter is the core decorated http.ResponseWriter. 99 | type simpleResponseWriter struct { 100 | http.ResponseWriter 101 | code int 102 | } 103 | 104 | func (rw simpleResponseWriter) WriteHeader(code int) { 105 | rw.code = code 106 | rw.ResponseWriter.WriteHeader(code) 107 | } 108 | -------------------------------------------------------------------------------- /spanner_test.go: -------------------------------------------------------------------------------- 1 | package money 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestNewHTTPSpanner(t *testing.T) { 11 | t.Run("DI", testNewHTTPSpannerNil) 12 | t.Run("Start", testStart) 13 | t.Run("DecorationNoMoneyContext", testDecorate) 14 | t.Run("DecorationMoneyContext", testDecorateWithMoney) 15 | } 16 | 17 | func testNewHTTPSpannerNil(t *testing.T) { 18 | var spanner *HTTPSpanner 19 | if spanner.Decorate("test", nil) != nil { 20 | t.Error("Decoration should leave handler unchanged") 21 | } 22 | } 23 | 24 | func testStart(t *testing.T) { 25 | var spanner = NewHTTPSpanner() 26 | if spanner.Start(context.Background(), Span{}) == nil { 27 | t.Error("was expecting a non-nil response") 28 | } 29 | } 30 | 31 | func testDecorate(t *testing.T) { 32 | var spanner = NewHTTPSpanner() 33 | 34 | handler := http.HandlerFunc( 35 | func(w http.ResponseWriter, r *http.Request) { 36 | _, ok := TrackerFromContext(r.Context()) 37 | if ok { 38 | t.Error("Tracker should not have been injected") 39 | } 40 | }) 41 | decorated := spanner.Decorate("test", handler) 42 | decorated.ServeHTTP(nil, httptest.NewRequest("GET", "localhost:9090/test", nil)) 43 | } 44 | 45 | func testDecorateWithMoney(t *testing.T) { 46 | var spanner = NewHTTPSpanner() 47 | 48 | handler := http.HandlerFunc( 49 | func(w http.ResponseWriter, r *http.Request) { 50 | _, ok := TrackerFromContext(r.Context()) 51 | if !ok { 52 | t.Error("Expected tracker to be present") 53 | } 54 | }) 55 | decorated := spanner.Decorate("test", handler) 56 | inputRequest := httptest.NewRequest("GET", "localhost:9090/test", nil) 57 | inputRequest.Header.Add(MoneyHeader, "trace-id=abc;parent-id=1;span-id=1") 58 | decorated.ServeHTTP(nil, inputRequest) 59 | } 60 | 61 | //create a test that simply finishes the tracker that was started 62 | -------------------------------------------------------------------------------- /trace.go: -------------------------------------------------------------------------------- 1 | package money 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math/rand" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // Trace Context decoding errors 13 | var ( 14 | errPairsCount = errors.New("expecting three pairs in trace context") 15 | errBadPair = errors.New("expected trace context header to have pairs") 16 | errBadTrace = errors.New("malformatted trace context header") 17 | ) 18 | 19 | // TraceContext encapsutes all the core information of any given span 20 | // In a single trace, the TID is the same across all spans and 21 | // the SID and the PID is what links all spans together 22 | type TraceContext struct { 23 | TID string //Trace ID 24 | SID int64 //Span ID 25 | PID int64 //Parent ID 26 | } 27 | 28 | // decodeTraceContext returns a TraceContext from the given value "raw" 29 | // raw is typically taken directly from http.Request headers 30 | // for now, it is overly strict with the expected format 31 | // TODO: could we use regex here instead for simplicity? 32 | func decodeTraceContext(raw string) (tc *TraceContext, err error) { 33 | tc = new(TraceContext) 34 | 35 | pairs := strings.Split(raw, ";") 36 | 37 | if len(pairs) != 3 { 38 | return nil, errPairsCount 39 | } 40 | 41 | seen := make(map[string]bool) 42 | 43 | for _, pair := range pairs { 44 | kv := strings.Split(pair, "=") 45 | 46 | if len(kv) != 2 { 47 | return nil, errBadPair 48 | } 49 | 50 | var k, v = kv[0], kv[1] 51 | 52 | switch { 53 | case k == tIDKey && !seen[k]: 54 | tc.TID, seen[k] = v, true 55 | 56 | case k == sIDKey && !seen[k]: 57 | var pv int64 58 | if pv, err = strconv.ParseInt(v, 10, 64); err != nil { 59 | return nil, err 60 | } 61 | 62 | tc.SID, seen[k] = pv, true 63 | case k == pIDKey && !seen[k]: 64 | var pv int64 65 | if pv, err = strconv.ParseInt(v, 10, 64); err != nil { 66 | return nil, err 67 | } 68 | tc.PID, seen[k] = pv, true 69 | 70 | default: 71 | return nil, errBadTrace 72 | } 73 | } 74 | 75 | return 76 | } 77 | 78 | // typeInferenceTC returns a concatenated string of all field values that exist in a trace context from a map[string]interface{} 79 | func typeInferenceTC(tc interface{}) string { 80 | tcs := tc.(map[string]interface{}) 81 | 82 | m := map[string]string{} 83 | 84 | for k, v := range tcs { 85 | switch v.(type) { 86 | case int: 87 | m[k] = fmt.Sprintf("%v", tcs[k].(int)) 88 | case float64: 89 | m[k] = fmt.Sprintf("%v", tcs[k].(float64)) 90 | case string: 91 | m[k] = tcs[k].(string) 92 | } 93 | } 94 | 95 | return fmt.Sprintf("%s=%v;%s=%v;%s=%v", pIDKey, m["PID"], sIDKey, m["SID"], tIDKey, m["TID"]) 96 | } 97 | 98 | // EncodeTraceContext encodes the TraceContext into a string. 99 | func encodeTraceContext(tc *TraceContext) string { 100 | return fmt.Sprintf("%s=%v;%s=%v;%s=%v", pIDKey, tc.PID, sIDKey, tc.SID, tIDKey, tc.TID) 101 | } 102 | 103 | // This is useful if you want to pass your trace context over an outgoing request or just need a string formatted trace context for any other purpose. 104 | func EncodeTraceContext(tc *TraceContext) string { 105 | return encodeTraceContext(tc) 106 | } 107 | 108 | // SubTrace creates a child trace context for current 109 | func SubTrace(current *TraceContext) *TraceContext { 110 | rand.Seed(time.Now().Unix()) 111 | return &TraceContext{ 112 | PID: current.SID, 113 | SID: rand.Int63(), 114 | TID: current.TID, 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /trace_test.go: -------------------------------------------------------------------------------- 1 | package money 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDecodeTraceContext(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | i string 13 | o *TraceContext 14 | e error 15 | }{ 16 | { 17 | i: "", 18 | o: nil, 19 | e: errPairsCount, 20 | }, 21 | { 22 | name: "ideal", 23 | i: "trace-id=de305d54-75b4-431b-adb2-eb6b9e546013;parent-id=3285573610483682037;span-id=3285573610483682037", 24 | o: &TraceContext{ 25 | PID: 3285573610483682037, 26 | SID: 3285573610483682037, 27 | TID: "de305d54-75b4-431b-adb2-eb6b9e546013", 28 | }, 29 | e: nil, 30 | }, 31 | 32 | { 33 | name: "duplicateEntries", 34 | i: "parent-id=1;parent-id=3285573610483682037;span-id=3285573610483682037", 35 | o: nil, 36 | e: errBadTrace, 37 | }, 38 | { 39 | name: "badPair", 40 | i: "parent-id=de305d54-75b=4-431b-adb2-eb6b9e546013;parent-id=3285573610483682037;span-id=3285573610483682037", 41 | o: nil, 42 | e: errBadPair, 43 | }, 44 | 45 | { 46 | name: "NoRealPairs", 47 | i: "one=1;two=2;three=3", 48 | o: nil, 49 | e: errBadTrace, 50 | }, 51 | } 52 | 53 | for _, test := range tests { 54 | t.Run(test.name, func(t *testing.T) { 55 | assert := assert.New(t) 56 | actualO, actualE := decodeTraceContext(test.i) 57 | assert.Equal(test.e, actualE) 58 | 59 | assert.Equal(test.o, actualO) 60 | }) 61 | } 62 | } 63 | 64 | func TestDecodeTraceContextOtherCases(t *testing.T) { 65 | t.Run("NonIntSID", func(t *testing.T) { 66 | i := "trace-id=de305d54-75b4-431b-adb2-eb6b9e546013;parent-id=3285573610483682037;span-id=NaN" 67 | tc, e := decodeTraceContext(i) 68 | 69 | if tc != nil || e == nil { 70 | t.Errorf("expected tc to be nil and error to be non-nil but got '%v' and '%v'", tc, e) 71 | } 72 | }) 73 | 74 | t.Run("NonIntPID", func(t *testing.T) { 75 | i := "trace-id=de305d54-75b4-431b-adb2-eb6b9e546013;parent-id=NaN;span-id=123" 76 | tc, e := decodeTraceContext(i) 77 | 78 | if tc != nil || e == nil { 79 | t.Errorf("expected tc to be nil and error to be non-nil but got '%v' and '%v'", tc, e) 80 | } 81 | }) 82 | } 83 | 84 | //TODO: need to if a string is in the right order accordance. 85 | // Tests if typeInferenceTC outputs a the fields of a TID in the correct order. 86 | func TestTypeInferenceTC(t *testing.T) { 87 | in := map[string]interface{}{ 88 | "PID": 1, 89 | "SID": 1, 90 | "TID": "one", 91 | } 92 | 93 | var expected = "parent-id=1;span-id=1;trace-id=one" 94 | var actual = typeInferenceTC(in) 95 | 96 | if actual != expected { 97 | t.Errorf("Wrong Format for Trace Context string, need '%v' but got '%v'", expected, actual) 98 | } 99 | } 100 | 101 | func TestEncodeTraceContext(t *testing.T) { 102 | in := &TraceContext{ 103 | PID: 1, 104 | TID: "one", 105 | SID: 1, 106 | } 107 | actual, expected := EncodeTraceContext(in), "parent-id=1;span-id=1;trace-id=one" 108 | 109 | if actual != expected { 110 | t.Errorf("Expected %v but got %v", expected, actual) 111 | } 112 | } 113 | 114 | func TestSubtrace(t *testing.T) { 115 | current := &TraceContext{ 116 | TID: "123", 117 | SID: 1, 118 | } 119 | st := SubTrace(current) 120 | 121 | if st.PID != current.SID { 122 | t.Errorf("Expected pid to be %v but got %v", current.PID, st.PID) 123 | } 124 | 125 | if st.SID == 0 { 126 | t.Error("Expected sid to be defined") 127 | } 128 | 129 | if st.TID != current.TID { 130 | t.Errorf("Expected tid to be %v but got %v", current.TID, st.TID) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tracker.go: -------------------------------------------------------------------------------- 1 | package money 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type contextKey int 12 | 13 | const ( 14 | //contextKeyTracker is the key for child spans management component 15 | contextKeyTracker contextKey = iota 16 | ) 17 | 18 | //Header keys 19 | const ( 20 | MoneyHeader = "X-MoneyTrace" 21 | MoneySpansHeader = "X-MoneySpans" 22 | 23 | //money-trace context keys 24 | tIDKey = "trace-id" 25 | pIDKey = "parent-id" 26 | sIDKey = "span-id" 27 | ) 28 | 29 | //Transactor is an HTTP transactor type 30 | type Transactor func(*http.Request) (*http.Response, error) 31 | 32 | // Tracker is the management interface for an active span. It can be used to create 33 | // child spans and to mark the current span as finished. 34 | type Tracker interface { 35 | Spanner 36 | 37 | // Finish concludes this span with the given result 38 | Finish(Result) 39 | 40 | // String provides the representation of the managed span 41 | String() string 42 | 43 | //DecorateTransactor provides a strategy to inspect transactor arguments and outputs 44 | DecorateTransactor(Transactor, ...SpanForwardingOptions) Transactor 45 | 46 | //Spans returns a list of string-encoded Money spans that have been created under this tracker 47 | Spans() []string 48 | } 49 | 50 | //SpanForwardingOptions allows gathering data from an HTTP response 51 | //into string-encoded golang money spans 52 | //application code is responsible to only inspect the response and if otherwise, put back data 53 | //(i.e if body is read) 54 | //An use case for this is extracting WRP spans into golang money spans 55 | type SpanForwardingOptions func(*http.Response) []string 56 | 57 | //HTTPTracker is the management type for child spans 58 | type HTTPTracker struct { 59 | Spanner 60 | m *sync.RWMutex 61 | span Span 62 | 63 | //spans contains the string-encoded value of all spans created under this tracker 64 | //should be modifiable by multiple goroutines 65 | spans []string 66 | 67 | done bool //indicates whether the span associated with this tracker is finished 68 | } 69 | 70 | //DecorateTransactor configures a transactor to both 71 | //inject Money Trace Context into outgoing requests 72 | //and extract Money Spans from their responses (if any) 73 | func (t *HTTPTracker) DecorateTransactor(transactor Transactor, options ...SpanForwardingOptions) Transactor { 74 | return func(r *http.Request) (resp *http.Response, e error) { 75 | t.m.RLock() 76 | r.Header.Add(MoneyHeader, EncodeTraceContext(t.span.TC)) 77 | t.m.RUnlock() 78 | 79 | if resp, e = transactor(r); e == nil { 80 | t.m.Lock() 81 | defer t.m.Unlock() 82 | 83 | //the default behavior is always run 84 | for k, vs := range resp.Header { 85 | if k == MoneySpansHeader { 86 | t.spans = append(t.spans, vs...) 87 | } 88 | } 89 | 90 | //options allow converting different span types into money-compatible ones 91 | for _, o := range options { 92 | t.spans = append(t.spans, o(resp)...) 93 | } 94 | } 95 | return 96 | } 97 | } 98 | 99 | //Start defines the money trace context for span s based 100 | //on the underlying HTTPTracker span before delegating the 101 | //start process to the Spanner 102 | //if such underlying span has already finished, the returned 103 | //tracker is nil 104 | func (t *HTTPTracker) Start(ctx context.Context, s Span) (tracker Tracker) { 105 | t.m.RLock() 106 | defer t.m.RUnlock() 107 | 108 | if !t.done { 109 | s.TC = SubTrace(t.span.TC) 110 | tracker = t.Spanner.Start(ctx, s) 111 | } 112 | 113 | return 114 | } 115 | 116 | //Finish is an idempotent operation that marks the end of the underlying HTTPTracker span 117 | func (t *HTTPTracker) Finish(r Result) { 118 | t.m.Lock() 119 | defer t.m.Unlock() 120 | 121 | if !t.done { 122 | t.span.Duration = time.Since(t.span.StartTime) 123 | t.span.Host, _ = os.Hostname() 124 | t.span.Name = r.Name 125 | t.span.AppName = r.AppName 126 | t.span.Code = r.Code 127 | t.span.Err = r.Err 128 | t.span.Success = r.Success 129 | 130 | t.spans = append(t.spans, t.span.String()) 131 | 132 | t.done = true 133 | } 134 | } 135 | 136 | //String returns the string representation of the span associated with this 137 | //HTTPTrackertracker once such span has finished, zero value otherwise 138 | func (t *HTTPTracker) String() (v string) { 139 | t.m.RLock() 140 | defer t.m.RUnlock() 141 | 142 | if t.done { 143 | v = t.span.String() 144 | } 145 | 146 | return 147 | } 148 | 149 | //Spans returns the list of string-encoded spans under this tracker 150 | //once the main span under the tracker is finished, zero value otherwise 151 | func (t *HTTPTracker) Spans() (spans []string) { 152 | t.m.RLock() 153 | defer t.m.RUnlock() 154 | 155 | if t.done { 156 | spans = make([]string, len(t.spans)) 157 | copy(spans, t.spans) 158 | } 159 | 160 | return 161 | } 162 | 163 | //TrackerFromContext extracts a tracker contained in the given context, if any 164 | func TrackerFromContext(ctx context.Context) (t Tracker, ok bool) { 165 | t, ok = ctx.Value(contextKeyTracker).(Tracker) 166 | return 167 | } 168 | --------------------------------------------------------------------------------