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.