Repository: sunpy/sunpy

Issue:
Missing metadata warnings aren't very helpful
I'm currently working on some code the raises a lot of Missing metadata warnings, that show up like this
```
WARNING: SunpyUserWarning: Missing metadata for observer: assuming Earth-based observer.
 [sunpy.map.mapbase]
```
This isn't very helpful though, as I don't know what line in my code, or object, is the cause of these warnings.

It would be good to add some extra information so the user knows what is causing the warnings, and therefore making them easier to diagnose and fix.


Discussion:
It's supposed to inform the user if any meta specific to a frame is missing if there is at least one meta present from that frame. Otherwise, it is assumed that the user purposefully omitted those.
In this case, I believe that the file you're making the map with has no meta at all for any frame. Could you please verify this?

For reference, #3692 
With #3692 the warning should be tell what the issue is. Can you double check that it is doing so?
I think the action here was to modify the warnings to show which line of code they are being emitted by, similar to python warnings:
```python
warnings_warn.py:13: UserWarning: This is a warning message
  warnings.warn('This is a warning message')
```
I'm not sure why sunpy warnings don't do this.
Will we not just need to pass `stacklevel=2` to each `warning.warn` to get this to work?
Nope, I am wrong.
If I run:
```python
import warnings

from sunpy.util.exceptions import SunpyUserWarning

warnings.warn("ABC", UserWarning)
warnings.warn("ABC", stacklevel=2)
warnings.warn("ABC", SunpyUserWarning)
warnings.warn("ABC", SunpyUserWarning, stacklevel=2)
```
I get:
```
sunpy/timeseries/warn.py:5: UserWarning: ABC
  warnings.warn("ABC", UserWarning)
sys:1: UserWarning: ABC
WARNING: SunpyUserWarning: ABC [__main__]
WARNING: SunpyUserWarning: ABC [warnings]

```
Nothing is returned that is useful unless I replace `SunpyWarning` to inherit from `Warning` instead of `AstropyWarning`
```
sunpy/timeseries/warn.py:5: UserWarning: ABC
  warnings.warn("ABC", UserWarning)
sys:1: UserWarning: ABC
sunpy/timeseries/warn.py:7: SunpyUserWarning: ABC
  warnings.warn("ABC", SunpyUserWarning)
sys:1: SunpyUserWarning: ABC
```
Maybe this is an upstream astropy issue then?
If I add
```python
warnings.warn("AstropyWarning", AstropyWarning)
warnings.warn("AstropyWarning", AstropyWarning, stacklevel=2)
```
I get
```
WARNING: AstropyWarning [__main__]
WARNING: AstropyWarning [warnings]
```
But `AstropyWarning` inherit `Warning` so I am not sure what is going on.
It's because they use a custom logger, in `astropy/logger.py`
So we need to adjust our logger settings instead of changing our warning class?
So they have their own `_showwarning` (https://docs.astropy.org/en/stable/_modules/astropy/logger.html) that changes how it prints the warning if `isinstance(args[0], AstropyWarning)` which should explain the difference. 
In astropy tables they have this block of code:
```python
class TableReplaceWarning(UserWarning):
    """
    Warning class for cases when a table column is replaced via the
    Table.__setitem__ syntax e.g. t['a'] = val.
    This does not inherit from AstropyWarning because we want to use
    stacklevel=3 to show the user where the issue occurred in their code.
    """
    pass

```
Looks like they don't want to report where the warning occurred by default.

Resolving patch:
diff --git a/sunpy/map/mapbase.py b/sunpy/map/mapbase.py
--- a/sunpy/map/mapbase.py
+++ b/sunpy/map/mapbase.py
@@ -37,7 +37,7 @@
 from sunpy.time import is_time, parse_time
 from sunpy.util import expand_list
 from sunpy.util.decorators import deprecate_positional_args_since, deprecated
-from sunpy.util.exceptions import SunpyUserWarning
+from sunpy.util.exceptions import SunpyMetadataWarning, SunpyUserWarning
 from sunpy.util.functools import seconddispatch
 from sunpy.visualization import axis_labels_from_ctype, peek_show, wcsaxes_compat
 
@@ -838,7 +838,7 @@ def observer_coordinate(self):
         warning_message = "".join(
             [f"For frame '{frame}' the following metadata is missing: {','.join(keys)}\n" for frame, keys in missing_meta.items()])
         warning_message = "Missing metadata for observer: assuming Earth-based observer.\n" + warning_message
-        warnings.warn(warning_message, SunpyUserWarning)
+        warnings.warn(warning_message, SunpyMetadataWarning, stacklevel=3)
 
         return get_earth(self.date)
 
diff --git a/sunpy/util/exceptions.py b/sunpy/util/exceptions.py
--- a/sunpy/util/exceptions.py
+++ b/sunpy/util/exceptions.py
@@ -8,7 +8,7 @@
 from astropy.utils.exceptions import AstropyWarning
 
 __all__ = ["SunpyWarning", "SunpyUserWarning", "SunpyDeprecationWarning",
-           "SunpyPendingDeprecationWarning"]
+           "SunpyPendingDeprecationWarning", "SunpyMetadataWarning"]
 
 
 class SunpyWarning(AstropyWarning):
@@ -29,6 +29,15 @@ class SunpyUserWarning(UserWarning, SunpyWarning):
     """
 
 
+class SunpyMetadataWarning(UserWarning):
+    """
+    Warning class for cases metadata is missing.
+
+    This does not inherit from SunpyWarning because we want to use
+    stacklevel=3 to show the user where the issue occurred in their code.
+    """
+
+
 class SunpyDeprecationWarning(FutureWarning, SunpyWarning):
     """
     A warning class to indicate a deprecated feature.


Test patch:
diff --git a/sunpy/instr/tests/test_iris.py b/sunpy/instr/tests/test_iris.py
--- a/sunpy/instr/tests/test_iris.py
+++ b/sunpy/instr/tests/test_iris.py
@@ -7,7 +7,7 @@
 import sunpy.data.test
 import sunpy.map
 from sunpy.instr import iris
-from sunpy.util import SunpyUserWarning
+from sunpy.util.exceptions import SunpyMetadataWarning
 
 
 def test_SJI_to_sequence():
@@ -26,7 +26,7 @@ def test_iris_rot():
                              'iris_l2_20130801_074720_4040000014_SJI_1400_t000.fits')
     iris_cube = iris.SJI_to_sequence(test_data, start=0, stop=None, hdu=0)
     irismap = iris_cube.maps[0]
-    with pytest.warns(SunpyUserWarning, match='Missing metadata for observer'):
+    with pytest.warns(SunpyMetadataWarning, match='Missing metadata for observer'):
         irismap_rot = irismap.rotate()
 
     assert isinstance(irismap_rot, sunpy.map.sources.SJIMap)
diff --git a/sunpy/map/sources/tests/test_mdi_source.py b/sunpy/map/sources/tests/test_mdi_source.py
--- a/sunpy/map/sources/tests/test_mdi_source.py
+++ b/sunpy/map/sources/tests/test_mdi_source.py
@@ -16,7 +16,7 @@
 from sunpy.coordinates import frames
 from sunpy.map import Map
 from sunpy.map.sources.soho import MDIMap, MDISynopticMap
-from sunpy.util.exceptions import SunpyUserWarning
+from sunpy.util.exceptions import SunpyMetadataWarning
 
 
 @pytest.fixture
@@ -156,5 +156,5 @@ def test_carrington(mdi):
 def test_synoptic_source(mdi_synoptic):
     assert isinstance(mdi_synoptic, MDISynopticMap)
     # Check that the WCS is valid
-    with pytest.warns(SunpyUserWarning, match='Missing metadata for observer'):
+    with pytest.warns(SunpyMetadataWarning, match='Missing metadata for observer'):
         mdi_synoptic.wcs
diff --git a/sunpy/map/tests/test_mapbase.py b/sunpy/map/tests/test_mapbase.py
--- a/sunpy/map/tests/test_mapbase.py
+++ b/sunpy/map/tests/test_mapbase.py
@@ -26,6 +26,7 @@
 from sunpy.coordinates import sun
 from sunpy.time import parse_time
 from sunpy.util import SunpyDeprecationWarning, SunpyUserWarning
+from sunpy.util.exceptions import SunpyMetadataWarning
 
 testpath = sunpy.data.test.rootdir
 
@@ -261,7 +262,7 @@ def test_heliographic_longitude_crln(hmi_test_map):
 
 def test_remove_observers(aia171_test_map):
     aia171_test_map._remove_existing_observer_location()
-    with pytest.warns(SunpyUserWarning,
+    with pytest.warns(SunpyMetadataWarning,
                       match='Missing metadata for observer: assuming Earth-based observer.*'):
         aia171_test_map.observer_coordinate
 
@@ -272,7 +273,7 @@ def test_partially_missing_observers(generic_map):
     generic_map.meta['crlt_obs'] = 0
     generic_map.meta['crln_obs'] = 0
     generic_map.meta.pop('dsun_obs')
-    with pytest.warns(SunpyUserWarning,
+    with pytest.warns(SunpyMetadataWarning,
                       match="Missing metadata for observer: assuming Earth-based observer.\n" +
                             "For frame 'heliographic_stonyhurst' the following metadata is missing: dsun_obs\n" +
                             "For frame 'heliographic_carrington' the following metadata is missing: dsun_obs\n"):
@@ -874,7 +875,7 @@ def test_missing_metadata_warnings():
         array_map = sunpy.map.Map(np.random.rand(20, 15), header)
         array_map.peek()
     # There should be 2 warnings for missing metadata (obstime and observer location)
-    assert len([w for w in record if w.category is SunpyUserWarning]) == 2
+    assert len([w for w in record if w.category in (SunpyMetadataWarning, SunpyUserWarning)]) == 2
 
 
 def test_fits_header(aia171_test_map):