├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── cmd ├── api.go └── root.go ├── config └── config.go ├── docker-compose.yml ├── docker └── nginx │ ├── Dockerfile │ └── nginx.conf ├── go.mod ├── go.sum ├── main.go ├── metrics ├── grafana │ └── provisioning │ │ ├── dashboards │ │ ├── all.yml │ │ └── go-processes_rev2.json │ │ └── datasources │ │ └── automatic.yml └── prometheus │ └── prometheus.yml └── src ├── api_errors └── api_errors.go ├── cache ├── cache.go └── redis_cache.go ├── context └── context.go ├── handlers ├── healthcheck.go ├── user.go └── user_test.go ├── log ├── logger.go ├── zap-key-value-encoder.go └── zap.go ├── metrics └── prometheus.go ├── middleware ├── assign_request_id.go ├── new_relic.go └── request_log.go ├── models └── user.go ├── mongo └── mongo.go ├── repository ├── user.go ├── user_mock.go └── user_mongo.go └── server ├── middleware.go ├── new_relic.go ├── routes.go └── server.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | #### joe made this: http://goel.io/joe 4 | 5 | .DS_Store 6 | 7 | #####=== Go ===##### 8 | 9 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 10 | *.o 11 | *.a 12 | *.so 13 | 14 | # Folders 15 | _obj 16 | _test 17 | 18 | # Architecture specific extensions/prefixes 19 | *.[568vq] 20 | [568vq].out 21 | 22 | *.cgo1.go 23 | *.cgo2.c 24 | _cgo_defun.c 25 | _cgo_gotypes.go 26 | _cgo_export.* 27 | 28 | _testmain.go 29 | 30 | *.exe 31 | *.test 32 | *.prof 33 | 34 | version.txt 35 | config/tdi.json 36 | *.coverprofile 37 | *.cover 38 | coverage.out 39 | coverage.html 40 | .coverprofile 41 | 42 | # Intellij 43 | .idea/ 44 | 45 | # Eclipse 46 | .project 47 | .settings/ 48 | 49 | # VSCODE 50 | .vscode 51 | 52 | # Vim 53 | *.swp 54 | 55 | # binary 56 | golang_api_skeleton 57 | 58 | # Dependencies 59 | vendor 60 | 61 | .DS_Store 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.7.x 5 | - 1.8.x 6 | - master 7 | 8 | script: 9 | - make test -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ########################### Build Base ########################### 2 | FROM golang:1.23-alpine as build_base 3 | 4 | RUN apk add git make 5 | 6 | # Add the current directory to be build 7 | WORKDIR /app 8 | 9 | ######## Environment variables ######## 10 | # Force the go compiler to use modules 11 | ENV GO111MODULE=on 12 | 13 | # Disable Go proxy 14 | ENV GOPROXY=direct 15 | 16 | ######## Install dependencies ######## 17 | COPY go.mod . 18 | COPY go.sum . 19 | COPY Makefile . 20 | RUN make setup 21 | 22 | ############################# Builder ############################ 23 | FROM build_base AS builder 24 | 25 | COPY . /app 26 | 27 | # Build the image for Linux instances 28 | RUN make build 29 | 30 | ############################# Runner ############################# 31 | FROM alpine:latest 32 | 33 | RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* 34 | RUN update-ca-certificates 35 | 36 | COPY --from=builder /app/golang_api_skeleton /app/golang_api_skeleton 37 | 38 | ENTRYPOINT ["/app/golang_api_skeleton", "api"] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all help run test docker-compose-build-api docker-compose-up-api docker-compose-stop-api 2 | 3 | RELEASE_VERSION := $(shell git rev-parse --short origin/master) 4 | #RELEASE_VERSION := $(shell git describe) # describe last tag 5 | 6 | all: help 7 | 8 | ## help: show this help message 9 | help: Makefile 10 | @echo 11 | @echo " Choose a command to run in "${APP_NAME}":" 12 | @echo 13 | @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' 14 | @echo 15 | 16 | ## setup: get dependencies 17 | setup: 18 | GO111MODULE=on go mod download 19 | 20 | ## build: build the application to linux 21 | build: 22 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build --ldflags="-X 'main.Version=${RELEASE_VERSION}'" -o golang_api_skeleton main.go 23 | 24 | ## test: run unit tests 25 | test: 26 | go test -race -cover -failfast -count=1 ./... 27 | 28 | ## lint: lints the whole application code 29 | lint: 30 | @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ${GOPATH}/bin v1.31.0 31 | @golangci-lint run -E golint -e "(.*Sync|.*buf\.Write)" 32 | 33 | ## docker-compose-build-api: build application docker image 34 | docker-compose-build-api: 35 | @docker-compose build 36 | 37 | ## docker-compose-up-api: up application docker image 38 | docker-compose-up-api: 39 | @docker-compose up 40 | 41 | ## docker-compose-stop-api: stop application docker container 42 | docker-compose-stop-api: 43 | @docker-compose stop 44 | 45 | ## run: run application locally using docker 46 | run: docker-compose-build-api docker-compose-up-api 47 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: ./golang_api_skeleton -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang API Skeleton 2 | > A simple API skeleton written in Go with metrics pre-configurated 3 | 4 | 5 | [![Build Status](https://travis-ci.org/michelaquino/golang_api_skeleton.svg?branch=master)](https://travis-ci.org/michelaquino/golang_api_skeleton) 6 | [![License][license-image]][license-url] 7 | 8 | 9 | ## Includes 10 | - [Zap - Uber Log library](https://github.com/uber-go/zap) 11 | - [Echo Framework](https://github.com/labstack/echo) 12 | - [MongoDB driver](https://github.com/mongodb/mongo-go-driver) 13 | - [Go-Redis](github.com/go-redis/redis) 14 | - [Prometheus](https://github.com/prometheus) 15 | - [Prometheus PushGateway](https://github.com/prometheus/pushgateway) 16 | - [Grafana](https://grafana.com/) 17 | 18 | ## Dependencies 19 | 20 | - Docker 21 | - Docker Compose 22 | 23 | ## Configuration 24 | - Docker Compose 25 | - Nginx with `proxy_pass` preconfigured 26 | - API 27 | - MongoDB 28 | - Redis 29 | - Prometheus 30 | - Prometheus Push Gateway 31 | - Grafana 32 | 33 | ## Run 34 | `make run` 35 | 36 | ## Usage 37 | `curl http://localhost/healthcheck` 38 | 39 | `curl -i -X POST -H 'Content-Type: application/json' -d '{"name": "user name", "email": "user@email.com"}' http://localhost/user` 40 | 41 | ### Metrics 42 | Access: 43 | - http://localhost:3000 to view Grafana metrics pre-configurated 44 | - http://localhost:9090 to view Prometheus server 45 | 46 | [license-image]: https://img.shields.io/badge/License-GPL3.0-blue.svg 47 | [license-url]: LICENSE 48 | [travis-image]: https://img.shields.io/travis/michelaquinoe/golang_api_skeleton/master.svg 49 | [travis-url]: https://travis-ci.org/michelaquino/golang_api_skeleton 50 | -------------------------------------------------------------------------------- /cmd/api.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/michelaquino/golang_api_skeleton/config" 5 | "github.com/spf13/cobra" 6 | 7 | "github.com/michelaquino/golang_api_skeleton/src/log" 8 | "github.com/michelaquino/golang_api_skeleton/src/server" 9 | ) 10 | 11 | var ( 12 | logger = log.GetLogger() 13 | ) 14 | 15 | var apiCmd = &cobra.Command{ 16 | Use: "api", 17 | Short: "Starts API server", 18 | Long: `Starts API server.`, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | config.Init() 21 | server.Start() 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | // rootCmd represents the base command when called without any subcommands 9 | var rootCmd = &cobra.Command{ 10 | Use: "go_api_skeleton", 11 | Short: "go_api_skeleton ", 12 | Long: `Golang API skeleton`, 13 | } 14 | 15 | // Execute adds all child commands to the root command sets flags appropriately. 16 | // This is called by main.main(). It only needs to happen once to the rootCmd. 17 | func Execute() error { 18 | return rootCmd.Execute() 19 | } 20 | 21 | func init() { 22 | cobra.OnInitialize(initConfig) 23 | rootCmd.AddCommand(apiCmd) 24 | } 25 | 26 | // initConfig reads in ENV variables if set. 27 | func initConfig() { 28 | viper.AutomaticEnv() 29 | } 30 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | const ( 10 | logEncodingKeyValue string = "key-value" 11 | logEncodingJSON string = "json" 12 | ) 13 | 14 | // Init application config 15 | func Init() { 16 | viper.AutomaticEnv() 17 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 18 | 19 | viper.SetDefault("api.host.port", 8888) 20 | 21 | viper.SetDefault("log.level", "ERROR") 22 | viper.SetDefault("log.file.name", "") 23 | viper.SetDefault("log.to.file", false) 24 | viper.SetDefault("log.encoding", logEncodingJSON) 25 | 26 | viper.SetDefault("new_relic.is.enabled", false) 27 | viper.SetDefault("new_relic.licence.key", "") 28 | viper.SetDefault("new_relic.proxy.url", "") 29 | 30 | viper.SetDefault("redis.url", "redis") 31 | viper.SetDefault("redis.password", "") 32 | 33 | viper.SetDefault("mongo.uri", "mongodb://mongodb:27017") 34 | } 35 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | nginx: 4 | build: ./docker/nginx 5 | ports: 6 | - 80:80 7 | depends_on: 8 | - api 9 | api: 10 | build: . 11 | depends_on: 12 | - mongodb 13 | - redis 14 | environment: 15 | API_HOST_PORT: 8888 16 | LOG_LEVEL: debug 17 | LOG_FILE_NAME: api.log 18 | LOG_TO_FILE: "false" 19 | NEW_RELIC_IS_ENABLED: "false" 20 | NEW_RELIC_LICENSE_KEY: key 21 | NEW_RELIC_PROXY_URL: http://newrelicproxy.com 22 | MONGO_URL: mongodb 23 | MONGO_PORT: 27017 24 | MONGO_TIMEOUT: 60 25 | MONGO_DATABASE_NAME: api 26 | REDIS_URL: redis 27 | restart: unless-stopped 28 | mongodb: 29 | image: mongo:4 30 | restart: unless-stopped 31 | redis: 32 | image: redis:6 33 | restart: unless-stopped 34 | prometheus: 35 | image: prom/prometheus 36 | container_name: prometheus 37 | volumes: 38 | - ./metrics/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml 39 | command: 40 | - '--config.file=/etc/prometheus/prometheus.yml' 41 | ports: 42 | - 9090:9090 43 | pushgateway: 44 | image: prom/pushgateway 45 | container_name: pushgateway 46 | ports: 47 | - 9091:9091 48 | grafana: 49 | image: grafana/grafana 50 | container_name: grafana 51 | ports: 52 | - 3000:3000 53 | volumes: 54 | - ./metrics/grafana/provisioning:/etc/grafana/provisioning 55 | -------------------------------------------------------------------------------- /docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | # Set nginx base image 2 | FROM nginx:latest 3 | 4 | # Copy custom configuration file from the current directory 5 | COPY nginx.conf /etc/nginx/nginx.conf -------------------------------------------------------------------------------- /docker/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 4; 2 | 3 | events { worker_connections 4096; } 4 | 5 | http { 6 | server { 7 | listen 80; 8 | 9 | location / { 10 | proxy_pass http://api:8888; 11 | 12 | # if ($http_origin ~* "https?://client\.local\.com") { 13 | # set $enable_cors "true"; 14 | # } 15 | 16 | # if ($enable_cors != "true") { 17 | # return 403; 18 | # } 19 | 20 | if ($request_method !~ ^(GET|DELETE|PUT|POST|OPTIONS)$) { 21 | return 405; 22 | } 23 | 24 | if ($request_method = 'OPTIONS') { 25 | add_header 'Access-Control-Allow-Origin' $http_origin always; 26 | add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, If-Modified-Since, Keep-Alive, Authorization' always; 27 | add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; 28 | add_header 'Access-Control-Allow-Credentials' 'true' always; 29 | return 204; 30 | } 31 | 32 | add_header 'Access-Control-Allow-Origin' $http_origin always; 33 | add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, Content-Type, Accept, If-Modified-Since, Keep-Alive, Authorization' always; 34 | add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; 35 | add_header 'Access-Control-Allow-Credentials' 'true' always; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/michelaquino/golang_api_skeleton 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/go-redis/redis/v8 v8.11.5 7 | github.com/google/uuid v1.6.0 8 | github.com/labstack/echo/v4 v4.13.3 9 | github.com/newrelic/go-agent v3.18.0+incompatible 10 | github.com/prometheus/client_golang v1.20.5 11 | github.com/satori/go.uuid v1.2.0 12 | github.com/spf13/cobra v1.8.1 13 | github.com/spf13/viper v1.19.0 14 | github.com/stretchr/testify v1.10.0 15 | go.mongodb.org/mongo-driver v1.17.2 16 | go.uber.org/zap v1.27.0 17 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 18 | ) 19 | 20 | require ( 21 | github.com/beorn7/perks v1.0.1 // indirect 22 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 23 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 24 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 25 | github.com/fsnotify/fsnotify v1.8.0 // indirect 26 | github.com/golang/snappy v0.0.4 // indirect 27 | github.com/hashicorp/hcl v1.0.0 // indirect 28 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 29 | github.com/klauspost/compress v1.17.11 // indirect 30 | github.com/labstack/gommon v0.4.2 // indirect 31 | github.com/magiconair/properties v1.8.9 // indirect 32 | github.com/mattn/go-colorable v0.1.14 // indirect 33 | github.com/mattn/go-isatty v0.0.20 // indirect 34 | github.com/mitchellh/mapstructure v1.5.0 // indirect 35 | github.com/montanaflynn/stats v0.7.1 // indirect 36 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 37 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 38 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 39 | github.com/prometheus/client_model v0.6.1 // indirect 40 | github.com/prometheus/common v0.62.0 // indirect 41 | github.com/prometheus/procfs v0.15.1 // indirect 42 | github.com/sagikazarmark/locafero v0.7.0 // indirect 43 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 44 | github.com/sourcegraph/conc v0.3.0 // indirect 45 | github.com/spf13/afero v1.12.0 // indirect 46 | github.com/spf13/cast v1.7.1 // indirect 47 | github.com/spf13/pflag v1.0.5 // indirect 48 | github.com/stretchr/objx v0.5.2 // indirect 49 | github.com/subosito/gotenv v1.6.0 // indirect 50 | github.com/valyala/bytebufferpool v1.0.0 // indirect 51 | github.com/valyala/fasttemplate v1.2.2 // indirect 52 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 53 | github.com/xdg-go/scram v1.1.2 // indirect 54 | github.com/xdg-go/stringprep v1.0.4 // indirect 55 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect 56 | go.uber.org/multierr v1.11.0 // indirect 57 | golang.org/x/crypto v0.32.0 // indirect 58 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect 59 | golang.org/x/net v0.34.0 // indirect 60 | golang.org/x/sync v0.10.0 // indirect 61 | golang.org/x/sys v0.29.0 // indirect 62 | golang.org/x/text v0.21.0 // indirect 63 | golang.org/x/time v0.9.0 // indirect 64 | google.golang.org/protobuf v1.36.4 // indirect 65 | gopkg.in/ini.v1 v1.67.0 // indirect 66 | gopkg.in/yaml.v3 v3.0.1 // indirect 67 | ) 68 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 10 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 11 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 12 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 13 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 14 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 15 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= 16 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= 17 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 18 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 19 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 20 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 21 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 22 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 23 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 24 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 25 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 26 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 27 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 28 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 29 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 30 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 31 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 32 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 33 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 34 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 35 | github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= 36 | github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= 37 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 38 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 39 | github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= 40 | github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 41 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 42 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 43 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 44 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 45 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 46 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 47 | github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= 48 | github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 49 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 50 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 51 | github.com/newrelic/go-agent v3.18.0+incompatible h1:0MUUHr33A9yIhTZh98JaqWV26iFezRABqicL8MQAYTM= 52 | github.com/newrelic/go-agent v3.18.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= 53 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 54 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 55 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 56 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 57 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= 58 | github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= 59 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 60 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 61 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 62 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 63 | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= 64 | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 65 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 66 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 67 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 68 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 69 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 70 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 71 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 72 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 73 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 74 | github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= 75 | github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= 76 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 77 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 78 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 79 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 80 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 81 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 82 | github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= 83 | github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= 84 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 85 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 86 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 87 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 88 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 89 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 90 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 91 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 92 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 93 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 94 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 95 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 96 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 97 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 98 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 99 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 100 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 101 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 102 | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= 103 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 104 | github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= 105 | github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= 106 | github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= 107 | github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= 108 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= 109 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= 110 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 111 | go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= 112 | go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= 113 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 114 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 115 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 116 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 117 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 118 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 119 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 120 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 121 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 122 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 123 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= 124 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= 125 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 126 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 127 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 128 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 129 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 130 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 131 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 132 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 133 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 134 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 135 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 136 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 138 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 139 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 140 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 142 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 143 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 144 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 145 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 146 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 147 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 148 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 149 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 150 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 151 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 152 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 153 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 154 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 155 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 156 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 157 | google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= 158 | google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 159 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 160 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 161 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 162 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 163 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 164 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= 165 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 166 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 167 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 168 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 169 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 170 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 171 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 172 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/michelaquino/golang_api_skeleton/cmd" 8 | ) 9 | 10 | // Version var is used to retrieve from binary git version of application 11 | var Version = "development" 12 | 13 | func main() { 14 | fmt.Println("Version:\t", Version) 15 | if err := cmd.Execute(); err != nil { 16 | log.Fatalf("cannot start application, an error has ocurred %s", err.Error()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /metrics/grafana/provisioning/dashboards/all.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'prometheus' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | disableDeletion: false 9 | editable: true 10 | options: 11 | path: /etc/grafana/provisioning/dashboards -------------------------------------------------------------------------------- /metrics/grafana/provisioning/dashboards/go-processes_rev2.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS-APL", 5 | "label": "prometheus-apl", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "grafana", 15 | "id": "grafana", 16 | "name": "Grafana", 17 | "version": "5.1.4" 18 | }, 19 | { 20 | "type": "panel", 21 | "id": "graph", 22 | "name": "Graph", 23 | "version": "5.0.0" 24 | }, 25 | { 26 | "type": "datasource", 27 | "id": "prometheus", 28 | "name": "Prometheus", 29 | "version": "5.0.0" 30 | } 31 | ], 32 | "annotations": { 33 | "list": [ 34 | { 35 | "builtIn": 1, 36 | "datasource": "-- Grafana --", 37 | "enable": true, 38 | "hide": true, 39 | "iconColor": "rgba(0, 211, 255, 1)", 40 | "name": "Annotations & Alerts", 41 | "type": "dashboard" 42 | } 43 | ] 44 | }, 45 | "description": "Process status published by Go Prometheus client library, e.g. memory used, fds open, GC details", 46 | "editable": true, 47 | "gnetId": 6671, 48 | "graphTooltip": 0, 49 | "id": null, 50 | "iteration": 1530003369936, 51 | "links": [], 52 | "panels": [ 53 | { 54 | "aliasColors": {}, 55 | "bars": false, 56 | "dashLength": 10, 57 | "dashes": false, 58 | "datasource": "Prometheus", 59 | "editable": true, 60 | "error": false, 61 | "fill": 1, 62 | "grid": {}, 63 | "gridPos": { 64 | "h": 8, 65 | "w": 12, 66 | "x": 0, 67 | "y": 0 68 | }, 69 | "id": 1, 70 | "isNew": true, 71 | "legend": { 72 | "alignAsTable": true, 73 | "avg": true, 74 | "current": true, 75 | "max": true, 76 | "min": false, 77 | "rightSide": false, 78 | "show": true, 79 | "total": false, 80 | "values": true 81 | }, 82 | "lines": true, 83 | "linewidth": 2, 84 | "links": [], 85 | "nullPointMode": "connected", 86 | "percentage": false, 87 | "pointradius": 5, 88 | "points": false, 89 | "renderer": "flot", 90 | "seriesOverrides": [ 91 | { 92 | "alias": "resident", 93 | "yaxis": 2 94 | } 95 | ], 96 | "spaceLength": 10, 97 | "stack": false, 98 | "steppedLine": false, 99 | "targets": [ 100 | { 101 | "expr": "process_resident_memory_bytes{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}", 102 | "format": "time_series", 103 | "intervalFactor": 2, 104 | "legendFormat": "{{pod}} - resident", 105 | "metric": "process_resident_memory_bytes", 106 | "refId": "A", 107 | "step": 4 108 | }, 109 | { 110 | "expr": "process_virtual_memory_bytes{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}", 111 | "format": "time_series", 112 | "intervalFactor": 2, 113 | "legendFormat": "{{pod}} - virtual", 114 | "metric": "process_virtual_memory_bytes", 115 | "refId": "B", 116 | "step": 4 117 | } 118 | ], 119 | "thresholds": [], 120 | "timeFrom": null, 121 | "timeShift": null, 122 | "title": "process memory", 123 | "tooltip": { 124 | "msResolution": false, 125 | "shared": true, 126 | "sort": 0, 127 | "value_type": "cumulative" 128 | }, 129 | "type": "graph", 130 | "xaxis": { 131 | "buckets": null, 132 | "mode": "time", 133 | "name": null, 134 | "show": true, 135 | "values": [] 136 | }, 137 | "yaxes": [ 138 | { 139 | "format": "bytes", 140 | "label": null, 141 | "logBase": 1, 142 | "max": null, 143 | "min": null, 144 | "show": true 145 | }, 146 | { 147 | "format": "short", 148 | "label": null, 149 | "logBase": 1, 150 | "max": null, 151 | "min": null, 152 | "show": true 153 | } 154 | ], 155 | "yaxis": { 156 | "align": false, 157 | "alignLevel": null 158 | } 159 | }, 160 | { 161 | "aliasColors": {}, 162 | "bars": false, 163 | "dashLength": 10, 164 | "dashes": false, 165 | "datasource": "Prometheus", 166 | "editable": true, 167 | "error": false, 168 | "fill": 1, 169 | "grid": {}, 170 | "gridPos": { 171 | "h": 8, 172 | "w": 12, 173 | "x": 12, 174 | "y": 0 175 | }, 176 | "id": 4, 177 | "isNew": true, 178 | "legend": { 179 | "alignAsTable": true, 180 | "avg": true, 181 | "current": true, 182 | "max": true, 183 | "min": false, 184 | "show": true, 185 | "total": false, 186 | "values": true 187 | }, 188 | "lines": true, 189 | "linewidth": 2, 190 | "links": [], 191 | "nullPointMode": "connected", 192 | "percentage": false, 193 | "pointradius": 5, 194 | "points": false, 195 | "renderer": "flot", 196 | "seriesOverrides": [ 197 | { 198 | "alias": "resident", 199 | "yaxis": 1 200 | } 201 | ], 202 | "spaceLength": 10, 203 | "stack": false, 204 | "steppedLine": false, 205 | "targets": [ 206 | { 207 | "expr": "rate(process_resident_memory_bytes{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}[$interval])", 208 | "format": "time_series", 209 | "intervalFactor": 2, 210 | "legendFormat": "{{pod}} - resident", 211 | "metric": "process_resident_memory_bytes", 212 | "refId": "A", 213 | "step": 4 214 | }, 215 | { 216 | "expr": "deriv(process_virtual_memory_bytes{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}[$interval])", 217 | "format": "time_series", 218 | "intervalFactor": 2, 219 | "legendFormat": "{{pod}} - virtual", 220 | "metric": "process_virtual_memory_bytes", 221 | "refId": "B", 222 | "step": 4 223 | } 224 | ], 225 | "thresholds": [], 226 | "timeFrom": null, 227 | "timeShift": null, 228 | "title": "process memory deriv", 229 | "tooltip": { 230 | "msResolution": false, 231 | "shared": true, 232 | "sort": 0, 233 | "value_type": "cumulative" 234 | }, 235 | "type": "graph", 236 | "xaxis": { 237 | "buckets": null, 238 | "mode": "time", 239 | "name": null, 240 | "show": true, 241 | "values": [] 242 | }, 243 | "yaxes": [ 244 | { 245 | "format": "bytes", 246 | "label": null, 247 | "logBase": 1, 248 | "max": null, 249 | "min": null, 250 | "show": true 251 | }, 252 | { 253 | "format": "short", 254 | "label": null, 255 | "logBase": 1, 256 | "max": null, 257 | "min": null, 258 | "show": true 259 | } 260 | ], 261 | "yaxis": { 262 | "align": false, 263 | "alignLevel": null 264 | } 265 | }, 266 | { 267 | "aliasColors": {}, 268 | "bars": false, 269 | "dashLength": 10, 270 | "dashes": false, 271 | "datasource": "Prometheus", 272 | "editable": true, 273 | "error": false, 274 | "fill": 1, 275 | "grid": {}, 276 | "gridPos": { 277 | "h": 7, 278 | "w": 12, 279 | "x": 0, 280 | "y": 8 281 | }, 282 | "id": 2, 283 | "isNew": true, 284 | "legend": { 285 | "alignAsTable": true, 286 | "avg": true, 287 | "current": true, 288 | "max": true, 289 | "min": false, 290 | "show": true, 291 | "total": false, 292 | "values": true 293 | }, 294 | "lines": true, 295 | "linewidth": 2, 296 | "links": [], 297 | "nullPointMode": "connected", 298 | "percentage": false, 299 | "pointradius": 5, 300 | "points": false, 301 | "renderer": "flot", 302 | "seriesOverrides": [ 303 | { 304 | "alias": "alloc rate", 305 | "yaxis": 2 306 | } 307 | ], 308 | "spaceLength": 10, 309 | "stack": false, 310 | "steppedLine": false, 311 | "targets": [ 312 | { 313 | "expr": "go_memstats_alloc_bytes{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}", 314 | "format": "time_series", 315 | "intervalFactor": 2, 316 | "legendFormat": "{{pod}} - bytes allocated", 317 | "metric": "go_memstats_alloc_bytes", 318 | "refId": "A", 319 | "step": 4 320 | }, 321 | { 322 | "expr": "rate(go_memstats_alloc_bytes_total{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}[30s])", 323 | "format": "time_series", 324 | "intervalFactor": 2, 325 | "legendFormat": "{{pod}} - alloc rate", 326 | "metric": "go_memstats_alloc_bytes_total", 327 | "refId": "B", 328 | "step": 4 329 | }, 330 | { 331 | "expr": "go_memstats_stack_inuse_bytes{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}", 332 | "format": "time_series", 333 | "intervalFactor": 2, 334 | "legendFormat": "{{pod}} - stack inuse", 335 | "metric": "go_memstats_stack_inuse_bytes", 336 | "refId": "C", 337 | "step": 4 338 | }, 339 | { 340 | "expr": "go_memstats_heap_inuse_bytes{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}", 341 | "format": "time_series", 342 | "hide": false, 343 | "intervalFactor": 2, 344 | "legendFormat": "{{pod}} - heap inuse", 345 | "metric": "go_memstats_heap_inuse_bytes", 346 | "refId": "D", 347 | "step": 4 348 | } 349 | ], 350 | "thresholds": [], 351 | "timeFrom": null, 352 | "timeShift": null, 353 | "title": "go memstats", 354 | "tooltip": { 355 | "msResolution": false, 356 | "shared": true, 357 | "sort": 0, 358 | "value_type": "cumulative" 359 | }, 360 | "type": "graph", 361 | "xaxis": { 362 | "buckets": null, 363 | "mode": "time", 364 | "name": null, 365 | "show": true, 366 | "values": [] 367 | }, 368 | "yaxes": [ 369 | { 370 | "format": "bytes", 371 | "label": null, 372 | "logBase": 1, 373 | "max": null, 374 | "min": null, 375 | "show": true 376 | }, 377 | { 378 | "format": "Bps", 379 | "label": null, 380 | "logBase": 1, 381 | "max": null, 382 | "min": null, 383 | "show": true 384 | } 385 | ], 386 | "yaxis": { 387 | "align": false, 388 | "alignLevel": null 389 | } 390 | }, 391 | { 392 | "aliasColors": {}, 393 | "bars": false, 394 | "dashLength": 10, 395 | "dashes": false, 396 | "datasource": "Prometheus", 397 | "editable": true, 398 | "error": false, 399 | "fill": 1, 400 | "grid": {}, 401 | "gridPos": { 402 | "h": 7, 403 | "w": 12, 404 | "x": 12, 405 | "y": 8 406 | }, 407 | "id": 5, 408 | "isNew": true, 409 | "legend": { 410 | "alignAsTable": true, 411 | "avg": true, 412 | "current": true, 413 | "max": true, 414 | "min": false, 415 | "show": true, 416 | "total": false, 417 | "values": true 418 | }, 419 | "lines": true, 420 | "linewidth": 2, 421 | "links": [], 422 | "nullPointMode": "connected", 423 | "percentage": false, 424 | "pointradius": 5, 425 | "points": false, 426 | "renderer": "flot", 427 | "seriesOverrides": [ 428 | { 429 | "alias": "alloc rate", 430 | "yaxis": 2 431 | } 432 | ], 433 | "spaceLength": 10, 434 | "stack": false, 435 | "steppedLine": false, 436 | "targets": [ 437 | { 438 | "expr": "deriv(go_memstats_alloc_bytes{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}[$interval])", 439 | "format": "time_series", 440 | "intervalFactor": 2, 441 | "legendFormat": "{{pod}} - bytes allocated", 442 | "metric": "go_memstats_alloc_bytes", 443 | "refId": "A", 444 | "step": 4 445 | }, 446 | { 447 | "expr": "rate(go_memstats_alloc_bytes_total{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}[$interval])", 448 | "format": "time_series", 449 | "intervalFactor": 2, 450 | "legendFormat": "{{pod}} - alloc rate", 451 | "metric": "go_memstats_alloc_bytes_total", 452 | "refId": "B", 453 | "step": 4 454 | }, 455 | { 456 | "expr": "deriv(go_memstats_stack_inuse_bytes{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}[$interval])", 457 | "format": "time_series", 458 | "intervalFactor": 2, 459 | "legendFormat": "{{pod}} - stack inuse", 460 | "metric": "go_memstats_stack_inuse_bytes", 461 | "refId": "C", 462 | "step": 4 463 | }, 464 | { 465 | "expr": "deriv(go_memstats_heap_inuse_bytes{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}[$interval])", 466 | "format": "time_series", 467 | "hide": false, 468 | "intervalFactor": 2, 469 | "legendFormat": "{{pod}} - heap inuse", 470 | "metric": "go_memstats_heap_inuse_bytes", 471 | "refId": "D", 472 | "step": 4 473 | } 474 | ], 475 | "thresholds": [], 476 | "timeFrom": null, 477 | "timeShift": null, 478 | "title": "go memstats deriv", 479 | "tooltip": { 480 | "msResolution": false, 481 | "shared": true, 482 | "sort": 0, 483 | "value_type": "cumulative" 484 | }, 485 | "type": "graph", 486 | "xaxis": { 487 | "buckets": null, 488 | "mode": "time", 489 | "name": null, 490 | "show": true, 491 | "values": [] 492 | }, 493 | "yaxes": [ 494 | { 495 | "format": "bytes", 496 | "label": null, 497 | "logBase": 1, 498 | "max": null, 499 | "min": null, 500 | "show": true 501 | }, 502 | { 503 | "format": "Bps", 504 | "label": null, 505 | "logBase": 1, 506 | "max": null, 507 | "min": null, 508 | "show": true 509 | } 510 | ], 511 | "yaxis": { 512 | "align": false, 513 | "alignLevel": null 514 | } 515 | }, 516 | { 517 | "aliasColors": {}, 518 | "bars": false, 519 | "dashLength": 10, 520 | "dashes": false, 521 | "datasource": "Prometheus", 522 | "editable": true, 523 | "error": false, 524 | "fill": 1, 525 | "grid": {}, 526 | "gridPos": { 527 | "h": 7, 528 | "w": 12, 529 | "x": 0, 530 | "y": 15 531 | }, 532 | "id": 3, 533 | "isNew": true, 534 | "legend": { 535 | "alignAsTable": true, 536 | "avg": true, 537 | "current": true, 538 | "max": true, 539 | "min": false, 540 | "show": true, 541 | "total": false, 542 | "values": true 543 | }, 544 | "lines": true, 545 | "linewidth": 2, 546 | "links": [], 547 | "nullPointMode": "connected", 548 | "percentage": false, 549 | "pointradius": 5, 550 | "points": false, 551 | "renderer": "flot", 552 | "seriesOverrides": [], 553 | "spaceLength": 10, 554 | "stack": false, 555 | "steppedLine": false, 556 | "targets": [ 557 | { 558 | "expr": "process_open_fds{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}", 559 | "format": "time_series", 560 | "intervalFactor": 2, 561 | "legendFormat": "{{pod}}", 562 | "metric": "process_open_fds", 563 | "refId": "A", 564 | "step": 4 565 | } 566 | ], 567 | "thresholds": [], 568 | "timeFrom": null, 569 | "timeShift": null, 570 | "title": "open fds", 571 | "tooltip": { 572 | "msResolution": false, 573 | "shared": true, 574 | "sort": 0, 575 | "value_type": "cumulative" 576 | }, 577 | "type": "graph", 578 | "xaxis": { 579 | "buckets": null, 580 | "mode": "time", 581 | "name": null, 582 | "show": true, 583 | "values": [] 584 | }, 585 | "yaxes": [ 586 | { 587 | "format": "short", 588 | "label": null, 589 | "logBase": 1, 590 | "max": null, 591 | "min": null, 592 | "show": true 593 | }, 594 | { 595 | "format": "short", 596 | "label": null, 597 | "logBase": 1, 598 | "max": null, 599 | "min": null, 600 | "show": true 601 | } 602 | ], 603 | "yaxis": { 604 | "align": false, 605 | "alignLevel": null 606 | } 607 | }, 608 | { 609 | "aliasColors": {}, 610 | "bars": false, 611 | "dashLength": 10, 612 | "dashes": false, 613 | "datasource": "Prometheus", 614 | "editable": true, 615 | "error": false, 616 | "fill": 1, 617 | "grid": {}, 618 | "gridPos": { 619 | "h": 7, 620 | "w": 12, 621 | "x": 12, 622 | "y": 15 623 | }, 624 | "id": 6, 625 | "isNew": true, 626 | "legend": { 627 | "alignAsTable": true, 628 | "avg": true, 629 | "current": true, 630 | "max": true, 631 | "min": false, 632 | "show": true, 633 | "total": false, 634 | "values": true 635 | }, 636 | "lines": true, 637 | "linewidth": 2, 638 | "links": [], 639 | "nullPointMode": "connected", 640 | "percentage": false, 641 | "pointradius": 5, 642 | "points": false, 643 | "renderer": "flot", 644 | "seriesOverrides": [], 645 | "spaceLength": 10, 646 | "stack": false, 647 | "steppedLine": false, 648 | "targets": [ 649 | { 650 | "expr": "deriv(process_open_fds{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}[$interval])", 651 | "format": "time_series", 652 | "intervalFactor": 2, 653 | "legendFormat": "{{pod}}", 654 | "metric": "process_open_fds", 655 | "refId": "A", 656 | "step": 4 657 | } 658 | ], 659 | "thresholds": [], 660 | "timeFrom": null, 661 | "timeShift": null, 662 | "title": "open fds deriv", 663 | "tooltip": { 664 | "msResolution": false, 665 | "shared": true, 666 | "sort": 0, 667 | "value_type": "cumulative" 668 | }, 669 | "type": "graph", 670 | "xaxis": { 671 | "buckets": null, 672 | "mode": "time", 673 | "name": null, 674 | "show": true, 675 | "values": [] 676 | }, 677 | "yaxes": [ 678 | { 679 | "format": "short", 680 | "label": null, 681 | "logBase": 1, 682 | "max": null, 683 | "min": null, 684 | "show": true 685 | }, 686 | { 687 | "format": "short", 688 | "label": null, 689 | "logBase": 1, 690 | "max": null, 691 | "min": null, 692 | "show": true 693 | } 694 | ], 695 | "yaxis": { 696 | "align": false, 697 | "alignLevel": null 698 | } 699 | }, 700 | { 701 | "aliasColors": {}, 702 | "bars": false, 703 | "dashLength": 10, 704 | "dashes": false, 705 | "datasource": "Prometheus", 706 | "editable": true, 707 | "error": false, 708 | "fill": 1, 709 | "grid": {}, 710 | "gridPos": { 711 | "h": 7, 712 | "w": 12, 713 | "x": 0, 714 | "y": 22 715 | }, 716 | "id": 7, 717 | "isNew": true, 718 | "legend": { 719 | "alignAsTable": true, 720 | "avg": true, 721 | "current": true, 722 | "max": true, 723 | "min": false, 724 | "show": true, 725 | "total": false, 726 | "values": true 727 | }, 728 | "lines": true, 729 | "linewidth": 2, 730 | "links": [], 731 | "nullPointMode": "connected", 732 | "percentage": false, 733 | "pointradius": 5, 734 | "points": false, 735 | "renderer": "flot", 736 | "seriesOverrides": [], 737 | "spaceLength": 10, 738 | "stack": false, 739 | "steppedLine": false, 740 | "targets": [ 741 | { 742 | "expr": "go_goroutines{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}", 743 | "format": "time_series", 744 | "intervalFactor": 2, 745 | "legendFormat": "{{pod}}", 746 | "metric": "go_goroutines", 747 | "refId": "A", 748 | "step": 4 749 | } 750 | ], 751 | "thresholds": [], 752 | "timeFrom": null, 753 | "timeShift": null, 754 | "title": "Goroutines", 755 | "tooltip": { 756 | "msResolution": false, 757 | "shared": true, 758 | "sort": 0, 759 | "value_type": "cumulative" 760 | }, 761 | "type": "graph", 762 | "xaxis": { 763 | "buckets": null, 764 | "mode": "time", 765 | "name": null, 766 | "show": true, 767 | "values": [] 768 | }, 769 | "yaxes": [ 770 | { 771 | "format": "short", 772 | "label": null, 773 | "logBase": 1, 774 | "max": null, 775 | "min": null, 776 | "show": true 777 | }, 778 | { 779 | "format": "short", 780 | "label": null, 781 | "logBase": 1, 782 | "max": null, 783 | "min": null, 784 | "show": true 785 | } 786 | ], 787 | "yaxis": { 788 | "align": false, 789 | "alignLevel": null 790 | } 791 | }, 792 | { 793 | "aliasColors": {}, 794 | "bars": false, 795 | "dashLength": 10, 796 | "dashes": false, 797 | "datasource": "Prometheus", 798 | "editable": true, 799 | "error": false, 800 | "fill": 1, 801 | "grid": {}, 802 | "gridPos": { 803 | "h": 7, 804 | "w": 12, 805 | "x": 12, 806 | "y": 22 807 | }, 808 | "id": 8, 809 | "isNew": true, 810 | "legend": { 811 | "alignAsTable": true, 812 | "avg": true, 813 | "current": true, 814 | "max": true, 815 | "min": false, 816 | "show": true, 817 | "total": false, 818 | "values": true 819 | }, 820 | "lines": true, 821 | "linewidth": 2, 822 | "links": [], 823 | "nullPointMode": "connected", 824 | "percentage": false, 825 | "pointradius": 5, 826 | "points": false, 827 | "renderer": "flot", 828 | "seriesOverrides": [], 829 | "spaceLength": 10, 830 | "stack": false, 831 | "steppedLine": false, 832 | "targets": [ 833 | { 834 | "expr": "go_gc_duration_seconds{namespace=~\"^($namespace)$\",pod=~\"^($pod)$\"}", 835 | "format": "time_series", 836 | "intervalFactor": 2, 837 | "legendFormat": "{{pod}}: {{quantile}}", 838 | "metric": "go_gc_duration_seconds", 839 | "refId": "A", 840 | "step": 4 841 | } 842 | ], 843 | "thresholds": [], 844 | "timeFrom": null, 845 | "timeShift": null, 846 | "title": "GC duration quantiles", 847 | "tooltip": { 848 | "msResolution": false, 849 | "shared": true, 850 | "sort": 0, 851 | "value_type": "cumulative" 852 | }, 853 | "type": "graph", 854 | "xaxis": { 855 | "buckets": null, 856 | "mode": "time", 857 | "name": null, 858 | "show": true, 859 | "values": [] 860 | }, 861 | "yaxes": [ 862 | { 863 | "format": "s", 864 | "label": null, 865 | "logBase": 1, 866 | "max": null, 867 | "min": null, 868 | "show": true 869 | }, 870 | { 871 | "format": "short", 872 | "label": null, 873 | "logBase": 1, 874 | "max": null, 875 | "min": null, 876 | "show": true 877 | } 878 | ], 879 | "yaxis": { 880 | "align": false, 881 | "alignLevel": null 882 | } 883 | } 884 | ], 885 | "refresh": "30s", 886 | "schemaVersion": 16, 887 | "style": "dark", 888 | "tags": [], 889 | "templating": { 890 | "list": [ 891 | { 892 | "allValue": ".*", 893 | "current": {}, 894 | "datasource": "Prometheus", 895 | "hide": 0, 896 | "includeAll": true, 897 | "label": null, 898 | "multi": true, 899 | "name": "namespace", 900 | "options": [], 901 | "query": "label_values(go_memstats_alloc_bytes, namespace)", 902 | "refresh": 2, 903 | "regex": "", 904 | "sort": 0, 905 | "tagValuesQuery": "", 906 | "tags": [], 907 | "tagsQuery": "", 908 | "type": "query", 909 | "useTags": false 910 | }, 911 | { 912 | "allValue": ".*", 913 | "current": {}, 914 | "datasource": "Prometheus", 915 | "hide": 0, 916 | "includeAll": true, 917 | "label": null, 918 | "multi": true, 919 | "name": "pod", 920 | "options": [], 921 | "query": "label_values(process_resident_memory_bytes, pod)", 922 | "refresh": 2, 923 | "regex": "", 924 | "sort": 0, 925 | "tagValuesQuery": "", 926 | "tags": [], 927 | "tagsQuery": "", 928 | "type": "query", 929 | "useTags": false 930 | }, 931 | { 932 | "auto": false, 933 | "auto_count": 30, 934 | "auto_min": "10s", 935 | "current": { 936 | "text": "5m", 937 | "value": "5m" 938 | }, 939 | "datasource": null, 940 | "hide": 0, 941 | "includeAll": false, 942 | "label": "", 943 | "multi": false, 944 | "name": "interval", 945 | "options": [ 946 | { 947 | "selected": false, 948 | "text": "1m", 949 | "value": "1m" 950 | }, 951 | { 952 | "selected": true, 953 | "text": "5m", 954 | "value": "5m" 955 | }, 956 | { 957 | "selected": false, 958 | "text": "10m", 959 | "value": "10m" 960 | }, 961 | { 962 | "selected": false, 963 | "text": "30m", 964 | "value": "30m" 965 | }, 966 | { 967 | "selected": false, 968 | "text": "1h", 969 | "value": "1h" 970 | } 971 | ], 972 | "query": "1m,5m,10m,30m,1h", 973 | "refresh": 2, 974 | "type": "interval" 975 | } 976 | ] 977 | }, 978 | "time": { 979 | "from": "now-30m", 980 | "to": "now" 981 | }, 982 | "timepicker": { 983 | "refresh_intervals": [ 984 | "5s", 985 | "10s", 986 | "30s", 987 | "1m", 988 | "5m", 989 | "15m", 990 | "30m", 991 | "1h", 992 | "2h", 993 | "1d" 994 | ], 995 | "time_options": [ 996 | "5m", 997 | "15m", 998 | "1h", 999 | "6h", 1000 | "12h", 1001 | "24h", 1002 | "2d", 1003 | "7d", 1004 | "30d" 1005 | ] 1006 | }, 1007 | "timezone": "browser", 1008 | "title": "Go Processes", 1009 | "uid": "ypFZFgvmz", 1010 | "version": 6 1011 | } -------------------------------------------------------------------------------- /metrics/grafana/provisioning/datasources/automatic.yml: -------------------------------------------------------------------------------- 1 | datasources: 2 | - name: Prometheus 3 | type: prometheus 4 | url: http://prometheus:9090 5 | -------------------------------------------------------------------------------- /metrics/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | 2 | global: 3 | scrape_interval: 10s 4 | 5 | scrape_configs: 6 | - job_name: pushgateway 7 | honor_labels: true 8 | static_configs: 9 | - targets: 10 | - pushgateway:9091 11 | -------------------------------------------------------------------------------- /src/api_errors/api_errors.go: -------------------------------------------------------------------------------- 1 | package apierror 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrUnexpected represents an unexpected error. 7 | ErrUnexpected = errors.New("An unexpected error as occur") 8 | 9 | // ErrNotFoundOnCache represents an error when the key was not found in cache. 10 | ErrNotFoundOnCache = errors.New("Not found on cache") 11 | 12 | // ErrGetCacheValue represents an error when an error occurs when getting cache's value. 13 | ErrGetCacheValue = errors.New("Not found on cache") 14 | ) 15 | -------------------------------------------------------------------------------- /src/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | // Cacher is an interface that represents an object that caches objects. 4 | type Cacher interface { 5 | Get(key string) (string, error) 6 | Set(key, value string, expireInSec int) error 7 | } 8 | -------------------------------------------------------------------------------- /src/cache/redis_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | apierror "github.com/michelaquino/golang_api_skeleton/src/api_errors" 10 | "github.com/michelaquino/golang_api_skeleton/src/log" 11 | 12 | goredis "github.com/go-redis/redis/v8" 13 | ) 14 | 15 | var ( 16 | logger = log.GetLogger() 17 | ) 18 | 19 | // Config has the redis configs 20 | type Config struct { 21 | Name string 22 | Topology string 23 | Host string 24 | Port int 25 | Password string 26 | SentinelMasterName string 27 | PoolSize int 28 | MinIdleConnections int 29 | PoolTimeout time.Duration 30 | ConnectionIdleTimeout time.Duration 31 | DialTimeout time.Duration 32 | ReadTimeout time.Duration 33 | WriteTimeout time.Duration 34 | MaxRetries int 35 | MaxRetryBackoff time.Duration 36 | } 37 | 38 | // RedisCache is a redis cache object. 39 | type RedisCache struct { 40 | client goredis.UniversalClient 41 | } 42 | 43 | // NewRedis instantiate a new redis cluster client 44 | func NewRedis(config Config) *RedisCache { 45 | addresses := config.getAddresses() 46 | var universalClient goredis.UniversalClient 47 | 48 | switch config.Topology { 49 | case "cluster": 50 | universalClient = goredis.NewClusterClient(&goredis.ClusterOptions{ 51 | Addrs: addresses, 52 | Password: config.Password, 53 | 54 | PoolSize: config.PoolSize, 55 | PoolTimeout: config.PoolTimeout, 56 | 57 | MinIdleConns: config.MinIdleConnections, 58 | IdleTimeout: config.ConnectionIdleTimeout, 59 | 60 | DialTimeout: config.DialTimeout, 61 | ReadTimeout: config.ReadTimeout, 62 | WriteTimeout: config.WriteTimeout, 63 | 64 | MaxRetries: config.MaxRetries, 65 | MaxRetryBackoff: config.MaxRetryBackoff, 66 | }) 67 | case "sentinel": 68 | universalClient = goredis.NewFailoverClient(&goredis.FailoverOptions{ 69 | MasterName: config.SentinelMasterName, 70 | 71 | SentinelAddrs: addresses, 72 | Password: config.Password, 73 | 74 | PoolSize: config.PoolSize, 75 | PoolTimeout: config.PoolTimeout, 76 | 77 | DialTimeout: config.DialTimeout, 78 | ReadTimeout: config.ReadTimeout, 79 | WriteTimeout: config.WriteTimeout, 80 | 81 | MaxRetries: config.MaxRetries, 82 | MaxRetryBackoff: config.MaxRetryBackoff, 83 | 84 | MinIdleConns: config.MinIdleConnections, 85 | IdleTimeout: config.ConnectionIdleTimeout, 86 | }) 87 | default: 88 | var redisAddress string 89 | if len(addresses) > 0 { 90 | redisAddress = addresses[0] 91 | } 92 | 93 | universalClient = goredis.NewClient(&goredis.Options{ 94 | Addr: redisAddress, 95 | Password: config.Password, 96 | 97 | PoolSize: config.PoolSize, 98 | PoolTimeout: config.PoolTimeout, 99 | 100 | MinIdleConns: config.MinIdleConnections, 101 | IdleTimeout: config.ConnectionIdleTimeout, 102 | 103 | ReadTimeout: config.ReadTimeout, 104 | WriteTimeout: config.WriteTimeout, 105 | }) 106 | } 107 | 108 | return &RedisCache{client: universalClient} 109 | } 110 | 111 | // Get is a method that gets a value from cache. 112 | func (r RedisCache) Get(ctx context.Context, key string) (string, error) { 113 | cacheValue, err := r.client.Get(ctx, key).Result() 114 | 115 | logAction := fmt.Sprintf("get key %s", key) 116 | if err == goredis.Nil { 117 | logger.Info(ctx, logAction, "", nil) 118 | return "", apierror.ErrNotFoundOnCache 119 | } 120 | 121 | if err != nil { 122 | logger.Error(ctx, logAction, err.Error(), nil) 123 | return "", apierror.ErrGetCacheValue 124 | } 125 | 126 | logger.Info(ctx, logAction, "success", nil) 127 | return cacheValue, nil 128 | } 129 | 130 | // Set is a method that sets a value to cache. 131 | func (r RedisCache) Set(ctx context.Context, key, value string, expireInSec int) error { 132 | expire := time.Duration(expireInSec) * time.Second 133 | 134 | logAction := fmt.Sprintf("get key %s with expiration %d", key, expireInSec) 135 | err := r.client.Set(ctx, key, value, expire).Err() 136 | if err != nil { 137 | logger.Error(ctx, logAction, err.Error(), nil) 138 | return err 139 | } 140 | 141 | logger.Debug(ctx, logAction, "success", nil) 142 | return nil 143 | } 144 | 145 | func (c Config) getAddresses() []string { 146 | addresses := strings.Split(c.Host, ",") 147 | for i, address := range addresses { 148 | addresses[i] = fmt.Sprintf("%s:%d", address, c.Port) 149 | } 150 | 151 | return addresses 152 | } 153 | -------------------------------------------------------------------------------- /src/context/context.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | type httpRequestIDKey string 11 | 12 | const requestIDKey httpRequestIDKey = "X-Request-ID" 13 | 14 | // SetRequestID is a method that adds to context an X-Request-ID value 15 | func SetRequestID(ctx context.Context, reqID string) context.Context { 16 | if reqID == "" { 17 | return ctx 18 | } 19 | 20 | if ctx.Value(requestIDKey) != nil { 21 | return ctx 22 | } 23 | 24 | return context.WithValue(ctx, requestIDKey, reqID) 25 | } 26 | 27 | // GetRequestID returns a requestID value based on context 28 | func GetRequestID(ctx context.Context) string { 29 | if requestID := ctx.Value(requestIDKey); requestID != nil { 30 | return fmt.Sprintf("%s", requestID) 31 | } 32 | 33 | return uuid.New().String() 34 | } 35 | -------------------------------------------------------------------------------- /src/handlers/healthcheck.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo/v4" 7 | "github.com/michelaquino/golang_api_skeleton/src/log" 8 | ) 9 | 10 | var ( 11 | logger = log.GetLogger() 12 | ) 13 | 14 | // Healthcheck is a method that responds only WORKING. 15 | func Healthcheck(echoContext echo.Context) error { 16 | logger.Info(echoContext.Request().Context(), "healthcheck", "success", nil) 17 | return echoContext.String(http.StatusOK, "WORKING") 18 | } 19 | -------------------------------------------------------------------------------- /src/handlers/user.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo/v4" 7 | "github.com/michelaquino/golang_api_skeleton/src/models" 8 | "github.com/michelaquino/golang_api_skeleton/src/repository" 9 | ) 10 | 11 | // UserHandler is a struct that stores an userRepository. 12 | type UserHandler struct { 13 | userRepository repository.UserRepository 14 | } 15 | 16 | // NewUserHandler returns a new pointer of user's struct. 17 | func NewUserHandler(userRepository repository.UserRepository) *UserHandler { 18 | return &UserHandler{ 19 | userRepository: userRepository, 20 | } 21 | } 22 | 23 | // CreateUser is a handler that creates a new user into database. 24 | func (h UserHandler) CreateUser(echoContext echo.Context) error { 25 | userModel := models.UserModel{} 26 | if err := echoContext.Bind(&userModel); err != nil { 27 | logger.Error(echoContext.Request().Context(), "bind payload to model", err.Error(), nil) 28 | return echoContext.NoContent(http.StatusBadRequest) 29 | } 30 | 31 | if err := h.userRepository.Insert(echoContext.Request().Context(), userModel); err != nil { 32 | logger.Error(echoContext.Request().Context(), "create user", err.Error(), nil) 33 | return echoContext.NoContent(http.StatusInternalServerError) 34 | } 35 | 36 | logger.Info(echoContext.Request().Context(), "create user", "success", nil) 37 | return echoContext.NoContent(http.StatusCreated) 38 | } 39 | -------------------------------------------------------------------------------- /src/handlers/user_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/michelaquino/golang_api_skeleton/src/repository" 13 | 14 | "github.com/labstack/echo/v4" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/mock" 17 | ) 18 | 19 | var ( 20 | userRepositoryMock *repository.UserRepositoryMock 21 | userHandler *UserHandler 22 | serverMock *httptest.Server 23 | ) 24 | 25 | func setupUserHandlerTest(t *testing.T) { 26 | userRepositoryMock = &repository.UserRepositoryMock{} 27 | userHandler = NewUserHandler(userRepositoryMock) 28 | 29 | serverMock = httptest.NewServer( 30 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | fmt.Fprintln(w, "response") 32 | })) 33 | } 34 | 35 | func Test_CreateUser_ShouldReturnStatusInternalServerErrorWhenRepositoryReturnError(t *testing.T) { 36 | setupUserHandlerTest(t) 37 | 38 | bodyPayload := strings.NewReader(`{"name": "123456", "email": "89978"}`) 39 | recorder, echoContext := getTestBaseObjects(bodyPayload) 40 | 41 | userRepositoryMock.On("Insert", mock.Anything, mock.Anything).Return(errors.New("Unexpected error")) 42 | 43 | userHandler.CreateUser(echoContext) 44 | assert.Equal(t, http.StatusInternalServerError, recorder.Code) 45 | } 46 | 47 | func Test_CreateUser_ShouldReturnStatusBadRequestWhenBindPayloadToModel(t *testing.T) { 48 | setupUserHandlerTest(t) 49 | 50 | invalidBodyPayload := strings.NewReader(`invalid json`) 51 | recorder, echoContext := getTestBaseObjects(invalidBodyPayload) 52 | 53 | userRepositoryMock.On("Insert", mock.Anything, mock.Anything).Return(nil) 54 | 55 | userHandler.CreateUser(echoContext) 56 | assert.Equal(t, http.StatusBadRequest, recorder.Code) 57 | } 58 | 59 | func Test_CreateUser_ShouldReturnStatusCreated(t *testing.T) { 60 | setupUserHandlerTest(t) 61 | 62 | bodyPayload := strings.NewReader(`{"name": "123456", "email": "89978"}`) 63 | recorder, echoContext := getTestBaseObjects(bodyPayload) 64 | 65 | userRepositoryMock.On("Insert", mock.Anything, mock.Anything).Return(nil) 66 | 67 | userHandler.CreateUser(echoContext) 68 | assert.Equal(t, http.StatusCreated, recorder.Code) 69 | } 70 | 71 | func getTestBaseObjects(body io.Reader) (*httptest.ResponseRecorder, echo.Context) { 72 | recorder := httptest.NewRecorder() 73 | request := httptest.NewRequest(http.MethodPost, serverMock.URL, body) 74 | request.Header.Add("Content-Type", "application/json") 75 | 76 | echoInstance := echo.New() 77 | echoContext := echoInstance.NewContext(request, recorder) 78 | 79 | echoContext.SetRequest(request) 80 | return recorder, echoContext 81 | } 82 | -------------------------------------------------------------------------------- /src/log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | // Logger is a interface to log object 11 | type Logger interface { 12 | // Debug write a debug log level 13 | Debug(context context.Context, action string, message string, extraFields map[string]string) 14 | 15 | // Info write a info log level 16 | Info(context context.Context, action string, message string, extraFields map[string]string) 17 | 18 | // Warn write a warning log level 19 | Warn(context context.Context, action string, message string, extraFields map[string]string) 20 | 21 | // Error write a error log level 22 | Error(context context.Context, action string, message string, extraFields map[string]string) 23 | 24 | // Fatal write a fatal log level. The logger then calls os.Exit(1), even if logging at Fatal level is disabled. 25 | Fatal(context context.Context, action string, message string, extraFields map[string]string) 26 | 27 | // Panic write a Panic log level. Then panics then panic, even if logging at Panic level is disabled. 28 | Panic(context context.Context, action string, message string, extraFields map[string]string) 29 | } 30 | 31 | var appLog *zapLog 32 | var onceLog sync.Once 33 | 34 | // GetLogger return a new instance of the log for the application 35 | func GetLogger() Logger { 36 | onceLog.Do(func() { 37 | encoding := viper.GetString("log.encoding") 38 | if encoding == "" { 39 | encoding = jsonEncoding 40 | } 41 | 42 | appLog = newZapLog(encoding) 43 | }) 44 | 45 | return appLog 46 | } 47 | -------------------------------------------------------------------------------- /src/log/zap-key-value-encoder.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "math" 7 | "sync" 8 | "time" 9 | "unicode/utf8" 10 | 11 | "go.uber.org/zap/buffer" 12 | "go.uber.org/zap/zapcore" 13 | ) 14 | 15 | const _hex = "0123456789abcdef" 16 | 17 | var bufferPool = buffer.NewPool() 18 | 19 | var _kvPool = sync.Pool{New: func() interface{} { 20 | return &keyValueEncoder{} 21 | }} 22 | 23 | func getKeyValueEncoder() *keyValueEncoder { 24 | return _kvPool.Get().(*keyValueEncoder) 25 | } 26 | 27 | func putKeyValueEncoder(enc *keyValueEncoder) { 28 | enc.EncoderConfig = nil 29 | enc.buf = nil 30 | _kvPool.Put(enc) 31 | } 32 | 33 | type keyValueEncoder struct { 34 | *zapcore.EncoderConfig 35 | buf *buffer.Buffer 36 | } 37 | 38 | // NewKeyValueEncoder creates a key=value encoder 39 | func NewKeyValueEncoder(cfg zapcore.EncoderConfig) zapcore.Encoder { 40 | return &keyValueEncoder{ 41 | EncoderConfig: &cfg, 42 | buf: bufferPool.Get(), 43 | } 44 | } 45 | 46 | func (enc *keyValueEncoder) AddArray(key string, arr zapcore.ArrayMarshaler) error { 47 | enc.addKey(key) 48 | return enc.AppendArray(arr) 49 | } 50 | 51 | func (enc *keyValueEncoder) AddObject(key string, obj zapcore.ObjectMarshaler) error { 52 | enc.addKey(key) 53 | return enc.AppendObject(obj) 54 | } 55 | 56 | func (enc *keyValueEncoder) AddBinary(key string, val []byte) { 57 | enc.AddString(key, base64.StdEncoding.EncodeToString(val)) 58 | } 59 | 60 | func (enc *keyValueEncoder) AddByteString(key string, val []byte) { 61 | enc.addKey(key) 62 | enc.AppendByteString(val) 63 | } 64 | 65 | func (enc *keyValueEncoder) AddBool(key string, val bool) { 66 | enc.addKey(key) 67 | enc.AppendBool(val) 68 | } 69 | 70 | func (enc *keyValueEncoder) AddComplex128(key string, val complex128) { 71 | enc.addKey(key) 72 | enc.AppendComplex128(val) 73 | } 74 | 75 | func (enc *keyValueEncoder) AddDuration(key string, val time.Duration) { 76 | enc.addKey(key) 77 | enc.AppendDuration(val) 78 | } 79 | 80 | func (enc *keyValueEncoder) AddFloat64(key string, val float64) { 81 | enc.addKey(key) 82 | enc.AppendFloat64(val) 83 | } 84 | 85 | func (enc *keyValueEncoder) AddInt64(key string, val int64) { 86 | enc.addKey(key) 87 | enc.AppendInt64(val) 88 | } 89 | 90 | func (enc *keyValueEncoder) AddReflected(key string, obj interface{}) error { 91 | marshaled, err := json.Marshal(obj) 92 | if err != nil { 93 | return err 94 | } 95 | enc.addKey(key) 96 | _, err = enc.buf.Write(marshaled) 97 | return err 98 | } 99 | 100 | func (enc *keyValueEncoder) OpenNamespace(key string) { 101 | } 102 | 103 | func (enc *keyValueEncoder) AddString(key, val string) { 104 | enc.addKey(key) 105 | enc.AppendString(val) 106 | } 107 | 108 | func (enc *keyValueEncoder) AddTime(key string, val time.Time) { 109 | enc.addKey(key) 110 | enc.AppendTime(val) 111 | } 112 | 113 | func (enc *keyValueEncoder) AddUint64(key string, val uint64) { 114 | enc.addKey(key) 115 | enc.AppendUint64(val) 116 | } 117 | 118 | func (enc *keyValueEncoder) AppendArray(arr zapcore.ArrayMarshaler) error { 119 | return arr.MarshalLogArray(enc) 120 | } 121 | 122 | func (enc *keyValueEncoder) AppendObject(obj zapcore.ObjectMarshaler) error { 123 | return obj.MarshalLogObject(enc) 124 | } 125 | 126 | func (enc *keyValueEncoder) AppendBool(val bool) { 127 | enc.buf.AppendBool(val) 128 | } 129 | 130 | func (enc *keyValueEncoder) AppendByteString(val []byte) { 131 | enc.safeAddByteString(val) 132 | } 133 | 134 | func (enc *keyValueEncoder) AppendComplex128(val complex128) { 135 | // Cast to a platform-independent, fixed-size type. 136 | r, i := float64(real(val)), float64(imag(val)) 137 | enc.buf.AppendByte('"') 138 | // Because we're always in a quoted string, we can use strconv without 139 | // special-casing NaN and +/-Inf. 140 | enc.buf.AppendFloat(r, 64) 141 | enc.buf.AppendByte('+') 142 | enc.buf.AppendFloat(i, 64) 143 | enc.buf.AppendByte('i') 144 | enc.buf.AppendByte('"') 145 | } 146 | 147 | func (enc *keyValueEncoder) AppendDuration(val time.Duration) { 148 | cur := enc.buf.Len() 149 | enc.EncodeDuration(val, enc) 150 | if cur == enc.buf.Len() { 151 | // User-supplied EncodeDuration is a no-op. Fall back to nanoseconds to keep 152 | // JSON valid. 153 | enc.AppendInt64(int64(val)) 154 | } 155 | } 156 | 157 | func (enc *keyValueEncoder) AppendInt64(val int64) { 158 | enc.buf.AppendInt(val) 159 | } 160 | 161 | func (enc *keyValueEncoder) AppendReflected(val interface{}) error { 162 | marshaled, err := json.Marshal(val) 163 | if err != nil { 164 | return err 165 | } 166 | _, err = enc.buf.Write(marshaled) 167 | return err 168 | } 169 | 170 | func (enc *keyValueEncoder) AppendString(val string) { 171 | enc.safeAddString(val) 172 | } 173 | 174 | func (enc *keyValueEncoder) AppendTime(val time.Time) { 175 | cur := enc.buf.Len() 176 | enc.EncodeTime(val, enc) 177 | if cur == enc.buf.Len() { 178 | // User-supplied EncodeTime is a no-op. Fall back to nanos since epoch to keep 179 | // output JSON valid. 180 | enc.AppendInt64(val.UnixNano()) 181 | } 182 | } 183 | 184 | func (enc *keyValueEncoder) AppendUint64(val uint64) { 185 | enc.buf.AppendUint(val) 186 | } 187 | 188 | func (enc *keyValueEncoder) AddComplex64(k string, v complex64) { enc.AddComplex128(k, complex128(v)) } 189 | func (enc *keyValueEncoder) AddFloat32(k string, v float32) { enc.AddFloat64(k, float64(v)) } 190 | func (enc *keyValueEncoder) AddInt(k string, v int) { enc.AddInt64(k, int64(v)) } 191 | func (enc *keyValueEncoder) AddInt32(k string, v int32) { enc.AddInt64(k, int64(v)) } 192 | func (enc *keyValueEncoder) AddInt16(k string, v int16) { enc.AddInt64(k, int64(v)) } 193 | func (enc *keyValueEncoder) AddInt8(k string, v int8) { enc.AddInt64(k, int64(v)) } 194 | func (enc *keyValueEncoder) AddUint(k string, v uint) { enc.AddUint64(k, uint64(v)) } 195 | func (enc *keyValueEncoder) AddUint32(k string, v uint32) { enc.AddUint64(k, uint64(v)) } 196 | func (enc *keyValueEncoder) AddUint16(k string, v uint16) { enc.AddUint64(k, uint64(v)) } 197 | func (enc *keyValueEncoder) AddUint8(k string, v uint8) { enc.AddUint64(k, uint64(v)) } 198 | func (enc *keyValueEncoder) AddUintptr(k string, v uintptr) { enc.AddUint64(k, uint64(v)) } 199 | func (enc *keyValueEncoder) AppendComplex64(v complex64) { enc.AppendComplex128(complex128(v)) } 200 | func (enc *keyValueEncoder) AppendFloat64(v float64) { enc.appendFloat(v, 64) } 201 | func (enc *keyValueEncoder) AppendFloat32(v float32) { enc.appendFloat(float64(v), 32) } 202 | func (enc *keyValueEncoder) AppendInt(v int) { enc.AppendInt64(int64(v)) } 203 | func (enc *keyValueEncoder) AppendInt32(v int32) { enc.AppendInt64(int64(v)) } 204 | func (enc *keyValueEncoder) AppendInt16(v int16) { enc.AppendInt64(int64(v)) } 205 | func (enc *keyValueEncoder) AppendInt8(v int8) { enc.AppendInt64(int64(v)) } 206 | func (enc *keyValueEncoder) AppendUint(v uint) { enc.AppendUint64(uint64(v)) } 207 | func (enc *keyValueEncoder) AppendUint32(v uint32) { enc.AppendUint64(uint64(v)) } 208 | func (enc *keyValueEncoder) AppendUint16(v uint16) { enc.AppendUint64(uint64(v)) } 209 | func (enc *keyValueEncoder) AppendUint8(v uint8) { enc.AppendUint64(uint64(v)) } 210 | func (enc *keyValueEncoder) AppendUintptr(v uintptr) { enc.AppendUint64(uint64(v)) } 211 | 212 | func (enc *keyValueEncoder) Clone() zapcore.Encoder { 213 | clone := enc.clone() 214 | clone.buf.Write(enc.buf.Bytes()) 215 | return clone 216 | } 217 | 218 | func (enc *keyValueEncoder) clone() *keyValueEncoder { 219 | clone := getKeyValueEncoder() 220 | clone.EncoderConfig = enc.EncoderConfig 221 | clone.buf = bufferPool.Get() 222 | return clone 223 | } 224 | 225 | func (enc *keyValueEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { 226 | final := enc.clone() 227 | 228 | if final.LevelKey != "" { 229 | final.addKey(final.LevelKey) 230 | cur := final.buf.Len() 231 | final.EncodeLevel(ent.Level, final) 232 | if cur == final.buf.Len() { 233 | final.AppendString(ent.Level.String()) 234 | } 235 | final.addElementSeparator() 236 | } 237 | if final.TimeKey != "" { 238 | final.AddTime(final.TimeKey, ent.Time) 239 | final.addElementSeparator() 240 | } 241 | if ent.LoggerName != "" && final.NameKey != "" { 242 | final.addKey(final.NameKey) 243 | cur := final.buf.Len() 244 | nameEncoder := final.EncodeName 245 | 246 | // if no name encoder provided, fall back to FullNameEncoder for backwards 247 | // compatibility 248 | if nameEncoder == nil { 249 | nameEncoder = zapcore.FullNameEncoder 250 | } 251 | 252 | nameEncoder(ent.LoggerName, final) 253 | if cur == final.buf.Len() { 254 | // User-supplied EncodeName was a no-op. Fall back to strings to 255 | // keep output valid. 256 | final.AppendString(ent.LoggerName) 257 | } 258 | final.addElementSeparator() 259 | } 260 | if ent.Caller.Defined && final.CallerKey != "" { 261 | final.addKey(final.CallerKey) 262 | cur := final.buf.Len() 263 | final.EncodeCaller(ent.Caller, final) 264 | if cur == final.buf.Len() { 265 | // User-supplied EncodeCaller was a no-op. Fall back to strings to 266 | // keep JSON valid. 267 | final.AppendString(ent.Caller.String()) 268 | } 269 | final.addElementSeparator() 270 | } 271 | if final.MessageKey != "" { 272 | final.addKey(enc.MessageKey) 273 | final.buf.AppendByte('"') 274 | final.AppendString(ent.Message) 275 | final.buf.AppendByte('"') 276 | final.addElementSeparator() 277 | } 278 | if enc.buf.Len() > 0 { 279 | final.buf.Write(enc.buf.Bytes()) 280 | } 281 | addFields(final, final, fields) 282 | final.addElementSeparator() 283 | if ent.Stack != "" && final.StacktraceKey != "" { 284 | final.AddString(final.StacktraceKey, ent.Stack) 285 | final.addElementSeparator() 286 | } 287 | if final.LineEnding != "" { 288 | final.buf.AppendString(final.LineEnding) 289 | } else { 290 | final.buf.AppendString(zapcore.DefaultLineEnding) 291 | } 292 | 293 | ret := final.buf 294 | putKeyValueEncoder(final) 295 | return ret, nil 296 | } 297 | 298 | func (enc *keyValueEncoder) addKey(key string) { 299 | enc.buf.AppendString(key) 300 | enc.buf.AppendByte('=') 301 | } 302 | 303 | func (enc *keyValueEncoder) addElementSeparator() { 304 | enc.buf.AppendByte(' ') 305 | } 306 | 307 | func (enc *keyValueEncoder) appendFloat(val float64, bitSize int) { 308 | switch { 309 | case math.IsNaN(val): 310 | enc.buf.AppendString(`"NaN"`) 311 | case math.IsInf(val, 1): 312 | enc.buf.AppendString(`"+Inf"`) 313 | case math.IsInf(val, -1): 314 | enc.buf.AppendString(`"-Inf"`) 315 | default: 316 | enc.buf.AppendFloat(val, bitSize) 317 | } 318 | } 319 | 320 | // safeAddString JSON-escapes a string and appends it to the internal buffer. 321 | // Unlike the standard library's encoder, it doesn't attempt to protect the 322 | // user from browser vulnerabilities or JSONP-related problems. 323 | func (enc *keyValueEncoder) safeAddString(s string) { 324 | for i := 0; i < len(s); { 325 | if enc.tryAddRuneSelf(s[i]) { 326 | i++ 327 | continue 328 | } 329 | r, size := utf8.DecodeRuneInString(s[i:]) 330 | if enc.tryAddRuneError(r, size) { 331 | i++ 332 | continue 333 | } 334 | enc.buf.AppendString(s[i : i+size]) 335 | i += size 336 | } 337 | } 338 | 339 | // safeAddByteString is no-alloc equivalent of safeAddString(string(s)) for s []byte. 340 | func (enc *keyValueEncoder) safeAddByteString(s []byte) { 341 | for i := 0; i < len(s); { 342 | if enc.tryAddRuneSelf(s[i]) { 343 | i++ 344 | continue 345 | } 346 | r, size := utf8.DecodeRune(s[i:]) 347 | if enc.tryAddRuneError(r, size) { 348 | i++ 349 | continue 350 | } 351 | enc.buf.Write(s[i : i+size]) 352 | i += size 353 | } 354 | } 355 | 356 | // tryAddRuneSelf appends b if it is valid UTF-8 character represented in a single byte. 357 | func (enc *keyValueEncoder) tryAddRuneSelf(b byte) bool { 358 | if b >= utf8.RuneSelf { 359 | return false 360 | } 361 | if 0x20 <= b && b != '\\' && b != '"' { 362 | enc.buf.AppendByte(b) 363 | return true 364 | } 365 | switch b { 366 | case '\\', '"': 367 | enc.buf.AppendByte('\\') 368 | enc.buf.AppendByte(b) 369 | case '\n': 370 | enc.buf.AppendByte('\\') 371 | enc.buf.AppendByte('n') 372 | case '\r': 373 | enc.buf.AppendByte('\\') 374 | enc.buf.AppendByte('r') 375 | case '\t': 376 | enc.buf.AppendByte('\\') 377 | enc.buf.AppendByte('t') 378 | default: 379 | // Encode bytes < 0x20, except for the escape sequences above. 380 | enc.buf.AppendString(`\u00`) 381 | enc.buf.AppendByte(_hex[b>>4]) 382 | enc.buf.AppendByte(_hex[b&0xF]) 383 | } 384 | return true 385 | } 386 | 387 | func (enc *keyValueEncoder) tryAddRuneError(r rune, size int) bool { 388 | if r == utf8.RuneError && size == 1 { 389 | enc.buf.AppendString(`\ufffd`) 390 | return true 391 | } 392 | return false 393 | } 394 | 395 | func addFields(kvEnc *keyValueEncoder, enc zapcore.ObjectEncoder, fields []zapcore.Field) { 396 | for i := range fields { 397 | fields[i].AddTo(enc) 398 | kvEnc.buf.AppendByte(' ') 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /src/log/zap.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | appContext "github.com/michelaquino/golang_api_skeleton/src/context" 11 | "go.uber.org/zap" 12 | "go.uber.org/zap/zapcore" 13 | ) 14 | 15 | const ( 16 | keyValueEncoding string = "key-value" 17 | jsonEncoding string = "json" 18 | ) 19 | 20 | // zapLog is the API logger 21 | type zapLog struct { 22 | logger *zap.Logger 23 | } 24 | 25 | func newZapLog(encoding string) *zapLog { 26 | err := zap.RegisterEncoder(keyValueEncoding, func(config zapcore.EncoderConfig) (zapcore.Encoder, error) { 27 | return NewKeyValueEncoder(config), nil 28 | }) 29 | 30 | if err != nil && encoding == keyValueEncoding { 31 | encoding = jsonEncoding 32 | } 33 | 34 | config := getConfig(encoding) 35 | logger, err := config.Build(zap.AddCallerSkip(1)) 36 | if err != nil { 37 | panic(fmt.Sprintf("can't initialize zap logger: %s", err.Error())) 38 | } 39 | 40 | return &zapLog{ 41 | logger: logger, 42 | } 43 | } 44 | 45 | // Debug write a debug log level 46 | func (z zapLog) Debug(ctx context.Context, action string, message string, extraFields map[string]string) { 47 | defer z.logger.Sync() 48 | fields := z.getExtraFields(ctx, action, extraFields) 49 | z.logger.Debug(message, fields...) 50 | } 51 | 52 | // Info write a info log level 53 | func (z zapLog) Info(ctx context.Context, action string, message string, extraFields map[string]string) { 54 | defer z.logger.Sync() 55 | fields := z.getExtraFields(ctx, action, extraFields) 56 | z.logger.Info(message, fields...) 57 | } 58 | 59 | // Warn write a warning log level 60 | func (z zapLog) Warn(ctx context.Context, action string, message string, extraFields map[string]string) { 61 | defer z.logger.Sync() 62 | fields := z.getExtraFields(ctx, action, extraFields) 63 | z.logger.Warn(message, fields...) 64 | } 65 | 66 | // Error write a error log level 67 | func (z zapLog) Error(ctx context.Context, action string, message string, extraFields map[string]string) { 68 | defer z.logger.Sync() 69 | fields := z.getExtraFields(ctx, action, extraFields) 70 | z.logger.Error(message, fields...) 71 | } 72 | 73 | // Fatal write a fatal log level. The logger then calls os.Exit(1), even if logging at Fatal level is disabled. 74 | func (z zapLog) Fatal(ctx context.Context, action string, message string, extraFields map[string]string) { 75 | defer z.logger.Sync() 76 | fields := z.getExtraFields(ctx, action, extraFields) 77 | z.logger.Fatal(message, fields...) 78 | } 79 | 80 | // Panic write a Panic log level. Then panics then panic, even if logging at Panic level is disabled. 81 | func (z zapLog) Panic(ctx context.Context, action string, message string, extraFields map[string]string) { 82 | defer z.logger.Sync() 83 | fields := z.getExtraFields(ctx, action, extraFields) 84 | z.logger.Panic(message, fields...) 85 | } 86 | 87 | // getExtraFields return a zap fields instance 88 | func (z zapLog) getExtraFields(ctx context.Context, action string, extraFields map[string]string) []zapcore.Field { 89 | zapFields := []zapcore.Field{} 90 | for key, value := range extraFields { 91 | zapFields = append(zapFields, zap.String(key, value)) 92 | } 93 | 94 | requestID := appContext.GetRequestID(ctx) 95 | zapFields = append(zapFields, zap.String("request_id", requestID), zap.String("action", action)) 96 | return zapFields 97 | } 98 | 99 | func getConfig(encoding string) zap.Config { 100 | return zap.Config{ 101 | Level: zap.NewAtomicLevelAt(getLogLevel()), 102 | Development: false, 103 | Sampling: &zap.SamplingConfig{ 104 | Initial: 100, 105 | Thereafter: 100, 106 | }, 107 | Encoding: encoding, 108 | EncoderConfig: zapcore.EncoderConfig{ 109 | TimeKey: "timestamp", 110 | NameKey: "logger", 111 | MessageKey: "message", 112 | CallerKey: "caller", 113 | LevelKey: "level", 114 | 115 | LineEnding: zapcore.DefaultLineEnding, 116 | EncodeLevel: zapcore.CapitalLevelEncoder, 117 | EncodeTime: millisecondsTimeEnconder, 118 | EncodeDuration: zapcore.StringDurationEncoder, 119 | EncodeCaller: zapcore.ShortCallerEncoder, 120 | }, 121 | OutputPaths: []string{"stdout"}, 122 | ErrorOutputPaths: []string{"stderr"}, 123 | } 124 | } 125 | 126 | func getLogLevel() zapcore.Level { 127 | logLevelConfig := strings.ToUpper(os.Getenv("LOG_LEVEL")) 128 | if logLevelConfig == "DEBUG" { 129 | return zap.DebugLevel 130 | } 131 | 132 | if logLevelConfig == "INFO" { 133 | return zap.InfoLevel 134 | } 135 | 136 | if logLevelConfig == "WARN" { 137 | return zap.WarnLevel 138 | } 139 | 140 | if logLevelConfig == "ERROR" { 141 | return zap.ErrorLevel 142 | } 143 | 144 | return zap.InfoLevel 145 | } 146 | 147 | func millisecondsTimeEnconder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { 148 | nanos := t.UnixNano() 149 | millis := float64(nanos) / float64(time.Millisecond) 150 | enc.AppendInt64(int64(millis)) 151 | } 152 | -------------------------------------------------------------------------------- /src/metrics/prometheus.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | var ( 6 | MongoDBDurationsSumary = prometheus.NewSummaryVec( 7 | prometheus.SummaryOpts{ 8 | Name: "mongodb_durations_seconds", 9 | Help: "MongoDB operation duration in seconds", 10 | }, 11 | []string{"path"}, 12 | ) 13 | 14 | MongoDBDurationsHistogram = prometheus.NewHistogramVec( 15 | prometheus.HistogramOpts{ 16 | Name: "mongodb_durations_histogram_seconds", 17 | Help: "MongoDB operation duration in seconds", 18 | }, 19 | []string{"path"}, 20 | ) 21 | ) 22 | 23 | func init() { 24 | prometheus.MustRegister(MongoDBDurationsSumary) 25 | prometheus.MustRegister(MongoDBDurationsHistogram) 26 | } 27 | -------------------------------------------------------------------------------- /src/middleware/assign_request_id.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/michelaquino/golang_api_skeleton/src/context" 6 | uuid "github.com/satori/go.uuid" 7 | ) 8 | 9 | // AssignRequestID is a middleware to set a request_id to response (as a header) 10 | // and to request (in the underlying context). If the value is not found in 11 | // X-Request-ID header from request the identifier will be generated. The 12 | // request_id will be used by http clients in external requests 13 | func AssignRequestID(next echo.HandlerFunc) echo.HandlerFunc { 14 | return func(c echo.Context) error { 15 | req := c.Request() 16 | res := c.Response() 17 | 18 | reqID := req.Header.Get(echo.HeaderXRequestID) 19 | if len(reqID) == 0 { 20 | reqID = uuid.NewV4().String() 21 | } 22 | 23 | ctx := context.SetRequestID(req.Context(), reqID) 24 | reqWithContext := req.WithContext(ctx) 25 | c.SetRequest(reqWithContext) 26 | 27 | res.Header().Set(echo.HeaderXRequestID, reqID) 28 | 29 | return next(c) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/middleware/new_relic.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | 7 | "github.com/labstack/echo/v4" 8 | newrelic "github.com/newrelic/go-agent" 9 | ) 10 | 11 | // NewRelicMiddleware is a middleware to send request info to New Relic. 12 | func NewRelicMiddleware(newrelicApp newrelic.Application) echo.MiddlewareFunc { 13 | return func(next echo.HandlerFunc) echo.HandlerFunc { 14 | return func(echoContext echo.Context) error { 15 | responseWriter := echoContext.Response().Writer 16 | 17 | // Copy struct request to remove body. 18 | requestCopy := *echoContext.Request() 19 | 20 | // Set body empty to send to New Relic 21 | requestCopy.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) 22 | 23 | transaction := newrelicApp.StartTransaction(requestCopy.URL.Path, responseWriter, &requestCopy) 24 | defer transaction.End() 25 | 26 | return next(echoContext) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/middleware/request_log.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/michelaquino/golang_api_skeleton/src/log" 9 | ) 10 | 11 | var ( 12 | logger = log.GetLogger() 13 | ) 14 | 15 | // RequestLogger is a middleware to log the request data 16 | func RequestLogger(next echo.HandlerFunc) echo.HandlerFunc { 17 | return func(c echo.Context) error { 18 | request := c.Request() 19 | response := c.Response() 20 | 21 | start := time.Now() 22 | if err := next(c); err != nil { 23 | logger.Error(c.Request().Context(), "request logger middleware", err.Error(), nil) 24 | c.Error(err) 25 | } 26 | stop := time.Now() 27 | 28 | extraFields := map[string]string{ 29 | "method": request.Method, 30 | "request_uri": request.RequestURI, 31 | "status": strconv.Itoa(response.Status), 32 | "request_time": stop.Sub(start).String(), 33 | } 34 | 35 | logger.Info(c.Request().Context(), "request logger middleware", "success", extraFields) 36 | return nil 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // UserModel is a struct that represents a user. 4 | type UserModel struct { 5 | Name string `json:"name,omitempty" bson:"name,omitempty"` 6 | Email string `json:"email,omitempty" bson:"email,omitempty"` 7 | } 8 | -------------------------------------------------------------------------------- /src/mongo/mongo.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/michelaquino/golang_api_skeleton/src/log" 10 | "github.com/michelaquino/golang_api_skeleton/src/metrics" 11 | "github.com/spf13/viper" 12 | "gopkg.in/mgo.v2/bson" 13 | 14 | "go.mongodb.org/mongo-driver/mongo" 15 | "go.mongodb.org/mongo-driver/mongo/options" 16 | "go.mongodb.org/mongo-driver/mongo/readpref" 17 | ) 18 | 19 | var ( 20 | mongoClient *mongo.Client 21 | onceDatabase sync.Once 22 | logger = log.GetLogger() 23 | ) 24 | 25 | // GetMongoClient return a copy of mongodb session 26 | func GetMongoClient(context context.Context) (*mongo.Client, error) { 27 | onceDatabase.Do(func() { 28 | var err error 29 | mongoClient, err = newMongoClient(context) 30 | if err != nil { 31 | logger.Error(context, "Getting new mongo connection", err.Error(), nil) 32 | } 33 | }) 34 | 35 | logger.Debug(context, "Getting mongo connection", "Success", nil) 36 | return mongoClient, nil 37 | } 38 | 39 | // NewMongoClient return a new mongo client 40 | func newMongoClient(ctx context.Context) (*mongo.Client, error) { 41 | logger.Debug(ctx, "Getting new mongo connection", "", nil) 42 | 43 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(viper.GetString("mongo.uri"))) 44 | if err != nil { 45 | logger.Error(ctx, "Connect", err.Error(), nil) 46 | return nil, err 47 | } 48 | 49 | contextTimeout, _ := context.WithTimeout(ctx, 5*time.Second) 50 | if err := client.Ping(contextTimeout, readpref.Primary()); err != nil { 51 | logger.Error(ctx, "MongoDB Ping", err.Error(), nil) 52 | panic(err) 53 | } 54 | 55 | logger.Debug(ctx, "Getting new mongo connection", "Success", nil) 56 | return client, nil 57 | } 58 | 59 | // Insert a new object on database 60 | func Insert(ctx context.Context, collectionName string, objectToInsert interface{}) error { 61 | // Now time for metrics 62 | now := time.Now() 63 | 64 | mongoClient, err := GetMongoClient(ctx) 65 | if err != nil { 66 | logger.Error(ctx, "Getting mongo client", err.Error(), nil) 67 | return err 68 | } 69 | 70 | logAction := fmt.Sprintf("Inserting object in collection %s", collectionName) 71 | logger.Info(ctx, logAction, "", nil) 72 | 73 | collection := mongoClient.Database(viper.GetString("mongo.database.name")).Collection(collectionName) 74 | if _, err := collection.InsertOne(ctx, objectToInsert); err != nil { 75 | logger.Error(ctx, logAction, err.Error(), nil) 76 | return err 77 | } 78 | 79 | // Send metrics to Prometheus 80 | metrics.MongoDBDurationsSumary.WithLabelValues("Insert").Observe(time.Since(now).Seconds()) 81 | metrics.MongoDBDurationsHistogram.WithLabelValues("Insert").Observe(time.Since(now).Seconds()) 82 | 83 | logger.Info(ctx, logAction, "object inserted with success", nil) 84 | return nil 85 | } 86 | 87 | func FindOne(ctx context.Context, collectionName string, query bson.M, object interface{}) error { 88 | // Now time for metrics 89 | now := time.Now() 90 | 91 | mongoClient, err := GetMongoClient(ctx) 92 | if err != nil { 93 | logger.Error(ctx, "Getting mongo client", err.Error(), nil) 94 | return err 95 | } 96 | 97 | logAction := fmt.Sprintf("getting object in collection %s", collectionName) 98 | logger.Info(ctx, logAction, "", nil) 99 | collection := mongoClient.Database(viper.GetString("mongo.database.name")).Collection(collectionName) 100 | 101 | result := collection.FindOne(ctx, query) 102 | if err := result.Decode(&object); err != nil { 103 | logger.Error(ctx, logAction, err.Error(), nil) 104 | return err 105 | } 106 | 107 | metrics.MongoDBDurationsSumary.WithLabelValues("FindOne").Observe(time.Since(now).Seconds()) 108 | metrics.MongoDBDurationsHistogram.WithLabelValues("FindOne").Observe(time.Since(now).Seconds()) 109 | 110 | logger.Info(ctx, logAction, "object got with success", nil) 111 | return nil 112 | } 113 | 114 | func FindAll(ctx context.Context, collectionName string, query bson.M) ([]interface{}, error) { 115 | // Now time for metrics 116 | now := time.Now() 117 | 118 | mongoClient, err := GetMongoClient(ctx) 119 | if err != nil { 120 | logger.Error(ctx, "Getting mongo client", err.Error(), nil) 121 | return nil, err 122 | } 123 | 124 | logAction := fmt.Sprintf("getting object list in collection %s", collectionName) 125 | logger.Info(ctx, logAction, "", nil) 126 | 127 | collection := mongoClient.Database(viper.GetString("mongo.database.name")).Collection(collectionName) 128 | 129 | cursor, err := collection.Find(ctx, query) 130 | if err != nil { 131 | logger.Error(ctx, logAction, err.Error(), nil) 132 | return nil, err 133 | } 134 | 135 | var objectList []interface{} 136 | for cursor.Next(ctx) { 137 | var result bson.D 138 | if err := cursor.Decode(&result); err != nil { 139 | logger.Error(ctx, logAction, err.Error(), nil) 140 | return nil, err 141 | } 142 | 143 | objectList = append(objectList, result) 144 | } 145 | 146 | if err := cursor.Err(); err != nil { 147 | logger.Error(ctx, logAction, err.Error(), nil) 148 | return nil, err 149 | } 150 | 151 | metrics.MongoDBDurationsSumary.WithLabelValues("FindAll").Observe(time.Since(now).Seconds()) 152 | metrics.MongoDBDurationsHistogram.WithLabelValues("FindAll").Observe(time.Since(now).Seconds()) 153 | 154 | logger.Info(ctx, logAction, "object list getted with success", nil) 155 | return objectList, nil 156 | } 157 | 158 | func Remove(ctx context.Context, collectionName string, query bson.M) error { 159 | // Now time for metrics 160 | now := time.Now() 161 | 162 | mongoClient, err := GetMongoClient(ctx) 163 | if err != nil { 164 | logger.Error(ctx, "Getting mongo client", err.Error(), nil) 165 | return err 166 | } 167 | 168 | logAction := fmt.Sprintf("removing object in collection %s", collectionName) 169 | logger.Info(ctx, logAction, "", nil) 170 | 171 | collection := mongoClient.Database(viper.GetString("mongo.database.name")).Collection(collectionName) 172 | if _, err := collection.DeleteOne(ctx, query); err != nil { 173 | logger.Error(ctx, logAction, err.Error(), nil) 174 | return err 175 | } 176 | 177 | metrics.MongoDBDurationsSumary.WithLabelValues("Remove").Observe(time.Since(now).Seconds()) 178 | metrics.MongoDBDurationsHistogram.WithLabelValues("Remove").Observe(time.Since(now).Seconds()) 179 | 180 | logger.Info(ctx, logAction, "object removed with success", nil) 181 | return nil 182 | } 183 | 184 | func Update(ctx context.Context, collectionName string, objectID bson.ObjectId, objectToUpdate interface{}) error { 185 | // Now time for metrics 186 | now := time.Now() 187 | 188 | logAction := fmt.Sprintf("updating object in collection %s", collectionName) 189 | logger.Info(ctx, logAction, "", nil) 190 | 191 | mongoClient, err := GetMongoClient(ctx) 192 | if err != nil { 193 | logger.Error(ctx, "Getting mongo client", err.Error(), nil) 194 | return err 195 | } 196 | 197 | query := bson.M{"_id": bson.ObjectIdHex(objectID.Hex())} 198 | change := bson.M{"$set": objectToUpdate} 199 | 200 | collection := mongoClient.Database(viper.GetString("mongo.database.name")).Collection(collectionName) 201 | if _, err := collection.UpdateOne(ctx, query, change); err != nil { 202 | logger.Error(ctx, logAction, err.Error(), nil) 203 | return err 204 | } 205 | 206 | metrics.MongoDBDurationsSumary.WithLabelValues("Update").Observe(time.Since(now).Seconds()) 207 | metrics.MongoDBDurationsHistogram.WithLabelValues("Update").Observe(time.Since(now).Seconds()) 208 | 209 | logger.Info(ctx, logAction, "object updated with success", nil) 210 | return nil 211 | } 212 | -------------------------------------------------------------------------------- /src/repository/user.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/michelaquino/golang_api_skeleton/src/models" 7 | ) 8 | 9 | // UserRepository is a interface that defines methods to user's repository. 10 | type UserRepository interface { 11 | 12 | // Insert is a method that inserts an user into database. 13 | Insert(ctx context.Context, userToInsert models.UserModel) error 14 | } 15 | -------------------------------------------------------------------------------- /src/repository/user_mock.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/michelaquino/golang_api_skeleton/src/models" 7 | testifyMock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // UserRepositoryMock is a mock that implements UserRepository interface. 11 | type UserRepositoryMock struct { 12 | testifyMock.Mock 13 | } 14 | 15 | // Insert is a method that inserts an user into the database. 16 | func (mock *UserRepositoryMock) Insert(ctx context.Context, userToInsert models.UserModel) error { 17 | args := mock.Called(ctx, userToInsert) 18 | return args.Error(0) 19 | } 20 | -------------------------------------------------------------------------------- /src/repository/user_mongo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/michelaquino/golang_api_skeleton/src/log" 7 | "github.com/michelaquino/golang_api_skeleton/src/models" 8 | "github.com/michelaquino/golang_api_skeleton/src/mongo" 9 | ) 10 | 11 | var ( 12 | userMongoCollectionName = "user" 13 | logger = log.GetLogger() 14 | ) 15 | 16 | // UserMongoRepository is a user repository for MongoDB. 17 | type UserMongoRepository struct{} 18 | 19 | // Insert is a method that inserts an user into database. 20 | func (u UserMongoRepository) Insert(ctx context.Context, userToInsert models.UserModel) error { 21 | // Execute Mongo's Insert 22 | err := mongo.Insert(ctx, userMongoCollectionName, &userToInsert) 23 | if err != nil { 24 | logger.Error(ctx, "create user", err.Error(), nil) 25 | return err 26 | } 27 | 28 | logger.Info(ctx, "create user", "Success", nil) 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /src/server/middleware.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/labstack/echo/v4/middleware" 6 | apiMiddleware "github.com/michelaquino/golang_api_skeleton/src/middleware" 7 | ) 8 | 9 | func configureMiddlewares(echoInstance *echo.Echo) { 10 | echoInstance.Pre(middleware.RemoveTrailingSlash()) 11 | echoInstance.Use(apiMiddleware.AssignRequestID) 12 | echoInstance.Use(apiMiddleware.RequestLogger) 13 | echoInstance.Use(middleware.Recover()) 14 | } 15 | -------------------------------------------------------------------------------- /src/server/new_relic.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | 8 | "github.com/labstack/echo/v4" 9 | apiMiddleware "github.com/michelaquino/golang_api_skeleton/src/middleware" 10 | newrelic "github.com/newrelic/go-agent" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | // configureNewRelic is the method that enables New Relic. 15 | func configureNewRelic(ctx context.Context, echoInstance *echo.Echo) { 16 | newRelicEnable := viper.GetBool("new_relic.is.enabled") 17 | if !newRelicEnable { 18 | return 19 | } 20 | 21 | newRelicApp, err := createNewRelicApp(ctx) 22 | if err != nil { 23 | logger.Error(ctx, "enabling New Relic", err.Error(), nil) 24 | } 25 | 26 | echoInstance.Use(apiMiddleware.NewRelicMiddleware(newRelicApp)) 27 | logger.Info(ctx, "enabling New Relic", "success", nil) 28 | } 29 | 30 | // createNewRelicApp is the method that creates New Relic configuration. 31 | func createNewRelicApp(ctx context.Context) (newrelic.Application, error) { 32 | licenseKeyEnvVar := viper.GetString("new_relic.licence.key") 33 | 34 | config := newrelic.NewConfig("My Awesome API", licenseKeyEnvVar) 35 | proxyURL, err := url.Parse(viper.GetString("new_relic.proxy.url")) 36 | if err != nil { 37 | logger.Error(ctx, "parse proxy url from env var", err.Error(), nil) 38 | } 39 | 40 | config.Transport = &http.Transport{ 41 | Proxy: http.ProxyURL(proxyURL), 42 | } 43 | 44 | newRelicApp, err := newrelic.NewApplication(config) 45 | if err != nil { 46 | logger.Error(ctx, "create New Relic APP ", err.Error(), nil) 47 | return nil, err 48 | } 49 | 50 | return newRelicApp, nil 51 | } 52 | -------------------------------------------------------------------------------- /src/server/routes.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/michelaquino/golang_api_skeleton/src/handlers" 6 | "github.com/michelaquino/golang_api_skeleton/src/repository" 7 | "github.com/prometheus/client_golang/prometheus/promhttp" 8 | ) 9 | 10 | func configureRoutes(echoInstance *echo.Echo) { 11 | // Metrics by Prometheus 12 | configureMetrics(echoInstance) 13 | 14 | // Healthcheck 15 | configureHealthcheckRoute(echoInstance) 16 | 17 | // User routes 18 | configureUserRoutes(echoInstance) 19 | } 20 | 21 | func configureHealthcheckRoute(echoInstance *echo.Echo) { 22 | echoInstance.GET("/healthcheck", handlers.Healthcheck) 23 | } 24 | 25 | func configureUserRoutes(echoInstance *echo.Echo) { 26 | userRepository := new(repository.UserMongoRepository) 27 | userHandler := handlers.NewUserHandler(userRepository) 28 | 29 | userGroup := echoInstance.Group("/user") 30 | userGroup.POST("", userHandler.CreateUser) 31 | } 32 | 33 | // configureMetrics is the method that configures Prometheus metrics' route. 34 | func configureMetrics(echoInstance *echo.Echo) { 35 | echoInstance.GET("/metrics", echo.WrapHandler(promhttp.Handler())) 36 | } 37 | -------------------------------------------------------------------------------- /src/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/labstack/echo/v4" 8 | "github.com/michelaquino/golang_api_skeleton/src/log" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | var ( 13 | logger = log.GetLogger() 14 | ) 15 | 16 | // Start HTTP server 17 | func Start() { 18 | echoInstance := echo.New() 19 | ctx := context.Background() 20 | 21 | // Configure New Relic 22 | configureNewRelic(ctx, echoInstance) 23 | 24 | // Middlewares 25 | configureMiddlewares(echoInstance) 26 | 27 | // Configure routes 28 | configureRoutes(echoInstance) 29 | 30 | port := viper.GetInt("api.host.port") 31 | logger.Info(ctx, "start api", fmt.Sprintf("Started at %d", port), nil) 32 | 33 | echoInstance.Logger.Fatal(echoInstance.Start(fmt.Sprintf(":%d", port))) 34 | } 35 | --------------------------------------------------------------------------------