github.com/grafana/pyroscope@v1.18.0/docs/sources/reference-server-api/index.md (about) 1 --- 2 description: Learn about the Pyroscope server API 3 menuTitle: "Reference: Server API" 4 title: Server HTTP API 5 aliases: 6 - ../configure-server/about-server-api/ # https://grafana.com/docs/pyroscope/latest/configure-server/about-server-api/ 7 weight: 650 8 --- 9 10 # Grafana Pyroscope server API 11 12 ## Authentication 13 14 Pyroscope doesn't include an authentication layer. Operators should use an authenticating reverse proxy for security. 15 16 In multi-tenant mode, Pyroscope requires the X-Scope-OrgID HTTP header set to a string identifying the tenant. 17 The authenticating reverse proxy handles this responsibility. For more information, refer to the [multi-tenancy documentation](https://grafana.com/docs/pyroscope/<PYROSCOPE_VERSION>/configure-server/about-tenant-ids/). 18 19 20 ## Connect API 21 22 The Pyroscope Connect API uses the [Connect protocol](https://connectrpc.com/), which provides a unified approach to building APIs that work seamlessly across multiple protocols and formats: 23 24 - **Protocol Flexibility**: Connect APIs work over both HTTP/1.1 and HTTP/2, supporting JSON and binary protobuf encoding 25 - **gRPC Compatibility**: Full compatibility with existing gRPC clients and servers while offering better browser and HTTP tooling support 26 - **Type Safety**: Generated from protobuf definitions, ensuring consistent types across client and server implementations 27 - **Developer Experience**: Simpler debugging with standard HTTP tools like curl, while maintaining the performance benefits of protobuf 28 29 The API definitions are available in the [`api/`](https://github.com/grafana/pyroscope/tree/main/api) directory of the Pyroscope repository, with protobuf schemas organized by service. 30 31 Pyroscope APIs are categorized into two scopes: 32 33 - **Public APIs** (`scope/public`): These APIs are considered stable. Breaking changes will be communicated in advance and include migration paths. 34 - **Internal APIs** (`scope/internal`): These APIs are used for internal communication between Pyroscope components. They may change without notice and should not be used by external clients. 35 36 ### Ingestion Path 37 38 #### `/push.v1.PusherService/Push` 39 40 41 42 A request body with the following fields is required: 43 44 |Field | Description | Example | 45 |:-----|:------------|:--------| 46 |`series[].labels[].name` | Label name | `service_name` | 47 |`series[].labels[].value` | Label value | `my_service` | 48 |`series[].samples[].ID` | UUID of the profile | `734FD599-6865-419E-9475-932762D8F469` | 49 |`series[].samples[].rawProfile` | raw_profile is the set of bytes of the pprof profile | `PROFILE_BASE64` | 50 51 {{< code >}} 52 ```curl 53 curl \ 54 -H "Content-Type: application/json" \ 55 -d '{ 56 "series": [ 57 { 58 "labels": [ 59 { 60 "name": "__name__", 61 "value": "process_cpu" 62 }, 63 { 64 "name": "service_name", 65 "value": "my_service" 66 } 67 ], 68 "samples": [ 69 { 70 "ID": "734FD599-6865-419E-9475-932762D8F469", 71 "rawProfile": "'$(cat cpu.pb.gz| base64 -w 0)'" 72 } 73 ] 74 } 75 ] 76 }' \ 77 http://localhost:4040/push.v1.PusherService/Push 78 ``` 79 80 ```python 81 import requests 82 import base64 83 body = { 84 "series": [ 85 { 86 "labels": [ 87 { 88 "name": "__name__", 89 "value": "process_cpu" 90 }, 91 { 92 "name": "service_name", 93 "value": "my_service" 94 } 95 ], 96 "samples": [ 97 { 98 "ID": "734FD599-6865-419E-9475-932762D8F469", 99 "rawProfile": base64.b64encode(open('cpu.pb.gz', 'rb').read()).decode('ascii') 100 } 101 ] 102 } 103 ] 104 } 105 url = 'http://localhost:4040/push.v1.PusherService/Push' 106 resp = requests.post(url, json=body) 107 print(resp) 108 print(resp.content) 109 ``` 110 111 {{< /code >}} 112 113 114 ### Querying profiling data 115 116 #### `/querier.v1.QuerierService/Diff` 117 118 Diff returns a diff of two profiles 119 120 A request body with the following fields is required: 121 122 |Field | Description | Example | 123 |:-----|:------------|:--------| 124 |`left.start` | Milliseconds since epoch. | `1676282400000` | 125 |`left.end` | Milliseconds since epoch. | `1676289600000` | 126 |`left.format` | | | 127 |`left.labelSelector` | Label selector string | `{namespace="my-namespace"}` | 128 |`left.maxNodes` | Limit the nodes returned to only show the node with the max_node's biggest total | | 129 |`left.profileIdSelector` | List of Profile UUIDs to query | `["7c9e6679-7425-40de-944b-e07fc1f90ae7"]` | 130 |`left.profileTypeID` | Profile Type ID string in the form <name>:<type>:<unit>:<period_type>:<period_unit>. | `process_cpu:cpu:nanoseconds:cpu:nanoseconds` | 131 |`left.stackTraceSelector.callSite[].name` | | | 132 |`left.stackTraceSelector.goPgo.aggregateCallees` | Aggregate callees causes the leaf location line number to be ignored, thus aggregating all callee samples (but not callers). | | 133 |`left.stackTraceSelector.goPgo.keepLocations` | Specifies the number of leaf locations to keep. | | 134 |`right.start` | Milliseconds since epoch. | `1676282400000` | 135 |`right.end` | Milliseconds since epoch. | `1676289600000` | 136 |`right.format` | | | 137 |`right.labelSelector` | Label selector string | `{namespace="my-namespace"}` | 138 |`right.maxNodes` | Limit the nodes returned to only show the node with the max_node's biggest total | | 139 |`right.profileIdSelector` | List of Profile UUIDs to query | `["7c9e6679-7425-40de-944b-e07fc1f90ae7"]` | 140 |`right.profileTypeID` | Profile Type ID string in the form <name>:<type>:<unit>:<period_type>:<period_unit>. | `process_cpu:cpu:nanoseconds:cpu:nanoseconds` | 141 |`right.stackTraceSelector.callSite[].name` | | | 142 |`right.stackTraceSelector.goPgo.aggregateCallees` | Aggregate callees causes the leaf location line number to be ignored, thus aggregating all callee samples (but not callers). | | 143 |`right.stackTraceSelector.goPgo.keepLocations` | Specifies the number of leaf locations to keep. | | 144 145 {{< code >}} 146 ```curl 147 curl \ 148 -H "Content-Type: application/json" \ 149 -d '{ 150 "left": { 151 "end": '$(date +%s)000', 152 "labelSelector": "{namespace=\"my-namespace\"}", 153 "profileIdSelector": [ 154 "7c9e6679-7425-40de-944b-e07fc1f90ae7" 155 ], 156 "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", 157 "start": '$(expr $(date +%s) - 3600 )000' 158 }, 159 "right": { 160 "end": '$(date +%s)000', 161 "labelSelector": "{namespace=\"my-namespace\"}", 162 "profileIdSelector": [ 163 "7c9e6679-7425-40de-944b-e07fc1f90ae7" 164 ], 165 "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", 166 "start": '$(expr $(date +%s) - 3600 )000' 167 } 168 }' \ 169 http://localhost:4040/querier.v1.QuerierService/Diff 170 ``` 171 172 ```python 173 import requests 174 import datetime 175 body = { 176 "left": { 177 "end": int(datetime.datetime.now().timestamp() * 1000), 178 "labelSelector": "{namespace=\"my-namespace\"}", 179 "profileIdSelector": [ 180 "7c9e6679-7425-40de-944b-e07fc1f90ae7" 181 ], 182 "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", 183 "start": int((datetime.datetime.now()- datetime.timedelta(hours = 1)).timestamp() * 1000) 184 }, 185 "right": { 186 "end": int(datetime.datetime.now().timestamp() * 1000), 187 "labelSelector": "{namespace=\"my-namespace\"}", 188 "profileIdSelector": [ 189 "7c9e6679-7425-40de-944b-e07fc1f90ae7" 190 ], 191 "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", 192 "start": int((datetime.datetime.now()- datetime.timedelta(hours = 1)).timestamp() * 1000) 193 } 194 } 195 url = 'http://localhost:4040/querier.v1.QuerierService/Diff' 196 resp = requests.post(url, json=body) 197 print(resp) 198 print(resp.content) 199 ``` 200 201 {{< /code >}} 202 #### `/querier.v1.QuerierService/LabelNames` 203 204 LabelNames returns a list of the existing label names. 205 206 A request body with the following fields is required: 207 208 |Field | Description | Example | 209 |:-----|:------------|:--------| 210 |`start` | Query from this point in time, given in Milliseconds since epoch. | `1676282400000` | 211 |`end` | Query to this point in time, given in Milliseconds since epoch. | `1676289600000` | 212 |`matchers` | List of Label selectors | | 213 214 {{< code >}} 215 ```curl 216 curl \ 217 -H "Content-Type: application/json" \ 218 -d '{ 219 "end": '$(date +%s)000', 220 "start": '$(expr $(date +%s) - 3600 )000' 221 }' \ 222 http://localhost:4040/querier.v1.QuerierService/LabelNames 223 ``` 224 225 ```python 226 import requests 227 import datetime 228 body = { 229 "end": int(datetime.datetime.now().timestamp() * 1000), 230 "start": int((datetime.datetime.now()- datetime.timedelta(hours = 1)).timestamp() * 1000) 231 } 232 url = 'http://localhost:4040/querier.v1.QuerierService/LabelNames' 233 resp = requests.post(url, json=body) 234 print(resp) 235 print(resp.content) 236 ``` 237 238 {{< /code >}} 239 #### `/querier.v1.QuerierService/LabelValues` 240 241 LabelValues returns the existing label values for the provided label names. 242 243 A request body with the following fields is required: 244 245 |Field | Description | Example | 246 |:-----|:------------|:--------| 247 |`start` | Query from this point in time, given in Milliseconds since epoch. | `1676282400000` | 248 |`end` | Query to this point in time, given in Milliseconds since epoch. | `1676289600000` | 249 |`matchers` | List of Label selectors | | 250 |`name` | Name of the label | `service_name` | 251 252 {{< code >}} 253 ```curl 254 curl \ 255 -H "Content-Type: application/json" \ 256 -d '{ 257 "end": '$(date +%s)000', 258 "name": "service_name", 259 "start": '$(expr $(date +%s) - 3600 )000' 260 }' \ 261 http://localhost:4040/querier.v1.QuerierService/LabelValues 262 ``` 263 264 ```python 265 import requests 266 import datetime 267 body = { 268 "end": int(datetime.datetime.now().timestamp() * 1000), 269 "name": "service_name", 270 "start": int((datetime.datetime.now()- datetime.timedelta(hours = 1)).timestamp() * 1000) 271 } 272 url = 'http://localhost:4040/querier.v1.QuerierService/LabelValues' 273 resp = requests.post(url, json=body) 274 print(resp) 275 print(resp.content) 276 ``` 277 278 {{< /code >}} 279 #### `/querier.v1.QuerierService/ProfileTypes` 280 281 ProfileType returns a list of the existing profile types. 282 283 A request body with the following fields is required: 284 285 |Field | Description | Example | 286 |:-----|:------------|:--------| 287 |`start` | Milliseconds since epoch. If missing or zero, only the ingesters will be queried. | `1676282400000` | 288 |`end` | Milliseconds since epoch. If missing or zero, only the ingesters will be queried. | `1676289600000` | 289 290 {{< code >}} 291 ```curl 292 curl \ 293 -H "Content-Type: application/json" \ 294 -d '{ 295 "end": '$(date +%s)000', 296 "start": '$(expr $(date +%s) - 3600 )000' 297 }' \ 298 http://localhost:4040/querier.v1.QuerierService/ProfileTypes 299 ``` 300 301 ```python 302 import requests 303 import datetime 304 body = { 305 "end": int(datetime.datetime.now().timestamp() * 1000), 306 "start": int((datetime.datetime.now()- datetime.timedelta(hours = 1)).timestamp() * 1000) 307 } 308 url = 'http://localhost:4040/querier.v1.QuerierService/ProfileTypes' 309 resp = requests.post(url, json=body) 310 print(resp) 311 print(resp.content) 312 ``` 313 314 {{< /code >}} 315 #### `/querier.v1.QuerierService/SelectMergeProfile` 316 317 SelectMergeProfile returns matching profiles aggregated in pprof format. It 318 will contain all information stored (so including filenames and line 319 number, if ingested). 320 321 A request body with the following fields is required: 322 323 |Field | Description | Example | 324 |:-----|:------------|:--------| 325 |`start` | Milliseconds since epoch. | `1676282400000` | 326 |`end` | Milliseconds since epoch. | `1676289600000` | 327 |`labelSelector` | Label selector string | `{namespace="my-namespace"}` | 328 |`maxNodes` | Limit the nodes returned to only show the node with the max_node's biggest total | | 329 |`profileIdSelector` | List of Profile UUIDs to query | `["7c9e6679-7425-40de-944b-e07fc1f90ae7"]` | 330 |`profileTypeID` | Profile Type ID string in the form <name>:<type>:<unit>:<period_type>:<period_unit>. | `process_cpu:cpu:nanoseconds:cpu:nanoseconds` | 331 |`stackTraceSelector.callSite[].name` | | | 332 |`stackTraceSelector.goPgo.aggregateCallees` | Aggregate callees causes the leaf location line number to be ignored, thus aggregating all callee samples (but not callers). | | 333 |`stackTraceSelector.goPgo.keepLocations` | Specifies the number of leaf locations to keep. | | 334 335 {{< code >}} 336 ```curl 337 curl \ 338 -H "Content-Type: application/json" \ 339 -d '{ 340 "end": '$(date +%s)000', 341 "labelSelector": "{namespace=\"my-namespace\"}", 342 "profileIdSelector": [ 343 "7c9e6679-7425-40de-944b-e07fc1f90ae7" 344 ], 345 "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", 346 "start": '$(expr $(date +%s) - 3600 )000' 347 }' \ 348 http://localhost:4040/querier.v1.QuerierService/SelectMergeProfile 349 ``` 350 351 ```python 352 import requests 353 import datetime 354 body = { 355 "end": int(datetime.datetime.now().timestamp() * 1000), 356 "labelSelector": "{namespace=\"my-namespace\"}", 357 "profileIdSelector": [ 358 "7c9e6679-7425-40de-944b-e07fc1f90ae7" 359 ], 360 "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", 361 "start": int((datetime.datetime.now()- datetime.timedelta(hours = 1)).timestamp() * 1000) 362 } 363 url = 'http://localhost:4040/querier.v1.QuerierService/SelectMergeProfile' 364 resp = requests.post(url, json=body) 365 print(resp) 366 print(resp.content) 367 ``` 368 369 {{< /code >}} 370 #### `/querier.v1.QuerierService/SelectMergeSpanProfile` 371 372 SelectMergeSpanProfile returns matching profiles aggregated in a flamegraph 373 format. It will combine samples from within the same callstack, with each 374 element being grouped by its function name. 375 376 A request body with the following fields is required: 377 378 |Field | Description | Example | 379 |:-----|:------------|:--------| 380 |`start` | Milliseconds since epoch. | `1676282400000` | 381 |`end` | Milliseconds since epoch. | `1676289600000` | 382 |`format` | | | 383 |`labelSelector` | Label selector string | `{namespace="my-namespace"}` | 384 |`maxNodes` | Limit the nodes returned to only show the node with the max_node's biggest total | | 385 |`profileTypeID` | Profile Type ID string in the form <name>:<type>:<unit>:<period_type>:<period_unit>. | `process_cpu:cpu:nanoseconds:cpu:nanoseconds` | 386 |`spanSelector` | List of Span IDs to query | `["9a517183f26a089d","5a4fe264a9c987fe"]` | 387 388 {{< code >}} 389 ```curl 390 curl \ 391 -H "Content-Type: application/json" \ 392 -d '{ 393 "end": '$(date +%s)000', 394 "labelSelector": "{namespace=\"my-namespace\"}", 395 "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", 396 "spanSelector": [ 397 "9a517183f26a089d", 398 "5a4fe264a9c987fe" 399 ], 400 "start": '$(expr $(date +%s) - 3600 )000' 401 }' \ 402 http://localhost:4040/querier.v1.QuerierService/SelectMergeSpanProfile 403 ``` 404 405 ```python 406 import requests 407 import datetime 408 body = { 409 "end": int(datetime.datetime.now().timestamp() * 1000), 410 "labelSelector": "{namespace=\"my-namespace\"}", 411 "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", 412 "spanSelector": [ 413 "9a517183f26a089d", 414 "5a4fe264a9c987fe" 415 ], 416 "start": int((datetime.datetime.now()- datetime.timedelta(hours = 1)).timestamp() * 1000) 417 } 418 url = 'http://localhost:4040/querier.v1.QuerierService/SelectMergeSpanProfile' 419 resp = requests.post(url, json=body) 420 print(resp) 421 print(resp.content) 422 ``` 423 424 {{< /code >}} 425 #### `/querier.v1.QuerierService/SelectMergeStacktraces` 426 427 SelectMergeStacktraces returns matching profiles aggregated in a flamegraph 428 format. It will combine samples from within the same callstack, with each 429 element being grouped by its function name. 430 431 A request body with the following fields is required: 432 433 |Field | Description | Example | 434 |:-----|:------------|:--------| 435 |`start` | Milliseconds since epoch. | `1676282400000` | 436 |`end` | Milliseconds since epoch. | `1676289600000` | 437 |`format` | | | 438 |`labelSelector` | Label selector string | `{namespace="my-namespace"}` | 439 |`maxNodes` | Limit the nodes returned to only show the node with the max_node's biggest total | | 440 |`profileIdSelector` | List of Profile UUIDs to query | `["7c9e6679-7425-40de-944b-e07fc1f90ae7"]` | 441 |`profileTypeID` | Profile Type ID string in the form <name>:<type>:<unit>:<period_type>:<period_unit>. | `process_cpu:cpu:nanoseconds:cpu:nanoseconds` | 442 |`stackTraceSelector.callSite[].name` | | | 443 |`stackTraceSelector.goPgo.aggregateCallees` | Aggregate callees causes the leaf location line number to be ignored, thus aggregating all callee samples (but not callers). | | 444 |`stackTraceSelector.goPgo.keepLocations` | Specifies the number of leaf locations to keep. | | 445 446 {{< code >}} 447 ```curl 448 curl \ 449 -H "Content-Type: application/json" \ 450 -d '{ 451 "end": '$(date +%s)000', 452 "labelSelector": "{namespace=\"my-namespace\"}", 453 "profileIdSelector": [ 454 "7c9e6679-7425-40de-944b-e07fc1f90ae7" 455 ], 456 "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", 457 "start": '$(expr $(date +%s) - 3600 )000' 458 }' \ 459 http://localhost:4040/querier.v1.QuerierService/SelectMergeStacktraces 460 ``` 461 462 ```python 463 import requests 464 import datetime 465 body = { 466 "end": int(datetime.datetime.now().timestamp() * 1000), 467 "labelSelector": "{namespace=\"my-namespace\"}", 468 "profileIdSelector": [ 469 "7c9e6679-7425-40de-944b-e07fc1f90ae7" 470 ], 471 "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", 472 "start": int((datetime.datetime.now()- datetime.timedelta(hours = 1)).timestamp() * 1000) 473 } 474 url = 'http://localhost:4040/querier.v1.QuerierService/SelectMergeStacktraces' 475 resp = requests.post(url, json=body) 476 print(resp) 477 print(resp.content) 478 ``` 479 480 {{< /code >}} 481 #### `/querier.v1.QuerierService/SelectSeries` 482 483 SelectSeries returns a time series for the total sum of the requested 484 profiles. 485 486 A request body with the following fields is required: 487 488 |Field | Description | Example | 489 |:-----|:------------|:--------| 490 |`start` | Milliseconds since epoch. | `1676282400000` | 491 |`end` | Milliseconds since epoch. | `1676289600000` | 492 |`aggregation` | | | 493 |`exemplarType` | | | 494 |`groupBy` | | `["pod"]` | 495 |`labelSelector` | Label selector string | `{namespace="my-namespace"}` | 496 |`limit` | Select the top N series by total value. | | 497 |`profileTypeID` | Profile Type ID string in the form <name>:<type>:<unit>:<period_type>:<period_unit>. | `process_cpu:cpu:nanoseconds:cpu:nanoseconds` | 498 |`stackTraceSelector.callSite[].name` | | | 499 |`stackTraceSelector.goPgo.aggregateCallees` | Aggregate callees causes the leaf location line number to be ignored, thus aggregating all callee samples (but not callers). | | 500 |`stackTraceSelector.goPgo.keepLocations` | Specifies the number of leaf locations to keep. | | 501 |`step` | | | 502 503 {{< code >}} 504 ```curl 505 curl \ 506 -H "Content-Type: application/json" \ 507 -d '{ 508 "end": '$(date +%s)000', 509 "groupBy": [ 510 "pod" 511 ], 512 "labelSelector": "{namespace=\"my-namespace\"}", 513 "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", 514 "start": '$(expr $(date +%s) - 3600 )000' 515 }' \ 516 http://localhost:4040/querier.v1.QuerierService/SelectSeries 517 ``` 518 519 ```python 520 import requests 521 import datetime 522 body = { 523 "end": int(datetime.datetime.now().timestamp() * 1000), 524 "groupBy": [ 525 "pod" 526 ], 527 "labelSelector": "{namespace=\"my-namespace\"}", 528 "profileTypeID": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", 529 "start": int((datetime.datetime.now()- datetime.timedelta(hours = 1)).timestamp() * 1000) 530 } 531 url = 'http://localhost:4040/querier.v1.QuerierService/SelectSeries' 532 resp = requests.post(url, json=body) 533 print(resp) 534 print(resp.content) 535 ``` 536 537 {{< /code >}} 538 #### `/querier.v1.QuerierService/Series` 539 540 Series returns profiles series matching the request. A series is a unique 541 label set. 542 543 A request body with the following fields is required: 544 545 |Field | Description | Example | 546 |:-----|:------------|:--------| 547 |`start` | Milliseconds since epoch. If missing or zero, only the ingesters will be queried. | `1676282400000` | 548 |`end` | queried. | `1676289600000` | 549 |`labelNames` | List of label_names to request. If empty will return all label names in the result. | | 550 |`matchers` | List of label selector to apply to the result. | `["{namespace=\"my-namespace\"}"]` | 551 552 {{< code >}} 553 ```curl 554 curl \ 555 -H "Content-Type: application/json" \ 556 -d '{ 557 "end": '$(date +%s)000', 558 "matchers": [ 559 "{namespace=\"my-namespace\"}" 560 ], 561 "start": '$(expr $(date +%s) - 3600 )000' 562 }' \ 563 http://localhost:4040/querier.v1.QuerierService/Series 564 ``` 565 566 ```python 567 import requests 568 import datetime 569 body = { 570 "end": int(datetime.datetime.now().timestamp() * 1000), 571 "matchers": [ 572 "{namespace=\"my-namespace\"}" 573 ], 574 "start": int((datetime.datetime.now()- datetime.timedelta(hours = 1)).timestamp() * 1000) 575 } 576 url = 'http://localhost:4040/querier.v1.QuerierService/Series' 577 resp = requests.post(url, json=body) 578 print(resp) 579 print(resp.content) 580 ``` 581 582 {{< /code >}} 583 584 585 586 ## Pyroscope Legacy HTTP API 587 588 Grafana Pyroscope exposes an HTTP API for querying profiling data and ingesting profiling data from other sources. 589 590 591 ### Ingestion 592 593 There is one primary endpoint: `POST /ingest`. 594 It accepts profile data in the request body and metadata as query parameters. 595 596 The following query parameters are accepted: 597 598 | Name | Description | Notes | 599 |:-------------------|:----------------------------------------|:-------------------------------| 600 | `name` | application name | required | 601 | `from` | UNIX time of when the profiling started | required | 602 | `until` | UNIX time of when the profiling stopped | required | 603 | `format` | format of the profiling data | optional (default is `folded`) | 604 | `sampleRate` | sample rate used in Hz | optional (default is `100` Hz) | 605 | `spyName` | name of the spy used | optional | 606 | `units` | name of the profiling data unit | optional (default is `samples` | 607 | `aggregrationType` | type of aggregation to merge profiles | optional (default is `sum`) | 608 609 610 `name` specifies application name. For example: 611 ``` 612 my.awesome.app.cpu{env=staging,region=us-west-1} 613 ``` 614 615 The request body contains profiling data, and the Content-Type header may be used alongside format to determine the data format. 616 617 Some of the query parameters depend on the format of profiling data. Pyroscope currently supports three major ingestion formats. 618 619 #### Text formats 620 621 These formats handle simple ingestion of profiling data, such as `cpu` samples, and typically don't support metadata (for example, labels) within the format. 622 All necessary metadata is derived from query parameters, and the format is specified by the `format` query parameter. 623 624 **Supported formats:** 625 626 - **Folded**: Also known as `collapsed`, this is the default format. Each line contains a stacktrace followed by the sample count for that stacktrace. For example: 627 ``` 628 foo;bar 100 629 foo;baz 200 630 ``` 631 632 - **Lines**: Similar to `folded`, but it represents each sample as a separate line rather than aggregating samples per stacktrace. For example: 633 ``` 634 foo;bar 635 foo;bar 636 foo;baz 637 foo;bar 638 ``` 639 640 #### The `pprof` format 641 642 The `pprof` format is a widely used binary profiling data format, particularly prevalent in the Go ecosystem. 643 644 When using this format, certain query parameters have specific behaviors: 645 646 - **format**: This should be set to `pprof`. 647 - **name**: This parameter contains the _prefix_ of the application name. Since a single request might include multiple profile types, the complete application name is formed by concatenating this prefix with the profile type. For instance, if you send CPU profiling data and set `name` to `my-app{}`, it is displayed in Pyroscope as `my-app.cpu{}`. 648 - **units**, **aggregationType**, and **sampleRate**: These parameters are ignored. The actual values are determined based on the profile types present in the data (refer to the "Sample Type Configuration" section for more details). 649 650 ##### Sample type configuration 651 652 Pyroscope server inherently supports standard Go profile types such as `cpu`, `inuse_objects`, `inuse_space`, `alloc_objects`, and `alloc_space`. When dealing with software that generates data in `pprof` format, you may need to supply a custom sample type configuration for Pyroscope to interpret the data correctly. 653 654 For an example Python script to ingest a `pprof` file with a custom sample type configuration, see **[this Python script](https://github.com/grafana/pyroscope/tree/main/examples/api/ingest_pprof.py).** 655 656 To ingest `pprof` data with custom sample type configuration, modify your requests as follows: 657 * Set Content-Type to `multipart/form-data`. 658 * Upload the profile data in a form file field named `profile`. 659 * Include the sample type configuration in a form file field named `sample_type_config`. 660 661 A sample type configuration is a JSON object formatted like this: 662 663 ```json 664 { 665 "inuse_space": { 666 "units": "bytes", 667 "aggregation": "average", 668 "display-name": "inuse_space_bytes", 669 "sampled": false 670 }, 671 "alloc_objects": { 672 "units": "objects", 673 "aggregation": "sum", 674 "display-name": "alloc_objects_count", 675 "sampled": true 676 }, 677 "cpu": { 678 "units": "samples", 679 "aggregation": "sum", 680 "display-name": "cpu_samples", 681 "sampled": true 682 }, 683 // pprof supports multiple profiles types in one file, 684 // so there can be multiple of these objects 685 } 686 ``` 687 688 Explanation of sample type configuration fields: 689 690 - **units** 691 - Supported values: `samples`, `objects`, `bytes` 692 - Description: Changes the units displayed in the frontend. `samples` = CPU samples, `objects` = objects in RAM, `bytes` = bytes in RAM. 693 - **display-name** 694 - Supported values: Any string. 695 - Description: This becomes a suffix of the app name, e.g., `my-app.inuse_space_bytes`. 696 - **aggregation** 697 - Supported values: `sum`, `average`. 698 - Description: Alters how data is aggregated on the frontend. Use `sum` for data to be summed over time (e.g., CPU samples, memory allocations), and `average` for data to be averaged over time (e.g., memory in-use objects). 699 - **sampled** 700 - Supported values: `true`, `false`. 701 - Description: Determines if the sample rate (specified in the pprof file) is considered. Set to `true` for sampled events (e.g., CPU samples), and `false` for memory profiles. 702 703 This configuration allows for customized visualization and analysis of various profile types within Pyroscope. 704 705 #### JFR format 706 707 This is the [Java Flight Recorder](https://openjdk.java.net/jeps/328) format, typically used by JVM-based profilers, also supported by our Java integration. 708 709 When this format is used, some of the query parameters behave slightly different: 710 * `format` should be set to `jfr`. 711 * `name` contains the _prefix_ of the application name. Since a single request may contain multiple profile types, the final application name is created concatenating this prefix and the profile type. For example, if you send cpu profiling data and set `name` to `my-app{}`, it will appear in pyroscope as `my-app.cpu{}`. 712 * `units` is ignored, and the actual units depends on the profile types available in the data. 713 * `aggregationType` is ignored, and the actual aggregation type depends on the profile types available in the data. 714 715 JFR ingestion support uses the profile metadata to determine which profile types are included, which depend on the kind of profiling being done. Currently supported profile types include: 716 * `cpu` samples, which includes only profiling data from runnable threads. 717 * `itimer` samples, similar to `cpu` profiling. 718 * `wall` samples, which includes samples from any threads independently of their state. 719 * `alloc_in_new_tlab_objects`, which indicates the number of new TLAB objects created. 720 * `alloc_in_new_tlab_bytes`, which indicates the size in bytes of new TLAB objects created. 721 * `alloc_outside_tlab_objects`, which indicates the number of new allocated objects outside any TLAB. 722 * `alloc_outside_tlab_bytes`, which indicates the size in bytes of new allocated objects outside any TLAB. 723 724 ##### JFR with labels 725 726 In order to ingest JFR data with dynamic labels, you have to make the following changes to your requests: 727 * use an HTTP form (`multipart/form-data`) Content-Type. 728 * send the JFR data in a form file field called `jfr`. 729 * send `LabelsSnapshot` protobuf message in a form file field called `labels`. 730 731 ```protobuf 732 message Context { 733 // string_id -> string_id 734 map<int64, int64> labels = 1; 735 } 736 message LabelsSnapshot { 737 // context_id -> Context 738 map<int64, Context> contexts = 1; 739 // string_id -> string 740 map<int64, string> strings = 2; 741 } 742 743 ``` 744 Where `context_id` is a parameter [set in async-profiler](https://github.com/pyroscope-io/async-profiler/pull/1/files#diff-34c624b2fbf52c68fc3f15dee43a73caec11b9524319c3a581cd84ec3fd2aacfR218) 745 746 #### Examples 747 748 Here's a sample code that uploads a very simple profile to pyroscope: 749 750 {{< code >}} 751 752 ```curl 753 printf "foo;bar 100\n foo;baz 200" | curl \ 754 -X POST \ 755 --data-binary @- \ 756 'http://localhost:4040/ingest?name=curl-test-app&from=1615709120&until=1615709130' 757 758 ``` 759 760 ```python 761 import requests 762 import urllib.parse 763 from datetime import datetime 764 765 now = round(datetime.now().timestamp()) / 10 * 10 766 params = {'from': f'{now - 10}', 'name': 'python.example{foo=bar}'} 767 768 url = f'http://localhost:4040/ingest?{urllib.parse.urlencode(params)}' 769 data = "foo;bar 100\n" \ 770 "foo;baz 200" 771 772 requests.post(url, data = data) 773 ``` 774 775 {{< /code >}} 776 777 778 Here's a sample code that uploads a JFR profile with labels to pyroscope: 779 780 {{< code >}} 781 782 ```curl 783 curl -X POST \ 784 -F jfr=@profile.jfr \ 785 -F labels=@labels.pb \ 786 "http://localhost:4040/ingest?name=curl-test-app&units=samples&aggregationType=sum&sampleRate=100&from=1655834200&until=1655834210&spyName=javaspy&format=jfr" 787 ``` 788 789 {{< /code >}} 790 791 792 ### Querying profile data 793 794 There is one primary endpoint for querying profile data: `GET /pyroscope/render`. 795 796 The search input is provided via query parameters. 797 The output is typically a JSON object containing one or more time series and a flame graph. 798 799 #### Query parameters 800 801 Here is an overview of the accepted query parameters: 802 803 | Name | Description | Notes | 804 |:-----------|:---------------------------------------------------------------------------------------|:-----------------------------------------------------| 805 | `query` | contains the profile type and label selectors | required | 806 | `from` | UNIX time for the start of the search window | required | 807 | `until` | UNIX time for the end of the search window | optional (default is `now`) | 808 | `format` | format of the profiling data | optional (default is `json`) | 809 | `maxNodes` | the maximum number of nodes the resulting flame graph will contain | optional (default is `max_flamegraph_nodes_default`) | 810 | `groupBy` | one or more label names to group the time series by (doesn't apply to the flame graph) | optional (default is no grouping) | 811 812 ##### `query` 813 814 The `query` parameter is the only required search input. It carries the profile type and any labels we want to use to narrow down the output. 815 The format for this parameter is similar to that of a PromQL query and can be defined as: 816 817 `<profile_type>{<label_name>="<label_value>", <label_name>="<label_value>", ...}` 818 819 Here is a specific example: 820 821 `process_cpu:cpu:nanoseconds:cpu:nanoseconds{service_name="my_application_name"}` 822 823 In a Kubernetes environment, a query could also look like: 824 825 `process_cpu:cpu:nanoseconds:cpu:nanoseconds{namespace="dev", container="my_application_name"}` 826 827 {{% admonition type="note" %}} 828 Refer to the [profiling types documentation](https://grafana.com/docs/pyroscope/<PYROSCOPE_VERSION>/configure-client/profile-types/) for more information and [profile-metrics.json](https://github.com/grafana/pyroscope/blob/main/public/app/constants/profile-metrics.json) for a list of valid profile types. 829 {{% /admonition %}} 830 831 ##### `from` and `until` 832 833 The `from` and `until` parameters determine the start and end of the time period for the query. 834 They can be provided in absolute and relative form. 835 836 **Absolute time** 837 838 This table details the options for passing absolute values. 839 840 | Option | Example | Notes | 841 |:-----------------------|:----------------------|:-------------------| 842 | Date | `20231223` | Format: `YYYYMMDD` | 843 | Unix Time seconds | `1577836800` | | 844 | Unix Time milliseconds | `1577836800000` | | 845 | Unix Time microseconds | `1577836800000000` | | 846 | Unix Time nanoseconds | `1577836800000000000` | | 847 848 **Relative time** 849 850 Relative values are always expressed as offsets from `now`. 851 852 | Option | Example | 853 |:---------------|:---------------------| 854 | 3 hours ago | `now-3h` | 855 | 30 minutes ago | `now-30m` | 856 | 2 days ago | `now-2d` | 857 | 1 week ago | `now-7d` or `now-1w` | 858 859 Note that a single offset has to be provided, values such as `now-3h30m` will not work. 860 861 **Validation** 862 863 The `from` and `until` parameters are subject to validation rules related to `max_query_lookback` and `max_query_length` server parameters. 864 You can find more details on these parameters in the [limits section](https://grafana.com/docs/pyroscope/<PYROSCOPE_VERSION>/configure-server/reference-configuration-parameters#limits) of the server configuration docs. 865 866 - If `max_query_lookback` is configured and`from` is before `now - max_query_lookback`, `from` will be set to `now - max_query_lookback`. 867 - If `max_query_lookback` is configured and `until` is before `now - max_query_lookback` the query will not be executed. 868 - If `max_query_length` is configured and the query interval is longer than this configuration, the query will no tbe executed. 869 870 #### `format` 871 872 The format can either be: 873 - `json`, in which case the response will contain a JSON object 874 - `dot`, in which case the response will be text containing a DOT representation of the profile 875 876 See the [Query output](#query-output) section for more information on the response structure. 877 878 #### `maxNodes` 879 880 The `maxNodes` parameter truncates the number of elements in the profile response, to allow tools (for example, a frontend) to render large profiles efficiently. 881 This is typically used for profiles that are known to have large stack traces. 882 883 When no value is provided, the default is taken from the `max_flamegraph_nodes_default` configuration parameter. 884 When a value is provided, it is capped to the `max_flamegraph_nodes_max` configuration parameter. 885 886 #### `groupBy` 887 888 The `groupBy` parameter impacts the output for the time series portion of the response. 889 When a valid label is provided, the response contains as many series as there are label values for the given label. 890 891 {{% admonition type="note" %}} 892 Pyroscope supports a single label for the group by functionality. 893 {{% /admonition %}} 894 895 ### Query output 896 897 The output of the `/pyroscope/render` endpoint is a JSON object based on the following [schema](https://github.com/grafana/pyroscope/blob/80959aeba2426f3698077fd8d2cd222d25d5a873/pkg/og/structs/flamebearer/flamebearer.go#L28-L43): 898 899 ```go 900 type FlamebearerProfileV1 struct { 901 Flamebearer FlamebearerV1 `json:"flamebearer"` 902 Metadata FlamebearerMetadataV1 `json:"metadata"` 903 Timeline *FlamebearerTimelineV1 `json:"timeline"` 904 Groups map[string]*FlamebearerTimelineV1 `json:"groups"` 905 } 906 ``` 907 908 #### `flamebearer` 909 910 The `flamebearer` field contains data in a form suitable for rendering a flame graph. 911 Data within the `flamebearer` is organized in separate arrays containing the profile symbols and the sample values. 912 913 #### `metadata` 914 915 The `metadata` field contains additional information that is helpful to interpret the `flamebearer` data such as the unit (nanoseconds, bytes), sample rate and more. 916 917 #### `timeline` 918 919 The `timeline` field represents the time series for the profile. 920 Pyroscope pre-computes the step interval (resolution) of the timeline using the query interval (`from` and `until`). The minimum step interval is 10 seconds. 921 922 The raw profile sample data is down-sampled to the step interval (resolution) using an aggregation function. Currently only `sum` is supported. 923 924 A timeline contains a start time, a list of sample values and the step interval: 925 926 ```json 927 { 928 "timeline": { 929 "startTime": 1577836800, 930 "samples": [ 931 100, 932 200, 933 400 934 ], 935 "durationDelta": 10 936 } 937 } 938 ``` 939 940 #### `groups` 941 942 The `groups` field is only populated when grouping is requested by the `groupBy` query parameter. 943 When this is the case, the `groups` field has an entry for every label value found for the query. 944 945 This example groups by a cluster: 946 947 ```json 948 { 949 "groups": { 950 "eu-west-2": { "startTime": 1577836800, "samples": [ 200, 300, 500 ] }, 951 "us-east-1": { "startTime": 1577836800, "samples": [ 100, 200, 400 ] } 952 } 953 } 954 ``` 955 956 ### Alternative query output 957 958 When the `format` query parameter is `dot`, the endpoint responds with a [DOT format](https://en.wikipedia.org/wiki/DOT_(graph_description_language)) data representing the queried profile. 959 This can be used to create an alternative visualization of the profile. 960 961 ### Example queries 962 963 This example queries a local Pyroscope server for a CPU profile from the `pyroscope` service for the last hour. 964 965 ```curl 966 curl \ 967 'http://localhost:4040/pyroscope/render?query=process_cpu%3Acpu%3Ananoseconds%3Acpu%3Ananoseconds%7Bservice_name%3D%22pyroscope%22%7D&from=now-1h' 968 ``` 969 970 Here is the same query made more readable: 971 972 ```curl 973 curl --get \ 974 --data-urlencode "query=process_cpu:cpu:nanoseconds:cpu:nanoseconds{service_name=\"pyroscope\"}" \ 975 --data-urlencode "from=now-1h" \ 976 http://localhost:4040/pyroscope/render 977 ``` 978 979 Here is the same example in Python: 980 981 ```python 982 import requests 983 984 application_name = 'my_application_name' 985 query = f'process_cpu:cpu:nanoseconds:cpu:nanoseconds{service_name="{application_name}"}' 986 query_from = 'now-1h' 987 url = f'http://localhost:4040/pyroscope/render?query={query}&from={query_from}' 988 989 requests.get(url) 990 ``` 991 992 See [this Python script](https://github.com/grafana/pyroscope/tree/main/examples/api/query.py) for a complete example. 993 994 ## Profile CLI 995 996 The `profilecli` tool can also be used to interact with the Pyroscope server API. 997 The tool supports operations such as ingesting profiles, querying for existing profiles, and more. 998 Refer to the [Profile CLI](https://grafana.com/docs/pyroscope/<PYROSCOPE_VERSION>/view-and-analyze-profile-data/profile-cli/) page for more information.